Browse Source

Add bounded AAudio voice output with AudioTrack fallback

master
evgeny 3 days ago
parent
commit
075ffc4d3b
  1. 3
      tools/chatgui-android/app/src/main/cpp/CMakeLists.txt
  2. 104
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt
  3. 159
      tools/chatgui-android/jni_bridge/android_call_output_jni.c

3
tools/chatgui-android/app/src/main/cpp/CMakeLists.txt

@ -76,6 +76,7 @@ add_library(utun SHARED
${JNI_DIR}/android_jni_bridge.c
${JNI_DIR}/android_stream_jni.c
${JNI_DIR}/android_call_video_jni.c
${JNI_DIR}/android_call_output_jni.c
${JNI_DIR}/android_udp_log.c
$<TARGET_OBJECTS:utun_core>
)
@ -87,4 +88,4 @@ target_compile_definitions(utun PRIVATE ${UTUN_DEFINES} __ANDROID__)
set_target_properties(utun PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
find_library(log-lib log)
target_link_libraries(utun ${log-lib} openssl_crypto utun_voice onnxruntime)
target_link_libraries(utun ${log-lib} openssl_crypto utun_voice onnxruntime aaudio dl)

104
tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioOutput.kt

@ -0,0 +1,104 @@
package com.utun.chat.data
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRouting
import android.media.AudioTimestamp
import android.media.AudioTrack
import android.os.Build
import android.os.Handler
/** Один выход без промежуточной PCM-очереди. I/O и close сериализованы; владелец — playback worker. */
internal class CallAudioOutput private constructor(
val track: AudioTrack?, private var handle: Long, private val manager: AudioManager
) {
val fast = handle != 0L
val name = if (fast) "AAudio" else "AudioTrack"
private val sample = LongArray(10)
private var accepted = 0L
private var closed = false
val playState: Int get() = track?.playState ?: if (closed) AudioTrack.PLAYSTATE_STOPPED else AudioTrack.PLAYSTATE_PLAYING
val bufferSizeInFrames: Int get() = track?.bufferSizeInFrames ?: sample[4].toInt()
val bufferCapacityInFrames: Int get() = track?.bufferCapacityInFrames ?: sample[5].toInt()
val underrunCount: Int get() = track?.underrunCount ?: sample[3].toInt()
val performanceMode: Int get() = track?.performanceMode ?: AudioTrack.PERFORMANCE_MODE_LOW_LATENCY
val startThresholdInFrames: Int get() = if (track != null && Build.VERSION.SDK_INT >= 31) track.startThresholdInFrames else bufferSizeInFrames
val routedDevice: AudioDeviceInfo? get() = track?.routedDevice ?:
manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull { it.id.toLong() == sample[6] }
@Synchronized fun play() { check(!closed); track?.play() }
@Synchronized fun pause() { if (!closed) track?.pause() }
@Synchronized fun write(pcm: ShortArray, offset: Int, count: Int): Int {
check(!closed)
val n = track?.write(pcm, offset, count, AudioTrack.WRITE_NON_BLOCKING) ?: CallOutputNative.write(handle, pcm, offset, count)
if (n > 0) accepted += n
return n
}
val playbackHeadPosition: Int @Synchronized get() = track?.playbackHeadPosition ?:
CallOutputNative.head(handle).coerceIn(0, accepted).toInt()
@Synchronized fun getTimestamp(stamp: AudioTimestamp): Boolean {
if (closed) return false
track?.let { return it.getTimestamp(stamp) }
CallOutputNative.sample(handle, sample)
if (sample[1] < 0 || sample[2] < 0) return false
stamp.framePosition = sample[1]; stamp.nanoTime = sample[2]
return true
}
@Synchronized fun setBufferSizeInFrames(frames: Int): Int {
check(!closed)
track?.let { return it.setBufferSizeInFrames(frames) }
val actual = CallOutputNative.buffer(handle, frames)
CallOutputNative.sample(handle, sample)
return actual
}
@Synchronized fun setStartThresholdInFrames(frames: Int): Int =
if (track != null && Build.VERSION.SDK_INT >= 31) track.setStartThresholdInFrames(frames) else frames
@Synchronized fun setPreferredDevice(device: AudioDeviceInfo?): Boolean = track?.setPreferredDevice(device) ?: true
fun addOnRoutingChangedListener(listener: AudioRouting.OnRoutingChangedListener, handler: Handler) {
track?.addOnRoutingChangedListener(listener, handler)
}
fun removeOnRoutingChangedListener(listener: AudioRouting.OnRoutingChangedListener) {
track?.removeOnRoutingChangedListener(listener)
}
@Synchronized fun release() {
if (closed) return
closed = true
if (handle != 0L) { CallOutputNative.close(handle); handle = 0 }
track?.release()
}
companion object {
fun open(manager: AudioManager, deviceId: Int, tryFast: Boolean): CallAudioOutput {
if (tryFast && Build.VERSION.SDK_INT >= 31) {
val handle = CallOutputNative.open(deviceId)
if (handle != 0L) return CallAudioOutput(null, handle, manager).also { CallOutputNative.sample(handle, it.sample) }
}
val min = AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT)
check(min > 0) { "AudioTrack minimum=$min" }
val track = AudioTrack.Builder()
.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build())
.setAudioFormat(AudioFormat.Builder().setSampleRate(48000).setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build())
.setBufferSizeInBytes((min * 2).coerceAtLeast(3840)).setTransferMode(AudioTrack.MODE_STREAM)
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY).build()
if (track.state != AudioTrack.STATE_INITIALIZED) { track.release(); error("AudioTrack not initialized") }
LogManager.addLog("INFO", "CallAudio", "output fallback=AudioTrack device=$deviceId tryFast=$tryFast")
return CallAudioOutput(track, 0, manager)
}
}
}
internal object CallOutputNative {
init { System.loadLibrary("utun") }
external fun open(deviceId: Int): Long
external fun write(handle: Long, pcm: ShortArray, offset: Int, count: Int): Int
external fun head(handle: Long): Long
external fun sample(handle: Long, out: LongArray)
external fun buffer(handle: Long, frames: Int): Int
external fun close(handle: Long)
external fun suspendClock(id: Long, epoch: Long)
}

