Skip to content

Instantly share code, notes, and snippets.

Last active February 19, 2024 09:11
  • Star 22 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save philhartung/87d336a3c432e2ce5452befcad1b945f to your computer and use it in GitHub Desktop.
Relay a Dante multicast stream to AES67. This assumes the AES67 device is synced to the same PTP master, as no PTP timestamping is done (timestamp from Dante is copied to AES67 RTP packet)
const dgram = require('dgram');
const client = dgram.createSocket({ type: 'udp4', reuseAddr: true });
const sdp = require('./sdp');
const addr = '';
const danteMulticast = '';
const aes67Multicast = '';
const samplerate = 48000;
const channels = 2;
const encoding = 'L24';
const name = 'Dante Multicast Relay';
const sessID = Math.round( / 1000);
const sessVersion = sessID;
const ptpMaster = '08-00-00-ff-fe-00-00-1f:0';
//rtp specific vars
var seqNum = 0;
client.on('listening', function() {
client.addMembership(danteMulticast, addr);
client.on('message', function(buffer, remote) {
//read values from buffer
var channelCount = buffer.readUInt8(0);
var timestampSeconds = buffer.readUInt32BE(1);
//bytes 6 and 7 seem to be always 0x00, maybe reserved bytes
var timestampMedia = buffer.readUInt16BE(7);
var pcmData = buffer.slice(9);
//calculate media timestamp for rtp
var timestampRTP = ((timestampSeconds * samplerate) + timestampMedia) & 0xffffffff;
//create RTP header
var rtpHeader = Buffer.alloc(12);
rtpHeader.writeUInt16BE(0x8061, 0);
rtpHeader.writeUInt16BE(seqNum, 2);
rtpHeader.writeInt32BE(timestampRTP, 4);
rtpHeader.writeUInt32BE(0xaf12af34, 8);
//create and send RTP packet
var rtpBuffer = Buffer.concat([rtpHeader, pcmData]);
client.send(rtpBuffer, 5004, aes67Multicast);
//increase seqnum
seqNum = (seqNum + 1) % 65536;
sdp.start(addr, aes67Multicast, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster);
var dgram = require('dgram');
var socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
var constructSDPMsg = function(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster){
var sapHeader = Buffer.alloc(8);
var sapContentType = Buffer.from('application/sdp\0');
var ip = addr.split('.');
//write version/options
//write hash
sapHeader.writeUInt16LE(0xefef, 2);
//write ip
sapHeader.writeUInt8(parseInt(ip[0]), 4);
sapHeader.writeUInt8(parseInt(ip[1]), 5);
sapHeader.writeUInt8(parseInt(ip[2]), 6);
sapHeader.writeUInt8(parseInt(ip[3]), 7);
var sdpConfig = [
'o=- '+sessID+' '+sessVersion+' IN IP4 '+addr,
'c=IN IP4 '+multicastAddr+'/32',
't=0 0',
'a=clock-domain:PTPv2 0',
'm=audio 5004 RTP/AVP 96',
'a=rtpmap:96 '+encoding+'/'+samplerate+'/'+channels,
var sdpBody = Buffer.from(sdpConfig.join('\r\n'));
return Buffer.concat([sapHeader, sapContentType, sdpBody]);
exports.start = function(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster){
sdpMSG = constructSDPMsg(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster);
socket.bind(9875, function(){
socket.send(sdpMSG, 9875, '', function(err){});
socket.send(sdpMSG, 9875, '', function(err){});
}, 30*1000);
Copy link

wasge commented Nov 17, 2020

I'm trying this and I get the AES67 transmitter on Dante Controller, but I don't get any Dante receiver to send the Dante audio. Can you give me any hint?

Copy link

philhartung commented Nov 17, 2020

Most Dante devices already have AES67 support built in. You should only use this script, if you have a Dante device that does not support AES67 (for example Dante Virtual Soundcard or Dante Via) and an AES67 device that is supposed to receive audio from Dante. What excatly are you trying to achive?

If you actually have a use case for this script, then you probably need to look at the config part at the top to get it to work, especially:

const addr = '';
const danteMulticast = '';

The addr is supposed to be the address of the network interface. The second part is a bit more complex. The danteMulticast needs to be the multicast address of the Dante multicast stream you are trying to relay. I don't (yet) have a tool to find out the address of the multicast stream, so you will have to use Wireshark and capture the network traffic for a short period of time. You then can filter the captured traffic for Dante multicast traffic with ip.dst == and udp.port == 4321. In the destination column you should only have addresses of Dante multicast traffic.

Copy link

wasge commented Nov 17, 2020

I'm so sorry. My brain did absolutely ignore the "multicast" word when figuring how to use the script. Now I got it working.

I have a digital mixing console which Dante expansion card is absolutely outdated and does not support AES67 (Presonus RM16, discontinued), so I'm looking for ways to connect it to Linux computers. Every other device I own (Dante AVIO adapters and a Dante - ADAT converter) are AES67 compatible.

Copy link

Good to hear you got it working! Make sure to adjust the samplerate, channels and enconding in the config section too. Also just a reminder, this script is very experimental and I wouldn't use this for production. Though in my test it did work quite well and was stable over a longer period of time. If you need low latency, you should use a more powerful system (i7-2600 test system worked well with latency <2ms and quite low latency jitter). This will work on a Raspberry Pi, but latency jitter will be higher.

Sadly Dante and Linux don't work well together. The options are pretty much AES67 (but in this case isn't supported by the device and thus wouldn't work normally) or Dante Hardware like the Dante AVIO USB (only 2x2) or the PCIe Cards (quite expensive) which are supposed to work under Linux (at least the Digigram LX-Dante).

Copy link

mmoduu commented Oct 21, 2022

This is amazing! It works very smooth with my i7-7567U. I have an AES67 box that acts as a PTPv2 master, Audinate AVIO dongle on the network, and this same computer runs Dante Virtual Soundcard.. I routed this computers Dante to the AVIO and then used that stream to be replicated with your script. Only issue was that the payload type was not correct on the SDP (RTP stream had payload type 97, but SDP told it was 96), but that was an easy fix of course.

Copy link

Play-AV commented Apr 24, 2023

This works excellently with Dante VSC (by far the most reliable windows driver I've found). As mmoduu said, you'll have to change the payload type to 97 and at least for me, when I'm trying to receive an 8 ch stream with the Merging ALSA driver, I have to map it to 8 channels.

Using an AVIO as the boundary clock between PTPv1 and v2. You can probably do this yourself or there's a project on here that describes how to intercept and then 'spawn' Dante VSC's PTP process to connect to a v2 source. It's for Mac but the premise should in theory work for Windows too if you re-write the code.

Copy link

I see this is a couple of years out, does it still work with the latest VSC ?

Copy link

Play-AV commented Jan 2, 2024

I see this is a couple of years out, does it still work with the latest VSC ?

I haven't updated my VSC in ~6 months. Has something changed in new versions?

I've been using essentially this code for ages now. It gives me the ability to do Dante VSC > Dante Via and Dante VSC > AES67 at 8ch (simultaneously obviously)
If you don't want to really think about it, any of the Dante AVIOs will provide you an easy way to get the GM up and running (this GM will work for both the Dante network and any AES67 devices). It's not the highest quality GM but it's fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment