WavFormat.kt

package com.shengzhiai.yugu.audio

import java.nio.ByteBuffer
import java.nio.ByteOrder

/** WAV 工具:PCM 加 WAV 头,解析 WAV 头。 */
public object WavFormat {

    /** WAV 头信息,[dataOffset] 与 [dataLength] 为 `data` 块在文件里的位置与长度。 */
    public data class Info(
        val formatTag: Int,
        val channels: Int,
        val sampleRate: Int,
        val bitsPerSample: Int,
        val dataOffset: Int,
        val dataLength: Int,
    ) {
        /** 是否 16 位整型 PCM。 */
        val isPcm16: Boolean get() = (formatTag == 1 || formatTag == 0xFFFE) && bitsPerSample == 16

        /** 时长,单位毫秒。 */
        val durationMs: Long
            get() {
                val bytesPerSecond = sampleRate.toLong() * channels * (bitsPerSample / 8).coerceAtLeast(1)
                return if (bytesPerSecond <= 0) 0 else dataLength * 1000L / bytesPerSecond
            }
    }

    /** 是否以 `RIFF....WAVE` 开头。 */
    @JvmStatic
    public fun isWav(bytes: ByteArray): Boolean =
        bytes.size >= 12 && tag(bytes, 0) == "RIFF" && tag(bytes, 8) == "WAVE"

    /** 解析 WAV 头,逐块查找 `fmt ` 与 `data`,不是 WAV 或头损坏时返回 null。 */
    @JvmStatic
    public fun parse(bytes: ByteArray): Info? {
        if (!isWav(bytes)) return null
        var pos = 12
        var fmt: IntArray? = null
        while (pos + 8 <= bytes.size) {
            val id = tag(bytes, pos)
            val size = le32(bytes, pos + 4)
            val body = pos + 8
            if (id == "fmt ") {
                if (size < 16 || body + 16 > bytes.size) return null
                var formatTag = le16(bytes, body)
                if (formatTag == 0xFFFE && size >= 40 && body + 26 <= bytes.size) {
                    // WAVE_FORMAT_EXTENSIBLE: the sub format GUID starts with the real tag.
                    formatTag = if (le16(bytes, body + 24) == 1) 0xFFFE else le16(bytes, body + 24)
                }
                fmt = intArrayOf(formatTag, le16(bytes, body + 2), le32(bytes, body + 4).toInt(), le16(bytes, body + 14))
            } else if (id == "data") {
                val f = fmt ?: return null
                val available = bytes.size - body
                // Streaming writers leave the size as 0 or 0xFFFFFFFF; use what is present.
                val len = if (size <= 0L || size > available) available else size.toInt()
                return Info(f[0], f[1], f[2], f[3], body, len)
            }
            if (size < 0 || size > Int.MAX_VALUE) return null
            pos = body + size.toInt() + (size.toInt() and 1)
        }
        return null
    }

    /** 给 16 位小端 PCM 加 44 字节标准 WAV 头。 */
    @JvmStatic
    @JvmOverloads
    public fun fromPcm(pcm: ByteArray, sampleRate: Int = 16_000, channels: Int = 1): ByteArray {
        val bits = 16
        val byteRate = sampleRate * channels * bits / 8
        val header = ByteBuffer.allocate(44).order(ByteOrder.LITTLE_ENDIAN)
        header.put("RIFF".toByteArray(Charsets.US_ASCII))
        header.putInt(36 + pcm.size)
        header.put("WAVE".toByteArray(Charsets.US_ASCII))
        header.put("fmt ".toByteArray(Charsets.US_ASCII))
        header.putInt(16)
        header.putShort(1)
        header.putShort(channels.toShort())
        header.putInt(sampleRate)
        header.putInt(byteRate)
        header.putShort((channels * bits / 8).toShort())
        header.putShort(bits.toShort())
        header.put("data".toByteArray(Charsets.US_ASCII))
        header.putInt(pcm.size)
        val out = ByteArray(44 + pcm.size)
        System.arraycopy(header.array(), 0, out, 0, 44)
        System.arraycopy(pcm, 0, out, 44, pcm.size)
        return out
    }

    /** 取 WAV 里的 PCM 数据,不是 WAV 时返回 null。 */
    @JvmStatic
    public fun pcmData(wav: ByteArray): ByteArray? {
        val info = parse(wav) ?: return null
        return wav.copyOfRange(info.dataOffset, info.dataOffset + info.dataLength)
    }

    private fun tag(b: ByteArray, at: Int): String =
        if (at + 4 > b.size) "" else String(b, at, 4, Charsets.US_ASCII)

    private fun le16(b: ByteArray, at: Int): Int = (b[at].toInt() and 0xFF) or ((b[at + 1].toInt() and 0xFF) shl 8)

    private fun le32(b: ByteArray, at: Int): Long =
        ((b[at].toLong() and 0xFF) or ((b[at + 1].toLong() and 0xFF) shl 8) or
            ((b[at + 2].toLong() and 0xFF) shl 16) or ((b[at + 3].toLong() and 0xFF) shl 24))
}