stt(mic): микрофон на 48 кГц (нативная частота) + даунсемплинг 48→16 — рваность ARGT78 давала ровные провалы 100 мс
This commit is contained in:
@@ -38,10 +38,10 @@ class SttMicStream(
|
|||||||
val recorder = runCatching {
|
val recorder = runCatching {
|
||||||
AudioRecord(
|
AudioRecord(
|
||||||
MediaRecorder.AudioSource.MIC,
|
MediaRecorder.AudioSource.MIC,
|
||||||
16_000,
|
MIC_SAMPLE_RATE,
|
||||||
AudioFormat.CHANNEL_IN_MONO,
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
6_400,
|
MIC_BUFFER_BYTES,
|
||||||
)
|
)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
@@ -58,13 +58,17 @@ class SttMicStream(
|
|||||||
job = scope.launch(Dispatchers.IO) {
|
job = scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
recorder.startRecording()
|
recorder.startRecording()
|
||||||
log("stt", "микрофон запущен (16 кГц, s16, mono)")
|
log("stt", "микрофон запущен ($MIC_SAMPLE_RATE Гц, s16, mono)")
|
||||||
val buf = ByteArray(CHUNK_BYTES) // = 1600 кадров (16-бит моно) = 100 мс
|
val buf = ByteArray(MIC_CHUNK_FRAMES * 2) // 100 мс при 48 кГц
|
||||||
while (isActive && shouldRun()) {
|
while (isActive && shouldRun()) {
|
||||||
val frames = recorder.read(buf, 0, CHUNK_BYTES / 2) // inFrameCount — КАДРЫ
|
val frames = recorder.read(buf, 0, MIC_CHUNK_FRAMES) // inFrameCount — КАДРЫ
|
||||||
if (frames > 0) {
|
if (frames > 0) {
|
||||||
val raw = micChunkBytes(buf, frames)
|
val raw = micChunkBytes(buf, frames)
|
||||||
val chunk = gainChunk(raw) // усиление: в дамп и на телефон — один и тот же чанк
|
// 48→16 кГц: каждый 3-й сэмпл (whisper/VAD ждут 16 кГц).
|
||||||
|
// Микрофон ARGT78 на 16 кГц писал рвано (ровные провалы 100 мс) —
|
||||||
|
// на 48 кГц (нативная частота драйвера) поток ожидается ровным.
|
||||||
|
val down = downsample48To16(raw)
|
||||||
|
val chunk = gainChunk(down) // усиление: в дамп и на телефон — один и тот же чанк
|
||||||
if (dumpWriter != null) {
|
if (dumpWriter != null) {
|
||||||
runCatching { dumpWriter.write(chunk) }
|
runCatching { dumpWriter.write(chunk) }
|
||||||
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
.onFailure { log("stt", "dump микрофона: ${it.message}") }
|
||||||
@@ -143,6 +147,32 @@ class SttMicStream(
|
|||||||
/** 100 мс при 16 кГц, s16, моно: 1600 кадров × 2 байта = 3200 байт. */
|
/** 100 мс при 16 кГц, s16, моно: 1600 кадров × 2 байта = 3200 байт. */
|
||||||
internal const val CHUNK_BYTES = 3_200
|
internal const val CHUNK_BYTES = 3_200
|
||||||
|
|
||||||
|
/** Микрофон очков пишем на нативной частоте драйвера (48 кГц), дальше даунсэмплим в 16 кГц. */
|
||||||
|
internal const val MIC_SAMPLE_RATE = 48_000
|
||||||
|
|
||||||
|
/** 100 мс при 48 кГц: 4800 кадров. */
|
||||||
|
internal const val MIC_CHUNK_FRAMES = 4_800
|
||||||
|
|
||||||
|
/** Минимальный буфер AudioRecord: 2 чанка по 100 мс. */
|
||||||
|
internal const val MIC_BUFFER_BYTES = MIC_CHUNK_FRAMES * 2 * 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 48 кГц → 16 кГц: каждый 3-й сэмпл s16le mono. frames — число кадров,
|
||||||
|
* не кратных 3 — хвост отбрасывается. На выходе — байты s16le 16 кГц.
|
||||||
|
*/
|
||||||
|
internal fun downsample48To16(buf: ByteArray, frames: Int = buf.size / 2): ByteArray {
|
||||||
|
val usable = frames - (frames % 3)
|
||||||
|
val out = ByteArray(usable / 3 * 2)
|
||||||
|
var i = 0
|
||||||
|
var k = 0
|
||||||
|
while (k < usable * 2) {
|
||||||
|
out[i++] = buf[k]
|
||||||
|
out[i++] = buf[k + 1]
|
||||||
|
k += 6
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AudioRecord.read(byte[], int, inFrameCount) возвращает ЧИСЛО КАДРОВ, а не байт.
|
* AudioRecord.read(byte[], int, inFrameCount) возвращает ЧИСЛО КАДРОВ, а не байт.
|
||||||
* 16-бит моно: кадр = 2 байта. Байты = frames * 2.
|
* 16-бит моно: кадр = 2 байта. Байты = frames * 2.
|
||||||
|
|||||||
@@ -118,4 +118,36 @@ class MicChunkTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun recorderWouldReturn(frames: Int) = frames
|
private fun recorderWouldReturn(frames: Int) = frames
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Даунсемплинг 48→16 кГц (TASK-mic-48k): микрофон ARGT78 на 16 кГц пишет
|
||||||
|
* рвано (ровные провалы тишины с периодом 100 мс) — пишем на 48 кГц
|
||||||
|
* (нативная частота драйвера) и прореживаем каждый 3-й сэмпл.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downsample_4800Frames_gives3200Bytes() {
|
||||||
|
val buf = ByteArray(MIC_CHUNK_FRAMES * 2) // 4800 кадров = 9600 байт
|
||||||
|
val out = downsample48To16(buf, MIC_CHUNK_FRAMES)
|
||||||
|
assertEquals(3_200, out.size, "4800 кадров 48кГц → 1600 сэмплов 16кГц = 3200 байт")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downsample_takesEveryThirdSample() {
|
||||||
|
// 6 кадров 48кГц (12 байт): 0,1,2,3,4,5 → 0 и 3 (каждый 3-й)
|
||||||
|
val buf = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
|
||||||
|
val out = downsample48To16(buf, 6)
|
||||||
|
assertEquals(4, out.size, "6 кадров → 2 сэмпла = 4 байта")
|
||||||
|
assertEquals(1, out[0].toInt() and 0xff)
|
||||||
|
assertEquals(2, out[1].toInt() and 0xff)
|
||||||
|
assertEquals(7, out[2].toInt() and 0xff)
|
||||||
|
assertEquals(8, out[3].toInt() and 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downsample_nonMultipleOf3_dropsTail() {
|
||||||
|
val buf = ByteArray(14) // 7 кадров: 6 usable + 1 отброшен
|
||||||
|
val out = downsample48To16(buf, 7)
|
||||||
|
assertEquals(4, out.size, "6 usable кадров → 2 сэмпла")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user