Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 154 additions & 7 deletions src/AudioTools/AudioLibs/VBANStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ class VBANConfig : public AudioInfo {
int max_write_size =
DEFAULT_BUFFER_SIZE * 2; // just good enough for 44100 stereo
uint8_t format = 0;

//reply for discovery packet
uint32_t device_flags = 0x00000001; // default: receiver only
uint32_t bitfeature = 0x00000001; // default: audio only
uint32_t device_color = 0x00FF00; // green default
//const char* stream_name_reply = "VBAN SPOT PING";
const char* device_name = nullptr; // nullptr means use MAC by default
const char* manufacturer_name = "ESP32 AudioTools";
const char* application_name = "VBAN Streamer";
const char* host_name = nullptr; // will fallback to WiFi.getHostname()
const char* user_name = "User";
const char* user_comment = "ESP32 VBAN Audio Device";
};

/**
Expand Down Expand Up @@ -356,10 +368,8 @@ class VBANStream : public AudioStream {

// receive incoming UDP packet
// Check if packet length meets VBAN specification:
if (len <= (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES) ||
len > VBAN_PACKET_MAX_LEN_BYTES) {
LOGE("Packet length %u bytes", len);
rx_buffer.reset();
if (len < VBAN_PACKET_HEADER_BYTES) {
LOGE("Too short to be VBAN (%u bytes)", len);
return;
}

Expand All @@ -369,6 +379,48 @@ class VBANStream : public AudioStream {
return;
}

uint8_t protocol = udpIncomingPacket[4] & VBAN_PROTOCOL_MASK;

if (protocol == VBAN_PROTOCOL_SERVICE) {
// Allow up to ~1024 bytes for service packets like Ping0
if (len > 1024) {
LOGE("Service packet length invalid: %u bytes", len);
return;
}
} else {
// Audio, serial, etc
if (len <= (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES) || len > VBAN_PACKET_MAX_LEN_BYTES) {
LOGE("Audio/other packet length invalid: %u bytes", len);
rx_buffer.reset();
return;
}
}

//LOGI("VBAN format byte: 0x%02X", udpIncomingPacket[7]);
//LOGD("VBAN protocol mask applied: 0x%02X", udpIncomingPacket[7] & VBAN_PROTOCOL_MASK);
//Serial.printf("Header[7] = 0x%02X\n", udpIncomingPacket[7]);


//-------------------------------------------------------------------------
//SUPPORT PING REQUEST
if ( protocol == VBAN_PROTOCOL_SERVICE ) {

uint8_t service_type = udpIncomingPacket[5];
uint8_t service_fnct = udpIncomingPacket[6];

if (service_type == VBAN_SERVICE_IDENTIFICATION) {
bool isReply = (service_fnct & VBAN_SERVICE_FNCT_REPLY) != 0;
uint8_t function = service_fnct & 0x7F;

if (!isReply && function == 0) {
LOGI("Received VBAN PING0 request");
sendVbanPing0Reply(packet);
}
}
return;
}
//--------------------------------------------------------------------------

vban_rx_data_bytes =
len - (VBAN_PACKET_HEADER_BYTES + VBAN_PACKET_COUNTER_BYTES);
vban_rx_pkt_nbr = (uint32_t*)&udpIncomingPacket[VBAN_PACKET_HEADER_BYTES];
Expand All @@ -378,10 +430,10 @@ class VBANStream : public AudioStream {
uint8_t vbanSampleRateIdx = udpIncomingPacket[4] & VBAN_SR_MASK;
uint8_t vbchannels = udpIncomingPacket[6] + 1;
uint8_t vbframes = udpIncomingPacket[5] + 1;
uint8_t vbformat = udpIncomingPacket[7] & VBAN_PROTOCOL_MASK;;
uint8_t vbformat_bits = udpIncomingPacket[7] & VBAN_BIT_RESOLUTION_MASK;;
uint8_t vbformat = udpIncomingPacket[7] & VBAN_PROTOCOL_MASK;
uint8_t vbformat_bits = udpIncomingPacket[7] & VBAN_BIT_RESOLUTION_MASK;
uint32_t vbanSampleRate = VBanSRList[vbanSampleRateIdx];

//LOGD("sample_count: %d - frames: %d", vban_rx_sample_count, vbframes);
//assert (vban_rx_sample_count == vbframes*vbchannels);

Expand Down Expand Up @@ -439,6 +491,101 @@ class VBANStream : public AudioStream {
}
}
}
//-------------------------------------------------------------------------------------
//implement ping reply based on VBAN standard
void sendVbanPing0Reply(AsyncUDPPacket& sourcePacket) {

// Prepare VBAN 28-byte service header
uint8_t header[28];
memset(header, 0, sizeof(header));
memcpy(header, "VBAN", 4);
header[4] = VBAN_PROTOCOL_SERVICE;
header[5] = VBAN_SERVICE_FNCT_PING0 | VBAN_SERVICE_FNCT_REPLY; // Service function + reply bit
header[6] = 0x00; // must be zero
// Copy incoming stream name from discovery packet
const uint8_t* data = sourcePacket.data();
memcpy(&header[8], &data[8], 16);
// Copy frame number (little endian)

uint32_t frameNumber = (uint32_t)((data[24] & 0xFF) | ((data[25] & 0xFF) << 8) | ((data[26] & 0xFF) << 16) | ((data[27] & 0xFF) << 24));
memcpy(&header[24], &frameNumber, 4);

// Construct the PING0 payload using the struct
VBAN_PING0 ping0;
memset(&ping0, 0, sizeof(ping0));

// Fill fields with your config data and fixed values
ping0.bitType = cfg.device_flags;
ping0.bitfeature = cfg.bitfeature;
ping0.bitfeatureEx = 0x00000000;
ping0.PreferedRate = 44100;
ping0.MinRate = 8000;
ping0.MaxRate = 96000;
ping0.color_rgb = cfg.device_color;

// Version string, 8 bytes total (zero padded)
memcpy(ping0.nVersion, "v1.0", 4);

// GPS_Position left empty (all zero), so no need to set
// USER_Position 8 bytes
memcpy(ping0.USER_Position, "USRPOS", 6);
// LangCode_ascii 8 bytes ("EN" + padding)
memset(ping0.LangCode_ascii, 0, sizeof(ping0.LangCode_ascii));
memcpy(ping0.LangCode_ascii, "EN", 2);
// reserved_ascii and reservedEx are zeroed by memset
// IP as string, max 32 bytes

char ipStr[16]; // Enough for "255.255.255.255\0"
sprintf(ipStr, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]);
safe_strncpy(ping0.DistantIP_ascii, ipStr, sizeof(ping0.DistantIP_ascii));
// Ports (network byte order)
ping0.DistantPort = htons(sourcePacket.remotePort());
ping0.DistantReserved = 0;

// Device name (64 bytes)
if (cfg.device_name && cfg.device_name[0] != '\0') {
safe_strncpy(ping0.DeviceName_ascii, cfg.device_name, sizeof(ping0.DeviceName_ascii));
} else {
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[64];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
safe_strncpy(ping0.DeviceName_ascii, macStr, sizeof(ping0.DeviceName_ascii));
}

// Manufacturer name (64 bytes)
safe_strncpy(ping0.ManufacturerName_ascii, cfg.manufacturer_name, sizeof(ping0.ManufacturerName_ascii));
// Application name (64 bytes)
safe_strncpy(ping0.ApplicationName_ascii, cfg.application_name, sizeof(ping0.ApplicationName_ascii));
// Host name (64 bytes)
const char* hostName = cfg.host_name;
if (!hostName || hostName[0] == '\0') {
hostName = WiFi.getHostname();
if (!hostName) hostName = "ESP32";
}
safe_strncpy(ping0.HostName_ascii, hostName, sizeof(ping0.HostName_ascii));

// UserName_utf8
safe_strncpy(ping0.UserName_utf8, cfg.user_name, sizeof(ping0.UserName_utf8));
//UserComment_utf8
safe_strncpy(ping0.UserComment_utf8, cfg.user_comment, sizeof(ping0.UserComment_utf8));

// Prepare final packet: header + payload
uint8_t packet[28 + sizeof(VBAN_PING0)];
memcpy(packet, header, 28);
memcpy(packet + 28, &ping0, sizeof(VBAN_PING0));

// Send UDP packet
udp.writeTo(packet, sizeof(packet), sourcePacket.remoteIP(), sourcePacket.remotePort());
}

// Safely copy a C-string with guaranteed null termination
void safe_strncpy(char* dest, const char* src, size_t dest_size) {
if (dest_size == 0) return;
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
}
//-----------------------------------------------------------------------------------
};

} // namespace audio_tools
45 changes: 43 additions & 2 deletions src/AudioTools/AudioLibs/vban/vban.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
// MODIFIED by R. Kinnett, https://github.com/rkinnett, 2020
//
///////////////////////////////////////////////////////////////////////

#include <stdint.h>
#include <string.h>

#ifndef __VBAN_H__
#define __VBAN_H__
Expand Down Expand Up @@ -104,7 +105,7 @@ enum VBanSampleRates
};


#define VBAN_PROTOCOL_MASK 0xE0
#define VBAN_PROTOCOL_MASK 0xE0
enum VBanProtocol
{
VBAN_PROTOCOL_AUDIO = 0x00,
Expand Down Expand Up @@ -159,6 +160,46 @@ enum VBanCodec
};


/********************************************************
* SERVICE SUB PROTOCOL *
********************************************************/
// VBAN SERVICE PROTOCOL definitions
#define VBAN_PROTOCOL_SERVICE 0x60

