Skip to content

Instantly share code, notes, and snippets.

@hatchjaw
Last active November 16, 2022 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hatchjaw/81f3b839ae6dcb57007a8ca14a872b7a to your computer and use it in GitHub Desktop.
Save hatchjaw/81f3b839ae6dcb57007a8ca14a872b7a to your computer and use it in GitHub Desktop.
Teensy JackTripClient - QNEthernet (basic)
//
// Created by tar on 16/09/22.
//
#include "JackTripClient.h"
JackTripClient::JackTripClient(IPAddress &serverIpAddress, uint16_t serverTcpPort) :
AudioStream(2, inputQueueArray),
qn::EthernetUDP(8),
clientIP(serverIpAddress),
serverIP(serverIpAddress), // Assume client and server on same subnet
serverTcpPort(serverTcpPort),
#ifdef USE_TIMER
timer(TeensyTimerTool::GPT1),
#endif
udpBuffer(UDP_PACKET_SIZE * 16) {
// Generate a MAC address (from the program-once area of Teensy's flash
// memory) to assign to the ethernet shield.
teensyMAC(clientMAC);
// Use the last byte of the MAC to set the last byte of the IP.
// (Maybe needs a more sophisticated approach.)
clientIP[3] += clientMAC[5];
serverHeader = new JackTripPacketHeader;
}
JackTripClient::~JackTripClient() {
delete serverHeader;
}
uint8_t JackTripClient::begin(uint16_t port) {
if (!active) {
Serial.println("JackTripClient is not connected to any Teensy audio objects.");
return 0;
}
Serial.print("JackTripClient: MAC address is: ");
for (int i = 0; i < 6; ++i) {
Serial.printf(i < 5 ? "%02X:" : "%02X", clientMAC[i]);
}
Serial.println();
// qn::Ethernet.setLocalIP(clientIP);
if (!qn::Ethernet.begin(clientIP, {255, 255, 255, 0}, {192, 168, 10, 1}) ||
!qn::Ethernet.waitForLink(5000)) {
Serial.println("JackTripClient: failed to start ethernet connection.");
return 0;
}
Serial.print("JackTripClient: IP is ");
Serial.println(qn::Ethernet.localIP());
Serial.printf("JackTripClient: Packet size is %d bytes\n", UDP_PACKET_SIZE);
#ifdef USE_TIMER
auto timerPeriod = 1'000'000.f * static_cast<float>(AUDIO_BLOCK_SAMPLES) / AUDIO_SAMPLE_RATE_EXACT;
Serial.printf("UDP send/receive timer period: %.3f\n", timerPeriod);
timer.begin([this] { updateImpl(); }, timerPeriod);
#endif
return EthernetUDP::begin(port);
}
bool JackTripClient::connect(uint16_t timeout) {
if (!active) {
Serial.println("JackTripClient is not connected to any Teensy audio objects.");
return false;
}
// Attempt TCP handshake with JackTrip server.
Serial.print("JackTripClient: Connecting to JackTrip server at ");
Serial.print(serverIP);
Serial.printf(":%d... ", serverTcpPort);
qn::EthernetClient c = qn::EthernetClient();
// c.setConnectionTimeout(timeout);
if (c.connect(serverIP, serverTcpPort)) {
Serial.println("Succeeded!");
} else {
Serial.println();
delay(timeout);
return false;
}
// Sending the local UDP port yields the remote UDP port in return.
auto port = localPort();
// Send the local port (little endian).
if (4 != c.write((const uint8_t *) &port, 4)) {
Serial.println("JackTripClient: failed to send UDP port to server.");
c.close();
return false;
}
// Patience...
while (c.available() < 4) {}
// Read the remote port.
if (4 != c.read((uint8_t *) &port, 4)) {
Serial.println("JackTripClient: failed to read UDP port from server.");
c.close();
connected = false;
return false;
} else {
connected = true;
}
serverUdpPort = port;
Serial.printf("JackTripClient: Server port is %d\n", serverUdpPort);
lastReceive = 0;
packetHeader.SeqNumber = 0;
packetHeader.TimeStamp = 0;
prevServerHeader.TimeStamp = 0;
prevServerHeader.SeqNumber = 0;
return connected;
}
void JackTripClient::stop() {
connected = false;
serverUdpPort = 0;
udpBuffer.clear();
packetStats.reset();
}
void JackTripClient::update(void) {
#ifdef USE_TIMER
doAudioOutput();
#else
updateImpl();
#endif
}
void JackTripClient::updateImpl() {
receivePackets();
#ifndef USE_TIMER
doAudioOutput();
#endif
sendPacket();
if (showStats && connected) {
packetStats.printStats();
udpBuffer.printStats();
}
}
bool JackTripClient::isConnected() const {
return connected;
}
void JackTripClient::receivePackets() {
if (!connected) return;
int size;
// Check for incoming UDP packets. Get as many packets as are available.
RECEIVE_CONDITION ((size = parsePacket()) > 0) {
lastReceive = 0;
if (size == EXIT_PACKET_SIZE && isExitPacket()) {
// Exit sequence
Serial.println("JackTripClient: Received exit packet");
Serial.printf(" maxmem: %d blocks\n", AudioMemoryUsageMax());
Serial.printf(" maxcpu: %f %%\n\n", AudioProcessorUsageMax());
stop();
return;
} else if (size != UDP_PACKET_SIZE) {
Serial.println("JackTripClient: Received a malformed packet");
} else {
// Read the UDP packet and write it into a circular buffer.
uint8_t in[size];
read(in, size);
udpBuffer.write(in, size);
// Read the header from the packet received from the server.
// serverHeader = reinterpret_cast<JackTripPacketHeader *>(in);
memcpy(serverHeader, in, sizeof(JackTripPacketHeader));
if (showStats && packetStats.awaitingFirstReceive()) {
Serial.println("===============================================================");
Serial.printf("Received first packet: Timestamp: %" PRIu64 "; SeqNumber: %" PRIu16 "\n",
serverHeader->TimeStamp,
serverHeader->SeqNumber);
packetHeader.TimeStamp = serverHeader->TimeStamp;
packetHeader.SeqNumber = serverHeader->SeqNumber;
Serial.println("===============================================================");
}
packetStats.registerReceive(*serverHeader);
}
}
if (lastReceive > RECEIVE_TIMEOUT_MS) {
Serial.printf("JackTripClient: Nothing received for %.1f s. Stopping.\n", RECEIVE_TIMEOUT_MS / 1000.f);
stop();
}
}
void JackTripClient::sendPacket() {
// Might have received an exit packet, so check whether still connected.
if (!connected) return;
// Get the location in the UDP buffer to which audio samples should be
// written.
uint8_t packet[UDP_PACKET_SIZE];
uint8_t *pos = packet + PACKET_HEADER_SIZE;
// Copy audio to the UDP buffer.
audio_block_t *inBlock[NUM_CHANNELS];
for (int channel = 0; channel < NUM_CHANNELS; channel++) {
inBlock[channel] = receiveReadOnly(channel);
// Only proceed if a block was returned, i.e. something is connected
// to one of this object's input channels.
if (inBlock[channel]) {
memcpy(pos, inBlock[channel]->data, CHANNEL_FRAME_SIZE);
pos += CHANNEL_FRAME_SIZE;
release(inBlock[channel]);
}
}
packetHeader.SeqNumber++;
packetHeader.TimeStamp += packetInterval;
packetInterval = 0;
// Copy the packet header to the UDP buffer.
memcpy(packet, &packetHeader, PACKET_HEADER_SIZE);
// Send the packet.
beginPacket(serverIP, serverUdpPort);
size_t written = write(packet, UDP_PACKET_SIZE);
if (written != UDP_PACKET_SIZE) {
Serial.println("JackTripClient: Net buffer is too small");
}
auto result = endPacket();
if (0 == result) {
Serial.println("JackTripClient: failed to send a packet.");
}
packetStats.registerSend(packetHeader);
}
void JackTripClient::doAudioOutput() {
if (!connected) return;
// Copy from UDP inBuffer to audio output.
// Write samples to output.
audio_block_t *outBlock[NUM_CHANNELS];
uint8_t data[UDP_PACKET_SIZE];
// Read a packet from the input UDP buffer
udpBuffer.read(data, UDP_PACKET_SIZE);
for (int channel = 0; channel < NUM_CHANNELS; channel++) {
outBlock[channel] = allocate();
// Only proceed if an audio block was allocated, i.e. the
// current output channel is connected to something.
if (outBlock[channel]) {
// Get the start of the sample data in the packet. Cast to desired
// bit-resolution.
auto start = (const int16_t *) (data + PACKET_HEADER_SIZE + CHANNEL_FRAME_SIZE * channel);
// Copy the samples to the output block.
memcpy(outBlock[channel]->data, start, CHANNEL_FRAME_SIZE);
// Finish up.
transmit(outBlock[channel], channel);
release(outBlock[channel]);
}
}
}
bool JackTripClient::isExitPacket() {
uint8_t packet[EXIT_PACKET_SIZE];
if (read(packet, EXIT_PACKET_SIZE) == EXIT_PACKET_SIZE) {
for (size_t i = 0; i < EXIT_PACKET_SIZE; ++i) {
if (packet[i] != 0xff) {
return false;
}
}
return true;
}
return false;
}
void JackTripClient::setShowStats(bool show, uint16_t intervalMS) {
showStats = show;
packetStats.setPrintInterval(intervalMS);
}
//
// Created by tar on 16/09/22.
//
#ifndef JACKTRIP_TEENSY_JACKTRIPCLIENT_H
#define JACKTRIP_TEENSY_JACKTRIPCLIENT_H
#undef USE_TIMER
#include <Audio.h>
#include <QNEthernet.h>
#include <TeensyID.h>
#ifdef USE_TIMER
#include <TeensyTimerTool.h>
#endif
#include "PacketHeader.h"
#include "CircularBuffer.h"
#include "PacketStats.h"
namespace qn = qindesign::network;
#define RECEIVE_CONDITION if
/**
* Inputs: signals produced by other audio components, to be sent to peers over
* the JackTrip protocol to do with as they will.
* Outputs: audio signals received over the JackTrip protocol.
*/
class JackTripClient : public AudioStream, qn::EthernetUDP {
public:
explicit JackTripClient(IPAddress &serverIpAddress, uint16_t serverTcpPort = 4464);
virtual ~JackTripClient();
/**
* Set up the client.
* @param port local UDP port on which to listen for packets.
* @return <em>true</em> on success, <em>false</em> on failure.
*/
uint8_t begin(uint16_t port) override;
bool isConnected() const;
/**
* Connect the client to the server.
* @param timeout
* @return <em>true</em> on success, <em>false</em> on failure.
*/
bool connect(uint16_t timeout = 1000);
void stop() override;
void setShowStats(bool show, uint16_t intervalMS = 1'000);
static uint16_t getNumChannels() { return NUM_CHANNELS; };
private:
static constexpr uint8_t NUM_CHANNELS{2};
static constexpr uint16_t UDP_PACKET_SIZE{
PACKET_HEADER_SIZE + NUM_CHANNELS * AUDIO_BLOCK_SAMPLES * sizeof(uint16_t)};
static constexpr uint32_t RECEIVE_TIMEOUT_MS{10'000};
static constexpr uint16_t AUDIO_BUFFER_SIZE{AUDIO_BLOCK_SAMPLES * NUM_CHANNELS * 2};
/**
* Size in bytes of one channel's worth of samples.
*/
static constexpr uint16_t CHANNEL_FRAME_SIZE{AUDIO_BLOCK_SAMPLES * sizeof(uint16_t)};
/**
* Size, in bytes, of JackTrip's exit packet
*/
static constexpr uint8_t EXIT_PACKET_SIZE{63};
/**
* "The heart of your object is it's update() function.
* The library will call your update function every 128 samples,
* or approximately every 2.9 milliseconds."
*/
void update(void) override;
/**
* Receive a JackTrip packet containing audio to route to this object's
* outputs.
* NB assumes that a new packet is ready each time it is called. This may
* well be a dangerous assumption.
*/
void receivePackets();
/**
* Check whether a packet received from the JackTrip server is an exit
* packet.
* @return
*/
bool isExitPacket();
/**
* Send a JackTrip packet containing audio routed to this object's inputs.
*/
void sendPacket();
/**
* Copy audio samples from incoming UDP data to Teensy audio output.
*/
void doAudioOutput();
/**
* MAC address to assign to Teensy's ethernet shield.
*/
byte clientMAC[6]{};
/**
* IP to assign to Teensy.
*/
IPAddress clientIP;
/**
* IP of the jacktrip server.
*/
IPAddress serverIP;
/**
* JackTrip server TCP port for initial handshake.
*/
uint16_t serverTcpPort;
/**
* JackTrip server UDP port.
*/
uint32_t serverUdpPort{0};
/*volatile*/ bool connected{false};
elapsedMillis lastReceive{0};
/**
* "The final required component is inputQueueArray[], which should be a
* private variable.
* The size must match the number passed to the AudioStream constructor."
*/
audio_block_t *inputQueueArray[2]{};
/**
* The header to send with every outgoing JackTrip packet.
* TimeStamp and SeqNumber should be incremented accordingly.
*/
JackTripPacketHeader packetHeader{
0,
0,
// Teensy's default block size is 128. Can be overridden with
// compiler flag -DAUDIO_BLOCK_SAMPLES (see platformio.ini).
AUDIO_BLOCK_SAMPLES,
samplingRateT::SR44,
16,
NUM_CHANNELS,
NUM_CHANNELS
};
elapsedMicros packetInterval{0};
JackTripPacketHeader prevServerHeader{};
JackTripPacketHeader *serverHeader;
#ifdef USE_TIMER
TeensyTimerTool::PeriodicTimer timer;
#endif
CircularBuffer<uint8_t> udpBuffer;
// CircularBuffer<int16_t> audioBuffer;
PacketStats packetStats;
bool showStats{false};
void updateImpl();
};
#endif //JACKTRIP_TEENSY_JACKTRIPCLIENT_H
#include <QNEthernet.h>
#include <Audio.h>
#include <OSCBundle.h>
#include <JackTripClient.h>
#include "WFS/WFS.h"
// Define this to wait for a serial connection before proceeding with execution
#define WAIT_FOR_SERIAL
// Define this to print packet stats.
#define SHOW_STATS
// Shorthand to block and do nothing
#define WAIT_INFINITE() while (true) yield();
// Local udp port on which to receive packets.
const uint16_t LOCAL_UDP_PORT = 8888;
// Remote server IP address -- should match address in IPv4 settings.
IPAddress jackTripServerIP{192, 168, 10, 10};
// Parameters for OSC over UDP multicast.
IPAddress oscMulticastIP{230, 0, 0, 20};
const uint16_t OSC_MULTICAST_PORT{41814};
//region Audio system objects
// Audio shield driver
AudioControlSGTL5000 audioShield;
AudioOutputI2S out;
JackTripClient jtc{jackTripServerIP};
WFS wfs;
AudioMixer4 mixerL;
AudioMixer4 mixerR;
// Audio system connections
AudioConnection patchCord00(jtc, 0, wfs, 0);
//AudioConnection patchCord10(jtc, 0, pt, 0);
//AudioConnection patchCord20(jtc, 0, out, 0);
AudioConnection patchCord30(jtc, 0, mixerL, 0);
AudioConnection patchCord35(jtc, 1, wfs, 1);
//AudioConnection patchCord40(jtc, 1, pt, 1);
//AudioConnection patchCord50(jtc, 1, out, 1);
AudioConnection patchCord60(jtc, 1, mixerR, 0);
//AudioConnection patchCord70(pt, 0, mixerL, 1);
//AudioConnection patchCord80(pt, 1, mixerR, 1);
// WFS outputs routed to Teensy outputs
AudioConnection patchCord85(wfs, 0, out, 0);
AudioConnection patchCord86(wfs, 1, out, 1);
// Mixer outputs routed to JackTripClient inputs -- to be sent to the JackTrip
// server over UDP.
AudioConnection patchCord90(mixerL, 0, jtc, 0);
AudioConnection patchCord91(mixerR, 0, jtc, 1);
//endregion
//region Warning params
elapsedMillis performanceReport;
elapsedMillis isLoopingReport;
const uint32_t PERF_REPORT_INTERVAL = 5000;
//endregion
namespace qn = qindesign::network;
qn::EthernetUDP udp;
//region Forward declarations
void startAudio();
void receiveOSC();
void parsePosition(OSCMessage &msg, int addrOffset);
void parseModule(OSCMessage &msg, int addrOffset);
//endregion
void setup() {
#ifdef WAIT_FOR_SERIAL
while (!Serial);
#endif
if (CrashReport) { // Print any crash report
Serial.println(CrashReport);
CrashReport.clear();
}
Serial.printf("Sampling rate: %f\n", AUDIO_SAMPLE_RATE_EXACT);
#ifdef SHOW_STATS
jtc.setShowStats(true, 5'000);
#endif
if (!jtc.begin(LOCAL_UDP_PORT)) {
Serial.println("Failed to initialise jacktrip client.");
WAIT_INFINITE()
}
// NB. Ethernet::begin() already called by JackTripClient
if (1 != udp.beginMulticast(oscMulticastIP, OSC_MULTICAST_PORT)) {
Serial.println("Failed to start listening for OSC messages.");
WAIT_INFINITE()
}
AudioMemory(32);
startAudio();
}
void loop() {
if (isLoopingReport > 1000) {
Serial.println("\tStill looping");
isLoopingReport = 0;
}
if (!jtc.isConnected()) {
jtc.connect(2500);
if (jtc.isConnected()) {
AudioProcessorUsageMaxReset();
AudioMemoryUsageMaxReset();
}
} else {
receiveOSC();
if (performanceReport > PERF_REPORT_INTERVAL) {
Serial.printf("Audio memory in use: %d blocks; processor %f %%\n",
AudioMemoryUsage(),
AudioProcessorUsage());
performanceReport = 0;
}
}
}
void parsePosition(OSCMessage &msg, int addrOffset) {
// Get the source index and coordinate axis, e.g. "0/x"
char path[20];
msg.getAddress(path, addrOffset + 1);
// Rough-and-ready check to prevent attempting to set an invalid source
// position.
auto sourceIdx{atoi(path)};
Serial.println(sourceIdx);
if (sourceIdx >= JackTripClient::getNumChannels()){
Serial.printf("Invalid source index: %d\n", sourceIdx);
return;
}
// Get the coordinate value (0-1).
auto pos = msg.getFloat(0);
Serial.printf("Setting \"%s\": %f\n", path, pos);
// Set the parameter.
wfs.setParamValue(path, pos);
}
void parseModule(OSCMessage &msg, int addrOffset){
char ipString[15];
IPAddress ip;
msg.getString(0, ipString, 15);
ip.fromString(ipString);
if (ip == qn::Ethernet.localIP()){
char id[2];
msg.getAddress(id, addrOffset + 1);
auto numericID = strtof(id, nullptr);
Serial.printf("Setting module ID: %f\n", numericID);
wfs.setParamValue("moduleID", numericID);
}
}
/**
* Expects messages of the form:
*
* set module ID
* /module/0 "[IP address]"
*
* set source 0 co-ordinates
* /source/0/x [0.0-1.0]
* /source/0/y [0.0-1.0]
*/
void receiveOSC() {
OSCBundle bundleIn;
OSCMessage messageIn;
int size;
if ((size = udp.parsePacket()) > 0) {
// Serial.printf("Packet size: %d\n", size);
uint8_t buffer[size];
udp.read(buffer, size);
// Try to read as bundle
bundleIn.fill(buffer, size);
if (!bundleIn.hasError() && bundleIn.size() > 0) {
// Serial.printf("OSCBundle::size: %d\n", bundleIn.size());
bundleIn.route("/source", parsePosition);
bundleIn.route("/module", parseModule);
} else {
// Try as message
messageIn.fill(buffer, size);
if (!messageIn.hasError() && messageIn.size() > 0) {
// Serial.printf("OSCMessage::size: %d\n", messageIn.size());
messageIn.route("/source", parsePosition);
messageIn.route("/module", parseModule);
}
}
}
}
void startAudio() {
audioShield.enable();
// "...0.8 corresponds to the maximum undistorted output for a full scale
// signal. Usually 0.5 is a comfortable listening level."
// https://www.pjrc.com/teensy/gui/?info=AudioControlSGTL5000
audioShield.volume(.8);
audioShield.audioProcessorDisable();
audioShield.autoVolumeDisable();
audioShield.dacVolumeRampDisable();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment