diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostAudio.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostAudio.kt index 81d4e3d6..e632dd1e 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostAudio.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/media/HostAudio.kt @@ -20,6 +20,7 @@ class AudioDeviceInfo(val id: Int, val type: Int, val isSink: Boolean, val isSou class AudioManager { companion object { const val GET_DEVICES_ALL = 3 + const val GET_DEVICES_OUTPUTS = 2 const val AUDIO_SESSION_ID_GENERATE = 0 const val RINGER_MODE_SILENT = 0; const val RINGER_MODE_VIBRATE = 1; const val RINGER_MODE_NORMAL = 2 const val STREAM_RING = 2 @@ -68,6 +69,14 @@ class AudioManager { var mode = 0 set(value) { operation("mode=$value"); field = value; operation("mode after=$value") } val callbacks = mutableSetOf() + fun interface OnCommunicationDeviceChangedListener { fun onCommunicationDeviceChanged(device: AudioDeviceInfo?) } + val communicationListeners = mutableSetOf() + fun addOnCommunicationDeviceChangedListener(executor: java.util.concurrent.Executor, listener: OnCommunicationDeviceChangedListener) { + operation("communication add"); communicationListeners.add(listener); operation("communication add after") + } + fun removeOnCommunicationDeviceChangedListener(listener: OnCommunicationDeviceChangedListener) { + operation("communication remove"); communicationListeners.remove(listener); operation("communication remove after") + } val callbackHistory = mutableListOf() var devices = arrayOf(AudioDeviceInfo(1, 2, true, false, "speaker"), AudioDeviceInfo(2, 15, false, true, "mic")) var communicationDevices: List? = null @@ -89,7 +98,10 @@ class AudioManager { callbacks.remove(callback) // Android допускает отсутствие такой регистрации. operation("unregister after") } - fun getDevices(flags: Int): Array { operation("devices"); check(flags == GET_DEVICES_ALL); return devices.copyOf() } + fun getDevices(flags: Int): Array { + operation("devices"); check(flags == GET_DEVICES_ALL || flags == GET_DEVICES_OUTPUTS) + return devices.filter { flags == GET_DEVICES_ALL || it.isSink }.toTypedArray() + } fun setCommunicationDevice(device: AudioDeviceInfo): Boolean { operation("select=${device.id}") if (!acceptCommunicationDevice || device !in availableCommunicationDevices) return false 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 e81a9fe5..505282b5 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 @@ -11,6 +11,8 @@ interface AudioRouting { class AudioTrack(val id: Int) : AudioRouting { companion object { const val MODE_STREAM = 1 + const val STATE_INITIALIZED = 1; const val PLAYSTATE_STOPPED = 1; const val PLAYSTATE_PLAYING = 3 + const val WRITE_NON_BLOCKING = 1; const val ERROR_DEAD_OBJECT = -6; const val PERFORMANCE_MODE_LOW_LATENCY = 1 private var nextId = 100 val created = mutableListOf() var nextFailures = emptySet() @@ -24,8 +26,22 @@ class AudioTrack(val id: Int) : AudioRouting { created.add(this) } private var head = 0 + val state: Int get() { access("state"); return if ("uninitialized" in failures) 0 else STATE_INITIALIZED } + var playState = PLAYSTATE_STOPPED + private set + var bufferSizeInFrames = 1920 + private set + val bufferCapacityInFrames = 3840 + val underrunCount = 0 + val performanceMode = PERFORMANCE_MODE_LOW_LATENCY + var startThresholdInFrames = 1920 + private set val playbackHeadPosition: Int get() { access("head"); return head } - fun play() { access("play") } + fun play() { access("play"); playState = PLAYSTATE_PLAYING } + fun pause() { access("pause"); playState = PLAYSTATE_STOPPED } + fun setBufferSizeInFrames(value: Int): Int { access("buffer"); bufferSizeInFrames = value; return value } + fun setStartThresholdInFrames(value: Int): Int { access("threshold"); startThresholdInFrames = value; return value } + fun getTimestamp(stamp: AudioTimestamp): Boolean { access("timestamp"); return false } var interruptOnStop = false fun stop() { access("stop"); if (interruptOnStop) Thread.currentThread().interrupt() } var releaseCalls = 0 @@ -46,6 +62,11 @@ class AudioTrack(val id: Int) : AudioRouting { if (result > 0) head += result / 2 return result } + fun write(samples: ShortArray, offset: Int, count: Int, mode: Int): Int { + check(mode == WRITE_NON_BLOCKING) + Thread.sleep(2) + return write(samples, offset, count) + } data class Write(val offset: Int, val count: Int, val result: Int, val pcm: ShortArray) val writes = LinkedBlockingDeque(64) @Volatile var onWrite: () -> Unit = {} @@ -87,15 +108,78 @@ class AudioTrack(val id: Int) : AudioRouting { return true } fun deliver(index: Int) { history[index].onRoutingChanged(this) } + class Builder { + private var size = 3840 + fun setAudioAttributes(value: AudioAttributes) = apply {} + fun setAudioFormat(value: AudioFormat) = apply {} + fun setBufferSizeInBytes(value: Int) = apply { size = value } + fun setTransferMode(value: Int) = apply { check(value == MODE_STREAM) } + fun setPerformanceMode(value: Int) = apply { check(value == PERFORMANCE_MODE_LOW_LATENCY) } + fun build() = AudioTrack(AudioAttributes.Builder().build(), AudioFormat(), size, MODE_STREAM, 0) + } } /** Конфигурация stereo PCM для настоящего RadioAudioEngine, без устройства вывода. */ class AudioFormat { - companion object { const val CHANNEL_OUT_STEREO = 12; const val ENCODING_PCM_16BIT = 2 } + companion object { + const val CHANNEL_OUT_STEREO = 12; const val CHANNEL_OUT_MONO = 4; const val CHANNEL_IN_MONO = 16 + const val ENCODING_PCM_16BIT = 2 + } class Builder { fun setSampleRate(value: Int) = apply { check(value == 48000) } fun setEncoding(value: Int) = apply { check(value == ENCODING_PCM_16BIT) } - fun setChannelMask(value: Int) = apply { check(value == CHANNEL_OUT_STEREO) } + fun setChannelMask(value: Int) = apply { check(value == CHANNEL_OUT_STEREO || value == CHANNEL_OUT_MONO) } fun build() = AudioFormat() } } + +class AudioTimestamp { + companion object { const val TIMEBASE_MONOTONIC = 0 } + var framePosition = 0L + var nanoTime = 0L +} + +object MediaRecorder { object AudioSource { const val VOICE_COMMUNICATION = 7 } } + +/** Настоящий capture worker блокируется в read до кадра/stop; отказы до/после release независимы. */ +class AudioRecord(source: Int, rate: Int, channels: Int, encoding: Int, size: Int) : AudioRouting { + companion object { + const val STATE_INITIALIZED = 1; const val RECORDSTATE_RECORDING = 3; const val READ_BLOCKING = 0; const val SUCCESS = 0 + val created = mutableListOf() + var nextFailures = emptySet() + fun getMinBufferSize(rate: Int, channels: Int, encoding: Int) = 1920 + } + @Volatile var failures = nextFailures + @Volatile var released = false + var releaseCalls = 0 + private set + private val monitor = Object() + val state: Int get() { access("state"); return if ("uninitialized" in failures) 0 else STATE_INITIALIZED } + @Volatile var recordingState = 1 + private set + var preferredDevice: AudioDeviceInfo? = null + private set + val routedDevice: AudioDeviceInfo? get() = preferredDevice + val listeners = mutableSetOf() + init { check("create" !in failures); created.add(this) } + private fun access(operation: String) { check(!released && operation !in failures) { "record failed: $operation" } } + fun startRecording() { access("start"); recordingState = RECORDSTATE_RECORDING } + fun stop() { access("stop"); synchronized(monitor) { recordingState = 1; monitor.notifyAll() } } + fun release() { + releaseCalls++ + check("release" !in failures) { "record release failed" } + released = true; synchronized(monitor) { recordingState = 1; monitor.notifyAll() } + check("release after" !in failures) { "record release failed after effect" } + } + fun read(out: ShortArray, offset: Int, count: Int, mode: Int): Int { + access("read"); check(mode == READ_BLOCKING) + synchronized(monitor) { if (recordingState == RECORDSTATE_RECORDING) monitor.wait(20) } + if (recordingState != RECORDSTATE_RECORDING) return -3 + out.fill(1, offset, offset + count); return count + } + fun getTimestamp(stamp: AudioTimestamp, base: Int): Int { access("timestamp"); return -3 } + fun setPreferredDevice(device: AudioDeviceInfo?): Boolean { access("prefer"); preferredDevice = device; return true } + fun addOnRoutingChangedListener(listener: AudioRouting.OnRoutingChangedListener, handler: Handler) { + access("add listener"); listeners.add(listener); access("add listener after") + } +}