AudioRecord 录制pcm转wav

发布于:2025-04-13 ⋅ 阅读:(45) ⋅ 点赞:(0)

PCM 格式校验

/**
 * 专业PCM文件验证(支持动态参数与多格式)
 * @param silenceThreshold 静音检测阈值(0.0~1.0),默认90%零值为静音
 * @return false表示文件无效(自动打印错误日志)
 */
fun validatePcmFile(
    file: File,
    sampleRate: Int,
    channelConfig: Int,
    audioFormat: Int,
    silenceThreshold: Float = 0.9f
): Boolean {
    // 基础参数校验
    require(silenceThreshold in 0.0f..1.0f) { "静音阈值必须在0~1之间" }
    require(audioFormat == AudioFormat.ENCODING_PCM_8BIT || 
            audioFormat == AudioFormat.ENCODING_PCM_16BIT || 
            audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
        "不支持的音频格式: $audioFormat"
    }

    // 基础检查
    if (!file.exists()) {
        Log.e(TAG, "PCM文件不存在: ${file.absolutePath}")
        return false
    }
    if (file.length() == 0L) {
        Log.e(TAG, "PCM文件为空: ${file.absolutePath}")
        return false
    }

    // 调试日志
    Log.d(TAG, "开始校验PCM文件: ${file.name} [大小=${file.length()} bytes]")

    try {
        // 计算位深度和字节对齐
        val bytesPerSample = when (audioFormat) {
            AudioFormat.ENCODING_PCM_8BIT -> 1
            AudioFormat.ENCODING_PCM_16BIT -> 2
            AudioFormat.ENCODING_PCM_FLOAT -> 4
            else -> 2 // 不会执行(已校验)
        }

        // 1. 最小帧检查
        val minFrameSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
        val minRequiredSize = minFrameSize * MIN_FRAME_MULTIPLIER
        if (file.length() < minRequiredSize) {
            Log.e(TAG, "PCM文件过小: ${file.length()} bytes (至少需要 $minRequiredSize bytes)")
            return false
        }

        // 2. 数据有效性检查
        val buffer = ByteArray(1024)
        var zeroCount = 0
        var totalSamples = 0

        FileInputStream(file).use { stream ->
            var bytesRead: Int
            while (true) {
                bytesRead = stream.read(buffer)
                if (bytesRead == -1) break

                val samples = bytesRead / bytesPerSample
                val byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead).apply {
                    order(ByteOrder.LITTLE_ENDIAN)
                }

                when (bytesPerSample) {
                    1 -> { // 8-bit unsigned
                        for (i in 0 until samples) {
                            val value = byteBuffer.get().toInt() and 0xFF
                            if (value == 128) zeroCount++ // 静音中点为128
                        }
                    }
                    2 -> { // 16-bit signed
                        for (i in 0 until samples) {
                            if (byteBuffer.short == 0.toShort()) zeroCount++
                        }
                    }
                    4 -> { // 32-bit float
                        for (i in 0 until samples) {
                            if (abs(byteBuffer.float - 0.0f) < 1e-6) zeroCount++
                        }
                    }
                }
                totalSamples += samples
            }
        }

        Log.d(TAG, "样本统计: 总数=$totalSamples, 零值=$zeroCount")

        // 静音检测
        if (totalSamples > 0) {
            val zeroRatio = zeroCount.toFloat() / totalSamples
            if (zeroRatio > silenceThreshold) {
                Log.e(TAG, "静音数据过多: ${"%.1f%%".format(zeroRatio * 100)} ≥ ${"%.0f%%".format(silenceThreshold * 100)}")
                return false
            }
        } else {
            Log.e(TAG, "文件无有效样本数据")
            return false
        }

        Log.i(TAG, "PCM文件验证通过")
        return true
    } catch (e: Exception) {
        Log.e(TAG, "验证异常: ${e.javaClass.simpleName} - ${e.message}", e)
        return false
    }
}

