diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/android/media/HostStreamAudio.kt b/tools/chatgui-android/video-tests/src/main/kotlin/android/media/HostStreamAudio.kt new file mode 100644 index 00000000..b95bc064 --- /dev/null +++ b/tools/chatgui-android/video-tests/src/main/kotlin/android/media/HostStreamAudio.kt @@ -0,0 +1,59 @@ +package android.media + +import java.util.concurrent.CopyOnWriteArrayList + +class AudioAttributes { + companion object { const val USAGE_MEDIA = 1; const val CONTENT_TYPE_MOVIE = 1 } + class Builder { + fun setUsage(value: Int) = this + fun setContentType(value: Int) = this + fun build() = AudioAttributes() + } +} +class AudioFormat { + companion object { const val CHANNEL_OUT_STEREO = 12; const val ENCODING_PCM_16BIT = 2 } + class Builder { + fun setSampleRate(value: Int) = this + fun setChannelMask(value: Int) = this + fun setEncoding(value: Int) = this + fun build() = AudioFormat() + } +} +/** Только JVM: SDK output остаётся наблюдаемым до подтверждённого release. */ +class AudioTrack { + companion object { + const val STATE_INITIALIZED = 1; const val MODE_STREAM = 1; const val WRITE_NON_BLOCKING = 1 + val created = CopyOnWriteArrayList() + var nextFailures = emptySet() + fun getMinBufferSize(rate: Int, channels: Int, encoding: Int) = 3840 + } + class Builder { + fun setAudioAttributes(value: AudioAttributes) = this + fun setAudioFormat(value: AudioFormat) = this + fun setBufferSizeInBytes(value: Int) = this + fun setTransferMode(value: Int) = this + fun build() = AudioTrack().also { created.add(it) } + } + var failures = nextFailures + var released = false + private set + val state get() = STATE_INITIALIZED + var playState = 0 + private set + val bufferSizeInFrames = 960 + var startThresholdInFrames = 1 + private set + var playbackHeadPosition = 0 + private set + val underrunCount = 0 + fun setStartThresholdInFrames(value: Int) { check("threshold" !in failures); startThresholdInFrames = value } + fun write(pcm: ShortArray, offset: Int, count: Int, mode: Int): Int { + check(!released); check("write" !in failures); playbackHeadPosition += count / 2; return count + } + fun pause() { check(!released); playState = 0 } + fun play() { check(!released); check("play" !in failures); playState = 1 } + fun release() { + check("release" !in failures) { "audio release failed" }; released = true + check("release after" !in failures) { "audio release failed after effect" } + } +} diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt b/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt index c30b323b..5e9a4da1 100644 --- a/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt +++ b/tools/chatgui-android/video-tests/src/main/kotlin/android/os/HostVideoOs.kt @@ -63,3 +63,4 @@ object SystemClock { fun elapsedRealtimeNanos(): Long = System.nanoTime() } object Build { object VERSION { const val SDK_INT = 36 } } +object Process { const val THREAD_PRIORITY_AUDIO = -16; fun setThreadPriority(value: Int) {} } diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt new file mode 100644 index 00000000..9ac85446 --- /dev/null +++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/StreamHost.kt @@ -0,0 +1,47 @@ +package com.utun.chat.data + +import android.media.AudioTrack +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicLong +import kotlinx.coroutines.flow.MutableStateFlow + +object CallController { var blocked = false; fun blocksMedia() = blocked } +object StreamController { private val ids = AtomicLong(100); fun nextId() = ids.incrementAndGet() } +object MediaAudioController { + data class State(val switching: Boolean = false) + val state = MutableStateFlow(State()) + var failures = emptySet() + val outputs = ConcurrentHashMap.newKeySet() + fun attachOutput(track: AudioTrack): AutoCloseable { + check("attach" !in failures); outputs.add(track) + return AutoCloseable { check("detach" !in failures); outputs.remove(track) } + } +} + +/** Native-границы viewer/mixer: fault injection до изменения владельца, никаких JNI/DSP/сети. */ +object HostStreams { + val ports = ConcurrentHashMap.newKeySet() + val signals = ConcurrentHashMap.newKeySet() + val playbacks = ConcurrentHashMap.newKeySet() + val records = ConcurrentLinkedQueue() + private val sequence = AtomicLong() + @Volatile var failures = emptySet() + @Volatile var audioEnabled = false + @Volatile var status = false + @Volatile var onWait: () -> Unit = {} + @Volatile var onPull: () -> Unit = {} + fun access(name: String) { check(name !in failures) { "stream $name failed" } } + fun open(id: Long): Boolean { access("open"); ports.add(id); return true } + fun stop(id: Long) { access("stop"); ports.remove(id) } + fun createSignal(): Long { access("signal create"); return sequence.incrementAndGet().also { signals.add(it) } } + fun closeSignal(handle: Long) { access("signal close"); check(signals.remove(handle)) } + fun notifySignal(handle: Long) { access("signal notify"); check(handle in signals) } + fun waitSignal(handle: Long): Int { onWait(); access("signal wait"); check(handle in signals); Thread.sleep(2); return 0 } + fun createPlayback(): Long { access("playback create"); return sequence.incrementAndGet().also { playbacks.add(it) } } + fun closePlayback(handle: Long) { access("playback close"); check(playbacks.remove(handle)) } + fun reset() { + ports.clear(); signals.clear(); playbacks.clear(); records.clear(); failures = emptySet() + audioEnabled = false; status = false; onWait = {}; onPull = {} + } +}