From 5d74bcd865a9da567f746902c482ae24277bf47a Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 17:33:34 +0100 Subject: [PATCH] Model radio worker I/O faults and partial native capture effects --- .../main/kotlin/android/media/HostTracks.kt | 20 ++++-- .../utun/chat/data/RouterHostEnvironment.kt | 62 +++++++++++++++---- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostTracks.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostTracks.kt index 88a4e323..e81a9fe5 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostTracks.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostTracks.kt @@ -1,6 +1,7 @@ package android.media import android.os.Handler +import java.util.concurrent.LinkedBlockingDeque interface AudioRouting { fun interface OnRoutingChangedListener { fun onRoutingChanged(router: AudioRouting) } @@ -22,8 +23,8 @@ class AudioTrack(val id: Int) : AudioRouting { failures = nextFailures created.add(this) } - var playbackHeadPosition = 0 - private set + private var head = 0 + val playbackHeadPosition: Int get() { access("head"); return head } fun play() { access("play") } var interruptOnStop = false fun stop() { access("stop"); if (interruptOnStop) Thread.currentThread().interrupt() } @@ -36,12 +37,21 @@ class AudioTrack(val id: Int) : AudioRouting { check("release after" !in failures) { "track release failed after effect" } } fun write(samples: ShortArray, offset: Int, count: Int): Int { + onWrite() access("write") check(offset >= 0 && offset + count <= samples.size) - playbackHeadPosition += count / 2 - return count + val result = forcedWriteResult ?: minOf(count, maxWriteSamples) + val write = Write(offset, count, result, samples.copyOfRange(offset, offset + count)) + if (!writes.offerLast(write)) { writes.pollFirst(); writes.offerLast(write) } + if (result > 0) head += result / 2 + return result } - var failures = emptySet() + data class Write(val offset: Int, val count: Int, val result: Int, val pcm: ShortArray) + val writes = LinkedBlockingDeque(64) + @Volatile var onWrite: () -> Unit = {} + @Volatile var maxWriteSamples = Int.MAX_VALUE + @Volatile var forcedWriteResult: Int? = null + @Volatile var failures = emptySet() var acceptPreference = true var autoRoute = true var emitOnPreference = false diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt index 18957cb0..aa3a4a9b 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt @@ -2,6 +2,7 @@ package com.utun.chat.data import java.util.Collections import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.ConcurrentLinkedQueue import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.MutableStateFlow @@ -40,7 +41,12 @@ object NativeLib { @Volatile private var radioGroup = 0L @Volatile private var radioMode = 0 @Volatile private var radioTx = false - var radioFailures = emptySet() + @Volatile var radioFailures = emptySet() + @Volatile var radioCaptureAlive = false + @Volatile var radioPullResult = 0 + val radioPullResults = ConcurrentLinkedQueue() + @Volatile var onRadioFeed: (Long) -> Unit = {} + val radioFeeds: MutableList = Collections.synchronizedList(mutableListOf()) fun radioSetActive(group: Long, active: Boolean) { radioCommands.add(RadioCommand("active=$active", group)) } fun radioConfigure(request: Long, settings: RadioAutoSettings): Boolean { radioRequest = request; return settings.valid() } fun radioAudioStart(group: Long, channels: Int): Boolean { @@ -50,19 +56,51 @@ object NativeLib { fun radioAudioStop() { radioCommands.add(RadioCommand("audio stop", radioGroup)) check("stop" !in radioFailures) { "native audio stop failed" } + radioTx = false; radioCaptureAlive = false + } + fun radioAudioPull(group: Long, out: ShortArray, playedFrames: Long): Int { + check("pull" !in radioFailures) { "native pull failed" } + val result = radioPullResults.poll() ?: radioPullResult + if (result in 1..out.size) out.fill(7, 0, result) + return result + } + fun radioAudioFeed(group: Long, samples: ShortArray): Boolean { + onRadioFeed(group) + radioFeeds.add(group) + check("feed" !in radioFailures) { "native feed failed" } + return "feed reject" !in radioFailures && group == radioGroup && radioCaptureAlive && samples.isNotEmpty() + } + fun radioCaptureStart(group: Long, channels: Int, mode: Int): Boolean { + radioCommands.add(RadioCommand("capture start", group)) + if ("capture reject" in radioFailures) return false + radioMode = if ("detector off" in radioFailures) 0 else mode + radioCaptureAlive = true + check("capture start" !in radioFailures) { "native capture start failed after effect" } + return true + } + fun radioCaptureStop() { + radioCommands.add(RadioCommand("capture stop", radioGroup)) + check("capture stop" !in radioFailures) { "native capture stop failed" } + radioTx = false; radioCaptureAlive = false + } + fun radioCaptureDiscontinuity(group: Long) { + radioCommands.add(RadioCommand("boundary", group)) + check("boundary" !in radioFailures) { "native boundary failed" } + } + fun radioAutoMode(): Int { + check("mode" !in radioFailures) { "native mode query failed" } + return if ("bad mode" in radioFailures) 99 else radioMode + } + fun radioTransmitting() = radioTx + fun radioTalkBegin(group: Long) { + radioCommands.add(RadioCommand("begin", group)); radioTx = true + check("begin" !in radioFailures) { "native begin failed after effect" } + } + fun radioTalkEnd(group: Long) { + radioCommands.add(RadioCommand("end", group)) + check("end" !in radioFailures) { "native end failed" } radioTx = false } - fun radioAudioPull(group: Long, out: ShortArray, playedFrames: Long): Int = 0 - fun radioAudioFeed(group: Long, samples: ShortArray): Boolean = group == radioGroup - fun radioCaptureStart(group: Long, channels: Int, mode: Int): Boolean { - radioMode = mode; radioCommands.add(RadioCommand("capture start", group)); return true - } - fun radioCaptureStop() { radioTx = false; radioCommands.add(RadioCommand("capture stop", radioGroup)) } - fun radioCaptureDiscontinuity(group: Long) { radioCommands.add(RadioCommand("boundary", group)) } - fun radioAutoMode() = radioMode - fun radioTransmitting() = radioTx - fun radioTalkBegin(group: Long) { radioTx = true; radioCommands.add(RadioCommand("begin", group)) } - fun radioTalkEnd(group: Long) { radioTx = false; radioCommands.add(RadioCommand("end", group)) } data class LocalInvite(val peer: Long, val request: Long, val link: String) val localInvites = mutableListOf() var sendLocal: (LocalInvite) -> Boolean = { true }