Clarify capture timeline and microphone permission contracts
This commit is contained in:
1 parent
c0c282ca3a
commit
69bc182ee9
3 files changed
+34
-6
No files matched your search
@@ -43,7 +43,7 @@ class AudioRecorderManager {
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
/** target — канал либо dm:conv_id; reply/request относятся к каналу, generation — к PM.
|
||||
/** channelId — канал либо dm:conv_id; replyTo/requestId относятся к каналу, generation — к PM.
|
||||
* true — Android/native захват запущен; false — отказ с диагностикой и откатом принадлежащих ресурсов. */
|
||||
@Synchronized fun startRecording(channelId: String, replyTo: String = "", requestId: Long = 0, generation: Long = 0): Boolean =
|
||||
startCapture(CaptureMode.RECORD, channelId, replyTo, requestId, generation)
|
||||
|
||||
@@ -6,9 +6,16 @@ import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/** Общая сетка PCM 48k stereo. Внутри поколения отсчёты идут по sequence, без перекладки
|
||||
* отдельных блоков по шумным timestamps. Медленный дрейф часов компенсируется непрерывной
|
||||
* интерполяцией; только реальные пропуски дают тишину. Все методы — под lock владельца. */
|
||||
/**
|
||||
* Непрерывная сетка PCM16 48 кГц stereo для InputBridge общего media-входа.
|
||||
* originNs и timestamps используют System.nanoTime; один pull выдаёт 960 stereo-фреймов (1920 Short).
|
||||
* Владелец сериализует все обращения своим lock и ждёт deadlineNs перед pull: конец блока плюс резерв 200 мс.
|
||||
* reset переключает поколение аппаратного входа, сохраняя общую выходную позицию/время.
|
||||
* До привязки, перед началом входа и при пропусках выдаётся тишина; поздний PCM не заменяет уже выданную тишину.
|
||||
* Внутри поколения sequence определяет непрерывность, шумные timestamps не переставляют блоки.
|
||||
* Медленный дрейф компенсируется интерполяцией до ±2000 ppm; скачок времени либо компенсируется bias,
|
||||
* либо заново привязывает свежий звук после остановки reader. Здесь нет I/O, ожиданий или фонового worker.
|
||||
*/
|
||||
internal class MediaCaptureTimeline(val originNs: Long, private val log: (String, String) -> Unit = { _, _ -> }) {
|
||||
companion object {
|
||||
const val FRAMES = 960
|
||||
@@ -30,10 +37,13 @@ internal class MediaCaptureTimeline(val originNs: Long, private val log: (String
|
||||
private var timestampBiasNs = 0L
|
||||
var generation = 0L
|
||||
private set
|
||||
/** Число отвергнутых 20мс блоков: поздние или очередь уже содержит 24 блока; накопление за жизнь объекта. */
|
||||
var lateFrames = 0L
|
||||
private set
|
||||
/** Число выданных stereo-фреймов тишины, включая ожидание привязки; reset не обнуляет. */
|
||||
var silentFrames = 0L
|
||||
private set
|
||||
/** Часть silentFrames после начала привязанного входа, когда PCM отсутствует по нужному sequence. */
|
||||
var missingFrames = 0L
|
||||
private set
|
||||
var timestampJitterMaxNs = 0L
|
||||
@@ -45,11 +55,16 @@ internal class MediaCaptureTimeline(val originNs: Long, private val log: (String
|
||||
val nextTimeNs: Long get() = originNs + position * 1_000_000_000 / 48000
|
||||
val deadlineNs: Long get() = nextTimeNs + PERIOD_NS + RESERVE_NS
|
||||
|
||||
/** Очистить очередь, привязку и оценку дрейфа/jitter для epoch; выходная position и накопленные счётчики PCM сохраняются. */
|
||||
fun reset(epoch: Long) {
|
||||
generation = epoch; pending.clear(); lastInputNs = -1; lastSequence = -1; lastCreatedNs = 0
|
||||
anchored = false; cursor = Double.NaN; clockOffset = 0.0; ratePpm = 0; timestampJitterMaxNs = 0; timestampBiasNs = 0; timestampJumps = 0
|
||||
}
|
||||
|
||||
/** Принять 1920 Short с sequence в stereo-фреймах и временем первого фрейма; PCM заимствуется до pull/reset.
|
||||
* false — чужое поколение, неповышающийся sequence, поздний PCM или полная очередь.
|
||||
* Неверная длина PCM либо отрицательные sequence/timeNs бросают исключение.
|
||||
* Вызывающий не должен менять массив принятого Frame. Необычные пропуски/скачки логируются через log владельца. */
|
||||
fun offer(frame: MicrophoneCapture.Frame): Boolean {
|
||||
require(frame.pcm.size == FRAMES * 2 && frame.sequence >= 0 && frame.timeNs >= 0)
|
||||
if (frame.generation != generation) return false
|
||||
@@ -99,6 +114,8 @@ internal class MediaCaptureTimeline(val originNs: Long, private val log: (String
|
||||
return true
|
||||
}
|
||||
|
||||
/** Требует nowNs >= deadlineNs. Вернуть новый Frame общей выходной сетки с текущим generation и продвинуть её на 20 мс.
|
||||
* timeNs/sequence относятся к сетке, createdNs=nowNs; массив принадлежит получателю. */
|
||||
fun pull(nowNs: Long): MicrophoneCapture.Frame {
|
||||
check(nowNs >= deadlineNs)
|
||||
val start = nextTimeNs
|
||||
|
||||
@@ -8,7 +8,11 @@ import androidx.core.content.ContextCompat
|
||||
import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.MainActivity
|
||||
|
||||
/* Запросы только из main thread; диалогом разрешений владеет видимая Activity. */
|
||||
/** App-scoped вход в запрос RECORD_AUDIO; сам диалог и pending callback принадлежат видимой Activity.
|
||||
* Activity устанавливает requester при onStart и снимает только свой обработчик при onStop.
|
||||
* Без Activity открывается MainActivity с EXTRA_REQUEST: исходное действие/onGranted не сохраняются,
|
||||
* после выдачи разрешения пользователь повторяет команду (например, PTT).
|
||||
*/
|
||||
object MicrophonePermission {
|
||||
const val EXTRA_REQUEST = "request_microphone_permission"
|
||||
var requester: ((String, () -> Unit) -> Unit)? = null
|
||||
@@ -16,8 +20,15 @@ object MicrophonePermission {
|
||||
fun granted(): Boolean = ContextCompat.checkSelfPermission(ChatApplication.instance, Manifest.permission.RECORD_AUDIO) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
|
||||
/** Main thread: при готовом разрешении вызвать onGranted синхронно, иначе передать запрос Activity.
|
||||
* Обработчик Activity решает, принять ли параллельный запрос; отказ разрешения не вызывает onGranted.
|
||||
* Ошибка открытия Activity логируется; исключения пользовательского requester/onGranted передаются caller. */
|
||||
fun request(reason: String, onGranted: () -> Unit = {}) {
|
||||
check(Looper.myLooper() == Looper.getMainLooper())
|
||||
check(Looper.myLooper() == Looper.getMainLooper()) {
|
||||
val message = "MicrophonePermission requires main thread: caller=${Thread.currentThread().name}"
|
||||
LogManager.addLog("ERROR", "PERMISSION", message)
|
||||
message
|
||||
}
|
||||
if (granted()) { onGranted(); return }
|
||||
LogManager.addLog("INFO", "PERMISSION", "Microphone required: $reason")
|
||||
val handler = requester
|
||||
|
||||
Reference in new issue
Block a user