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

PCM转WAV后音频慢放、调采样率变调问题求助

问题描述

从语音通话功能中提取PCM格式音频数据,转换为字节数组后写入.wav文件并下载,播放该文件时语音呈现慢放效果。尝试直接将采样率改为16000时音频更慢;提高采样率则语速加快但音调严重失真,求解决思路。

代码片段

const audioData = {
  track: null,
  channels: []
}

// 存储PCM数据的逻辑
const currentChannelData = buffer.getChannelData(channel);
console.log("PCM data in channel", channel, currentChannelData);
audioData.channels.push(Array.from(currentChannelData));
console.log(buffer.sampleRate)
sampleRate = buffer.sampleRate;

// 日志显示采样率为48000

// 转换并保存WAV文件的逻辑
function saveWavFile() {
    // Concatenate all channels
    const mergedChannels = audioData.channels.flat();
    const wavByteArray = pcmToWav(mergedChannels, sampleRate);

    // Create Blob from Uint8Array
    const wavBlob = new Blob([wavByteArray], { type: 'audio/wav' });
    const url = URL.createObjectURL(wavBlob);
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.style = 'display: none';
    a.href = url;
    a.download = 'recording.wav';
    a.click();
    window.URL.revokeObjectURL(url);
  }

function pcmToWav(channelData, sampleRate) {
    const numFrames = channelData.length;
    const arrayBuffer = new ArrayBuffer(44 + numFrames * 2);
    const view = new DataView(arrayBuffer);

    // Write the WAV container
    writeString(view, 0, 'RIFF');
    view.setUint32(4, 36 + numFrames * 2, true);
    writeString(view, 8, 'WAVE');
    writeString(view, 12, 'fmt ');
    view.setUint32(16, 16, true);
    view.setUint16(20, 1, true); // PCM format
    view.setUint16(22, 1, true); // Mono (1 channel)
    view.setUint32(24, sampleRate, true);
    view.setUint32(28, sampleRate * 2, true); // Byte rate
    view.setUint16(32, 2, true); // Block align
    view.setUint16(34, 16, true); // 16-bit sample size
    writeString(view, 36, 'data');
    view.setUint32(40, numFrames * 2, true);

    // Write the PCM samples
    for (let i = 0; i < numFrames; i++) {
        view.setInt16(44 + i * 2, channelData[i] * 0x7FFF, true);
    }

    // Convert ArrayBuffer to Uint8Array
    const byteArray = new Uint8Array(arrayBuffer);

    return byteArray;
  }
解决思路

核心问题定位

慢放的本质是WAV文件头声明的参数与实际PCM数据的格式/结构不匹配,代码中最明显的错误是多通道数据的合并逻辑错误。

具体修复步骤

1. 修正多通道合并逻辑

当前用audioData.channels.flat()将多通道数据直接拼接,比如立体声的左、右通道数据会变成[左1,左2,...左N,右1,右2,...右N],但WAV头声明的是单声道,播放器会把这些数据当成连续的单声道帧,相当于把2倍长度的数据用单声道播放,自然出现慢放。

两种修正方案:

  • 转单声道(推荐,适合语音场景):对所有通道的对应采样点取平均值:

    // 替换saveWavFile中的mergedChannels逻辑
    const mergedChannels = [];
    const channelCount = audioData.channels.length;
    const frameCount = audioData.channels[0].length;
    for (let i = 0; i < frameCount; i++) {
      let sum = 0;
      for (let ch = 0; ch < channelCount; ch++) {
        sum += audioData.channels[ch][i];
      }
      // 多通道取平均,保证音量正常
      mergedChannels.push(sum / channelCount);
    }
    
  • 保留立体声:修改WAV头的声道数为2,并采用交错方式写入数据:

    // 1. 修改pcmToWav中的声道相关参数
    view.setUint16(22, 2, true); // 改为2声道
    view.setUint32(28, sampleRate * 2 * 2, true); // 字节率=48000*2*2=192000
    view.setUint16(32, 4, true); // 块对齐=2*2=4
    
    // 2. 修改mergedChannels为交错结构
    const mergedChannels = [];
    const frameCount = audioData.channels[0].length;
    for (let i = 0; i < frameCount; i++) {
      mergedChannels.push(audioData.channels[0][i]); // 左声道
      mergedChannels.push(audioData.channels[1][i]); // 右声道
    }
    

2. 禁止直接修改采样率

不能仅修改WAV头的采样率参数,这会导致播放器用错误的速率解析数据,出现语速/音调失真。如果需要将采样率从48000转成其他值(比如16000),必须使用重采样算法,可以用Web Audio API实现:

async function resamplePCM(inputData, inputSampleRate, outputSampleRate) {
  const audioContext = new AudioContext({ sampleRate: outputSampleRate });
  const buffer = audioContext.createBuffer(1, inputData.length, inputSampleRate);
  buffer.copyToChannel(new Float32Array(inputData), 0);
  
  const offlineCtx = new OfflineAudioContext(
    1, 
    Math.floor(inputData.length * outputSampleRate / inputSampleRate), 
    outputSampleRate
  );
  const source = offlineCtx.createBufferSource();
  source.buffer = buffer;
  source.connect(offlineCtx.destination);
  source.start();
  
  const resampledBuffer = await offlineCtx.startRendering();
  return Array.from(resampledBuffer.getChannelData(0));
}

// 在saveWavFile中使用
const resampledData = await resamplePCM(mergedChannels, sampleRate, 16000);
const wavByteArray = pcmToWav(resampledData, 16000);

3. 验证WAV头参数一致性

确保pcmToWav中的参数计算正确:

  • 字节率 = 采样率 × 声道数 × 位深/8
  • 块对齐 = 声道数 × 位深/8
  • 数据块大小 = 帧数 × 声道数 × 位深/8

比如单声道16位48000采样率:

  • 字节率 = 48000×1×2 = 96000
  • 块对齐 = 1×2 = 2
  • 数据块大小 = 帧数×2

原始代码中这部分计算是正确的,但修改声道数后要对应更新这些值。


内容的提问来源于stack exchange,提问作者Xavier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 17:29:57