stt(dump): WAV-заголовок — размеры little-endian (плееры открывают дампы)

This commit is contained in:
2026-08-24 21:36:53 +03:00
parent 0d79b5aa33
commit e21c27b0ee
3 changed files with 142 additions and 2 deletions
+62
View File
@@ -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 НЕ делать.
@@ -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())
@@ -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)
}
}