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

JavaScript WebSocket音频流转Python PyAudio仅输出噪音问题排查

问题解决与代码修正

核心问题分析

  1. 端口不匹配:JavaScript中WebSocket连接的端口是9001,但Python服务端监听的是8888,数据无法正确传输。
  2. 采样率硬编码错误:JS中固定使用44100作为输入采样率,而现代浏览器AudioContext默认采样率多为48000,导致降采样计算逻辑错误。
  3. 数据范围溢出:降采样时未将音频数据限制在[-1,1]区间,超出Int16类型的合法取值范围(-32768~32767),造成音频失真。
  4. 声道不确定性:未强制麦克风输出单通道,若获取到立体声数据,仅取左声道会导致数据异常。

修正后的代码

1. Python WebSocket服务端

统一端口并简化数据写入逻辑:

import pyaudio
import asyncio
import websockets

CHUNK_SIZE = 1024

audio = pyaudio.PyAudio()
# 确保输出格式与JS发送的完全匹配:单通道、16000采样率、Int16格式
stream = audio.open(format=pyaudio.paInt16,
                    channels=1,
                    rate=16000,
                    output=True,
                    frames_per_buffer=CHUNK_SIZE)


async def websocket_handler(websocket, path):
    try:
        async for message in websocket:
            # 直接写入原始字节数据,无需额外转换
            stream.write(message)
    finally:
        stream.stop_stream()
        stream.close()
        audio.terminate()

async def start_websocket_server():
    # 监听8888端口,与JS客户端保持一致
    async with websockets.serve(websocket_handler, '', 8888):
        await asyncio.Future()  # 保持服务持续运行


if __name__ == '__main__':
    asyncio.run(start_websocket_server())

2. JavaScript客户端

修正端口、采样率获取逻辑、降采样函数及麦克风参数:

//================= CONFIG =================
// 统一端口为8888,与服务端匹配
let websocket_uri = 'ws://127.0.0.1:8888';
let bufferSize = 4096,
    AudioContext,
    context,
    processor,
    input,
    globalStream,
    websocket;

// Initialize WebSocket
initWebSocket();

//================= RECORDING =================
function startRecording() {
    let streamStreaming = true;
    AudioContext = window.AudioContext || window.webkitAudioContext;
    context = new AudioContext({
      latencyHint: 'interactive',
    });
    processor = context.createScriptProcessor(bufferSize, 1, 1);
    processor.connect(context.destination);
    context.resume();
  
    var handleSuccess = function (stream) {
      globalStream = stream;
      input = context.createMediaStreamSource(stream);
      input.connect(processor);
  
      processor.onaudioprocess = function (e) {
        var left = e.inputBuffer.getChannelData(0);
        // 使用AudioContext实际采样率,避免硬编码错误
        var left16 = downsampleBuffer(left, context.sampleRate, 16000);
        websocket.send(left16);
      };
    };
  
    // 强制麦克风输出单通道,关闭降噪减少干扰
    navigator.mediaDevices.getUserMedia({
      audio: {
        channelCount: 1,
        echoCancellation: false,
        noiseSuppression: false
      },
      video: false
    }).then(handleSuccess);
} // closes function startRecording()

function stopRecording() {
    let streamStreaming = false;
  
    let track = globalStream.getTracks()[0];
    track.stop();
  
    input.disconnect(processor);
    processor.disconnect(context.destination);
    context.close().then(function () {
      input = null;
      processor = null;
      context = null;
      AudioContext = null;
    });
} // closes function stopRecording()

function initWebSocket() {
    websocket = new WebSocket(websocket_uri);
  
    websocket.onopen = function() {
      console.log("connected to server");
      document.getElementById("webSocketStatus").innerHTML = 'Connected';
    }
    
    websocket.onclose = function(e) {
      console.log("connection closed (" + e.code + ")");
      document.getElementById("webSocketStatus").innerHTML = 'Not Connected';
    }
    
    websocket.onmessage = function(e) {
      console.log(e.data);
  
      try {
        result = JSON.parse(e.data);
      }  catch (e) {
        document.querySelector('.message').innerHTML = 'Error retrieving data: ' + e;
      }
  
      if (typeof(result) !== 'undefined' && typeof(result.error) !== 'undefined') {
        document.querySelector('.message').innerHTML = 'Error: ' + result.error;
      }
      else {
        document.querySelector('.message').innerHTML = 'Welcome!';
      }
    }
} // closes function initWebSocket()

function downsampleBuffer (buffer, sampleRate, outSampleRate) {
    if (outSampleRate === sampleRate) {
      return new Int16Array(buffer.map(x => Math.max(-1, Math.min(1, x)) * 0x7fff)).buffer;
    }
    if (outSampleRate > sampleRate) {
      throw 'downsampling rate should be smaller than original sample rate';
    }
    var sampleRateRatio = sampleRate / outSampleRate;
    var newLength = Math.round(buffer.length / sampleRateRatio);
    var result = new Int16Array(newLength);
    var offsetResult = 0;
    var offsetBuffer = 0;
    while (offsetResult < result.length) {
      var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
      var accum = 0,
        count = 0;
      for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
        accum += buffer[i];
        count++;
      }
      // 将数据严格限制在[-1,1]区间,再转换为Int16格式
      var normalized = Math.max(-1, Math.min(1, accum / count));
      result[offsetResult] = normalized * 0x7fff;
      offsetResult++;
      offsetBuffer = nextOffsetBuffer;
    }
    return result.buffer;
} // closes function downsampleBuffer()

额外说明

  • 运行前需安装依赖:pip install pyaudio websockets numpy
  • 浏览器需在HTTPS或localhost环境下才能调用麦克风API
  • 若仍有噪音,可调整bufferSize大小,或检查系统音频设备的输入输出设置

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 07:23:09