diff --git a/TASK-wav-header.md b/TASK-wav-header.md new file mode 100644 index 0000000..09e208d --- /dev/null +++ b/TASK-wav-header.md @@ -0,0 +1,62 @@ +# TASK-wav-header.md — починить размеры в WAV-заголовке (big-endian → little-endian) + +## Симптом + +Дампы `stt_dump_*.wav` (app-glasses WavWriter) не открываются в плеерах. Проверка hexdump: +- `RIFF 00 02 e1 a4` — размер записан big-endian (Java `RandomAccessFile.writeInt`), а WAV требует little-endian. +- Правильно: `a4 e1 02 00` для файла 188844 байт (data 188800). + +## Точка правки + +`app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/WavWriter.kt`, метод `close()`: +```kotlin +runCatching { + RandomAccessFile(path, "rw").use { raf -> + raf.seek(4) + raf.writeInt((36 + dataBytes).toInt()) // RIFF size — BIG-ENDIAN, БАГ + raf.seek(40) + raf.writeInt(dataBytes.toInt()) // data size — BIG-ENDIAN, БАГ + } +} +``` + +`headerBytes()` уже использует `ByteBuffer.order(LITTLE_ENDIAN)` — там всё правильно (placeholder -1 одинаков в любом порядке, поэтому баг не виден до close()). + +## Фикс + +Писать размеры little-endian. Минимальная правка без новых зависимостей: +```kotlin +runCatching { + RandomAccessFile(path, "rw").use { raf -> + raf.seek(4) + raf.write(leInt(36 + dataBytes)) + raf.seek(40) + raf.write(leInt(dataBytes)) + } +} +``` +с helper рядом: +```kotlin +/** 4 байта little-endian (WAV-заголовок), в отличие от RandomAccessFile.writeInt (big-endian). */ +private fun leInt(v: Long): ByteArray = + ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v.toInt()).array() +``` +(`ByteBuffer`/`ByteOrder` уже импортированы в файле.) + +НЕ трогать: остальное в WavWriter (write/headerBytes), SttMicStream, охраняемые 4 файла, протокол. + +## Тесты + +В `app-glasses/src/test/.../stt/` (рядом с MicChunkTest) — новый `WavWriterTest`: +1. Записать 3200 байт через WavWriter во временный файл, close(), прочитать заголовок: + - байты 4..8 (RIFF size) == 36 + 3200 (little-endian, сравнить с ожидаемым ByteBuffer); + - байты 40..44 (data size) == 3200 (little-endian); + - python-wave-эквивалент: nframes == 1600 (можно проверить в JVM: прочитать заголовок вручную). +2. Пустой файл (0 байт данных) — RIFF size == 36, data size == 0. +3. Закрытие дважды (close два раза) — размеры не портятся (closed-флаг). + +## Проверка + +1. `./gradlew :app-glasses:testDebugUnitTest` — зелёные (старые + новые). +2. `./gradlew :app-glasses:assembleDebug` — собирается. +3. Коммит: `stt(dump): WAV-заголовок — размеры little-endian (плееры открывают дампы)`. Push НЕ делать. diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/WavWriter.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/WavWriter.kt index 75050a2..19ace3f 100644 --- a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/WavWriter.kt +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/stt/WavWriter.kt @@ -42,13 +42,17 @@ class WavWriter( runCatching { RandomAccessFile(path, "rw").use { raf -> raf.seek(4) - raf.writeInt((36 + dataBytes).toInt()) // размер RIFF + raf.write(leInt(36 + dataBytes)) // размер RIFF (little-endian) raf.seek(40) - raf.writeInt(dataBytes.toInt()) // размер data + raf.write(leInt(dataBytes)) // размер data (little-endian) } } } + /** 4 байта little-endian (WAV-заголовок), в отличие от RandomAccessFile.writeInt (big-endian). */ + private fun leInt(v: Long): ByteArray = + ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v.toInt()).array() + private fun headerBytes(): ByteArray = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN).apply { put("RIFF".toByteArray()) diff --git a/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/WavWriterTest.kt b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/WavWriterTest.kt new file mode 100644 index 0000000..7d73b2d --- /dev/null +++ b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/stt/WavWriterTest.kt @@ -0,0 +1,74 @@ +package pw.binom.viewmate.glasses.stt + +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +/** + * Регрессия: размеры в WAV-заголовке должны быть little-endian — из-за big-endian + * (RandomAccessFile.writeInt) плееры не открывали дампы stt_dump_*.wav: + * для файла 188844 байта байты 4..8 были 00 02 e1 a4 вместо a4 e1 02 00. + */ +class WavWriterTest { + + @Test + fun close_3200Data_riffAndDataSizesLittleEndian() { + val bytes = writeAndClose(3_200) + assertContentEquals(leIntBytes(36 + 3_200), bytes.copyOfRange(4, 8), + "RIFF size = 36 + data, little-endian (байты 4..8) — не big-endian из writeInt") + assertContentEquals(leIntBytes(3_200), bytes.copyOfRange(40, 44), + "data size = 3200, little-endian (байты 40..44)") + assertEquals(1_600, leIntAt(bytes, 40) / 2, + "nframes = data/2 (16 кГц s16le mono) — как считает python wave") + } + + @Test + fun close_noData_riffSize36DataZero() { + val bytes = writeAndClose(0) + assertContentEquals(leIntBytes(36), bytes.copyOfRange(4, 8), "пустой data: RIFF size = 36") + assertEquals(0, leIntAt(bytes, 40), "пустой data: data size = 0") + } + + @Test + fun closeTwice_sizesUnchanged() { + val file = tmpFile() + val w = WavWriter(file) + w.write(ByteArray(3_200)) + w.close() + w.close() + val bytes = file.readBytes() + assertEquals(44 + 3_200, bytes.size, "второй close ничего не дописывает в файл") + assertContentEquals(leIntBytes(36 + 3_200), bytes.copyOfRange(4, 8), + "двойной close не портит размеры (флаг closed)") + assertContentEquals(leIntBytes(3_200), bytes.copyOfRange(40, 44)) + } + + private fun writeAndClose(dataBytes: Int): ByteArray { + val file = tmpFile() + val w = WavWriter(file) + if (dataBytes > 0) w.write(ByteArray(dataBytes)) + w.close() + assertEquals(44L + dataBytes, file.length(), "файл = 44-байтный заголовок + data") + return file.readBytes() + } + + private fun tmpFile(): File { + val f = Files.createTempFile("wvwriter-test", ".wav").toFile() + f.deleteOnExit() + return f + } + + /** 4 байта little-endian — ожидаемый вид записи в WAV-заголовке. */ + private fun leIntBytes(v: Int): ByteArray = + ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array() + + /** Прочитать 4 байта little-endian с позиции offset (то, как rидер получит int32). */ + private fun leIntAt(bytes: ByteArray, offset: Int): Int { + fun b(i: Int) = bytes[i].toInt() and 0xff + return b(offset) or (b(offset + 1) shl 8) or (b(offset + 2) shl 16) or (b(offset + 3) shl 24) + } +}