// Service Types (format_nbc)
#define VBAN_SERVICE_IDENTIFICATION 0x00
#define VBAN_SERVICE_CHATUTF8 0x01
#define VBAN_SERVICE_RTPACKETREGISTER 0x20
#define VBAN_SERVICE_RTPACKET 0x21

// Service Functions (format_nbs)
#define VBAN_SERVICE_FNCT_PING0 0x00
#define VBAN_SERVICE_FNCT_REPLY 0x80

struct VBAN_PING0 {
uint32_t bitType;
uint32_t bitfeature;
uint32_t bitfeatureEx;
uint32_t PreferedRate;
uint32_t MinRate;
uint32_t MaxRate;
uint32_t color_rgb;
uint8_t nVersion[4];
char GPS_Position[8]; // Keep empty (all zero)
char USER_Position[8];
char LangCode_ascii[8];
char reserved_ascii[8];
char reservedEx[64];
char DistantIP_ascii[32];
uint16_t DistantPort;
uint16_t DistantReserved;
char DeviceName_ascii[64];
char ManufacturerName_ascii[64];
char ApplicationName_ascii[64];
char HostName_ascii[64];
char UserName_utf8[128];
char UserComment_utf8[128];
} __attribute__((packed));
/********************************************************
* TEXT SUB PROTOCOL *
********************************************************/
Expand Down