JavaScript WebSocket音频流转Python PyAudio仅输出噪音问题排查
问题解决与代码修正
核心问题分析
- 端口不匹配:JavaScript中WebSocket连接的端口是9001,但Python服务端监听的是8888,数据无法正确传输。
- 采样率硬编码错误:JS中固定使用44100作为输入采样率,而现代浏览器
AudioContext默认采样率多为48000,导致降采样计算逻辑错误。 - 数据范围溢出:降采样时未将音频数据限制在[-1,1]区间,超出Int16类型的合法取值范围(-32768~32767),造成音频失真。
- 声道不确定性:未强制麦克风输出单通道,若获取到立体声数据,仅取左声道会导致数据异常。
修正后的代码
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
相关产品推荐
相关产品推荐

