通过Websocket将JS音频流传输至C#服务器遇重复字节及波形头损坏问题
WebSocket音频流传输问题:重复字节与Wave头损坏的解决方法
问题原因分析
- 重复接收相同字节:客户端直接对
ArrayBuffer调用btoa()是错误的,btoa()仅支持二进制字符串输入,传入ArrayBuffer会被强制转为固定字符串"[object ArrayBuffer]",导致每次发送的Base64编码完全一致,服务器接收的字节自然重复。 - Wave头损坏报错:客户端发送的是原始PCM音频数据,缺少WAV文件必需的文件头结构,而
SoundPlayer仅能识别完整的WAV格式文件,因此抛出头损坏异常。
解决方案
一、客户端JavaScript代码修复
- 修正Float32到Int16的转换逻辑:完整转换所有音频样本,适配有符号Int16的取值范围。
- 实现正确的ArrayBuffer转Base64方法:先将缓冲区转为Uint8Array,再生成二进制字符串后编码。
修复后的客户端代码:
function audioplayer(){ var stream; var bufferSize = 1024 * 16; var audioContext = new AudioContext(); // 注:ScriptProcessorNode已废弃,推荐使用AudioWorklet替代,此处保留兼容现有逻辑 var processor = audioContext.createScriptProcessor(bufferSize, 1, 1); processor.connect(audioContext.destination); navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then( streamObj => { stream = streamObj; input = audioContext.createMediaStreamSource(stream); input.connect(processor); processor.onaudioprocess = e => { microphoneProcess(e); }; }); } function microphoneProcess(e) { const left = e.inputBuffer.getChannelData(0); // 获取单声道音频数据 const pcm16Buffer = convertFloat32ToInt16(left); // 转换为16位PCM格式 const base64Audio = arrayBufferToBase64(pcm16Buffer); // 转为Base64编码 var obj = { command:"mic" , audio : base64Audio }; ws.send(JSON.stringify(obj)); // 通过WebSocket发送 } function convertFloat32ToInt16(buffer) { const int16Buffer = new Int16Array(buffer.length); for (let i = 0; i < buffer.length; i++) { // Float32范围是-1~1,映射到Int16的-32768~32767 let val = buffer[i] * 32767; // 限制值范围防止溢出 val = Math.max(-32768, Math.min(32767, val)); int16Buffer[i] = val; } return int16Buffer.buffer; } // 正确将ArrayBuffer转为Base64编码 function arrayBufferToBase64(buffer) { const uint8Array = new Uint8Array(buffer); let binary = ''; const len = uint8Array.length; for (let i = 0; i < len; i++) { binary += String.fromCharCode(uint8Array[i]); } return btoa(binary); }
二、C#服务器端代码修复
客户端发送的是原始16位单声道PCM数据,SoundPlayer无法直接播放,提供两种可行方案:
方案1:手动添加WAV文件头
为原始PCM数据补充标准WAV文件头,再使用SoundPlayer播放:
if (data == null) return; // 音频参数需与客户端一致:单声道、16位采样、44100Hz采样率 int sampleRate = 44100; int channels = 1; int bitsPerSample = 16; using (MemoryStream wavStream = new MemoryStream()) { BinaryWriter writer = new BinaryWriter(wavStream); // 写入RIFF标识 writer.Write(new char[] { 'R', 'I', 'F', 'F' }); // 文件总长度(头长度36 + PCM数据长度) writer.Write(36 + data.Length); // 写入WAVE标识 writer.Write(new char[] { 'W', 'A', 'V', 'E' }); // 写入fmt子块 writer.Write(new char[] { 'f', 'm', 't', ' ' }); writer.Write(16); // PCM格式的子块大小 writer.Write((short)1); // 音频格式(PCM=1) writer.Write((short)channels); // 声道数 writer.Write(sampleRate); // 采样率 writer.Write(sampleRate * channels * bitsPerSample / 8); // 字节率 writer.Write((short)(channels * bitsPerSample / 8)); // 块对齐 writer.Write((short)bitsPerSample); // 采样位深 // 写入data子块 writer.Write(new char[] { 'd', 'a', 't', 'a' }); writer.Write(data.Length); // PCM数据长度 // 写入原始PCM数据 writer.Write(data); writer.Flush(); wavStream.Position = 0; try { using (SoundPlayer player = new SoundPlayer(wavStream)) { player.Play(); } } catch (Exception ex) { // 异常处理 Console.WriteLine(ex.Message); } } DataRecv.Text = data.Length.ToString();
方案2:使用NAudio库播放原始PCM
NAudio是.NET生态常用音频处理库,支持直接播放原始PCM数据:
- 通过NuGet安装
NAudio包 - 修改服务器代码:
using NAudio.Wave; // ... if (data == null) return; // 配置音频参数(需与客户端一致) WaveFormat waveFormat = new WaveFormat(44100, 16, 1); using (MemoryStream ms = new MemoryStream(data)) using (RawSourceWaveStream rawStream = new RawSourceWaveStream(ms, waveFormat)) using (WaveOutEvent waveOut = new WaveOutEvent()) { waveOut.Init(rawStream); waveOut.Play(); // 等待播放完成(按需调整,异步场景可省略) while (waveOut.PlaybackState == PlaybackState.Playing) { System.Threading.Thread.Sleep(100); } } DataRecv.Text = data.Length.ToString();
内容的提问来源于stack exchange,提问作者Mr X
相关产品推荐
相关产品推荐

