feat: media_async + media_index modules, media_files table, tests
- media_async: thread-per-task async engine with primitives (sha256, sign, copy, uuid, file_size) - media_index: media_files DB table with register_async for indexing chunks - chat_msg: async media submission via register_async, simplified chat_msg_submit fields - voice_recorder/attachment_sender: write single file (no chunk splitting on caller side) - inputbar/messagelist: updated to new simplified submit interface - test_media_async: 4-thread stress test (200K ops / 2 sec, 0 errors) - test_media_index: 9 cases covering commit edge cases + async flow - build: Makefile.am, CMakeLists.txt, utun_sources.cmake updated
This commit is contained in:
1 parent
ce5a84da24
commit
65dbfa6813
43 files changed
+1983
-391
No files matched your search
@@ -9,7 +9,7 @@ data class Channel(val id: String, val name: String, val lastMsgAt: Long = 0, va
|
||||
data class Message(val id: Long, val author: String, val text: String, val ts: Long,
|
||||
val isOutgoing: Boolean = false, val contentType: String = "text/plain",
|
||||
val filePath: String = "", val waveform: List<Float> = emptyList(),
|
||||
val voiceDurationMs: Int = 0)
|
||||
val voiceDurationMs: Int = 0, val fileSize: Long = 0)
|
||||
|
||||
class ChatRepository {
|
||||
private val myNodeId: Long = NativeLib.getMyNodeId()
|
||||
@@ -53,6 +53,9 @@ class ChatRepository {
|
||||
val contentType = obj.optString("contentType", "text/plain")
|
||||
val filePath = obj.optString("filePath", "")
|
||||
|
||||
var displayText = text
|
||||
var fileSize = 0L
|
||||
|
||||
/* Parse base64 waveform from voice message text (format: "wf=<100 chars A-Za-z0-9+/>;dur=1234;") */
|
||||
var waveform = emptyList<Float>()
|
||||
var voiceDurationMs = 0
|
||||
@@ -88,16 +91,30 @@ class ChatRepository {
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
/* Parse file attachment: "filename.ext|file_size|block_size|num_blocks|sigs" */
|
||||
if (contentType == "application/octet-stream") {
|
||||
try {
|
||||
val pipeIdx = text.indexOf('|')
|
||||
if (pipeIdx > 0) {
|
||||
displayText = text.substring(0, pipeIdx)
|
||||
val metaParts = text.substring(pipeIdx + 1).split("|")
|
||||
if (metaParts.isNotEmpty())
|
||||
fileSize = metaParts[0].toLongOrNull() ?: 0L
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
list.add(Message(
|
||||
id = obj.getLong("id"),
|
||||
author = obj.getString("author"),
|
||||
text = text,
|
||||
text = displayText,
|
||||
ts = obj.getLong("ts"),
|
||||
isOutgoing = obj.optBoolean("isOutgoing", false),
|
||||
contentType = contentType,
|
||||
filePath = filePath,
|
||||
waveform = waveform,
|
||||
voiceDurationMs = voiceDurationMs
|
||||
voiceDurationMs = voiceDurationMs,
|
||||
fileSize = fileSize
|
||||
))
|
||||
}
|
||||
} catch (e: Exception) { Log.w("utun-gui", "getMessages error", e) }
|
||||
|
||||
@@ -36,6 +36,9 @@ class ConfigProvider(private val context: Context) {
|
||||
private val logUdpIp = stringPreferencesKey("log_udp_ip")
|
||||
private val logUdpPort = intPreferencesKey("log_udp_port")
|
||||
private val serversJson = stringPreferencesKey("servers")
|
||||
private val storageAutoload = intPreferencesKey("storage_autoload")
|
||||
private val storageAutoloadMaxsizeMb = intPreferencesKey("storage_autoload_maxsize_mb")
|
||||
private val storageMaxsizeGb = intPreferencesKey("storage_maxsize_gb")
|
||||
private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
|
||||
|
||||
private val gson = Gson()
|
||||
@@ -130,6 +133,9 @@ class ConfigProvider(private val context: Context) {
|
||||
key == "node.listen_port" -> context.dataStore.data.first()[nodeListenPort] ?: 12345
|
||||
key == "control.port" -> context.dataStore.data.first()[controlPort] ?: 9999
|
||||
key == "log_udp.port" -> context.dataStore.data.first()[logUdpPort] ?: 9999
|
||||
key == "storage_autoload" -> context.dataStore.data.first()[storageAutoload] ?: 1
|
||||
key == "storage_autoload_maxsize_mb" -> context.dataStore.data.first()[storageAutoloadMaxsizeMb] ?: 10
|
||||
key == "storage_maxsize_gb" -> context.dataStore.data.first()[storageMaxsizeGb] ?: 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
@@ -149,6 +155,9 @@ class ConfigProvider(private val context: Context) {
|
||||
"debug.categories" -> prefs[debugCategories] = value
|
||||
"log_udp.ip" -> prefs[logUdpIp] = value
|
||||
"log_udp.port" -> prefs[logUdpPort] = value.toIntOrNull() ?: 0
|
||||
"storage_autoload" -> prefs[storageAutoload] = value.toIntOrNull() ?: 1
|
||||
"storage_autoload_maxsize_mb" -> prefs[storageAutoloadMaxsizeMb] = value.toIntOrNull() ?: 10
|
||||
"storage_maxsize_gb" -> prefs[storageMaxsizeGb] = value.toIntOrNull() ?: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -62,7 +62,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
||||
|
||||
when {
|
||||
isVoice -> VoiceBubble(message, textColor, isActive, playbackState, onPlayVoice, onSeekVoice)
|
||||
isMedia -> Text("\uD83D\uDCC4 ${message.text}", fontSize = 15.sp, color = textColor)
|
||||
isMedia -> FileBubble(message, textColor)
|
||||
else -> {
|
||||
Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f))
|
||||
Spacer(Modifier.height(2.dp))
|
||||
@@ -167,6 +167,26 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileBubble(message: Message, textColor: Color) {
|
||||
Column {
|
||||
Text(message.text, fontSize = 15.sp, color = textColor, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(formatFileSize(message.fileSize), fontSize = 12.sp, color = textColor.copy(alpha = 0.5f))
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFileSize(bytes: Long): String {
|
||||
if (bytes <= 0) return ""
|
||||
if (bytes < 1024) return "${bytes}B"
|
||||
val kb = bytes / 1024.0
|
||||
if (kb < 1024.0) return if (kb < 10.0) "%.1fKB".format(kb) else "%.0fKB".format(kb)
|
||||
val mb = kb / 1024.0
|
||||
if (mb < 1024.0) return if (mb < 10.0) "%.1fMB".format(mb) else "%.0fMB".format(mb)
|
||||
val gb = mb / 1024.0
|
||||
return "%.1fGB".format(gb)
|
||||
}
|
||||
|
||||
private fun formatDurationMs(ms: Int): String {
|
||||
val secs = ms / 1000
|
||||
return "%d:%02d".format(secs / 60, secs % 60)
|
||||
|
||||
@@ -42,7 +42,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
|
||||
var dirty by remember { mutableStateOf(false) }
|
||||
|
||||
var tabIndex by remember { mutableStateOf(0) }
|
||||
val tabs = listOf("Settings", "Audio")
|
||||
val tabs = listOf("Settings", "Audio", "Storage")
|
||||
|
||||
val handleBack = {
|
||||
if (dirty) {
|
||||
@@ -73,6 +73,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
|
||||
when (tabIndex) {
|
||||
0 -> GeneralSettingsTab(vm, dirty, onDirty = { dirty = it }, onExit)
|
||||
1 -> AudioSettingsTab(vm)
|
||||
2 -> StorageSettingsTab()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,6 +415,52 @@ private fun AudioSettingsTab(vm: ChatViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Storage Settings Tab ── */
|
||||
|
||||
@Composable
|
||||
private fun StorageSettingsTab() {
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var autoload by remember { mutableStateOf(true) }
|
||||
var autoloadMaxSizeMb by remember { mutableStateOf("10") }
|
||||
var maxStorageGb by remember { mutableStateOf("1") }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
autoload = provider.getInt("storage_autoload") != 0
|
||||
autoloadMaxSizeMb = provider.getInt("storage_autoload_maxsize_mb").toString()
|
||||
maxStorageGb = provider.getInt("storage_maxsize_gb").toString()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text("Media Storage", style = MaterialTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Autoload media", modifier = Modifier.weight(1f))
|
||||
Switch(checked = autoload, onCheckedChange = {
|
||||
autoload = it
|
||||
scope.launch { provider.setValue("storage_autoload", if (it) "1" else "0") }
|
||||
})
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 4.dp))
|
||||
|
||||
OutlinedTextField(autoloadMaxSizeMb,
|
||||
{ v -> autoloadMaxSizeMb = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..500) scope.launch { provider.setValue("storage_autoload_maxsize_mb", v) } },
|
||||
label = { Text("Autoload max size (MB)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true, modifier = Modifier.padding(top = 4.dp))
|
||||
|
||||
OutlinedTextField(maxStorageGb,
|
||||
{ v -> maxStorageGb = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..100) scope.launch { provider.setValue("storage_maxsize_gb", v) } },
|
||||
label = { Text("Max storage size (GB)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Labeled Slider ── */
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -489,8 +489,8 @@ int utun_bridge_voice_get_compressor(void) {
|
||||
}
|
||||
|
||||
void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
|
||||
float rise_rate_per_500ms, float target_level_db) {
|
||||
voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_500ms, target_level_db);
|
||||
float rise_rate_per_sec, float target_level_db) {
|
||||
voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db);
|
||||
}
|
||||
|
||||
float utun_bridge_voice_get_peak_level(void) {
|
||||
|
||||
@@ -86,7 +86,7 @@ int utun_bridge_voice_get_preset(void);
|
||||
void utun_bridge_voice_set_compressor(int enabled);
|
||||
int utun_bridge_voice_get_compressor(void);
|
||||
void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
|
||||
float rise_rate_per_500ms, float target_level_db);
|
||||
float rise_rate_per_sec, float target_level_db);
|
||||
float utun_bridge_voice_get_peak_level(void);
|
||||
|
||||
/* ── Attachment ── */
|
||||
|
||||
@@ -12,18 +12,6 @@
|
||||
#include <unistd.h>
|
||||
#include <libgen.h>
|
||||
|
||||
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_TARGET 30
|
||||
|
||||
static uint32_t media_calc_block_size(uint64_t file_size) {
|
||||
if (file_size == 0) return 0;
|
||||
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
|
||||
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
|
||||
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
|
||||
return (uint32_t)target;
|
||||
}
|
||||
|
||||
int attachment_send(const char* channel_id, const char* src_file_path, const char* db_path) {
|
||||
if (!channel_id || !src_file_path || !db_path) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: invalid args");
|
||||
@@ -33,27 +21,15 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: no uasync"); return -1; }
|
||||
|
||||
FILE* src = fopen(src_file_path, "rb");
|
||||
if (!src) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: cannot open %s", src_file_path); return -1; }
|
||||
|
||||
fseek(src, 0, SEEK_END);
|
||||
uint64_t file_size = (uint64_t)ftell(src);
|
||||
fseek(src, 0, SEEK_SET);
|
||||
|
||||
if (file_size == 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "attachment_send: empty file %s", src_file_path); fclose(src); return -1; }
|
||||
|
||||
/* extract basename and ext from path */
|
||||
/* extract basename and ext */
|
||||
char path_copy[1024];
|
||||
snprintf(path_copy, sizeof(path_copy), "%s", src_file_path);
|
||||
char* fname = basename(path_copy);
|
||||
char* dot = strrchr(fname, '.');
|
||||
char ext[32] = "";
|
||||
if (dot) {
|
||||
snprintf(ext, sizeof(ext), "%s", dot + 1);
|
||||
*dot = '\0';
|
||||
}
|
||||
if (dot) { snprintf(ext, sizeof(ext), "%s", dot + 1); *dot = '\0'; }
|
||||
|
||||
/* generate dt/suffix */
|
||||
/* generate dt/suffix for unique filename */
|
||||
time_t t = time(NULL);
|
||||
struct tm tm_buf; localtime_r(&t, &tm_buf);
|
||||
char dt[32]; snprintf(dt, sizeof(dt), "%04d%02d%02d-%02d%02d%02d",
|
||||
@@ -62,10 +38,7 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
|
||||
int rnd = rand() & 0xFFFF;
|
||||
char suffix[8]; snprintf(suffix, sizeof(suffix), "%04x", rnd);
|
||||
|
||||
uint32_t block_size = media_calc_block_size(file_size);
|
||||
int num_blocks = (int)((file_size + block_size - 1) / block_size);
|
||||
|
||||
/* ensure media dir */
|
||||
/* ensure media dir exists */
|
||||
char media_dir[1024];
|
||||
snprintf(media_dir, sizeof(media_dir), "%s/media/%s", db_path, channel_id);
|
||||
{
|
||||
@@ -77,46 +50,26 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
|
||||
mkdir(tmp, 0755);
|
||||
}
|
||||
|
||||
/* write blocks */
|
||||
for (int n = 0; n < num_blocks; n++) {
|
||||
char block_path[1280];
|
||||
snprintf(block_path, sizeof(block_path), "%s/%s_%d_%s_%s.%s",
|
||||
media_dir, dt, n, fname, suffix, ext[0] ? ext : "bin");
|
||||
FILE* dst = fopen(block_path, "wb");
|
||||
if (!dst) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "attachment_send: cannot create block %s", block_path);
|
||||
continue;
|
||||
}
|
||||
uint8_t buf[65536];
|
||||
uint64_t remaining = block_size;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf);
|
||||
size_t rd = fread(buf, 1, to_read, src);
|
||||
if (rd == 0) break;
|
||||
fwrite(buf, 1, rd, dst);
|
||||
remaining -= rd;
|
||||
}
|
||||
fclose(dst);
|
||||
}
|
||||
fclose(src);
|
||||
/* single file path — no splitting */
|
||||
char media_file[1280];
|
||||
snprintf(media_file, sizeof(media_file), "%s/%s_%s_%s.%s",
|
||||
media_dir, dt, fname, suffix, ext[0] ? ext : "bin");
|
||||
|
||||
/* post chat_msg_submit */
|
||||
/* post chat_msg_submit — media_copy=1 so media_index copies src to dest */
|
||||
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + 1);
|
||||
if (!req) return -1;
|
||||
|
||||
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id);
|
||||
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
|
||||
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt);
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", fname);
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix);
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext[0] ? ext : "bin");
|
||||
req->media_num_blocks = (uint32_t)num_blocks;
|
||||
snprintf(req->media_src, sizeof(req->media_src), "%s", src_file_path);
|
||||
snprintf(req->media_dest, sizeof(req->media_dest), "%s", media_file);
|
||||
req->media_copy = 1;
|
||||
req->data = (uint8_t*)(req + 1);
|
||||
req->data_len = 0;
|
||||
req->timestamp = 0;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "attachment_send: ch=%s file=%s blocks=%d size=%llu",
|
||||
channel_id, src_file_path, num_blocks, (unsigned long long)file_size);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "attachment_send: ch=%s src=%s dst=%s",
|
||||
channel_id, src_file_path, media_file);
|
||||
|
||||
uasync_post(ua, chat_core_submit_trampoline, req);
|
||||
return 0;
|
||||
|
||||
@@ -58,6 +58,8 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR)
|
||||
${SRC_DIR}/proxy
|
||||
${SRC_DIR}/lwip_tcp
|
||||
${SRC_DIR}/BBR
|
||||
${SRC_DIR}/media_delivery
|
||||
${SRC_DIR}/media_async
|
||||
${TOOLS_DIR}
|
||||
${CONFIG_DIR}
|
||||
PARENT_SCOPE
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_TARGET 30
|
||||
#define OPUS_MAGIC 0x5355504F
|
||||
#define FRAME_MS 20
|
||||
#define MAX_PACKET 4000
|
||||
@@ -85,9 +82,9 @@ int voice_recorder_init(const char* db_path) {
|
||||
cfg.block_duration_ms = 20;
|
||||
cfg.lookback_ms = 200;
|
||||
cfg.lookahead_ms = 100;
|
||||
cfg.max_gain_db = 30.0f;
|
||||
cfg.rise_rate_per_500ms = 2.0f;
|
||||
cfg.target_level = 0.25f;
|
||||
cfg.max_gain_db = 25.0f;
|
||||
cfg.rise_rate_per_sec = 10.0f;
|
||||
cfg.target_level = 1.0f;
|
||||
audio_compressor_configure(g_rec->compressor, &cfg);
|
||||
}
|
||||
|
||||
@@ -199,14 +196,6 @@ int voice_recorder_feed(const int16_t* samples, int count) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint32_t media_calc_block_size(uint64_t file_size) {
|
||||
if (file_size == 0) return 0;
|
||||
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
|
||||
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
|
||||
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
|
||||
return (uint32_t)target;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -364,37 +353,8 @@ int voice_recorder_stop(int* out_duration_ms) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* get file size and calculate blocks */
|
||||
FILE* fsize = fopen(temp_path, "rb"); uint64_t file_size = 0;
|
||||
if (fsize) { fseek(fsize, 0, SEEK_END); file_size = (uint64_t)ftell(fsize); fclose(fsize); }
|
||||
|
||||
uint32_t block_size = media_calc_block_size(file_size);
|
||||
int num_blocks = (int)((file_size + block_size - 1) / block_size);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: file_size=%llu block_size=%u num_blocks=%d",
|
||||
(unsigned long long)file_size, block_size, num_blocks);
|
||||
|
||||
/* split file into blocks */
|
||||
FILE* src = fopen(temp_path, "rb");
|
||||
if (!src) { unlink(temp_path); voice_cleanup_locked(g_rec); pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; }
|
||||
|
||||
for (int n = 0; n < num_blocks; n++) {
|
||||
char block_path[1280];
|
||||
snprintf(block_path, sizeof(block_path), "%s/%s_%d_%s_%s.opus",
|
||||
media_dir, dt, n, basename, suffix);
|
||||
FILE* dst = fopen(block_path, "wb");
|
||||
if (!dst) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: cannot create block %s", block_path); continue; }
|
||||
uint8_t buf[65536];
|
||||
uint64_t remaining = block_size;
|
||||
while (remaining > 0) {
|
||||
size_t rd = fread(buf, 1, remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf), src);
|
||||
if (rd == 0) break;
|
||||
fwrite(buf, 1, rd, dst);
|
||||
remaining -= rd;
|
||||
}
|
||||
fclose(dst);
|
||||
}
|
||||
fclose(src);
|
||||
unlink(temp_path);
|
||||
/* 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+/";
|
||||
@@ -421,18 +381,16 @@ int voice_recorder_stop(int* out_duration_ms) {
|
||||
|
||||
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_dt, sizeof(req->media_dt), "%s", dt);
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename);
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix);
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "opus");
|
||||
req->media_num_blocks = (uint32_t)num_blocks;
|
||||
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 dt=%s stem=%s blocks=%d",
|
||||
req->channel_id, dt, basename, num_blocks);
|
||||
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);
|
||||
|
||||
@@ -505,7 +463,7 @@ int voice_recorder_is_compressor_enabled(void) {
|
||||
}
|
||||
|
||||
void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
|
||||
float rise_rate_per_500ms, float target_level_db) {
|
||||
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);
|
||||
@@ -516,11 +474,11 @@ void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int
|
||||
cfg.lookback_ms = lookback_ms;
|
||||
cfg.lookahead_ms = lookahead_ms;
|
||||
cfg.max_gain_db = (float)max_gain_db;
|
||||
cfg.rise_rate_per_500ms = rise_rate_per_500ms;
|
||||
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_500ms, target_level_db);
|
||||
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);
|
||||
|
||||
@@ -24,7 +24,7 @@ int voice_recorder_get_preset(void);
|
||||
void voice_recorder_set_compressor_enabled(int enabled);
|
||||
int voice_recorder_is_compressor_enabled(void);
|
||||
void voice_recorder_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
|
||||
float rise_rate_per_500ms, float target_level_db);
|
||||
float rise_rate_per_sec, float target_level_db);
|
||||
float voice_recorder_get_peak_level(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
Reference in new issue
Block a user