pcm 添加 wav 头信息

 /**
     * 头部信息共44字节
     * @param sampleRate
     * @param channels
     * @param bitDepth
     * @param dataSize
     * @return
     * @throws IOException
     */
    fun getWavHeader(sampleRate: Int, channels: Int, bitDepth: Int, dataSize: Long): ByteArray {
        val header = ByteArray(44)
        // ChunkID,RIFF标识
        header[0] = 'R'.code.toByte()
        header[1] = 'I'.code.toByte()
        header[2] = 'F'.code.toByte()
        header[3] = 'F'.code.toByte()


        // ChunkSize,文件长度
        val totalSize = dataSize + 36
        header[4] = (totalSize and 0xffL).toByte()
        header[5] = ((totalSize shr 8) and 0xffL).toByte()
        header[6] = ((totalSize shr 16) and 0xffL).toByte()
        header[7] = ((totalSize shr 24) and 0xffL).toByte()
        // Format,WAVE标识
        header[8] = 'W'.code.toByte()
        header[9] = 'A'.code.toByte()
        header[10] = 'V'.code.toByte()
        header[11] = 'E'.code.toByte()


        // Subchunk1ID,fmt标识
        header[12] = 'f'.code.toByte()
        header[13] = 'm'.code.toByte()
        header[14] = 't'.code.toByte()
        header[15] = ' '.code.toByte()


        // Subchunk1Size,格式信息长度
        header[16] = 16
        header[17] = 0
        header[18] = 0
        header[19] = 0


        // AudioFormat,音频格式(PCM为1)
        header[20] = 1
        header[21] = 0


        // NumChannels,声道数
        header[22] = channels.toByte()
        header[23] = 0


        // SampleRate,采样率
        header[24] = (sampleRate and 0xff).toByte()
        header[25] = ((sampleRate shr 8) and 0xff).toByte()
        header[26] = ((sampleRate shr 16) and 0xff).toByte()
        header[27] = ((sampleRate shr 24) and 0xff).toByte()
        // ByteRate,比特率
        val byteRate = sampleRate * channels * bitDepth / 8
        header[28] = (byteRate and 0xff).toByte()
        header[29] = ((byteRate shr 8) and 0xff).toByte()
        header[30] = ((byteRate shr 16) and 0xff).toByte()
        header[31] = ((byteRate shr 24) and 0xff).toByte()
        // BlockAlign,块对齐
        val blockAlign = channels * bitDepth / 8
        header[32] = blockAlign.toByte()
        header[33] = 0


        // BitsPerSample,采样位深度
        header[34] = bitDepth.toByte()
        header[35] = 0


        // Subchunk2ID,data标识
        header[36] = 'd'.code.toByte()
        header[37] = 'a'.code.toByte()
        header[38] = 't'.code.toByte()
        header[39] = 'a'.code.toByte()


        // Subchunk2Size,音频数据长度 dataHdrLength
        header[40] = (dataSize and 0xffL).toByte()
        header[41] = ((dataSize shr 8) and 0xffL).toByte()
        header[42] = ((dataSize shr 16) and 0xffL).toByte()
        header[43] = ((dataSize shr 24) and 0xffL).toByte()
        return header
    }

WAV

DEPSEK

偏移地址 字段名称 说明
00-03 ChunkId 固定值 "RIFF" (ASCII编码)
04-07 ChunkSize 文件总字节数 - 8(从地址08开始到文件末尾的总字节数,小端存储)
08-11 fccType 固定值 "WAVE" (ASCII编码)
12-15 SubChunkId1 固定值 "fmt "(包含末尾空格)
16-19 SubChunkSize1 fmt块数据大小(标准PCM为16,扩展格式可能为1840,小端存储)
20-21 FormatTag 编码格式:1=PCM,3=IEEE浮点(小端存储)
22-23 Channels 声道数:1=单声道,2=双声道(小端存储)
24-27 SamplesPerSec 采样率(Hz,如44100,小端存储)
28-31 BytesPerSec 字节率 = 采样率 × 声道数 × 位深/8(小端存储)
32-33 BlockAlign 每帧字节数 = 声道数 × 位深/8(小端存储)
34-35 BitsPerSample 位深:8/16/24/32(小端存储)
36-39 SubChunkId2 固定值 "data"
40-43 SubChunkSize2 音频数据长度(字节数 = 采样总数 × 声道数 × 位深/8,小端存储)
44-… Data 音频原始数据(二进制格式,与FormatTag和BitsPerSample匹配)

