最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How can I stream audio to the browser, chunk by chunk? - Stack Overflow

programmeradmin2浏览0评论

I'm trying to make an internet radio station and I would like to frequently change songs and overlay sound freely. I'd like to rate-limit the audio so the feed can be altered before its sent out. I'd also like to serve continuous content if that is possible

So far, we have a close to successful attempt with websockets, but the quality is a bit messy:

Here's the code:

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=".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>

And to generate the strangely formatted PCM WAV:

ffmpeg -i src.mp3 -ar 32000 -ac 1 -acodec pcm_u8 src.wav

Is there a way to get cleaner audio output?

I'm trying to make an internet radio station and I would like to frequently change songs and overlay sound freely. I'd like to rate-limit the audio so the feed can be altered before its sent out. I'd also like to serve continuous content if that is possible

So far, we have a close to successful attempt with websockets, but the quality is a bit messy:

Here's the code:

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>

And to generate the strangely formatted PCM WAV:

ffmpeg -i src.mp3 -ar 32000 -ac 1 -acodec pcm_u8 src.wav

Is there a way to get cleaner audio output?

Share Improve this question edited Jun 17, 2024 at 10:46 double-beep 5,53719 gold badges40 silver badges49 bronze badges asked Dec 10, 2022 at 8:20 Barak BinyaminBarak Binyamin 4145 silver badges14 bronze badges
Add a ment  | 

1 Answer 1

Reset to default 6

Ok, so it looks like many radio stations like Today's Hits (which can be found in the howler.js radio example), stream audio by using the live streaming standard

First they send an http message "content-type: audio/mpeg", as well as a few other messages like Transfer-Encoding to signal the browser that a stream is ing and to keep the connection to the server. We can see the http messages if we look at the network tab of the developers console on safari and examine the requests

According to the MDN docs there are a handful of formats that are standardized for streaming, wav doesn't look to be one of them at the moment, but mp3 is though

Here's a working example...
server.js

const EventEmitter = require('events')
const schedule     = require('node-schedule')
const express      = require('express')
const http         = require("http")
const fs           = require('fs')

const app          = express()

const SAMPLE_SIZE = 32000                         // samples/sec
const PACKET_SIZE = SAMPLE_SIZE                   // 1 second worth of data
const UPDATE_TIME = '* * * * * *'                 // every second
const PATH        = "./src.mp3"
let   PACKET_NUM  = 0
const eventEmitter= new EventEmitter ()

async function getpacket(req,res){
  const file_descriptor     = fs.openSync(PATH, 'r', null)
  const read_offset         = PACKET_NUM * 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_NUM} to ${req.socket.remoteAddress}`) // safari sometimes requests two streams at the same time
  res.write(buffer)
}

app.get("/", (req,res)=>{
  res.sendFile("index.html",{root: '.'})
})

app.get("/src.mp3", async (req,res)=>{
    res.writeHead(200,"OK",{"Content-Type":"audio/mpeg"})

    const updateHandler = () =>{ getpacket(req,res) }
    eventEmitter.on("update", updateHandler) // On update event, send another packet

    req.socket.on("close",()=>{
      eventEmitter.removeListener("update",updateHandler)
      console.log(`Client ${req.socket.remoteAddress} disconected from server`)
    })
})

// This creates a schedule to make an update event on every second
schedule.scheduleJob(UPDATE_TIME, function(){
  PACKET_NUM+=1
  eventEmitter.emit("update")
})

const server = http.createServer(app)
server.listen(3000)

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Testing</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>

<body>
    <audio controls id="music"></audio>

    <div onclick="listen()">Click to Listen</div>
    <div onclick="stop()">Click to Stop</div>

    <script>    
    async function listen(){
       let music = document.getElementById('music')
       music.src="/src.mp3"
       music.play()
    }

    async function stop(){
       let music = document.getElementById('music')
       music.pause()
       music.src=""
    }
    </script>

</body>
</html>

And to generate the mp3 file from the wav file

ffmpeg -i src.wav -ab 128k src.mp3

If server.js is running on your machine you can go to http://localhost:3000/src.mp3 to see the live stream, and http://localhost:3000/ to see the example of it's support in the browser

It is possible to update src.mp3 live with ffmpeg

Packages and helpful resources

  • EventEmitter is a default node package
  • node-schedule
  • crontab-style string
  • express
  • http
  • fs is default node package
  • html lock screen player metadata

Some ffmpeg insights for live editing

  • ffmpeg pipe
  • ffmpeg concat wavs
  • overlay wav files at position
  • mixing wav files with ffmpeg
发布评论

评论列表(0)

  1. 暂无评论