Skip to content

Instantly share code, notes, and snippets.

@arcs-
Last active March 11, 2025 20:15
Show Gist options
  • Save arcs-/96dafa9beb699a8b823b54efdbc83e36 to your computer and use it in GitHub Desktop.
Save arcs-/96dafa9beb699a8b823b54efdbc83e36 to your computer and use it in GitHub Desktop.
Record Player to Sonos on a Raspberry
const http = require('http');
const { spawn } = require('child_process');
const { DeviceDiscovery } = require('sonos');
const ip = require('ip');
const PORT = 8080;
const HOST = ip.address();
const SOURCE = 'hw:1,0';
const STREAM_NAME = 'Vinyl';
const SONOS_GROUP = 'Pool';
const httpConnections = [];
// AUDIO STREAM
// --------------------------
function createAudioStream() {
console.log('> Start streaming audio');
// https://linux.die.net/man/1/arecord
const arecord = [
'arecord',
'--device', SOURCE,
'--buffer-time', '60000000', // = 1 minutes
'--period-time', '20000000', // = 20 seconds
'--format', 'dat', // = [-f S16_LE -c2 -r48000]
'--duration', 60 * 30, // kill it after 30 minutes, and we'll restart it
'--mmap', // read input buffer directly
];
// https://linux.die.net/man/1/lame
const lame = [
'lame',
'-r', // assume raw input
'-s 48', // sample rate (must match arecord)
'--preset extreme', // optimized VBR ~220-270 kbps
'-', // read from stdin
];
const audioInputStream = spawn('sh', ['-c', `${arecord.join(' ')} | ${lame.join(' ')}`]);
audioInputStream.stdout.on('data', (data) => {
for (const stream of httpConnections) stream.write(data);
});
audioInputStream.stderr.on('data', (data) => {
console.error('stream info:', ''+data);
if (data.includes('overrun')) {
audioInputStream.kill();
setTimeout(createAudioStream, 200);
console.log('> Restarting audio stream');
}
});
audioInputStream.on('exit', (code) => {
console.log('stream exited with:', ''+code);
setTimeout(createAudioStream, 200);
console.log('> Restarting audio stream');
});
}
createAudioStream();
// SERVER
// --------------------------
http.createServer((req, res) => {
console.log('> Request for connection');
res.writeHead(200, { 'Content-Type': 'audio/mpeg', Connection: 'close' });
httpConnections.push(res);
res.on('close', () => {
httpConnections.slice(httpConnections.indexOf(res), 1);
console.log('connection closed by client');
});
}).listen(PORT);
console.log('> Start HTTP server at', HOST, PORT);
// SONOS
// ------------------------------
function connectSonosGroup(group) {
const configuration = {
uri: `x-rincon-mp3radio://${HOST}:${PORT}/${STREAM_NAME}`,
metadata: `
<DIDL-Lite
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/"
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
>
<item id="-1" restricted="true">
<dc:title>Schallplatte</dc:title>
<<upnp:album>Schallplatte</upnp:album>
<dc:creator>Plattenspieler</dc:creator>
<upnp:albumArtURI>https://stillh.art/static/playerimage.jpeg</upnp:albumArtURI>
<upnp:class>object.item.audioItem.audioBroadcast.#artistRadio</upnp:class>
</item>
</DIDL-Lite>
`
};
group
.CoordinatorDevice()
.play(configuration)
.then(() => { console.log(`${group.Name} connected`); })
.catch((err) => { console.error('SONOS Error: ', err); });
}
setInterval(() => {
if(httpConnections.length === 0) {
console.log('> Find Sonos');
DeviceDiscovery().once('DeviceAvailable', (device) => {
device.getAllGroups().then((groups) => {
groups.forEach((group) => {
if (group.Name.includes(SONOS_GROUP)) {
connectSonosGroup(group);
}
});
});
});
}
}, 10 * 1000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment