Browse Source

Compress stream audio before Opus using radio settings

master
evgeny 7 days ago
parent
commit
6bd14c53b5
  1. 9
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  2. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  3. 57
      tools/chatgui-android/jni_bridge/android_stream_jni.c

9
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt

@ -12,6 +12,7 @@ import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.net.Inet4Address import java.net.Inet4Address
import java.net.Inet6Address import java.net.Inet6Address
@ -34,6 +35,8 @@ data class VoiceSettings(
val targetLevelDb: Int = -3 val targetLevelDb: Int = -3
) )
data class RadioCompressorSettings(val enabled: Boolean = true, val maxGainDb: Int = 15, val riseRate: Int = 6)
class ConfigProvider(private val context: Context) { class ConfigProvider(private val context: Context) {
private val nodePrivateKey = stringPreferencesKey("node_private_key") private val nodePrivateKey = stringPreferencesKey("node_private_key")
private val nodePublicKey = stringPreferencesKey("node_public_key") private val nodePublicKey = stringPreferencesKey("node_public_key")
@ -88,6 +91,12 @@ class ConfigProvider(private val context: Context) {
private val radioCompressorEnabledKey = intPreferencesKey("radio_compressor_enabled") private val radioCompressorEnabledKey = intPreferencesKey("radio_compressor_enabled")
private val radioCompressorMaxGainDbKey = intPreferencesKey("radio_compressor_max_gain_db") private val radioCompressorMaxGainDbKey = intPreferencesKey("radio_compressor_max_gain_db")
// Трансляция наблюдает те же настройки TX, что и рация; история PCM у каждого владельца своя.
val radioCompressorSettings: Flow<RadioCompressorSettings> = context.dataStore.data.map { prefs ->
RadioCompressorSettings((prefs[radioCompressorEnabledKey] ?: 1) != 0,
prefs[radioCompressorMaxGainDbKey] ?: 15, prefs[chatCompressorRiseRate] ?: 6)
}.distinctUntilChanged()
private fun keysFilePath(): String { private fun keysFilePath(): String {
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
return "$dir/keys.conf" return "$dir/keys.conf"

5
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -209,7 +209,8 @@ object NativeLib {
fun streamStop(id: Long) { nativeStreamStop(id) } fun streamStop(id: Long) { nativeStreamStop(id) }
fun streamPull(id: Long): ByteArray? = nativeStreamPull(id) fun streamPull(id: Long): ByteArray? = nativeStreamPull(id)
fun streamOpusCreate(encode: Boolean): Long = nativeStreamOpusCreate(encode) fun streamOpusCreate(encode: Boolean): Long = nativeStreamOpusCreate(encode)
fun streamOpusEncode(handle: Long, pcm: ShortArray): ByteArray? = nativeStreamOpusEncode(handle, pcm) fun streamOpusEncode(handle: Long, pcm: ShortArray, compressor: RadioCompressorSettings): ByteArray? =
nativeStreamOpusEncode(handle, pcm, compressor.enabled, compressor.maxGainDb, compressor.riseRate)
fun streamOpusDecode(handle: Long, packet: ByteArray): ShortArray? = nativeStreamOpusDecode(handle, packet) fun streamOpusDecode(handle: Long, packet: ByteArray): ShortArray? = nativeStreamOpusDecode(handle, packet)
fun streamOpusClose(handle: Long) { nativeStreamOpusClose(handle) } fun streamOpusClose(handle: Long) { nativeStreamOpusClose(handle) }
private external fun nativeStreamOpen(id: Long, group: Long, source: Long, width: Int, height: Int, rotation: Int, private external fun nativeStreamOpen(id: Long, group: Long, source: Long, width: Int, height: Int, rotation: Int,
@ -220,7 +221,7 @@ object NativeLib {
private external fun nativeStreamStop(id: Long) private external fun nativeStreamStop(id: Long)
private external fun nativeStreamPull(id: Long): ByteArray? private external fun nativeStreamPull(id: Long): ByteArray?
private external fun nativeStreamOpusCreate(encode: Boolean): Long private external fun nativeStreamOpusCreate(encode: Boolean): Long
private external fun nativeStreamOpusEncode(handle: Long, pcm: ShortArray): ByteArray? private external fun nativeStreamOpusEncode(handle: Long, pcm: ShortArray, compressor: Boolean, maxGainDb: Int, riseRate: Int): ByteArray?
private external fun nativeStreamOpusDecode(handle: Long, packet: ByteArray): ShortArray? private external fun nativeStreamOpusDecode(handle: Long, packet: ByteArray): ShortArray?
private external fun nativeStreamOpusClose(handle: Long) private external fun nativeStreamOpusClose(handle: Long)

57
tools/chatgui-android/jni_bridge/android_stream_jni.c

@ -2,14 +2,23 @@
#ifdef __ANDROID__ #ifdef __ANDROID__
#include <jni.h> #include <jni.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h>
#include "../../../lib/debug_config.h" #include "../../../lib/debug_config.h"
#include "../../../lib/mem.h" #include "../../../lib/mem.h"
#include "../../../lib/opus_codec.h" #include "../../../lib/opus_codec.h"
#include "../../../lib/audio_compressor.h"
#include "../libutun_lite/stream_bridge.h" #include "../libutun_lite/stream_bridge.h"
#include "../../../src/stream/stream_media.h" #include "../../../src/stream/stream_media.h"
struct android_stream_opus { opus_codec_encoder_t *encoder; opus_codec_decoder_t *decoder; }; struct android_stream_opus {
opus_codec_encoder_t *encoder;
opus_codec_decoder_t *decoder;
struct audio_compressor *compressor;
int compressor_enabled, max_gain_db, rise_rate;
uint64_t frames;
uint32_t reported_clips;
};
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpen( JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpen(
JNIEnv *env, jobject obj, jlong id, jlong group, jlong source, jint width, jint height, jint rotation, jboolean audio, jlong epoch) { JNIEnv *env, jobject obj, jlong id, jlong group, jlong source, jint width, jint height, jint rotation, jboolean audio, jlong epoch) {
@ -61,29 +70,65 @@ JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusCreate
codec->encoder = opus_codec_encoder_create(48000, 1); codec->encoder = opus_codec_encoder_create(48000, 1);
if (!codec->encoder || opus_codec_encoder_application_set(codec->encoder, OPUS_CODEC_APP_AUDIO) < 0 if (!codec->encoder || opus_codec_encoder_application_set(codec->encoder, OPUS_CODEC_APP_AUDIO) < 0
|| opus_codec_encoder_bitrate_set(codec->encoder, 64000) < 0) goto fail; || opus_codec_encoder_bitrate_set(codec->encoder, 64000) < 0) goto fail;
codec->compressor = audio_compressor_create();
if (!codec->compressor) goto fail;
} else { } else {
codec->decoder = opus_codec_decoder_create(48000, 1); codec->decoder = opus_codec_decoder_create(48000, 1);
if (!codec->decoder) goto fail; if (!codec->decoder) goto fail;
} }
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus created encode=%d rate=48000 channels=1 frame=960", encode); DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus created codec=%p encode=%d rate=48000 channels=1 frame=960", (void *)codec, encode);
return (jlong)(intptr_t)codec; return (jlong)(intptr_t)codec;
fail: fail:
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus initialization failed encode=%d", encode); DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus initialization failed encode=%d", encode);
audio_compressor_destroy(codec->compressor);
opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec); return 0; opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec); return 0;
} }
JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusEncode( JNIEXPORT jbyteArray JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusEncode(
JNIEnv *env, jobject obj, jlong handle, jshortArray samples) { JNIEnv *env, jobject obj, jlong handle, jshortArray samples, jboolean enabled, jint max_gain_db, jint rise_rate) {
(void)obj; (void)obj;
struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle; struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle;
if (!codec || !codec->encoder || !samples || (*env)->GetArrayLength(env, samples) != 960) { if (!codec || !codec->encoder || !codec->compressor || !samples || (*env)->GetArrayLength(env, samples) != 960) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode invalid handle/frame"); return NULL; DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode invalid handle/frame"); return NULL;
} }
if (max_gain_db < 1 || max_gain_db > 60 || rise_rate < 1 || rise_rate > 100) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor invalid max_gain=%d rise=%d", max_gain_db, rise_rate); return NULL;
}
if (codec->compressor_enabled != !!enabled || codec->max_gain_db != max_gain_db || codec->rise_rate != rise_rate) {
audio_compressor_config_t config = {0};
config.sample_rate = 48000; config.channels = 1; config.block_duration_ms = 20;
config.lookback_ms = 200; config.lookahead_ms = 0; config.target_level = 0.25f;
config.max_gain_db = max_gain_db; config.rise_rate_per_sec = rise_rate; config.release_rate_per_sec = rise_rate * 5.0f;
if (audio_compressor_configure(codec->compressor, &config) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor configuration failed codec=%p", (void *)codec); return NULL;
}
audio_compressor_set_enabled(codec->compressor, enabled);
codec->compressor_enabled = !!enabled; codec->max_gain_db = max_gain_db; codec->rise_rate = rise_rate; codec->reported_clips = 0;
DEBUG_INFO(DEBUG_CATEGORY_STREAM,
"JNI stream compressor applied codec=%p enabled=%d max_gain=%ddB rise=%d release=%d target=0.25 history=200ms delay=0ms",
(void *)codec, !!enabled, max_gain_db, rise_rate, rise_rate * 5);
}
int16_t pcm[960]; uint8_t packet[1275]; int16_t pcm[960]; uint8_t packet[1275];
(*env)->GetShortArrayRegion(env, samples, 0, 960, pcm); (*env)->GetShortArrayRegion(env, samples, 0, 960, pcm);
if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream PCM read failed"); return NULL; } if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream PCM read failed"); return NULL; }
int report = codec->frames % 50 == 0, input_peak = 0, output_peak = 0;
if (report) for (unsigned i = 0; i < 960; i++) if (abs((int)pcm[i]) > input_peak) input_peak = abs((int)pcm[i]);
if (audio_compressor_process_frame(codec->compressor, pcm, pcm, 960) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream compressor processing failed codec=%p", (void *)codec); return NULL;
}
int length = opus_codec_encode(codec->encoder, pcm, 960, packet, sizeof(packet)); int length = opus_codec_encode(codec->encoder, pcm, 960, packet, sizeof(packet));
if (length < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode failed result=%d", length); return NULL; } if (length < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus encode failed result=%d", length); return NULL; }
codec->frames++;
if (report) {
for (unsigned i = 0; i < 960; i++) if (abs((int)pcm[i]) > output_peak) output_peak = abs((int)pcm[i]);
uint32_t clips = audio_compressor_clip_count(codec->compressor);
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "JNI stream audio codec=%p frames=%llu enabled=%d gain=%.1fdB peaks=%d->%d clips=%u packet=%d",
(void *)codec, (unsigned long long)codec->frames, codec->compressor_enabled,
audio_compressor_gain_db(codec->compressor), input_peak, output_peak, clips, length);
if (clips != codec->reported_clips) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "JNI stream audio clipped codec=%p samples=%u gain=%.1fdB",
(void *)codec, clips - codec->reported_clips, audio_compressor_gain_db(codec->compressor));
codec->reported_clips = clips;
}
jbyteArray output = (*env)->NewByteArray(env, length); jbyteArray output = (*env)->NewByteArray(env, length);
if (output) (*env)->SetByteArrayRegion(env, output, 0, length, (const jbyte *)packet); if (output) (*env)->SetByteArrayRegion(env, output, 0, length, (const jbyte *)packet);
else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus output allocation failed"); else DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "JNI stream Opus output allocation failed");
@ -112,7 +157,9 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStreamOpusClose(J
(void)env; (void)obj; (void)env; (void)obj;
struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle; struct android_stream_opus *codec = (struct android_stream_opus *)(intptr_t)handle;
if (!codec) return; if (!codec) return;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus release encode=%d", codec->encoder != NULL); DEBUG_INFO(DEBUG_CATEGORY_STREAM, "JNI stream Opus release codec=%p encode=%d frames=%llu", (void *)codec,
codec->encoder != NULL, (unsigned long long)codec->frames);
audio_compressor_destroy(codec->compressor);
opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec); opus_codec_encoder_destroy(codec->encoder); opus_codec_decoder_destroy(codec->decoder); u_free(codec);
} }
#endif #endif

Loading…
Cancel
Save