stt: debug-дампы аудио-пути (очки: сырой микрофон в WAV; телефон: принятые PCM) по старту от stt_dump.marker
This commit is contained in:
@@ -5,6 +5,8 @@ import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -27,6 +29,9 @@ class SttMicStream(
|
||||
) {
|
||||
private var job: Job? = null
|
||||
|
||||
/** Debug-дампер сырого микрофона (открывается по маркеру stt_dump.marker). */
|
||||
private var dump: WavWriter? = null
|
||||
|
||||
fun start(filesDir: File, shouldRun: () -> Boolean) {
|
||||
job?.cancel()
|
||||
val recorder = runCatching {
|
||||
@@ -42,10 +47,13 @@ class SttMicStream(
|
||||
if (recorder == null || recorder.state != AudioRecord.STATE_INITIALIZED) {
|
||||
recorder?.release()
|
||||
log("stt", "микрофон недоступен — отладка: PCM из файла")
|
||||
dump = null
|
||||
job = scope.launch { sendWavFileAsStream(filesDir, shouldRun) }
|
||||
return
|
||||
}
|
||||
|
||||
dump = openLiveMicDump(filesDir)
|
||||
val dumpWriter = dump
|
||||
job = scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
recorder.startRecording()
|
||||
@@ -55,6 +63,10 @@ class SttMicStream(
|
||||
val n = recorder.read(buf, 0, buf.size)
|
||||
if (n > 0) {
|
||||
val chunk = if (n == buf.size) buf else buf.copyOf(n)
|
||||
if (dumpWriter != null) {
|
||||
runCatching { dumpWriter.write(chunk) }
|
||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||
}
|
||||
onChunk(chunk)
|
||||
}
|
||||
}
|
||||
@@ -63,6 +75,8 @@ class SttMicStream(
|
||||
} finally {
|
||||
runCatching { recorder.stop() }
|
||||
recorder.release()
|
||||
runCatching { dumpWriter?.close() }
|
||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||
log("stt", "микрофон остановлен")
|
||||
}
|
||||
}
|
||||
@@ -71,6 +85,24 @@ class SttMicStream(
|
||||
fun stop() {
|
||||
job?.cancel()
|
||||
job = null
|
||||
runCatching { dump?.close() }
|
||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-дампы (задача audio-dump): если в filesDir лежит stt_dump.marker,
|
||||
* открыть stt_dump_HHmmss.wav — ВСЕ чанки живого микрофона допишутся туда
|
||||
* до отправки в onChunk (эмуляторную ветку не трогаем).
|
||||
*/
|
||||
private fun openLiveMicDump(filesDir: File): WavWriter? {
|
||||
if (!File(filesDir, "stt_dump.marker").exists()) return null
|
||||
val stamp = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss"))
|
||||
val path = File(filesDir, "stt_dump_$stamp.wav")
|
||||
return runCatching {
|
||||
WavWriter(path).also { log("stt", "dump микрофона: $path") }
|
||||
}
|
||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package pw.binom.viewmate.glasses.stt
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* Минимальный WAV-райтер (PCM s16le): 44-байтовый RIFF-заголовок пишется один раз
|
||||
* при создании файла, [write] дописывает чанки, на close в заголовок дописываются
|
||||
* реальные размеры (best-effort — при сбое ридер читает файл до EOF, размеры
|
||||
* 0xFFFFFFFF — конвенция стриминговых WAV).
|
||||
*
|
||||
* Не потокобезопасен — вызывать из одного потока (поток микрофона).
|
||||
*/
|
||||
class WavWriter(
|
||||
private val path: File,
|
||||
private val sampleRate: Int = 16_000,
|
||||
private val channels: Int = 1,
|
||||
) {
|
||||
private val out: FileOutputStream
|
||||
private var dataBytes = 0L
|
||||
private var closed = false
|
||||
|
||||
init {
|
||||
out = FileOutputStream(path)
|
||||
out.write(headerBytes())
|
||||
}
|
||||
|
||||
/** Дописать чанк s16le PCM (16 кГц, mono). */
|
||||
fun write(pcm: ByteArray) {
|
||||
out.write(pcm)
|
||||
dataBytes += pcm.size
|
||||
}
|
||||
|
||||
/** Закрыть файл и прописать реальные размеры в заголовок (идемпотентно). */
|
||||
fun close() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
out.close()
|
||||
runCatching {
|
||||
RandomAccessFile(path, "rw").use { raf ->
|
||||
raf.seek(4)
|
||||
raf.writeInt((36 + dataBytes).toInt()) // размер RIFF
|
||||
raf.seek(40)
|
||||
raf.writeInt(dataBytes.toInt()) // размер data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun headerBytes(): ByteArray =
|
||||
ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
put("RIFF".toByteArray())
|
||||
putInt(-1) // placeholder
|
||||
put("WAVE".toByteArray())
|
||||
put("fmt ".toByteArray())
|
||||
putInt(16)
|
||||
putShort(1.toShort()) // PCM
|
||||
putShort(channels.toShort())
|
||||
putInt(sampleRate)
|
||||
putInt(sampleRate * channels * 2)
|
||||
putShort((channels * 2).toShort())
|
||||
putShort(16.toShort())
|
||||
put("data".toByteArray())
|
||||
putInt(-1) // placeholder
|
||||
}.array()
|
||||
}
|
||||
Reference in New Issue
Block a user