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 badges1 Answer
Reset to default 6Ok, 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