Model call audio hardware failures and blocking capture on the JVM
This commit is contained in:
1 parent
e857b9de75
commit
7304647baa
2 files changed
+100
-4
No files matched your search
@@ -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<AudioDeviceCallback>()
|
||||
fun interface OnCommunicationDeviceChangedListener { fun onCommunicationDeviceChanged(device: AudioDeviceInfo?) }
|
||||
val communicationListeners = mutableSetOf<OnCommunicationDeviceChangedListener>()
|
||||
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<AudioDeviceCallback>()
|
||||
var devices = arrayOf(AudioDeviceInfo(1, 2, true, false, "speaker"), AudioDeviceInfo(2, 15, false, true, "mic"))
|
||||
var communicationDevices: List<AudioDeviceInfo>? = null
|
||||
@@ -89,7 +98,10 @@ class AudioManager {
|
||||
callbacks.remove(callback) // Android допускает отсутствие такой регистрации.
|
||||
operation("unregister after")
|
||||
}
|
||||
fun getDevices(flags: Int): Array<AudioDeviceInfo> { operation("devices"); check(flags == GET_DEVICES_ALL); return devices.copyOf() }
|
||||
fun getDevices(flags: Int): Array<AudioDeviceInfo> {
|
||||
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
|
||||
|
||||
@@ -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<AudioTrack>()
|
||||
var nextFailures = emptySet<String>()
|
||||
@@ -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<Write>(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<AudioRecord>()
|
||||
var nextFailures = emptySet<String>()
|
||||
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<AudioRouting.OnRoutingChangedListener>()
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user