WAV 格式检验

/**
 * 专业WAV文件验证(增强版)
 * 注意:WAV_HEADER_SIZE常量在此版本中已不再需要,因采用动态块遍历机制
 * @return false表示文件无效(自动打印错误日志)
 */
fun validateWavFile(file: File): Boolean {
    // 基础文件检查
    if (!file.exists()) {
        Log.e(TAG, "❌ WAV文件不存在: ${file.absolutePath}")
        return false
    }
    if (file.length() < MIN_WAV_FILE_SIZE) { // 至少需要包含RIFF头、WAVE标识和一个子块
        Log.e(TAG, "❌ 文件过小: ${file.length()} bytes (至少需要 $MIN_WAV_FILE_SIZE bytes)")
        return false
    }

    try {
        RandomAccessFile(file, "r").use { raf ->
            /* ------------------------- RIFF头验证 ------------------------- */
            // 读取前4字节验证RIFF标识
            val riffHeader = ByteArray(4).apply { raf.read(this) }
            if (String(riffHeader) != "RIFF") {
                Log.e(TAG, "❌ 无效的RIFF头: ${String(riffHeader)} (应为\"RIFF\")")
                return false
            }
            Log.d(TAG, "✅ RIFF头验证通过")

            /* ----------------------- RIFF块大小验证 ----------------------- */
            // 读取小端序的RIFF块大小(文件总大小-8)
            val riffChunkSize = raf.readLittleEndianInt()
            val expectedRiffSize = file.length() - 8
            if (riffChunkSize != expectedRiffSize.toInt()) {
                Log.e(TAG, "❌ RIFF大小不匹配 | 声明:$riffChunkSize | 实际:$expectedRiffSize")
                return false
            }
            Log.d(TAG, "✅ RIFF块大小验证通过 (${riffChunkSize}bytes)")

            /* ----------------------- WAVE标识验证 ------------------------ */
            val waveHeader = ByteArray(4).apply { raf.read(this) }
            if (String(waveHeader) != "WAVE") {
                Log.e(TAG, "❌ 无效的WAVE标识: ${String(waveHeader)} (应为\"WAVE\")")
                return false
            }
            Log.d(TAG, "✅ WAVE标识验证通过")

            /* --------------------- 子块遍历验证 --------------------- */
            var fmtVerified = false
            var dataSize = 0L
            
            chunkLoop@ while (raf.filePointer < file.length()) {
                // 读取当前块头信息
                val chunkId = ByteArray(4).apply { raf.read(this) }.toString(Charsets.US_ASCII)
                val chunkSize = raf.readLittleEndianInt().toLong() and 0xFFFFFFFFL // 转为无符号

                when (chunkId) {
                    "fmt " -> { // 格式块验证
                        /* --------------- 基本格式块验证 --------------- */
                        if (chunkSize < 16) {
                            Log.e(TAG, "❌ fmt块过小: $chunkSize bytes (至少需要16 bytes)")
                            return false
                        }
                        
                        // 读取音频格式(1=PCM)
                        val formatTag = raf.readLittleEndianShort()
                        if (formatTag != 1.toShort()) {
                            Log.e(TAG, "❌ 非PCM格式 | 格式码:$formatTag (应为1)")
                            return false
                        }
                        Log.d(TAG, "✅ PCM格式验证通过")

                        // 跳过声道数、采样率等参数(此处不验证具体音频参数)
                        raf.skipBytes(6) // 跳过2(short)+4(int)
                        
                        // 验证位深是否为整数字节
                        val bitsPerSample = raf.readLittleEndianShort()
                        if (bitsPerSample % 8 != 0) {
                            Log.e(TAG, "❌ 非常规位深: ${bitsPerSample}bits (应为8的倍数)")
                            return false
                        }
                        Log.d(TAG, "✅ 位深验证通过 (${bitsPerSample}bits)")

                        fmtVerified = true
                        raf.skipBytes(chunkSize.toInt() - 16) // 跳过剩余格式数据
                    }
                    
                    "data" -> { // 数据块验证
                        /* --------------- 数据块大小验证 --------------- */
                        dataSize = chunkSize
                        val declaredDataEnd = raf.filePointer + chunkSize
                        val actualDataEnd = file.length()
                        
                        if (declaredDataEnd > actualDataEnd) {
                            Log.e(TAG, "❌ 数据块越界 | 声明结束位置:$declaredDataEnd | 文件大小:$actualDataEnd")
                            return false
                        }
                        Log.d(TAG, "✅ 数据块大小验证通过 (${chunkSize}bytes)")
                        break@chunkLoop // 找到数据块后终止遍历
                    }
                    
                    else -> { // 其他块处理
                        Log.d(TAG, "⏭ 跳过块: $chunkId (${chunkSize}bytes)")
                        raf.skipBytes(chunkSize.toInt())
                    }
                }
            }

            /* ------------------- 最终完整性检查 ------------------- */
            if (!fmtVerified) {
                Log.e(TAG, "❌ 缺少必需的fmt块")
                return false
            }
            if (dataSize == 0L) {
                Log.e(TAG, "❌ 缺少data块")
                return false
            }
            
            return true
        }
    } catch (e: Exception) {
        Log.e(TAG, "❌ 文件解析异常: ${e.javaClass.simpleName} - ${e.message}")
        return false
    }
}

