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

如何分块向浏览器流式传输音频?优化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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 14:25:16