Browse Source

Validate Android voice streams and keep playback cancellation off UI waits

master
evgeny 2 weeks ago
parent
commit
c863b80917
  1. 43
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt
  2. 37
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

43
tools/chatgui-android/app/src/main/java/com/utun/chat/data/AudioRecorderManager.kt

@ -196,24 +196,27 @@ class AudioRecorderManager {
fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) { fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) {
stopPlayback() stopPlayback()
playing = true playing = true
val myGen = playGeneration
playThread = Thread { playThread = Thread {
val myGen = playGeneration var handle = 0L
var track: AudioTrack? = null
try { try {
val info = IntArray(3) val info = IntArray(3)
val handle = NativeLib.voiceDecodeOpen(filePath, info) handle = NativeLib.voiceDecodeOpen(filePath, info)
if (handle == 0L) { if (handle == 0L) {
LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath") LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath")
playing = false; onComplete(); return@Thread return@Thread
} }
val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2] val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2]
/* skip to startMs by decoding and discarding frames */ /* skip to startMs by decoding and discarding frames */
val frameSamples = FRAME_SAMPLES * channels val frameSamples = sampleRate / 50 * channels
val skipFrames = startMs / FRAME_MS val skipFrames = startMs / FRAME_MS
val skipBuf = ShortArray(frameSamples) val skipBuf = ShortArray(frameSamples)
var skippedSamples = 0L var skippedSamples = 0L
for (f in 0 until skipFrames) { for (f in 0 until skipFrames) {
if (myGen != playGeneration) return@Thread
val dec = NativeLib.voiceDecodeRead(handle, skipBuf) val dec = NativeLib.voiceDecodeRead(handle, skipBuf)
if (dec <= 0) break if (dec <= 0) break
skippedSamples += dec skippedSamples += dec
@ -223,43 +226,49 @@ class AudioRecorderManager {
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO, if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT) AudioFormat.ENCODING_PCM_16BIT)
val track = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, val output = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO, if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufSize, AudioTrack.MODE_STREAM) AudioFormat.ENCODING_PCM_16BIT, bufSize, AudioTrack.MODE_STREAM)
track.play() track = output
output.play()
val pcm = ShortArray(frameSamples) val pcm = ShortArray(frameSamples)
var readTotal = 0L var readTotal = 0L
var read: Int var read: Int
while (playing) { while (playing && myGen == playGeneration) {
read = NativeLib.voiceDecodeRead(handle, pcm) read = NativeLib.voiceDecodeRead(handle, pcm)
if (read <= 0) break if (read <= 0) break
track.write(pcm, 0, read) if (myGen != playGeneration) break
output.write(pcm, 0, read)
readTotal += read readTotal += read
val total = (skippedSamples + readTotal).toFloat() / (totalSamples * channels).coerceAtLeast(1).toFloat() val total = (skippedSamples + readTotal).toFloat() / totalSamples.coerceAtLeast(1).toFloat()
val eMs = startMs + (readTotal * FRAME_MS / frameSamples).toInt() val eMs = startMs + (readTotal * FRAME_MS / frameSamples).toInt()
onProgress?.invoke(total, eMs) onProgress?.invoke(total, eMs)
} }
val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100 val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100
try { Thread.sleep(drainMs) } catch (_: InterruptedException) {} if (myGen == playGeneration) Thread.sleep(drainMs)
track.stop(); track.release() } catch (_: InterruptedException) {
NativeLib.voiceDecodeClose(handle) LogManager.addLog("DEBUG", "AudioPlayer", "playback cancelled")
} catch (e: Exception) { } catch (e: Exception) {
LogManager.addLog("ERROR", "AudioPlayer", "play error: ${e.message}") LogManager.addLog("ERROR", "AudioPlayer", "play error: ${e.message}")
} } finally {
if (playGeneration == myGen) { track?.apply {
playing = false; onComplete() try { stop() } catch (e: Exception) { LogManager.addLog("WARN", "AudioPlayer", "stop failed: ${e.message}") }
release()
}
if (handle != 0L) NativeLib.voiceDecodeClose(handle)
if (playGeneration == myGen) { playing = false; onComplete() }
} }
}.apply { start() } }.apply { start() }
} }
fun pausePlayback() { fun pausePlayback() {
playGeneration++; playing = false playGeneration++; playing = false
playThread?.join(500); playThread = null playThread?.interrupt(); playThread = null
} }
fun stopPlayback() { fun stopPlayback() {
playGeneration++; playing = false playGeneration++; playing = false
playThread?.join(500); playThread = null playThread?.interrupt(); playThread = null
} }
} }