/* ------------------------- 扩展函数:小端序读取 ------------------------- */
private fun RandomAccessFile.readLittleEndianInt(): Int {
    return ByteArray(4).apply { read(this) }.let {
        (it[3].toInt() shl 24) or (it[2].toInt() shl 16) or (it[1].toInt() shl 8) or it[0].toInt()
    }
}

private fun RandomAccessFile.readLittleEndianShort(): Short {
    return ByteArray(2).apply { read(this) }.let {
        ((it[1].toInt() shl 8) or it[0].toInt()).toShort()
    }
}

companion object {
    private const val TAG = "WavValidator"
    private const val MIN_WAV_FILE_SIZE = 44L // RIFF头(12) + fmt块(24) + data块头(8)
}

小端序?

在 Android 中,AudioRecord 录制的音频数据默认是 PCM 格式,且字节序(Endianness)为 小端序(Little-Endian)。这是 Android 音频系统的默认行为,与大多数移动设备和 x86/ARM 平台的处理器架构一致。

大 2 小

    /**************** 字节序转换实现 ****************/
    private fun convertEndian(inputFile: File): File? {
        return try {
            val outputFile = createTempPcmFile("converted_")
            
            FileInputStream(inputFile).use { input ->
                FileOutputStream(outputFile).use { output ->
                    val buffer = ByteArray(4096) // 4KB缓冲区
                    var bytesRead: Int

                    while (input.read(buffer).also { bytesRead = it } != -1) {
                        // 确保读取的是完整short
                        val validLength = bytesRead - (bytesRead % 2)
                        if (validLength == 0) continue

                        // 转换字节序
                        convertByteOrder(buffer, validLength)
                        output.write(buffer, 0, validLength)
                    }
                }
            }
            outputFile
        } catch (e: Exception) {
            Log.e(TAG, "Endian conversion failed", e)
            null
        }
    }

    private fun convertByteOrder(data: ByteArray, length: Int) {
        val byteBuffer = ByteBuffer.wrap(data, 0, length)
        val shortBuffer = byteBuffer.order(ByteOrder.BIG_ENDIAN).asShortBuffer()
        val shorts = ShortArray(shortBuffer.remaining())
        shortBuffer.get(shorts)
        
        ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(shorts)
    }

参考地址

PCM 2 WAV:
https://blog.csdn.net/qq_36451275/article/details/135057683

PCM 2 WAV:
https://blog.csdn.net/m0_54198552/article/details/145653031

depsek

转:Android音频开发(4):PCM转WAV格式音频
https://www.jianshu.com/p/90c77197f1d4


网站公告

今日签到

点亮在社区的每一天
去签到