Browse Source
- C99 port of AudioCompressor (libutun_lite/audio_compressor.h/c) - Voice recorder: PCM buffer + compressor + Opus encode + block splitting + uasync send - Attachment sender: file splitting into blocks + uasync send - Bridge API + JNI: voice start/feed/stop/cancel, attachment send, Opus decode for playback - Kotlin: AudioRecorderManager (AudioRecord + JNI feed), AudioPlayer (AudioTrack + JNI decode) - UI: PTT button with press-to-talk, attach button with file picker, recording overlay - Message display: voice messages (play button), file attachments (icon + name) - Settings: Opus preset selector (Low/Standard/High), compressor toggle - AndroidManifest: RECORD_AUDIO, storage permissionstopo_upd
18 changed files with 1725 additions and 18 deletions
@ -0,0 +1,189 @@
|
||||
package com.utun.chat.data |
||||
|
||||
import android.Manifest |
||||
import android.content.pm.PackageManager |
||||
import android.media.AudioFormat |
||||
import android.media.AudioManager |
||||
import android.media.AudioRecord |
||||
import android.media.AudioTrack |
||||
import android.media.MediaRecorder |
||||
import androidx.core.content.ContextCompat |
||||
import com.utun.chat.ChatApplication |
||||
|
||||
class AudioRecorderManager { |
||||
companion object { |
||||
const val SAMPLE_RATE = 48000 |
||||
const val CHANNELS = 1 |
||||
const val FRAME_MS = 20 |
||||
const val FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS / 1000 |
||||
const val MIN_DURATION_MS = 500 |
||||
} |
||||
|
||||
private var audioRecord: AudioRecord? = null |
||||
private var recordingThread: Thread? = null |
||||
@Volatile private var isRecording = false |
||||
|
||||
fun hasPermission(): Boolean { |
||||
val ctx = ChatApplication.instance |
||||
return ContextCompat.checkSelfPermission(ctx, Manifest.permission.RECORD_AUDIO) == |
||||
PackageManager.PERMISSION_GRANTED |
||||
} |
||||
|
||||
fun startRecording(channelId: String): Boolean { |
||||
if (isRecording) return false |
||||
if (!hasPermission()) return false |
||||
|
||||
val ok = NativeLib.voiceStart(channelId) |
||||
if (!ok) { |
||||
LogManager.addLog("ERROR", "AudioRecorder", "native voiceStart failed") |
||||
return false |
||||
} |
||||
|
||||
val bufferSize = AudioRecord.getMinBufferSize( |
||||
SAMPLE_RATE, |
||||
AudioFormat.CHANNEL_IN_MONO, |
||||
AudioFormat.ENCODING_PCM_16BIT |
||||
).coerceAtLeast(FRAME_SAMPLES * 2) |
||||
|
||||
audioRecord = try { |
||||
AudioRecord( |
||||
MediaRecorder.AudioSource.MIC, |
||||
SAMPLE_RATE, |
||||
AudioFormat.CHANNEL_IN_MONO, |
||||
AudioFormat.ENCODING_PCM_16BIT, |
||||
bufferSize * 2 |
||||
) |
||||
} catch (e: Exception) { |
||||
LogManager.addLog("ERROR", "AudioRecorder", "AudioRecord create failed: ${e.message}") |
||||
NativeLib.voiceCancel() |
||||
return false |
||||
} |
||||
|
||||
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { |
||||
LogManager.addLog("ERROR", "AudioRecorder", "AudioRecord not initialized") |
||||
audioRecord?.release() |
||||
audioRecord = null |
||||
NativeLib.voiceCancel() |
||||
return false |
||||
} |
||||
|
||||
isRecording = true |
||||
LogManager.addLog("INFO", "AudioRecorder", "started ch=$channelId bufferSize=$bufferSize") |
||||
audioRecord?.startRecording() |
||||
|
||||
recordingThread = Thread { |
||||
val buffer = ShortArray(FRAME_SAMPLES) |
||||
var totalRead = 0L |
||||
|
||||
while (isRecording && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { |
||||
val read = audioRecord?.read(buffer, 0, FRAME_SAMPLES) ?: break |
||||
if (read <= 0) break |
||||
NativeLib.voiceFeed(buffer.copyOf(read)) |
||||
totalRead += read |
||||
} |
||||
LogManager.addLog("DEBUG", "AudioRecorder", "thread done totalSamples=$totalRead") |
||||
}.apply { |
||||
priority = Thread.MAX_PRIORITY |
||||
start() |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
fun stopRecording(): Int { |
||||
if (!isRecording) return 0 |
||||
isRecording = false |
||||
|
||||
recordingThread?.join(1000) |
||||
recordingThread = null |
||||
|
||||
audioRecord?.apply { |
||||
try { stop() } catch (_: Exception) {} |
||||
release() |
||||
} |
||||
audioRecord = null |
||||
|
||||
val duration = NativeLib.voiceStop() |
||||
LogManager.addLog("INFO", "AudioRecorder", "stopped duration=${duration}ms") |
||||
return duration |
||||
} |
||||
|
||||
fun cancelRecording() { |
||||
if (!isRecording) return |
||||
isRecording = false |
||||
|
||||
recordingThread?.join(1000) |
||||
recordingThread = null |
||||
|
||||
audioRecord?.apply { |
||||
try { stop() } catch (_: Exception) {} |
||||
release() |
||||
} |
||||
audioRecord = null |
||||
|
||||
NativeLib.voiceCancel() |
||||
LogManager.addLog("INFO", "AudioRecorder", "cancelled") |
||||
} |
||||
|
||||
fun isActive(): Boolean = isRecording |
||||
|
||||
@Volatile private var playing = false |
||||
private var playThread: Thread? = null |
||||
|
||||
fun playVoiceFile(filePath: String, onProgress: (Float) -> Unit = {}, onComplete: () -> Unit = {}) { |
||||
if (playing) stopPlayback() |
||||
playing = true |
||||
|
||||
playThread = Thread { |
||||
try { |
||||
val info = IntArray(3) |
||||
val handle = NativeLib.voiceDecodeOpen(filePath, info) |
||||
if (handle == 0L) { |
||||
LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath") |
||||
playing = false; return@Thread |
||||
} |
||||
val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2] |
||||
|
||||
val bufSize = AudioTrack.getMinBufferSize( |
||||
sampleRate, |
||||
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO, |
||||
AudioFormat.ENCODING_PCM_16BIT |
||||
) |
||||
|
||||
val track = AudioTrack( |
||||
AudioManager.STREAM_MUSIC, |
||||
sampleRate, |
||||
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO, |
||||
AudioFormat.ENCODING_PCM_16BIT, |
||||
bufSize, |
||||
AudioTrack.MODE_STREAM |
||||
) |
||||
track.play() |
||||
|
||||
val pcm = ShortArray(FRAME_SAMPLES * channels) |
||||
var readTotal = 0L |
||||
var read: Int |
||||
while (playing) { |
||||
read = NativeLib.voiceDecodeRead(handle, pcm) |
||||
if (read <= 0) break |
||||
track.write(pcm, 0, read) |
||||
readTotal += read |
||||
if (totalSamples > 0) onProgress(readTotal.toFloat() / totalSamples.toFloat()) |
||||
} |
||||
track.stop() |
||||
track.release() |
||||
NativeLib.voiceDecodeClose(handle) |
||||
} catch (e: Exception) { |
||||
LogManager.addLog("ERROR", "AudioPlayer", "play error: ${e.message}") |
||||
} |
||||
playing = false |
||||
onComplete() |
||||
}.apply { start() } |
||||
} |
||||
|
||||
fun stopPlayback() { |
||||
playing = false |
||||
playThread?.join(500) |
||||
playThread = null |
||||
} |
||||
} |
||||
@ -0,0 +1,123 @@
|
||||
#include "attachment_sender.h" |
||||
#include "instance_lite.h" |
||||
#include "../../../lib/debug_config.h" |
||||
#include "../../../lib/mem.h" |
||||
#include "../../../lib/u_async.h" |
||||
#include "../../../src/chat/chat_core.h" |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <sys/stat.h> |
||||
#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"); |
||||
return -1; |
||||
} |
||||
|
||||
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 */ |
||||
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'; |
||||
} |
||||
|
||||
/* generate dt/suffix */ |
||||
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", |
||||
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; |
||||
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 */ |
||||
char media_dir[1024]; |
||||
snprintf(media_dir, sizeof(media_dir), "%s/media/%s", db_path, channel_id); |
||||
{ |
||||
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] = '\0'; mkdir(tmp, 0755); } |
||||
} |
||||
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); |
||||
|
||||
/* post chat_msg_submit */ |
||||
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; |
||||
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); |
||||
|
||||
uasync_post(ua, chat_core_submit_trampoline, req); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,14 @@
|
||||
#ifndef ATTACHMENT_SENDER_H |
||||
#define ATTACHMENT_SENDER_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
int attachment_send(const char* channel_id, const char* src_file_path, const char* db_path); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif |
||||
@ -0,0 +1,321 @@
|
||||
#include "audio_compressor.h" |
||||
#include "../../../lib/debug_config.h" |
||||
#include "../../../lib/mem.h" |
||||
#include <math.h> |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
|
||||
#define BLOCK_LEVELS_CHUNK 64 |
||||
#define PENDING_CHUNK 32 |
||||
|
||||
struct pending_block { |
||||
int16_t* samples; |
||||
size_t count; |
||||
size_t level_index; |
||||
}; |
||||
|
||||
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 max_gain; |
||||
float gain_smoothed; |
||||
|
||||
float* block_levels; |
||||
size_t block_levels_count; |
||||
size_t block_levels_cap; |
||||
|
||||
struct pending_block* pending; |
||||
size_t pending_head; |
||||
size_t pending_tail; |
||||
size_t pending_cap; |
||||
|
||||
int16_t* accumulator; |
||||
size_t accum_count; |
||||
|
||||
int16_t* output; |
||||
size_t output_size; |
||||
size_t output_cap; |
||||
|
||||
int dbg_counter; |
||||
}; |
||||
|
||||
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; |
||||
} |
||||
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 = ac->pending_head; i != ac->pending_tail; i = (i + 1) % ac->pending_cap) |
||||
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_500ms, (float)ac->cfg.block_duration_ms / 500.0f); |
||||
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, |
||||
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/500ms 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, |
||||
ac->cfg.rise_rate_per_500ms, 20.0f * log10f(ac->cfg.target_level)); |
||||
} |
||||
|
||||
void audio_compressor_reset(struct audio_compressor* ac) { |
||||
if (!ac) return; |
||||
for (size_t i = ac->pending_head; i != ac->pending_tail; i = (i + 1) % ac->pending_cap) |
||||
u_free(ac->pending[i].samples); |
||||
ac->pending_head = 0; |
||||
ac->pending_tail = 0; |
||||
ac->block_levels_count = 0; |
||||
ac->accum_count = 0; |
||||
ac->output_size = 0; |
||||
ac->gain_smoothed = 1.0f; |
||||
ac->dbg_counter = 0; |
||||
} |
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { |
||||
if (!ac) return; |
||||
ac->enabled = enabled ? 1 : 0; |
||||
} |
||||
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac) { |
||||
return ac ? ac->enabled : 0; |
||||
} |
||||
|
||||
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 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; |
||||
} |
||||
|
||||
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); |
||||
|
||||
struct pending_block pb; |
||||
pb.samples = ac->accumulator; |
||||
pb.count = ac->accum_count; |
||||
pb.level_index = ac->block_levels_count - 1; |
||||
|
||||
/* ring buffer enqueue */ |
||||
if ((ac->pending_tail + 1) % ac->pending_cap == ac->pending_head) { |
||||
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK; |
||||
struct pending_block* tmp2 = u_realloc(ac->pending, new_cap * sizeof(struct pending_block)); |
||||
if (!tmp2) { u_free(pb.samples); return; } |
||||
if (ac->pending && ac->pending_head > ac->pending_tail) { |
||||
size_t wrap = ac->pending_cap - ac->pending_head; |
||||
memmove(tmp2 + new_cap - wrap, tmp2 + ac->pending_head, wrap * sizeof(struct pending_block)); |
||||
ac->pending_head = new_cap - wrap; |
||||
} |
||||
ac->pending = tmp2; |
||||
ac->pending_cap = new_cap; |
||||
} |
||||
ac->pending[ac->pending_tail] = pb; |
||||
ac->pending_tail = (ac->pending_tail + 1) % ac->pending_cap; |
||||
|
||||
ac->accumulator = NULL; |
||||
ac->accum_count = 0; |
||||
} |
||||
} |
||||
|
||||
while (ac->pending_head != ac->pending_tail) { |
||||
size_t pending_cnt = (ac->pending_tail >= ac->pending_head) |
||||
? (ac->pending_tail - ac->pending_head) |
||||
: (ac->pending_cap - ac->pending_head + ac->pending_tail); |
||||
if (pending_cnt <= (size_t)ac->lookahead_blocks) break; |
||||
process_pending_block(ac); |
||||
} |
||||
} |
||||
|
||||
static void process_pending_block(struct audio_compressor* ac) { |
||||
if (ac->pending_head == ac->pending_tail) return; |
||||
|
||||
struct pending_block* block = &ac->pending[ac->pending_head]; |
||||
size_t idx = block->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]; |
||||
} |
||||
if (envelope < 0.0001f) envelope = 0.0001f; |
||||
|
||||
float G_raw = ac->cfg.target_level / envelope; |
||||
float G_prev = ac->gain_smoothed; |
||||
|
||||
if (G_raw > ac->gain_smoothed) { |
||||
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block; |
||||
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise; |
||||
} else { |
||||
ac->gain_smoothed = G_raw; |
||||
} |
||||
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain; |
||||
|
||||
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f); |
||||
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", |
||||
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db); |
||||
} |
||||
ac->dbg_counter++; |
||||
|
||||
for (size_t i = 0; i < block->count; i++) { |
||||
float v = (float)block->samples[i] * ac->gain_smoothed; |
||||
if (v > 32767.0f) v = 32767.0f; |
||||
if (v < -32768.0f) v = -32768.0f; |
||||
block->samples[i] = (int16_t)(int)v; |
||||
} |
||||
append_output(ac, block->samples, block->count); |
||||
u_free(block->samples); |
||||
|
||||
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap; |
||||
} |
||||
|
||||
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); |
||||
|
||||
struct pending_block pb; |
||||
pb.samples = ac->accumulator; |
||||
pb.count = ac->accum_count; |
||||
pb.level_index = ac->block_levels_count - 1; |
||||
|
||||
if ((ac->pending_tail + 1) % ac->pending_cap == ac->pending_head) { |
||||
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK; |
||||
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(struct pending_block)); |
||||
if (!tmp) { u_free(pb.samples); return; } |
||||
if (ac->pending && ac->pending_head > ac->pending_tail) { |
||||
size_t wrap = ac->pending_cap - ac->pending_head; |
||||
memmove(tmp + new_cap - wrap, tmp + ac->pending_head, wrap * sizeof(struct pending_block)); |
||||
ac->pending_head = new_cap - wrap; |
||||
} |
||||
ac->pending = tmp; |
||||
ac->pending_cap = new_cap; |
||||
} |
||||
ac->pending[ac->pending_tail] = pb; |
||||
ac->pending_tail = (ac->pending_tail + 1) % ac->pending_cap; |
||||
|
||||
ac->accumulator = NULL; |
||||
ac->accum_count = 0; |
||||
} |
||||
|
||||
int remain = (int)((ac->pending_tail >= ac->pending_head) |
||||
? (ac->pending_tail - ac->pending_head) |
||||
: (ac->pending_cap - ac->pending_head + ac->pending_tail)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %d pending blocks (gainSmoothed=%.1fdB)", |
||||
remain, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f)); |
||||
|
||||
while (ac->pending_head != ac->pending_tail) |
||||
process_pending_block(ac); |
||||
} |
||||
|
||||
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; |
||||
} |
||||
@ -0,0 +1,43 @@
|
||||
#ifndef AUDIO_COMPRESSOR_H |
||||
#define AUDIO_COMPRESSOR_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
struct audio_compressor; |
||||
|
||||
typedef struct { |
||||
int sample_rate; |
||||
int channels; |
||||
int block_duration_ms; |
||||
int lookback_ms; |
||||
int lookahead_ms; |
||||
float max_gain_db; |
||||
float rise_rate_per_500ms; |
||||
float target_level; |
||||
} audio_compressor_config_t; |
||||
|
||||
struct audio_compressor* audio_compressor_create(void); |
||||
void audio_compressor_destroy(struct audio_compressor* ac); |
||||
|
||||
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg); |
||||
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); |
||||
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac); |
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif |
||||
@ -0,0 +1,448 @@
|
||||
#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 <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <pthread.h> |
||||
#include <sys/stat.h> |
||||
#include <unistd.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 |
||||
|
||||
static const int s_bitrates[] = {16000, 32000, 64000}; |
||||
static const int s_complexities[] = {2, 5, 10}; |
||||
|
||||
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; |
||||
|
||||
int64_t start_time_ms; |
||||
}; |
||||
|
||||
static struct voice_recorder* g_rec = NULL; |
||||
static pthread_mutex_t g_init_mtx = PTHREAD_MUTEX_INITIALIZER; |
||||
|
||||
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 = 30.0f; |
||||
cfg.rise_rate_per_500ms = 2.0f; |
||||
cfg.target_level = 0.25f; |
||||
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); |
||||
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) { |
||||
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) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_start: already active"); |
||||
pthread_mutex_unlock(&g_rec->mtx); |
||||
pthread_mutex_unlock(&g_init_mtx); |
||||
return -1; |
||||
} |
||||
|
||||
if (sample_rate <= 0) sample_rate = 48000; |
||||
if (channels <= 0) channels = 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; } |
||||
|
||||
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 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 int64_t now_ms(void) { |
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); |
||||
return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL; |
||||
} |
||||
|
||||
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; |
||||
} |
||||
|
||||
int voice_recorder_stop(int* out_duration_ms) { |
||||
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) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: not active"); |
||||
pthread_mutex_unlock(&g_rec->mtx); |
||||
pthread_mutex_unlock(&g_init_mtx); |
||||
return -1; |
||||
} |
||||
|
||||
int64_t elapsed_ms = now_ms() - g_rec->start_time_ms; |
||||
if (out_duration_ms) *out_duration_ms = (int)elapsed_ms; |
||||
|
||||
struct UASYNC* ua = instance_lite_get_uasync(); |
||||
if (!ua) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_recorder_stop: no uasync, discarding"); |
||||
voice_cleanup_locked(g_rec); |
||||
pthread_mutex_unlock(&g_rec->mtx); |
||||
pthread_mutex_unlock(&g_init_mtx); |
||||
return -1; |
||||
} |
||||
|
||||
if (elapsed_ms < 500) { |
||||
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; |
||||
} |
||||
|
||||
int16_t* final_pcm = NULL; |
||||
size_t final_pcm_count = 0; |
||||
|
||||
if (g_rec->compressor && g_rec->compressor_enabled) { |
||||
audio_compressor_flush(g_rec->compressor); |
||||
final_pcm = (int16_t*)audio_compressor_output(g_rec->compressor); |
||||
final_pcm_count = audio_compressor_output_size(g_rec->compressor); |
||||
} else { |
||||
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); |
||||
{ |
||||
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] = '\0'; mkdir(tmp, 0755); } |
||||
} |
||||
mkdir(tmp, 0755); |
||||
} |
||||
|
||||
/* 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", temp_path); |
||||
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: %d frames (%.1fs) dur=%lldms compressor=%d", |
||||
frame_count, duration_sec, (long long)elapsed_ms, g_rec->compressor_enabled); |
||||
|
||||
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; |
||||
} |
||||
|
||||
/* 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); |
||||
|
||||
/* 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); |
||||
|
||||
/* build and post chat_msg_submit */ |
||||
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + 1); |
||||
if (!req) { voice_cleanup_locked(g_rec); pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; } |
||||
|
||||
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; |
||||
req->data = (uint8_t*)(req + 1); |
||||
req->data_len = 0; |
||||
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); |
||||
|
||||
uasync_post(ua, chat_core_submit_trampoline, req); |
||||
|
||||
voice_cleanup_locked(g_rec); |
||||
pthread_mutex_unlock(&g_rec->mtx); |
||||
pthread_mutex_unlock(&g_init_mtx); |
||||
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 (%d kbps)", preset, s_bitrates[preset] / 1000); |
||||
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; |
||||
} |
||||
@ -0,0 +1,31 @@
|
||||
#ifndef VOICE_RECORDER_H |
||||
#define VOICE_RECORDER_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
struct voice_recorder; |
||||
|
||||
int voice_recorder_init(const char* db_path); |
||||
void voice_recorder_deinit(void); |
||||
|
||||
int voice_recorder_start(const char* channel_id, int sample_rate, int channels); |
||||
int voice_recorder_feed(const int16_t* samples, int count); |
||||
int voice_recorder_stop(int* out_duration_ms); |
||||
void voice_recorder_cancel(void); |
||||
int voice_recorder_is_active(void); |
||||
|
||||
void voice_recorder_set_preset(int preset); |
||||
int voice_recorder_get_preset(void); |
||||
void voice_recorder_set_compressor_enabled(int enabled); |
||||
int voice_recorder_is_compressor_enabled(void); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif |
||||
Loading…
Reference in new issue