|
|
|
@ -5,17 +5,22 @@ import android.media.AudioFormat |
|
|
|
import android.media.AudioRecord |
|
|
|
import android.media.AudioRecord |
|
|
|
import android.media.AudioTimestamp |
|
|
|
import android.media.AudioTimestamp |
|
|
|
import android.media.MediaRecorder |
|
|
|
import android.media.MediaRecorder |
|
|
|
|
|
|
|
import android.os.Build |
|
|
|
|
|
|
|
|
|
|
|
/** Реестр трансляционного захвата. Рация заимствует его только при совпадении выбранного входа. |
|
|
|
/** Реестр трансляционного захвата. Рация заимствует его только при совпадении выбранного входа. |
|
|
|
* Системный default при отсутствии явного выбора означает тот же вход, который уже использует трансляция. */ |
|
|
|
* Системный default при отсутствии явного выбора означает тот же вход, который уже использует трансляция. */ |
|
|
|
object AndroidMicrophoneCapture { |
|
|
|
object AndroidMicrophoneCapture { |
|
|
|
private var stream: MicrophoneCapture? = null |
|
|
|
private var stream: MicrophoneCapture? = null |
|
|
|
|
|
|
|
private data class Status(val input: Int?, val initialInput: Int?, val silenced: Boolean) |
|
|
|
|
|
|
|
@Volatile private var streamStatus: Status? = null |
|
|
|
|
|
|
|
|
|
|
|
private class Record(private val record: AudioRecord) : MicrophoneCapture.Device { |
|
|
|
private class Record(private val record: AudioRecord, private val isStream: Boolean) : MicrophoneCapture.Device { |
|
|
|
override val channels = record.channelCount |
|
|
|
override val channels = record.channelCount |
|
|
|
override val inputId: Int? get() = record.routedDevice?.id |
|
|
|
override val inputId: Int? get() = record.routedDevice?.id |
|
|
|
private val stamp = AudioTimestamp() |
|
|
|
private val stamp = AudioTimestamp() |
|
|
|
private var total = 0L |
|
|
|
private var total = 0L |
|
|
|
|
|
|
|
private var initialInput: Int? = null |
|
|
|
|
|
|
|
private val preferred = record.preferredDevice?.id |
|
|
|
@Volatile private var running = true |
|
|
|
@Volatile private var running = true |
|
|
|
override fun read(): MicrophoneCapture.Frame { |
|
|
|
override fun read(): MicrophoneCapture.Frame { |
|
|
|
val pcm = ShortArray(960 * channels) |
|
|
|
val pcm = ShortArray(960 * channels) |
|
|
|
@ -26,20 +31,36 @@ object AndroidMicrophoneCapture { |
|
|
|
used += count |
|
|
|
used += count |
|
|
|
} |
|
|
|
} |
|
|
|
check(running) { "AudioRecord stopped" } |
|
|
|
check(running) { "AudioRecord stopped" } |
|
|
|
|
|
|
|
val actual = inputId |
|
|
|
|
|
|
|
if (initialInput == null) { |
|
|
|
|
|
|
|
initialInput = actual |
|
|
|
|
|
|
|
LogManager.addLog("INFO", if (isStream) "stream" else "radio", |
|
|
|
|
|
|
|
"capture route selected=$preferred actual=$actual source=${record.audioSource} channels=$channels") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
val silenced = Build.VERSION.SDK_INT >= 29 && record.activeRecordingConfiguration?.isClientSilenced == true |
|
|
|
|
|
|
|
if (isStream) streamStatus = Status(actual, initialInput, silenced) |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
check(preferred == null || actual == preferred) { "Selected radio microphone unavailable: selected=$preferred actual=$actual" } |
|
|
|
|
|
|
|
check(!silenced) { "Android silenced radio microphone input=$actual" } |
|
|
|
|
|
|
|
val protected = streamStatus |
|
|
|
|
|
|
|
check(protected == null || (!protected.silenced && protected.input == protected.initialInput)) { |
|
|
|
|
|
|
|
"Radio input conflicts with stream: stream=$protected radio=$actual" |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
val first = total; total += 960 |
|
|
|
val first = total; total += 960 |
|
|
|
val measured = record.getTimestamp(stamp, AudioTimestamp.TIMEBASE_MONOTONIC) == AudioRecord.SUCCESS |
|
|
|
val measured = record.getTimestamp(stamp, AudioTimestamp.TIMEBASE_MONOTONIC) == AudioRecord.SUCCESS |
|
|
|
val time = if (measured) stamp.nanoTime - (stamp.framePosition - first) * 1_000_000_000 / 48000 else 0L |
|
|
|
val time = if (measured) stamp.nanoTime - (stamp.framePosition - first) * 1_000_000_000 / 48000 else 0L |
|
|
|
return MicrophoneCapture.Frame(pcm, time, first) |
|
|
|
return MicrophoneCapture.Frame(pcm, time, first) |
|
|
|
} |
|
|
|
} |
|
|
|
override fun stop() { running = false; record.stop() } |
|
|
|
override fun stop() { running = false; record.stop() } |
|
|
|
override fun close() { record.release() } |
|
|
|
override fun close() { record.release(); if (isStream) streamStatus = null } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fun own(record: AudioRecord, released: () -> Unit = {}): MicrophoneCapture { |
|
|
|
fun own(record: AudioRecord, isStream: Boolean = false, released: () -> Unit = {}): MicrophoneCapture { |
|
|
|
try { |
|
|
|
try { |
|
|
|
record.startRecording() |
|
|
|
record.startRecording() |
|
|
|
check(record.recordingState == AudioRecord.RECORDSTATE_RECORDING) { "Microphone did not start" } |
|
|
|
check(record.recordingState == AudioRecord.RECORDSTATE_RECORDING) { "Microphone did not start" } |
|
|
|
return MicrophoneCapture(Record(record), { LogManager.addLog("WARN", "radio", it) }, released) |
|
|
|
return MicrophoneCapture(Record(record, isStream), { level, message -> LogManager.addLog(level, "radio", message) }, released) |
|
|
|
} catch (e: Exception) { record.release(); throw e } |
|
|
|
} catch (e: Exception) { record.release(); throw e } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -52,7 +73,7 @@ object AndroidMicrophoneCapture { |
|
|
|
if (record.state != AudioRecord.STATE_INITIALIZED || record.channelCount != 2) { |
|
|
|
if (record.state != AudioRecord.STATE_INITIALIZED || record.channelCount != 2) { |
|
|
|
record.release(); error("CAMCORDER stereo capture unavailable") |
|
|
|
record.release(); error("CAMCORDER stereo capture unavailable") |
|
|
|
} |
|
|
|
} |
|
|
|
val capture = own(record, released) |
|
|
|
val capture = own(record, true, released) |
|
|
|
stream = capture |
|
|
|
stream = capture |
|
|
|
LogManager.addLog("INFO", "stream", "capture opened source=CAMCORDER channels=2 rate=48000 input=${capture.inputId}") |
|
|
|
LogManager.addLog("INFO", "stream", "capture opened source=CAMCORDER channels=2 rate=48000 input=${capture.inputId}") |
|
|
|
return capture.subscribe("stream") |
|
|
|
return capture.subscribe("stream") |
|
|
|
|