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 ,扩展格式可能为18 或40 ,小端存储) |
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