stt(mic): вынести кадровую арифметику в micChunkBytes + регрессионные тесты MicChunkTest
- CHUNK_BYTES=3200, micChunkBytes(buf, frames): кадры read() -> байты (*2) - 4 теста: полный чанк 3200 Б без копирования, короткий read, документация старого бага (copyOf(кадры) = полка), inFrameCount=1600
This commit is contained in:
@@ -58,16 +58,11 @@ class SttMicStream(
|
||||
try {
|
||||
recorder.startRecording()
|
||||
log("stt", "микрофон запущен (16 кГц, s16, mono)")
|
||||
val buf = ByteArray(3_200) // = 1600 кадров (16-бит моно) = 100 мс
|
||||
val buf = ByteArray(CHUNK_BYTES) // = 1600 кадров (16-бит моно) = 100 мс
|
||||
while (isActive && shouldRun()) {
|
||||
// AudioRecord.read(byte[], int, inFrameCount): 3-й аргумент — В КАДРАХ,
|
||||
// а НЕ в байтах (16-бит моно: кадр = 2 байта). Просим buf.size/2 кадров
|
||||
// (= buf.size байт = 100 мс), получаем frames — и переводиМ В БАЙТЫ (×2).
|
||||
// Старый баг: n (кадры) брался как байты → ушла половина куска.
|
||||
val frames = recorder.read(buf, 0, buf.size / 2)
|
||||
val frames = recorder.read(buf, 0, CHUNK_BYTES / 2) // inFrameCount — КАДРЫ
|
||||
if (frames > 0) {
|
||||
val bytes = frames * 2
|
||||
val chunk = if (bytes == buf.size) buf else buf.copyOf(bytes)
|
||||
val chunk = micChunkBytes(buf, frames)
|
||||
if (dumpWriter != null) {
|
||||
runCatching { dumpWriter.write(chunk) }
|
||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||
@@ -143,6 +138,20 @@ class SttMicStream(
|
||||
}
|
||||
}
|
||||
|
||||
/** 100 мс при 16 кГц, s16, моно: 1600 кадров × 2 байта = 3200 байт. */
|
||||
internal const val CHUNK_BYTES = 3_200
|
||||
|
||||
/**
|
||||
* AudioRecord.read(byte[], int, inFrameCount) возвращает ЧИСЛО КАДРОВ, а не байт.
|
||||
* 16-бит моно: кадр = 2 байта. Байты = frames * 2.
|
||||
* Старый баг (заделан в c940a7e): return read() (кадры) сравнивали с buf.size (байты)
|
||||
* — всегда ложь → copyOf(n) резал кусок пополам (терялись 2-е 50 мс каждого 100 мс).
|
||||
*/
|
||||
internal fun micChunkBytes(buf: ByteArray, frames: Int): ByteArray {
|
||||
val bytes = frames * 2
|
||||
return if (bytes == buf.size) buf else buf.copyOf(bytes)
|
||||
}
|
||||
|
||||
/** FloatArray [-1, 1] → s16le ByteArray (16 кГц, mono). */
|
||||
internal fun floatSamplesToPcm16(samples: FloatArray): ByteArray {
|
||||
val out = ByteArray(samples.size * 2)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package pw.binom.viewmate.glasses.stt
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotSame
|
||||
import kotlin.test.assertSame
|
||||
|
||||
/**
|
||||
* Регрессия c940a7e: AudioRecord.read возвращает ЧИСЛО КАДРОВ, не байт.
|
||||
* 16-бит моно: кадр = 2 байта. Старики брали return read() как байты —
|
||||
* и copyOf(кадры) резал каждый 100-мс кусок пополам (терялись 2-е 50 мс).
|
||||
*/
|
||||
class MicChunkTest {
|
||||
|
||||
@Test
|
||||
fun fullChunk_all1600Frames_returnsAll3200Bytes() {
|
||||
val buf = ByteArray(CHUNK_BYTES)
|
||||
val out = micChunkBytes(buf, CHUNK_BYTES / 2) // 1600 кадров = полная 100 мс
|
||||
assertEquals(CHUNK_BYTES, out.size, "полный чанк = 3200 байт (100 мс), а не 1600")
|
||||
assertSame(buf, out, "полный чанк не копируется — реюзаем buf без аллокаций")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shortRead_800Frames_gives1600Bytes_not800() {
|
||||
val buf = ByteArray(CHUNK_BYTES)
|
||||
val out = micChunkBytes(buf, 800) // 800 кадров = 1600 байт
|
||||
assertEquals(1_600, out.size, "байты = кадры × 2, не равны количеству кадров")
|
||||
assertNotSame(buf, out, "короткий чанк копируется, иначе хвост buf — мусор")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun oldLogic_lostSecondHalf_ofEachChunk() {
|
||||
// Задокументированный баг: n (кадры) брался как байты.
|
||||
val buf = ByteArray(CHUNK_BYTES)
|
||||
val frames = recorderWouldReturn(1600) // полный 100-мс запрос вернул 1600 кадров
|
||||
val oldChunk = if (frames == buf.size) buf else buf.copyOf(frames) // СТАРОЕ
|
||||
assertEquals(1_600, oldChunk.size, "старая логика теряла 2-ю половину куска — отсюда «как через один»")
|
||||
val newChunk = micChunkBytes(buf, frames)
|
||||
assertEquals(3_200, newChunk.size, "новая логика отдаёт полные 100 мс")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun framesRequested_forFull100MsIs1600() {
|
||||
assertEquals(1_600, CHUNK_BYTES / 2, "read() просит КАДРОВ: 1600 (не 3200)")
|
||||
}
|
||||
|
||||
private fun recorderWouldReturn(frames: Int) = frames
|
||||
}
|
||||
Reference in New Issue
Block a user