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 = {}) {
stopPlayback()
playing = true
val myGen = playGeneration
playThread = Thread {
val myGen = playGeneration
var handle = 0L
var track: AudioTrack? = null
try {
val info = IntArray(3)
val handle = NativeLib.voiceDecodeOpen(filePath, info)
handle = NativeLib.voiceDecodeOpen(filePath, info)
if (handle == 0L) {
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]
/* skip to startMs by decoding and discarding frames */
val frameSamples = FRAME_SAMPLES * channels
val frameSamples = sampleRate / 50 * channels
val skipFrames = startMs / FRAME_MS
val skipBuf = ShortArray(frameSamples)
var skippedSamples = 0L
for (f in 0 until skipFrames) {
if (myGen != playGeneration) return@Thread
val dec = NativeLib.voiceDecodeRead(handle, skipBuf)
if (dec <= 0) break
skippedSamples += dec
@ -223,43 +226,49 @@ class AudioRecorderManager {
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
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,
AudioFormat.ENCODING_PCM_16BIT, bufSize, AudioTrack.MODE_STREAM)
track.play()
track = output
output.play()
val pcm = ShortArray(frameSamples)
var readTotal = 0L
var read: Int
while (playing) {
while (playing && myGen == playGeneration) {
read = NativeLib.voiceDecodeRead(handle, pcm)
if (read <= 0) break
track.write(pcm, 0, read)
if (myGen != playGeneration) break
output.write(pcm, 0, 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()
onProgress?.invoke(total, eMs)
}
val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100
try { Thread.sleep(drainMs) } catch (_: InterruptedException) {}
track.stop(); track.release()
NativeLib.voiceDecodeClose(handle)
if (myGen == playGeneration) Thread.sleep(drainMs)
} catch (_: InterruptedException) {
LogManager.addLog("DEBUG", "AudioPlayer", "playback cancelled")
} catch (e: Exception) {
LogManager.addLog("ERROR", "AudioPlayer", "play error: ${e.message}")
}
if (playGeneration == myGen) {
playing = false; onComplete()
} finally {
track?.apply {
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() }
}
fun pausePlayback() {
playGeneration++; playing = false
playThread?.join(500); playThread = null
playThread?.interrupt(); playThread = null
}
fun stopPlayback() {
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 <string.h>
#include <stdarg.h>
#include <limits.h>
#include <unistd.h>
#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;
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(&ch, 2, 1, f) != 1) { fclose(f); return NULL; }
if (fread(&fs, 2, 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 ||
(sr != 8000 && sr != 12000 && sr != 16000 && sr != 24000 && sr != 48000) ||
(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);
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));
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->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);
h->total_samples = 0;
for (;;) {
uint16_t plen;
if (fread(&plen, 2, 1, f) != 1 || plen == 0) break;
fseek(f, plen, SEEK_CUR);
uint16_t plen = 0;
if (fread(&plen, 2, 1, f) != 1) goto invalid_stream;
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;
}
fseek(f, pos, SEEK_SET);
if (fseek(f, pos, SEEK_SET)) goto invalid_stream;
*out_sample_rate = h->sample_rate;
*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",
file_path, h->sample_rate, h->channels, h->frame_samples, h->total_samples);
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) {
@ -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) {
uint16_t plen;
if (fread(&plen, 2, 1, h->file) != 1 || plen == 0) break;
if (plen > sizeof(packet)) { fseek(h->file, plen, SEEK_CUR); break; }
if (fread(packet, 1, plen, h->file) != plen) break;
if (fread(&plen, 2, 1, h->file) != 1 || plen == 0 || plen > 4000 || fread(packet, 1, plen, h->file) != plen) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: stream changed after validation"); return -1;
}
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;
}
h->pcm_consumed += written;

Loading…
Cancel
Save