怎样制作网页链接教程seo站点是什么意思
pcm转wav
- PCM 格式校验
- pcm 添加 wav 头信息
- WAV
- 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 -> 1AudioFormat.ENCODING_PCM_16BIT -> 2AudioFormat.ENCODING_PCM_FLOAT -> 4else -> 2 // 不会执行(已校验)}// 1. 最小帧检查val minFrameSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)val minRequiredSize = minFrameSize * MIN_FRAME_MULTIPLIERif (file.length() < minRequiredSize) {Log.e(TAG, "PCM文件过小: ${file.length()} bytes (至少需要 $minRequiredSize bytes)")return false}// 2. 数据有效性检查val buffer = ByteArray(1024)var zeroCount = 0var totalSamples = 0FileInputStream(file).use { stream ->var bytesRead: Intwhile (true) {bytesRead = stream.read(buffer)if (bytesRead == -1) breakval samples = bytesRead / bytesPerSampleval byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead).apply {order(ByteOrder.LITTLE_ENDIAN)}when (bytesPerSample) {1 -> { // 8-bit unsignedfor (i in 0 until samples) {val value = byteBuffer.get().toInt() and 0xFFif (value == 128) zeroCount++ // 静音中点为128}}2 -> { // 16-bit signedfor (i in 0 until samples) {if (byteBuffer.short == 0.toShort()) zeroCount++}}4 -> { // 32-bit floatfor (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() / totalSamplesif (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 + 36header[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] = 16header[17] = 0header[18] = 0header[19] = 0// AudioFormat,音频格式(PCM为1)header[20] = 1header[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 / 8header[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 / 8header[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,音频数据长度 dataHdrLengthheader[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() - 8if (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 = falsevar dataSize = 0LchunkLoop@ 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 = trueraf.skipBytes(chunkSize.toInt() - 16) // 跳过剩余格式数据}"data" -> { // 数据块验证/* --------------- 数据块大小验证 --------------- */dataSize = chunkSizeval declaredDataEnd = raf.filePointer + chunkSizeval 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: Intwhile (input.read(buffer).also { bytesRead = it } != -1) {// 确保读取的是完整shortval 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