You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过Websocket将JS音频流传输至C#服务器遇重复字节及波形头损坏问题

WebSocket音频流传输问题:重复字节与Wave头损坏的解决方法

问题原因分析

  • 重复接收相同字节:客户端直接对ArrayBuffer调用btoa()是错误的,btoa()仅支持二进制字符串输入,传入ArrayBuffer会被强制转为固定字符串"[object ArrayBuffer]",导致每次发送的Base64编码完全一致,服务器接收的字节自然重复。
  • Wave头损坏报错:客户端发送的是原始PCM音频数据,缺少WAV文件必需的文件头结构,而SoundPlayer仅能识别完整的WAV格式文件,因此抛出头损坏异常。

解决方案

一、客户端JavaScript代码修复

  1. 修正Float32到Int16的转换逻辑:完整转换所有音频样本,适配有符号Int16的取值范围。
  2. 实现正确的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数据:

  1. 通过NuGet安装NAudio包
  2. 修改服务器代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 09:50:25