37
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -38,6 +38,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdarg.h> #include <stdarg.h>
#include <limits.h>
#include <unistd.h> #include <unistd.h>
#ifdef __ANDROID__ #ifdef __ANDROID__
@ -1371,15 +1372,18 @@ void* utun_bridge_voice_decode_open(const char* file_path,
uint32_t magic = 0; uint32_t sr = 0; uint16_t ch = 0, fs = 0; uint32_t magic = 0; uint32_t sr = 0; uint16_t ch = 0, fs = 0;
if (fread(&magic, 4, 1, f) != 1 || magic != OPUS_MAGIC_D) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: bad magic"); fclose(f); return NULL; } if (fread(&magic, 4, 1, f) != 1 || magic != OPUS_MAGIC_D) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: bad magic"); fclose(f); return NULL; }
if (fread(&sr, 4, 1, f) != 1) { fclose(f); return NULL; } if (fread(&sr, 4, 1, f) != 1 || fread(&ch, 2, 1, f) != 1 || fread(&fs, 2, 1, f) != 1 ||
if (fread(&ch, 2, 1, f) != 1) { fclose(f); return NULL; } (sr != 8000 && sr != 12000 && sr != 16000 && sr != 24000 && sr != 48000) ||
if (fread(&fs, 2, 1, f) != 1) { fclose(f); return NULL; } (ch != 1 && ch != 2) || fs != sr / 50) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid header rate=%u channels=%u frame=%u", sr, ch, fs);
fclose(f); return NULL;
}
opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch); opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch);
if (!dec) { fclose(f); return NULL; } if (!dec) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: decoder allocation failed"); fclose(f); return NULL; }
struct voice_decode_handle* h = u_calloc(1, sizeof(*h)); struct voice_decode_handle* h = u_calloc(1, sizeof(*h));
if (!h) { opus_codec_decoder_destroy(dec); fclose(f); return NULL; } if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: handle allocation failed"); opus_codec_decoder_destroy(dec); fclose(f); return NULL; }
h->dec = dec; h->file = f; h->dec = dec; h->file = f;
h->sample_rate = (int)sr; h->channels = (int)ch; h->frame_samples = (int)fs; h->sample_rate = (int)sr; h->channels = (int)ch; h->frame_samples = (int)fs;
@ -1388,12 +1392,16 @@ void* utun_bridge_voice_decode_open(const char* file_path,
long pos = ftell(f); long pos = ftell(f);
h->total_samples = 0; h->total_samples = 0;
for (;;) { for (;;) {
uint16_t plen; uint16_t plen = 0;
if (fread(&plen, 2, 1, f) != 1 || plen == 0) break; if (fread(&plen, 2, 1, f) != 1) goto invalid_stream;
fseek(f, plen, SEEK_CUR); if (!plen) {
if (!h->total_samples || fgetc(f) != EOF || ferror(f)) goto invalid_stream;
break;
}
if (plen > 4000 || h->total_samples > INT_MAX - (int)fs * ch || fseek(f, plen, SEEK_CUR)) goto invalid_stream;
h->total_samples += (int)fs * (int)ch; h->total_samples += (int)fs * (int)ch;
} }
fseek(f, pos, SEEK_SET); if (fseek(f, pos, SEEK_SET)) goto invalid_stream;
*out_sample_rate = h->sample_rate; *out_sample_rate = h->sample_rate;
*out_channels = h->channels; *out_channels = h->channels;
@ -1402,6 +1410,9 @@ void* utun_bridge_voice_decode_open(const char* file_path,
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_decode_open: %s rate=%d ch=%d fs=%d total=%d", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_decode_open: %s rate=%d ch=%d fs=%d total=%d",
file_path, h->sample_rate, h->channels, h->frame_samples, h->total_samples); file_path, h->sample_rate, h->channels, h->frame_samples, h->total_samples);
return h; return h;
invalid_stream:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: truncated/invalid stream path=%s samples=%d", file_path, h->total_samples);
utun_bridge_voice_decode_close(h); return NULL;
} }
int utun_bridge_voice_decode_read(void* handle, int16_t* buf, int max_samples) { int utun_bridge_voice_decode_read(void* handle, int16_t* buf, int max_samples) {
@ -1414,12 +1425,12 @@ int utun_bridge_voice_decode_read(void* handle, int16_t* buf, int max_samples) {
while (written + h->frame_samples * h->channels <= max_samples) { while (written + h->frame_samples * h->channels <= max_samples) {
uint16_t plen; uint16_t plen;
if (fread(&plen, 2, 1, h->file) != 1 || plen == 0) break; if (fread(&plen, 2, 1, h->file) != 1 || plen == 0 || plen > 4000 || fread(packet, 1, plen, h->file) != plen) {
if (plen > sizeof(packet)) { fseek(h->file, plen, SEEK_CUR); break; } DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: stream changed after validation"); return -1;
if (fread(packet, 1, plen, h->file) != plen) break; }
int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples); int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples);
if (decoded < 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_decode_read: error %d", decoded); break; } if (decoded != h->frame_samples) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid packet result=%d", decoded); return -1; }
written += decoded * h->channels; written += decoded * h->channels;
} }
h->pcm_consumed += written; h->pcm_consumed += written;

Loading…
Cancel
Save