AudioRecord 录制pcm转wav

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

1、Java实现wav音频文件换为pcm音频文件(AudioUtils.java) 2、Java实现播放pcm音频文件(PCMPlay.java) WAVwav是一种无损的音频文件格式,WAV符合 PIFF(Resource Interchange File Format)规范。所有的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。 PCM:PCM(Pulse Code Modulation----脉码调制录音)。所谓PCM录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由[1]、[0]等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。 简单来说:wav是一种无损的音频文件格式,pcm是没有压缩的编码方式。 WAVPCM的关系 WAV可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中,和AVI一样,只要安装好了相应的Decode,就可以欣赏这些WAV了。在Windows平台下,基于PCM编码的WAV是被支持得最好的音频格式,所有音频软件都能完美支持,由于本身可以达到较高的音质的要求,因此,WAV也是音乐编辑创作的首选格式,适合保存音乐素材。因此,基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互换之中,例如MP3换成WMA。 简单来说:pcm是无损wav文件中音频数据的一种编码方式,但wav还可以用其它方式编码。
好的,以下是用 Android 的 AudioRecord录制 wav 音频文件的代码示例: ```java private void startRecording() { // 设置音频录制参数 int audioSource = MediaRecorder.AudioSource.MIC; int sampleRate = 44100; int channelConfig = AudioFormat.CHANNEL_IN_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioRecord audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); // 开始录制音频 audioRecord.startRecording(); byte[] audioData = new byte[bufferSizeInBytes]; // 创建 wav 文件 String filePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.wav"; try { FileOutputStream fos = new FileOutputStream(filePath); // 写入 wav 文件头 writeWaveFileHeader(fos, audioData.length); while (isRecording) { // 读取录制的音频数据 int readSize = audioRecord.read(audioData, 0, bufferSizeInBytes); if (AudioRecord.ERROR_INVALID_OPERATION != readSize) { // 写入 wav 文件 fos.write(audioData, 0, readSize); } } // 关闭文件流 fos.close(); } catch (IOException e) { e.printStackTrace(); } // 停止录制音频 audioRecord.stop(); audioRecord.release(); } private void writeWaveFileHeader(FileOutputStream fos, int audioDataLength) throws IOException { // 音频数据大小 long dataSize = audioDataLength; // 文件大小 long fileSize = 36 + dataSize; // 采样率 int sampleRate = 44100; // 声道数 int numChannels = 1; // 位深度 int bitsPerSample = 16; byte[] header = new byte[44]; // ChunkID header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; // ChunkSize header[4] = (byte) (fileSize & 0xff); header[5] = (byte) ((fileSize >> 8) & 0xff); header[6] = (byte) ((fileSize >> 16) & 0xff); header[7] = (byte) ((fileSize >> 24) & 0xff); // Format header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; // Subchunk1ID header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; // Subchunk1Size header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; // AudioFormat header[20] = 1; header[21] = 0; // NumChannels header[22] = (byte) numChannels; header[23] = 0; // SampleRate header[24] = (byte) (sampleRate & 0xff); header[25] = (byte) ((sampleRate >> 8) & 0xff); header[26] = (byte) ((sampleRate >> 16) & 0xff); header[27] = (byte) ((sampleRate >> 24) & 0xff); // ByteRate int byteRate = sampleRate * numChannels * bitsPerSample / 8; header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); // BlockAlign int blockAlign = numChannels * bitsPerSample / 8; header[32] = (byte) (blockAlign & 0xff); header[33] = (byte) ((blockAlign >> 8) & 0xff); // BitsPerSample header[34] = (byte) bitsPerSample; header[35] = 0; // Subchunk2ID header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; // Subchunk2Size header[40] = (byte) (dataSize & 0xff); header[41] = (byte) ((dataSize >> 8) & 0xff); header[42] = (byte) ((dataSize >> 16) & 0xff); header[43] = (byte) ((dataSize >> 24) & 0xff); fos.write(header, 0, 44); } ``` 以上代码中,我们首先设置了音频录制的参数:音频源为麦克风,采样率为 44100 Hz,声道数为单声道,位深度为 16 位。然后通过 AudioRecord.getMinBufferSize() 方法获取音频数据缓冲区的大小,并创建 AudioRecord 实例。 接着,我们开始录制音频,并创建一个 FileOutputStream 实例来写入 wav 文件。在录制过程中,我们不断读取录制的音频数据,并将其写入 wav 文件。最后,我们在停止录制音频之前,关闭文件流,并释放 AudioRecord 实例。 在写入 wav 文件之前,我们需要先写入 wav 文件头。wav 文件头的格式如下: | 字段 | 长度 | 说明 | | --- | --- | --- | | ChunkID | 4 | 固定值 "RIFF" | | ChunkSize | 4 | 文件大小(不包括 ChunkID 和 ChunkSize)| | Format | 4 | 固定值 "WAVE" | | Subchunk1ID | 4 | 固定值 "fmt " | | Subchunk1Size | 4 | 16(表示 Subchunk1 的大小)| | AudioFormat | 2 | 音频格式(1 表示 PCM)| | NumChannels | 2 | 声道数 | | SampleRate | 4 | 采样率 | | ByteRate | 4 | 每秒传输的字节数 | | BlockAlign | 2 | 每个采样点占用的字节数 | | BitsPerSample | 2 | 位深度 | | Subchunk2ID | 4 | 固定值 "data" | | Subchunk2Size | 4 | 音频数据大小 | 我们可以通过 writeWaveFileHeader() 方法来写入 wav 文件头。在该方法中,我们需要传入 wav 文件的 FileOutputStream 对象和音频数据的长度,然后按照上述格式写入 wav 文件头。最后,我们在 wav 文件头的末尾写入音频数据大小即可。 希望以上代码能够帮助到你!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周周都刷火焰猫头鹰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值