7.6 KiB
TASK-mic-gain.md — усиление микрофона очков (фикс «пустой фразы»)
Симптом (пользователь, 24.08 вечер)
Очки: TripleClick → «слушаю» → пользователь говорит → Click («отправить фразу») → очки показывают «думаю...» и СРАЗУ возвращаются в «слушаю». Ответа нет. Впечатление: «процесс говорения сломан».
Диагноз (подтверждён дампами и логами 24.08)
Цепь: очки SttMicStream (AudioRecord MIC 16 кГц s16 mono) → BT SPP → телефон SttStreamer (Silero VAD → sherpa whisper-small) → StopStt(cancel=false) → GlassesServer → full.isBlank() → SttCancel(REASON_USER) → очки возвращаются в «слушаю».
Факты:
- Жесты/протокол/цикл здоровы — симуляция
DEBUG_GESTURE(triple_click → click) воспроизводит симптом, очки логируютClick → отправить фразу,stt отменено — возврат в «слушаю» (SttCancel). Это штатное поведение для ПУСТОЙ фразы. - Микрофон RayNeo X2 пишет сигнал пиком 1–9% шкалы (пики 181–2966 из 32768; RMS 9–184). Дампы
stt_dump_1930*.wav(19:30, голос пользователя): после нормализации faster-whisper-base распознаёт «тихо тихо», «спокойная музыка» — РЕЧЬ есть, но амплитуда ничтожна. - Транспорт очки→телефон чист (analyze.py: порядок окон 98%, лаг 0, объём совпадает) — телефон получает тот же тихий PCM.
- STT-пайплайн здоров — на эмуляторе (WAV нормального уровня) распознавание работает; на амплитудах ~0.005–0.09 Silero VAD/whisper-small не видят речи →
fullпуст → пустой клик → возврат в «слушаю». - Вчерашний фикс кадров/байтов (
c940a7e+fc84ba1+40edd88) правильный и НЕ трогается — он убрал «через один», но уровень микрофона не поднял.
Корень: на пути аудио очки→телефон нет усиления. Микрофон тихий, VAD/Whisper не видят речь.
Задача
Добавить фиксированное цифровое усиление в app-glasses SttMicStream.kt, в живой ветке микрофона: чанк после micChunkBytes(buf, frames) усилить в MIC_GAIN раз (с clamp), и уже усиленный чанк отправлять onChunk(...) и писать в дамп.
- Точка правки:
app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/SttMicStream.kt - Тесты:
app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/MicChunkTest.kt(существующий файл, добавить тесты) - НЕ трогать:
HostToGlasses.kt,GlassesServer.kt,HostConnection.kt,MainActivity.kt(охраняемые 4 файла фикса7557d72), телефонную сторону (app-phone), протокол (lib-core),WavWriter.kt, существующую функциюmicChunkBytesи её тесты. SttMicStream.ktписать можно (не в охране).
Реализация
В SttMicStream.kt, рядом с micChunkBytes:
/** Коэффициент усиления микрофона (микрофон RayNeo X2 пишет пик 1–9% шкалы). */
internal const val MIC_GAIN = 10f
/**
* Цифровое усиление чанка s16le mono с насыщением.
* Каждый сэмпл: v = (s * gain).roundToInt().coerceIn(-32768, 32767).
* Размер чанка не меняется (3200 байт = 1600 сэмплов).
*/
internal fun gainChunk(bytes: ByteArray, gain: Float = MIC_GAIN): ByteArray
Разбор s16le: младший байт + (старший << 8), знак как в s16leToFloat (app-phone) — if (s >= 0x8000) s -= 0x10000.
В цикле записи (живая ветка, после фикса 40edd88):
val frames = recorder.read(buf, 0, CHUNK_BYTES / 2)
if (frames > 0) {
val raw = micChunkBytes(buf, frames)
val chunk = gainChunk(raw) // ← усиление здесь
if (dumpWriter != null) runCatching { dumpWriter.write(chunk) }.onFailure { ... }
onChunk(chunk)
}
ВАЖНО: дамп (WavWriter) пишет УСИЛЕННЫЙ чанк (то же, что уходит на телефон) — чтобы дампы очков отражали то, что реально получает STT. (Решение принято: диагностика по дампам очков = тому, что видит телефон; сырой микрофон больше не пишем.)
Обоснование gain = 10: лучший утренний пик речи 2966 (9.05%) → 29660 (90.5%); тихая фраза 483 (1.5%) → 4830 (14.7%) — уже видно VAD. При крике возможен клиппинг — приемлемо (лучше клиппинг, чем пустота). Если на железе VAD захочет ложно срабатывать — константу подстроим.
Тесты (добавить в MicChunkTest.kt)
gainChunkтихого чанка (сэмплы ±1000): пик результата ≥ 9000, размер = 3200 байт.gainChunkгромкого чанка (сэмплы ±30000): ни один сэмпл не выходит за ±32767 (clamp работает).gainChunkнулевого чанка: байты без изменений.gainChunkс gain = 1f: байты побайтово идентичны входу (для gain=1 не аллоцировать заново — вернутьbytes).- Сохранение порядка:
gainChunkне перемешивает сэмплы (знак/порядок сохраняется — проверка первых 2 сэмплов).
Проверка
./gradlew :app-glasses:testDebugUnitTest— зелёные (было 95, станет 95 + новые).- Полная сборка:
./gradlew :app-phone:assembleDebug :app-glasses:assembleDebug :lib-core:jvmTest. - Коммит отдельный:
stt(mic): усиление микрофона очков ×10 — VAD/Whisper видят речь (пустая фраза). push НЕ делать — приёмка на железе.
Приёмка на железе (делает Hermes + пользователь, НЕ агент)
- Установить оба APK (очки + телефон) с этого коммита, force-stop,
am start. - Маркеры дампов
stt_dump.markerуже лежат на обоих устройствах — дампы пишутся. - Пользователь: TripleClick → говорит «поставь спокойную музыку» → Click.
- Hermes снимает: дампы очков (пик речи должен стать ~50–90%),
stt_dump_phone.raw(телефон получил усиленный PCM), logcat:SttDoneс непустым текстом → LLM-запрос → ответ на очки.