EcritureWav.kt 71 lignes · 2742 octets
package fr.ebii.card2vcf.audio

import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
 * Écrit un fichier WAV PCM (16 kHz mono 16 bits par défaut).
 *
 * Séparée de [AudioNoteRecorder] pour être testable en JVM pur, sans dépendance Android.
 * L'en-tête RIFF/fmt/data est réécrit à la clôture avec les tailles exactes.
 */
class EcritureWav(
    private val fichier: File,
    private val frequence: Int = 16_000,
) {
    private val raf = RandomAccessFile(fichier, "rw")
    private var octetsData: Long = 0L

    init {
        // Réserve 44 octets pour l'en-tête ; sera réécrit à fermer()
        raf.write(ByteArray(44))
    }

    /** Écrit des échantillons PCM 16 bits signés (little-endian). */
    fun ecrireEchantillons(data: ShortArray, longueur: Int) {
        val buf = ByteArray(longueur * 2)
        val bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN)
        for (i in 0 until longueur) bb.putShort(data[i])
        raf.write(buf)
        octetsData += longueur * 2L
    }

    /**
     * Finalise l'en-tête WAV et ferme le fichier.
     * @return Nombre d'octets de données PCM écrits.
     */
    fun fermer(): Long {
        // 10 min à 16 kHz 16 bits mono ≈ 18,3 Mo — largement sous Int.MAX_VALUE.
        // La garde protège contre un enregistrement pathologiquement long ou un bug de comptage.
        check(octetsData <= Int.MAX_VALUE) { "Fichier WAV trop grand : $octetsData octets (max ${Int.MAX_VALUE})" }
        val tailleData = octetsData.toInt()
        // tailleFichier = octets après "RIFF"+int32 = 4(WAVE)+8(fmt tag+size)+16(fmt)+8(data tag+size)+data
        val tailleFichier = tailleData + 36
        val canaux = 1
        val bitsParEchantillon = 16
        val byteRate = frequence * canaux * bitsParEchantillon / 8
        val blockAlign = canaux * bitsParEchantillon / 8

        val entete = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN)
        entete.put("RIFF".toByteArray(Charsets.US_ASCII))
        entete.putInt(tailleFichier)
        entete.put("WAVE".toByteArray(Charsets.US_ASCII))
        entete.put("fmt ".toByteArray(Charsets.US_ASCII))
        entete.putInt(16)                           // taille du chunk fmt
        entete.putShort(1)                          // format PCM
        entete.putShort(canaux.toShort())
        entete.putInt(frequence)
        entete.putInt(byteRate)
        entete.putShort(blockAlign.toShort())
        entete.putShort(bitsParEchantillon.toShort())
        entete.put("data".toByteArray(Charsets.US_ASCII))
        entete.putInt(tailleData)

        raf.seek(0)
        raf.write(entete.array())
        raf.close()
        return octetsData
    }
}