Add controlled native stream and Android audio test boundaries

This commit is contained in:
evgeny committed 2026-10-29 13:34:24 +01:00
1 parent 43a749d8ab
commit 5dffa9917f
3 files changed
+107

No files matched your search

@@ -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<AudioTrack>()
var nextFailures = emptySet<String>()
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" }
}
}
@@ -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) {} }
@@ -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<String>()
val outputs = ConcurrentHashMap.newKeySet<AudioTrack>()
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<Long>()
val signals = ConcurrentHashMap.newKeySet<Long>()
val playbacks = ConcurrentHashMap.newKeySet<Long>()
val records = ConcurrentLinkedQueue<ByteArray>()
private val sequence = AtomicLong()
@Volatile var failures = emptySet<String>()
@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 = {}
}
}