如何分块向浏览器流式传输音频?优化WebSocket音频输出音质
问题:如何提升网络电台的音频清晰度?
我正尝试搭建一个网络电台,希望能频繁切换歌曲、自由叠加音效;还希望对音频进行限流,以便在发送前修改音频流,同时尽可能实现连续内容播放。
目前我们使用WebSocket进行了接近成功的尝试,但音质有些糟糕:
现有实现代码
server.js
const express = require('express'); const app = express() const http = require('http') const server = http.createServer(app) const { Server } = require("socket.io") const io = new Server(server) const fs = require('fs') const SRC_PATH = 'src.wav' const PACKET_SIZE = 6400 let PACKET = 0 function getpacket(socket){ const file_descriptor = fs.openSync(SRC_PATH, 'r', null) const read_offset = PACKET * PACKET_SIZE const buffer = Buffer.alloc(PACKET_SIZE) const buffer_write_offset = 0 const num_bytes_to_read = PACKET_SIZE const num_bytes_read = fs.readSync(file_descriptor, buffer, buffer_write_offset, num_bytes_to_read, read_offset) fs.closeSync(file_descriptor) console.log(`Sending packet ${PACKET}`) socket.emit("data", buffer) PACKET++ } app.use('/', express.static('.')) io.on('connection', (socket) => { console.log("connected...") socket.on("get", ()=>{getpacket(socket)}) }) server.listen(3000, () => { console.log('listening on *:3000'); })
index.html
<!DOCTYPE html> <html> <head> <title>Testing</title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script> </head> <body> <div onclick="listen()">Click to Listen</div> <script> const socketio = io() const SAMPLE_RATE = 32000 // samples/second async function listen(){ // Set up the new audio context const audioContext = new AudioContext() socketio.once("data", (arrayBuff)=>{ const buffer = new Uint8Array(arrayBuff) addTobuffer(buffer, audioContext) }) requestData() } function requestData(){ socketio.emit("get") } async function addTobuffer(data, audioContext){ // Set up the new audio source const audioSource = await audioContext.createBufferSource() // create audio buffer from data, const audioBuffer = await createAudioBuffer(audioContext,data) // Asign the data buffer to the audioSource audioSource.buffer = audioBuffer // Connect the audio source to the audio context audioSource.connect(audioContext.destination) audioSource.start(0) // wait until just before the end and then get more data const packetLength = (data.length/SAMPLE_RATE)*1000-10 await new Promise(resolve=>setTimeout(resolve,packetLength)) socketio.once("data", (arrayBuff)=>{ const buffer = new Uint8Array(arrayBuff) addTobuffer(buffer, audioContext) }) requestData() } async function createAudioBuffer(audioContext,data){ /* uint8 pcm to float */ const number_of_channels = 1 const number_of_bytes = data.length const audioBuffer = audioContext.createBuffer(number_of_channels, number_of_bytes, SAMPLE_RATE) const nowBuffering = audioBuffer.getChannelData(0) for (let index=0; index<number_of_bytes;index++){ const thirtytwofloat = new Float32Array(1) thirtytwofloat[0] = (data[index]-(255/2))/255 nowBuffering[index] = thirtytwofloat[0] } return audioBuffer } </script> </body> </html>
生成音频文件的命令
ffmpeg -i src.mp3 -ar 32000 -ac 1 -acodec pcm_u8 src.wav
音质优化方案
1. 升级音频编码格式
当前使用的pcm_u8是8位无符号PCM,动态范围极小,是音质差的核心原因。替换为16位有符号PCM(行业标准格式):
修改ffmpeg命令:
ffmpeg -i src.mp3 -ar 32000 -ac 1 -acodec pcm_s16le src.wav
2. 修复前端音频转换逻辑
针对16位PCM调整转换代码,同时提升效率:
async function createAudioBuffer(audioContext, data){ const number_of_channels = 1; // 16位PCM每个采样占2字节,计算实际采样数 const sample_count = data.length / 2; const audioBuffer = audioContext.createBuffer(number_of_channels, sample_count, SAMPLE_RATE); const nowBuffering = audioBuffer.getChannelData(0); // 用DataView高效解析16位有符号小端数据,转换为[-1,1]范围的浮点数 const view = new DataView(data.buffer); for (let i = 0; i < sample_count; i++) { nowBuffering[i] = view.getInt16(i * 2, true) / 32768; } return audioBuffer; }
3. 优化播放时序(消除卡顿)
用音频播放结束事件替代定时器,避免时序偏差,同时提前预加载下一包:
async function addTobuffer(data, audioContext){ const audioSource = audioContext.createBufferSource(); const audioBuffer = await createAudioBuffer(audioContext, data); audioSource.buffer = audioBuffer; audioSource.connect(audioContext.destination); // 提前请求下一包,减少等待时间 requestData(); audioSource.start(); // 播放结束后自动处理下一包 audioSource.onended = () => { socketio.once("data", (arrayBuff) => { const buffer = new Uint8Array(arrayBuff); addTobuffer(buffer, audioContext); }); }; }
4. 服务器端IO优化
避免重复打开/关闭文件,提升响应速度:
// 提前打开文件描述符,复用连接 const file_descriptor = fs.openSync(SRC_PATH, 'r', null); function getpacket(socket){ const read_offset = PACKET * PACKET_SIZE; const buffer = Buffer.alloc(PACKET_SIZE); const num_bytes_read = fs.readSync(file_descriptor, buffer, 0, PACKET_SIZE, read_offset); console.log(`Sending packet ${PACKET}`); // 处理最后一包字节数不足的情况 socket.emit("data", buffer.slice(0, num_bytes_read)); PACKET++; } // 服务器关闭时释放文件资源 server.on('close', () => { fs.closeSync(file_descriptor); });
可选:提升采样率
如果带宽允许,将采样率从32000提升到44100(CD标准)或48000,进一步提升音质:
修改ffmpeg命令:
ffmpeg -i src.mp3 -ar 44100 -ac 1 -acodec pcm_s16le src.wav
同时前端SAMPLE_RATE对应改为44100。
内容的提问来源于stack exchange,提问作者Barak Binyamin
相关产品推荐
相关产品推荐

