From d78ed4429e2b3d52ad39079dd842320d9e436ce5 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 25 Aug 2026 01:39:12 +0300 Subject: [PATCH] =?UTF-8?q?stt(mic):=20=D0=BC=D0=B8=D0=BA=D1=80=D0=BE?= =?UTF-8?q?=D1=84=D0=BE=D0=BD=20=D0=BD=D0=B0=2048=20=D0=BA=D0=93=D1=86=20(?= =?UTF-8?q?=D0=BD=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D0=B0=D1=8F=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=82=D0=B0)=20+=20=D0=B4=D0=B0=D1=83=D0=BD?= =?UTF-8?q?=D1=81=D0=B5=D0=BC=D0=BF=D0=BB=D0=B8=D0=BD=D0=B3=2048=E2=86=921?= =?UTF-8?q?6=20=E2=80=94=20=D1=80=D0=B2=D0=B0=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20ARGT78=20=D0=B4=D0=B0=D0=B2=D0=B0=D0=BB=D0=B0=20=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D1=8B=20100=20=D0=BC=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../viewmate/glasses/stt/SttMicStream.kt | 42 ++++++++++++++++--- .../viewmate/glasses/stt/MicChunkTest.kt | 32 ++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt index 6b95d0a..8e4f42e 100644 --- a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt @@ -38,10 +38,10 @@ class SttMicStream( val recorder = runCatching { AudioRecord( MediaRecorder.AudioSource.MIC, - 16_000, + MIC_SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, - 6_400, + MIC_BUFFER_BYTES, ) }.getOrNull() @@ -58,13 +58,17 @@ class SttMicStream( job = scope.launch(Dispatchers.IO) { try { recorder.startRecording() - log("stt", "микрофон запущен (16 кГц, s16, mono)") - val buf = ByteArray(CHUNK_BYTES) // = 1600 кадров (16-бит моно) = 100 мс + log("stt", "микрофон запущен ($MIC_SAMPLE_RATE Гц, s16, mono)") + val buf = ByteArray(MIC_CHUNK_FRAMES * 2) // 100 мс при 48 кГц 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) { 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) { runCatching { dumpWriter.write(chunk) } .onFailure { log("stt", "dump микрофона: ${it.message}") } @@ -143,6 +147,32 @@ class SttMicStream( /** 100 мс при 16 кГц, s16, моно: 1600 кадров × 2 байта = 3200 байт. */ 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) возвращает ЧИСЛО КАДРОВ, а не байт. * 16-бит моно: кадр = 2 байта. Байты = frames * 2. diff --git a/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt index 2343315..b7f8dae 100644 --- a/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt +++ b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt @@ -118,4 +118,36 @@ class MicChunkTest { } 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 сэмпла") + } }