-
-
Save hatchjaw/81f3b839ae6dcb57007a8ca14a872b7a to your computer and use it in GitHub Desktop.
Teensy JackTripClient - QNEthernet (basic)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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