159
tools/chatgui-android/jni_bridge/android_call_output_jni.c

@ -0,0 +1,159 @@
/* AAudio I/O без дополнительной PCM-очереди. Kotlin сериализует операции одного handle.
* write передаёт по аппаратному периоду: blocking pacing AAudio держит заданный запас,
* NON_BLOCKING способен заполнить всю ёмкость MMAP независимо от рабочего bufferSize. */
#include <aaudio/AAudio.h>
#include <jni.h>
#include <dlfcn.h>
#include <stdint.h>
#include <time.h>
#include "debug_config.h"
#include "mem.h"
#include "call/call_audio.h"
struct call_output {
AAudioStream* stream;
int burst;
int error;
};
static int64_t monotonic_ns(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t)now.tv_sec * 1000000000 + now.tv_nsec;
}
static void output_error(const char* operation, int result) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "AAudio %s result=%d reason=%s", operation, result, AAudio_convertResultToText(result));
}
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_CallOutputNative_open(JNIEnv* env, jobject self, jint device) {
(void)env; (void)self;
/* Речевые атрибуты появились в API 28; libutun загружается также на API 26. */
void (*set_usage)(AAudioStreamBuilder*, aaudio_usage_t) = dlsym(RTLD_DEFAULT, "AAudioStreamBuilder_setUsage");
void (*set_content)(AAudioStreamBuilder*, aaudio_content_type_t) = dlsym(RTLD_DEFAULT, "AAudioStreamBuilder_setContentType");
if (!set_usage || !set_content) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AAudio unavailable: voice attributes missing"); return 0;
}
AAudioStreamBuilder* builder = NULL;
int result = AAudio_createStreamBuilder(&builder);
if (result < 0) { output_error("builder", result); return 0; }
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, 1);
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE);
if (device > 0) AAudioStreamBuilder_setDeviceId(builder, device);
set_usage(builder, AAUDIO_USAGE_VOICE_COMMUNICATION);
set_content(builder, AAUDIO_CONTENT_TYPE_SPEECH);
AAudioStream* stream = NULL;
result = AAudioStreamBuilder_openStream(builder, &stream);
int deleted = AAudioStreamBuilder_delete(builder);
if (deleted < 0) output_error("builder delete", deleted);
if (result < 0) { output_error("open", result); return 0; }
int burst = AAudioStream_getFramesPerBurst(stream);
int performance = AAudioStream_getPerformanceMode(stream);
bool (*is_mmap)(AAudioStream*) = dlsym(RTLD_DEFAULT, "AAudioStream_isMMapUsed");
int accepted = performance == AAUDIO_PERFORMANCE_MODE_LOW_LATENCY && burst > 0 && burst <= 480 &&
AAudioStream_getSampleRate(stream) == 48000 && AAudioStream_getChannelCount(stream) == 1 &&
AAudioStream_getFormat(stream) == AAUDIO_FORMAT_PCM_I16 &&
(device <= 0 || AAudioStream_getDeviceId(stream) == device);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AAudio candidate accepted=%d device=%d requested=%d rate=%d channels=%d "
"performance=%d sharing=%d mmap=%d burst=%d capacity=%d", accepted, AAudioStream_getDeviceId(stream), device,
AAudioStream_getSampleRate(stream), AAudioStream_getChannelCount(stream), performance,
AAudioStream_getSharingMode(stream), is_mmap ? is_mmap(stream) : -1, burst, AAudioStream_getBufferCapacityInFrames(stream));
if (accepted) {
result = AAudioStream_setBufferSizeInFrames(stream, 960);
if (result <= 0) { output_error("buffer", result); accepted = 0; }
}
if (accepted) {
result = AAudioStream_requestStart(stream);
if (result < 0) { output_error("start", result); accepted = 0; }
}
if (!accepted) {
result = AAudioStream_close(stream);
if (result < 0) output_error("close rejected", result);
return 0;
}
struct call_output* output = u_calloc(1, sizeof(*output));
if (!output) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio allocation failed");
result = AAudioStream_close(stream);
if (result < 0) output_error("close allocation failure", result);
return 0;
}
output->stream = stream; output->burst = burst;
return (jlong)(intptr_t)output;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_CallOutputNative_write(
JNIEnv* env, jobject self, jlong handle, jshortArray input, jint offset, jint count) {
(void)self;
struct call_output* output = (struct call_output*)(intptr_t)handle;
if (!output || !input || offset < 0 || count <= 0 || count > 960 || offset > (*env)->GetArrayLength(env, input) - count) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio invalid write offset=%d count=%d", offset, count); return AAUDIO_ERROR_ILLEGAL_ARGUMENT;
}
if (output->error) return output->error;
int16_t pcm[960];
(*env)->GetShortArrayRegion(env, input, offset, count, pcm);
if ((*env)->ExceptionCheck(env)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio JNI PCM copy failed"); return AAUDIO_ERROR_INTERNAL; }
int accepted = 0;
int64_t deadline = monotonic_ns() + 100000000;
while (accepted < count) {
int64_t timeout = deadline - monotonic_ns();
if (timeout <= 0) break;
int n = count - accepted < output->burst ? count - accepted : output->burst;
int result = AAudioStream_write(output->stream, pcm + accepted, n, timeout);
if (result < 0) { output_error("write", result); output->error = result; return accepted ? accepted : result; }
if (!result) break;
accepted += result;
}
return accepted;
}
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_CallOutputNative_head(JNIEnv* env, jobject self, jlong handle) {
(void)env; (void)self;
struct call_output* output = (struct call_output*)(intptr_t)handle;
if (!output) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio head without output"); return -1; }
return AAudioStream_getFramesRead(output->stream);
}
/* head, timestamp frame/ns, underruns, buffer, capacity, device, performance, error, written. */
JNIEXPORT void JNICALL Java_com_utun_chat_data_CallOutputNative_sample(JNIEnv* env, jobject self, jlong handle, jlongArray out) {
(void)self;
struct call_output* output = (struct call_output*)(intptr_t)handle;
if (!output || !out || (*env)->GetArrayLength(env, out) != 10) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio invalid sample"); return;
}
AAudioStream* s = output->stream;
int64_t frame = -1, ns = -1;
int result = AAudioStream_getTimestamp(s, CLOCK_MONOTONIC, &frame, &ns);
if (result < 0) { frame = -1; ns = -1; }
jlong values[10] = {AAudioStream_getFramesRead(s), frame, ns, AAudioStream_getXRunCount(s),
AAudioStream_getBufferSizeInFrames(s), AAudioStream_getBufferCapacityInFrames(s), AAudioStream_getDeviceId(s),
AAudioStream_getPerformanceMode(s), output->error, AAudioStream_getFramesWritten(s)};
(*env)->SetLongArrayRegion(env, out, 0, 10, values);
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_CallOutputNative_buffer(JNIEnv* env, jobject self, jlong handle, jint frames) {
(void)env; (void)self;
struct call_output* output = (struct call_output*)(intptr_t)handle;
if (!output || frames < 960) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AAudio invalid buffer=%d", frames); return -1; }
int result = AAudioStream_setBufferSizeInFrames(output->stream, frames);
if (result < 0) output_error("buffer change", result);
return result;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_CallOutputNative_close(JNIEnv* env, jobject self, jlong handle) {
(void)env; (void)self;
struct call_output* output = (struct call_output*)(intptr_t)handle;
if (!output) return;
int result = AAudioStream_close(output->stream);
if (result < 0) output_error("close", result);
u_free(output);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_CallOutputNative_suspendClock(JNIEnv* env, jobject self, jlong id, jlong epoch) {
(void)env; (void)self;
call_audio_suspend_device(id, epoch);
}
Loading…
Cancel
Save