如何在C#中检测WAV文件是否仅完成部分上传?
这确实是个很典型的坑——WAV文件的元数据(包括时长信息)存在文件头部,上传刚开始时,发送端可能就已经把完整的RIFF头写入共享路径的文件了,哪怕后续音频数据传输中断,文件大小会被预分配成完整尺寸,MCI这类工具只会读取头部的时长字段,自然会返回“完整时长”,但实际后面的音频数据都是空字节(表现为静音)。
给你几个可行的检测方案,从严谨到便捷:
方案1:解析WAV结构,检测实际有效音频数据
核心思路是:跳过WAV头部的声明信息,直接扫描音频数据块的实际有效内容(非零字节部分),再结合音频参数计算真实时长。
第一步:解析WAV头部获取关键参数
先从WAV文件里读出采样率、声道数、位深这些核心参数,用来计算每秒音频对应的字节数:
private static (int SampleRate, int Channels, int BitsPerSample) GetWavAudioParams(string wavFilePath) { using (var stream = new FileStream(wavFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { // 跳过RIFF头,定位到fmt块的参数区域 stream.Seek(20, SeekOrigin.Begin); // 读取声道数 byte[] channelBytes = new byte[2]; stream.Read(channelBytes, 0, 2); short channels = BitConverter.ToInt16(channelBytes, 0); // 读取采样率 byte[] rateBytes = new byte[4]; stream.Read(rateBytes, 0, 4); int sampleRate = BitConverter.ToInt32(rateBytes, 0); // 跳过字节率、块对齐,读取位深 stream.Seek(6, SeekOrigin.Current); byte[] bitBytes = new byte[2]; stream.Read(bitBytes, 0, 2); short bitsPerSample = BitConverter.ToInt16(bitBytes, 0); return (sampleRate, channels, bitsPerSample); } }
第二步:扫描音频数据块的有效长度
从文件末尾往前找第一个非零字节,确定实际有音频数据的长度:
private static long GetValidAudioDataLength(string wavFilePath) { using (var stream = new FileStream(wavFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { // 先定位到data块的起始位置 long dataChunkStart = FindDataChunkStart(stream); if (dataChunkStart == -1) return 0; long totalDataBytes = stream.Length - dataChunkStart; long currentPos = totalDataBytes; byte[] buffer = new byte[4096]; // 用4KB块读取,平衡效率和内存 int bytesRead; // 从末尾往前逐块扫描非零字节 while (currentPos > 0) { long readPos = dataChunkStart + Math.Max(0, currentPos - buffer.Length); stream.Seek(readPos, SeekOrigin.Begin); bytesRead = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, currentPos)); // 从当前块的末尾往前找第一个非零字节 for (int i = bytesRead - 1; i >= 0; i--) { if (buffer[i] != 0) { return readPos - dataChunkStart + i + 1; } } currentPos -= bytesRead; } return 0; } } // 辅助方法:遍历WAV子块,找到data块的起始位置 private static long FindDataChunkStart(FileStream stream) { stream.Seek(0, SeekOrigin.Begin); byte[] riffHeader = new byte[12]; stream.Read(riffHeader, 0, 12); // 简单校验是否为标准RIFF WAV文件 if (Encoding.ASCII.GetString(riffHeader, 0, 4) != "RIFF" || Encoding.ASCII.GetString(riffHeader, 8, 4) != "WAVE") return -1; // 遍历所有子块,找到data块 byte[] chunkHeader = new byte[8]; while (stream.Position < stream.Length) { stream.Read(chunkHeader, 0, 8); string chunkId = Encoding.ASCII.GetString(chunkHeader, 0, 4); int chunkSize = BitConverter.ToInt32(chunkHeader, 4); if (chunkId == "data") { return stream.Position; } // 跳过当前块的内容,继续找下一个块 stream.Seek(chunkSize, SeekOrigin.Current); } return -1; }
第三步:计算真实时长
结合上面两个方法的结果,算出实际有效的录音时长:
public static int GetActualRecordingLength(string wavFilePath) { var (sampleRate, channels, bitsPerSample) = GetWavAudioParams(wavFilePath); long validDataLength = GetValidAudioDataLength(wavFilePath); if (validDataLength == 0) return 0; // 计算每秒音频对应的字节数 int bytesPerSecond = sampleRate * channels * (bitsPerSample / 8); // 转换成毫秒级时长 return (int)(validDataLength * 1000L / bytesPerSecond); }
方案2:从上传流程根源避免问题(可控场景)
如果上传逻辑是你这边可以控制的,建议做以下优化,从源头避免误判:
- 上传时先写入临时文件(比如命名为
xxx.wav.tmp) - 只有当100%上传完成后,再将临时文件重命名为正式的
.wav文件 - 检测时只处理正式命名的文件,临时文件直接忽略
这种方式比事后检测更可靠,尤其适合网络不稳定的场景。
注意事项
- 读取共享路径文件时,一定要加上
FileShare.ReadWrite参数,避免上传进程占用文件导致读取失败 - 上述代码仅针对PCM格式的WAV文件(MCI默认处理的
waveaudio类型),如果是压缩格式WAV(比如ADPCM),需要调整解析逻辑 - 块读取的大小可以根据实际文件规模调整,4KB是兼顾效率和内存的均衡选择
内容的提问来源于stack exchange,提问作者komodosp
相关产品推荐
相关产品推荐

