971 lines
29 KiB
C
971 lines
29 KiB
C
#include <Python.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include "ws.h"
|
|
#include <pthread.h>
|
|
#include <complex.h>
|
|
#include <time.h>
|
|
#include "quisk.h"
|
|
#include "filter.h"
|
|
|
|
#define TCI_STREAM_DATA_BYTES 16384
|
|
#define TCI_RX_BUF_SIZE 1024
|
|
#define TCI_COMMAND_SIZE 64
|
|
#define BAD_TCI_IF 999999999
|
|
|
|
static int verbose; // non-zero for verbose log messages
|
|
static int tci_port;
|
|
|
|
// Implement the ExpertSDR2 Version 1.4 protocol:
|
|
// The Tx audio parameters are copied from the Rx audio stream.
|
|
// The only sample type is FLOAT32 type 3.
|
|
// It is unclear whether Stream.length is the number of floats, or half that for two channels. And version 1.4 lacks Stream.channels.
|
|
// The protocol name for TCI version 1.4 is "protocol:ESDR,1.4;" and the port is 40001.
|
|
// The protocol name for TCI version 2.0 is "protocol:ExpertSDR3,2.0;" and the port is 50001.
|
|
// The TCI modulations are: am, sam, dsb, lsb, usb, cw, nfm, wfm, spec, digl, digu, drm.
|
|
|
|
// These are TCI parameters:
|
|
static long long quisk_dds;
|
|
static long long client_dds=-1;
|
|
static long long quisk_vfo;
|
|
static long long client_vfo=-1;
|
|
static int quisk_if;
|
|
static int client_if=BAD_TCI_IF;
|
|
static int quisk_trx;
|
|
static int client_trx=-1;
|
|
static int quisk_split_enable;
|
|
static int client_split_enable=-1;
|
|
static char quisk_modulation[TCI_COMMAND_SIZE];
|
|
static char client_modulation[TCI_COMMAND_SIZE]={0};
|
|
|
|
static ws_cli_conn_t tci_clients_list[MAX_CLIENTS]; // list of TCI clients
|
|
static int tci_clients_count;
|
|
static int tci_started; // Did we start the TCI server?
|
|
|
|
// Tx Audio
|
|
ws_cli_conn_t tci_tx_audio_client; // single client providing Tx audio
|
|
double tci_tx_audio_time; // seconds since the start of Tx audio
|
|
uint64_t tci_tx_audio_samples; // number of samples requested since the start of Tx audio
|
|
int tci_tx_audio_rate; // samples per second
|
|
int tci_tx_audio_request; // the Stream.length to request in TX_CHRONO
|
|
|
|
static pthread_mutex_t clients_list_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_mutex_t tx_buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
enum StreamType // ExpertSDR3
|
|
{
|
|
IQ_STREAM = 0, // Receiver IQ signal stream
|
|
RX_AUDIO_STREAM = 1, // Receiver audio stream
|
|
TX_AUDIO_STREAM = 2, // Audio stream for transmitter
|
|
TX_CHRONO = 3, // Time marker flow for audio signal transmission
|
|
LINEOUT_STREAM = 4 // Linear audio output audio stream
|
|
} ;
|
|
|
|
enum SampleType // ExpertSDR3
|
|
{
|
|
TCI_INT16 = 0, // 16-bit integral character type
|
|
TCI_INT24 = 1, // 24-bit integral character type
|
|
TCI_INT32 = 2, // 32-bit integral character type
|
|
TCI_FLOAT32 = 3, // 32-bit type with a floating point
|
|
} ;
|
|
|
|
struct ClientData { // data for each client
|
|
char msg_buf[TCI_RX_BUF_SIZE];
|
|
int msg_length;
|
|
int send_Rx_audio_stream;
|
|
int audio_stream_samplerate;
|
|
int audio_stream_sample_type;
|
|
int audio_stream_channels;
|
|
int audio_stream_samples;
|
|
int audio_stream_bytes_per_sample;
|
|
// Rx stream decimation filters
|
|
struct quisk_cHB45Filter HalfBand1;
|
|
struct quisk_cFilter filtDecim3;
|
|
struct quisk_cFilter filtDecim48to24;
|
|
// Tx stream interpolation filters
|
|
struct quisk_cHB45Filter HalfBandTx1;
|
|
struct quisk_cHB45Filter HalfBandTx2;
|
|
struct quisk_cFilter filtTxInterp3;
|
|
int send_IQ_stream;
|
|
int IQ_stream_samplerate;
|
|
} ;
|
|
|
|
struct _Stream {
|
|
uint32_t receiver; // receiver number
|
|
uint32_t sample_rate; // sampling rate
|
|
uint32_t format; // sample type determined in SampleType
|
|
uint32_t codec; // compression algorithm (not implemented), always 0
|
|
uint32_t crc; // checksum (not implemented), always 0
|
|
int32_t length; // number of samples
|
|
uint32_t type; // stream type determined in StreamType
|
|
uint32_t channels; // number of channels for ExpertSDR3
|
|
uint32_t reserv[8]; // reserved
|
|
uint8_t data[TCI_STREAM_DATA_BYTES]; // samples
|
|
} ;
|
|
|
|
struct _TxChrono {
|
|
uint32_t receiver; // receiver number
|
|
uint32_t sample_rate; // sampling rate
|
|
uint32_t format; // sample type determined in SampleType
|
|
uint32_t codec; // compression algorithm (not implemented), always 0
|
|
uint32_t crc; // checksum (not implemented), always 0
|
|
int32_t length; // number of samples
|
|
uint32_t type; // stream type determined in StreamType
|
|
uint32_t channels; // number of channels for ExpertSDR3
|
|
uint32_t reserv[8]; // reserved
|
|
} ;
|
|
|
|
static struct _TxChrono TxChrono = {
|
|
.receiver = 0,
|
|
.sample_rate = 48000,
|
|
.format = TCI_FLOAT32,
|
|
.codec = 0,
|
|
.crc = 0,
|
|
.length = 0,
|
|
.type = TX_CHRONO,
|
|
.channels = 2,
|
|
.reserv = {0, 0, 0, 0, 0, 0, 0, 0}
|
|
} ;
|
|
|
|
struct _StreamTypes {
|
|
char * name;
|
|
time_t when;
|
|
} ;
|
|
|
|
#define NUM_STREAM_TYPES 5
|
|
struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = {
|
|
{"iq ", 0},
|
|
{"rx_audio ", 0},
|
|
{"tx_audio ", 0},
|
|
{"tx_chrono", 0},
|
|
{"lineout ", 0}
|
|
} ;
|
|
|
|
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
|
|
static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx);
|
|
|
|
static void PrintStream(const char * msg, size_t size, int always)
|
|
{
|
|
struct _Stream * pt = (struct _Stream *)msg;
|
|
|
|
if (always || time(NULL) - StreamTypes[pt->type].when > 5) {
|
|
StreamTypes[pt->type].when = time(NULL);
|
|
if (pt->type == TX_AUDIO_STREAM)
|
|
QuiskPrintf("TCI Receiving %slength %5d:", StreamTypes[pt->type].name, (int)size);
|
|
else
|
|
QuiskPrintf("TCI Sending %s length %5d:", StreamTypes[pt->type].name, (int)size);
|
|
QuiskPrintf("%d %5d %d %d %d %5d %d %d\n", (int)(pt->receiver), (int)(pt->sample_rate), (int)(pt->format), (int)(pt->codec),
|
|
(int)(pt->crc), (int)(pt->length), (int)(pt->type), (int)(pt->channels));
|
|
}
|
|
}
|
|
|
|
static int sendframe_txt(ws_cli_conn_t client, const char * msg)
|
|
{
|
|
if (verbose)
|
|
QuiskPrintf("TCI Send text %s\n", msg);
|
|
return ws_sendframe_txt(client, msg);
|
|
}
|
|
|
|
static int sendframe_txt_bcast(uint16_t port, const char * msg)
|
|
{
|
|
if (verbose)
|
|
QuiskPrintf("TCI Broadcast text %s\n", msg);
|
|
return ws_sendframe_txt_bcast(port, msg);
|
|
}
|
|
|
|
static size_t sendframe_bin(ws_cli_conn_t client, const char * msg, size_t size)
|
|
{
|
|
if (verbose)
|
|
PrintStream(msg, size, 0);
|
|
return ws_sendframe_bin(client, msg, size);
|
|
}
|
|
|
|
static int text_message(ws_cli_conn_t client, struct ClientData * ctx)
|
|
{ // This function alters the message ctx->msg_buf.
|
|
int i;
|
|
char char_buf[TCI_COMMAND_SIZE];
|
|
char * saveptr, * command;
|
|
char * arg1, * arg2, * arg3;
|
|
|
|
if (verbose)
|
|
QuiskPrintf("TCI Received text %s\n", ctx->msg_buf);
|
|
//QuiskPrintf("TCI Received text %s %20.6lf\n", ctx->msg_buf, QuiskTimeSec());
|
|
for (i = 0; ctx->msg_buf[i]; i++) {
|
|
char_buf[i] = ctx->msg_buf[i]; // make a copy of the command
|
|
ctx->msg_buf[i] = tolower(ctx->msg_buf[i]);
|
|
}
|
|
char_buf[i] = 0;
|
|
command = strtok_r(ctx->msg_buf, ":", &saveptr);
|
|
if (command == NULL)
|
|
return 0;
|
|
arg1 = strtok_r(NULL, ",;", &saveptr);
|
|
arg2 = strtok_r(NULL, ",;", &saveptr);
|
|
arg3 = strtok_r(NULL, ",;", &saveptr);
|
|
// The command will be broadcast to all clients unless the return value is zero.
|
|
switch (command[0]) {
|
|
case 'a':
|
|
if (strcmp(command, "audio_start") == 0) {
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (i = 0; i < tci_clients_count; i++) {
|
|
if (tci_clients_list[i] == client) {
|
|
ctx->send_Rx_audio_stream = 1;
|
|
// Rx stream filters
|
|
memset(&ctx->HalfBand1,
|
|
0, sizeof(struct quisk_cHB45Filter));
|
|
quisk_filt_cInit(&ctx->filtDecim3,
|
|
quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double));
|
|
quisk_filt_cInit(&ctx->filtDecim48to24,
|
|
quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double));
|
|
|
|
break;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "audio_stop") == 0) {
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (i = 0; i < tci_clients_count; i++) {
|
|
if (tci_clients_list[i] == client) {
|
|
ctx->send_Rx_audio_stream = 0;
|
|
break;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "audio_stream_sample_type") == 0) {
|
|
if (strcmp(arg1, "float32") == 0) {
|
|
ctx->audio_stream_sample_type = TCI_FLOAT32;
|
|
ctx->audio_stream_bytes_per_sample = 4;
|
|
sendframe_txt(client, char_buf);
|
|
}
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "audio_samplerate") == 0) {
|
|
i = atoi(arg1);
|
|
if (i != 48000 && i != 24000 && i != 12000 && i != 8000)
|
|
return 0;
|
|
ctx->audio_stream_samplerate = i;
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "audio_stream_channels") == 0) {
|
|
if (strcmp(arg1, "1") == 0)
|
|
ctx->audio_stream_channels = 1;
|
|
else if (strcmp(arg1, "2") == 0)
|
|
ctx->audio_stream_channels = 2;
|
|
else
|
|
return 0;
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "audio_stream_samples") == 0) {
|
|
return 0; // do not send
|
|
}
|
|
break;
|
|
case 'd':
|
|
if (strcmp(command, "dds") == 0) {
|
|
if (arg2) {
|
|
client_dds = atoll(arg2);
|
|
// Echo comes from quisk_tci_set_params()
|
|
}
|
|
else {
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
|
|
sendframe_txt(client, char_buf);
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
case 'i':
|
|
if (strcmp(command, "iq_start") == 0) {
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (i = 0; i < tci_clients_count; i++) {
|
|
if (tci_clients_list[i] == client) {
|
|
ctx->send_IQ_stream = 1;
|
|
break;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "iq_stop") == 0) {
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (i = 0; i < tci_clients_count; i++) {
|
|
if (tci_clients_list[i] == client) {
|
|
ctx->send_IQ_stream = 0;
|
|
break;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "iq_samplerate") == 0) {
|
|
ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate;
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "iq_samplerate:%d;", quisk_sound_state.sample_rate);
|
|
sendframe_txt(client, char_buf);
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "if") == 0) {
|
|
if (arg3) {
|
|
client_if = atoi(arg3);
|
|
// Echo comes from quisk_tci_set_params() UNIMPLEMENTED
|
|
}
|
|
else {
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
|
|
sendframe_txt(client, char_buf);
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
case 'm':
|
|
if (strcmp(command, "modulation") == 0) {
|
|
if (arg2) {
|
|
strncpy(client_modulation, arg2, TCI_COMMAND_SIZE - 1);
|
|
// Echo comes from quisk_tci_set_params()
|
|
}
|
|
else {
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
|
|
sendframe_txt(client, char_buf);
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
case 's':
|
|
if (strcmp(command, "split_enable") == 0) {
|
|
if (arg2) {
|
|
if (strcmp(arg2, "true") == 0)
|
|
client_split_enable = 1;
|
|
else
|
|
client_split_enable = 0;
|
|
// Echo comes from quisk_tci_set_params()
|
|
}
|
|
else {
|
|
if (quisk_split_enable)
|
|
sendframe_txt(client, "split_enable:0,true;");
|
|
else
|
|
sendframe_txt(client, "split_enable:0,false;");
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
case 't':
|
|
if (strcmp(command, "trx") == 0) {
|
|
if (arg2) {
|
|
if (strcmp(arg2, "true") == 0) {
|
|
if (quisk_trx == 0) {
|
|
pthread_mutex_lock(&tx_buffer_mutex);
|
|
client_trx = 1;
|
|
tci_tx_audio_request = TCI_STREAM_DATA_BYTES /
|
|
ctx->audio_stream_bytes_per_sample / ctx->audio_stream_channels;
|
|
CircularBuffer(0, NULL, tci_tx_audio_request * 2, 0);
|
|
tci_tx_audio_client = client;
|
|
tci_tx_audio_time = QuiskTimeSec();
|
|
tci_tx_audio_samples = 0;
|
|
tci_tx_audio_rate = ctx->audio_stream_samplerate;
|
|
pthread_mutex_unlock(&tx_buffer_mutex);
|
|
// Tx stream filters
|
|
memset(&ctx->HalfBandTx1,
|
|
0, sizeof(struct quisk_cHB45Filter));
|
|
memset(&ctx->HalfBandTx2,
|
|
0, sizeof(struct quisk_cHB45Filter));
|
|
quisk_filt_cInit(&ctx->filtTxInterp3,
|
|
quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
|
}
|
|
}
|
|
else if (client == tci_tx_audio_client) {
|
|
client_trx = 0;
|
|
tci_tx_audio_client = 0;
|
|
}
|
|
// Echo comes from quisk_tci_set_params()
|
|
}
|
|
else {
|
|
if (quisk_trx)
|
|
sendframe_txt(client, "trx:0,true;");
|
|
else
|
|
sendframe_txt(client, "trx:0,false;");
|
|
}
|
|
return 0;
|
|
}
|
|
else if (strcmp(command, "tx_stream_audio_buffering") == 0) {
|
|
return 0; // do not send
|
|
}
|
|
break;
|
|
case 'v':
|
|
if (strcmp(command, "vfo") == 0) {
|
|
if (arg3) {
|
|
client_vfo = atoll(arg3);
|
|
// Echo comes from quisk_tci_set_params()
|
|
}
|
|
else {
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
|
sendframe_txt(client, char_buf);
|
|
}
|
|
return 0;
|
|
}
|
|
break;
|
|
}
|
|
return 1; // send to all clients
|
|
}
|
|
|
|
static void onopen(ws_cli_conn_t client)
|
|
{
|
|
char *cli;
|
|
char command[TCI_COMMAND_SIZE];
|
|
struct ClientData * ctx;
|
|
|
|
// Make parameters for each client:
|
|
ctx = malloc(sizeof(struct ClientData));
|
|
ctx->msg_length = 0;
|
|
ctx->send_Rx_audio_stream = 0;
|
|
ctx->send_IQ_stream = 0;
|
|
ctx->audio_stream_samplerate = 48000;
|
|
ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate;
|
|
ctx->audio_stream_sample_type = TCI_FLOAT32;
|
|
ctx->audio_stream_channels = 2;
|
|
ctx->audio_stream_samples = 0;
|
|
ctx->audio_stream_bytes_per_sample = 4;
|
|
ws_set_connection_context(client, ctx);
|
|
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
tci_clients_list[tci_clients_count++] = client;
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
|
|
if (verbose) {
|
|
cli = ws_getaddress(client);
|
|
QuiskPrintf("TCI *Connection opened, addr: %s\n", cli);
|
|
}
|
|
|
|
sendframe_txt(client, "protocol:ExpertSDR3,2.0;");
|
|
//sendframe_txt(client, "vfo_limits:30000,30000000;"); Not known due to transverters
|
|
int if_limit = quisk_sound_state.sample_rate / 2;
|
|
snprintf(command, TCI_COMMAND_SIZE, "if_limits:%d,%d;", - if_limit, if_limit);
|
|
sendframe_txt(client, command);
|
|
sendframe_txt(client, "trx_count:1;");
|
|
sendframe_txt(client, "channel_count:1;");
|
|
sendframe_txt(client, "device:QuiskSDR;");
|
|
sendframe_txt(client, "receive_only:false;");
|
|
sendframe_txt(client, "modulations_list:usb,lsb,cw,am,fm,digl,digu;");
|
|
sendframe_txt(client, "ready;");
|
|
|
|
sendframe_txt(client, "start;");
|
|
sendframe_txt(client, "tx_enable:0,true;");
|
|
snprintf(command, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
|
|
sendframe_txt(client, command);
|
|
snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
|
|
sendframe_txt(client, command);
|
|
snprintf(command, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation);
|
|
sendframe_txt(client, command);
|
|
snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
|
sendframe_txt(client, command);
|
|
if (quisk_trx)
|
|
sendframe_txt(client, "trx:0,true;");
|
|
else
|
|
sendframe_txt(client, "trx:0,false;");
|
|
if (quisk_split_enable)
|
|
sendframe_txt(client, "split_enable:0,true;");
|
|
else
|
|
sendframe_txt(client, "split_enable:0,false;");
|
|
}
|
|
|
|
static void onclose(ws_cli_conn_t client)
|
|
{
|
|
int i, count;
|
|
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
count = tci_clients_count;
|
|
tci_clients_count = 0;
|
|
for (i = 0; i < count; i++)
|
|
if (tci_clients_list[i] != client)
|
|
tci_clients_list[tci_clients_count++] = tci_clients_list[i];
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
|
|
free(ws_get_connection_context(client));
|
|
if (verbose) {
|
|
char * cli = ws_getaddress(client);
|
|
QuiskPrintf("TCI *Connection closed, addr: %s\n", cli);
|
|
}
|
|
}
|
|
|
|
static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t size, int type)
|
|
{
|
|
char echo_copy[TCI_RX_BUF_SIZE];
|
|
int n;
|
|
static complex double * cCopy = NULL;
|
|
static int cCopy_size = 0;
|
|
struct ClientData * ctx = ws_get_connection_context(client);
|
|
|
|
if (type == WS_FR_OP_TXT) {
|
|
if (size > TCI_RX_BUF_SIZE / 2)
|
|
return; // prevent overflow
|
|
if (ctx->msg_length + size >= TCI_RX_BUF_SIZE - 2)
|
|
ctx->msg_length = 0; // prevent overflow
|
|
memcpy(ctx->msg_buf + ctx->msg_length, msg, size); // append msg to buffer
|
|
ctx->msg_length += size;
|
|
ctx->msg_buf[ctx->msg_length] = '\0';
|
|
char * semicolon;
|
|
char save;
|
|
while ((semicolon = strchr(ctx->msg_buf, ';')) != NULL) {
|
|
save = *(semicolon + 1);
|
|
*(semicolon + 1) = '\0';
|
|
strncpy(echo_copy, ctx->msg_buf, TCI_RX_BUF_SIZE);
|
|
if (text_message(client, ctx)) // should we send to all clients?
|
|
sendframe_txt_bcast(tci_port, echo_copy);
|
|
*(semicolon + 1) = save;
|
|
size_t processed_len = (semicolon - ctx->msg_buf) + 1;
|
|
size_t remaining_len = ctx->msg_length - processed_len;
|
|
memmove(ctx->msg_buf, ctx->msg_buf + processed_len, remaining_len);
|
|
ctx->msg_length = remaining_len;
|
|
ctx->msg_buf[ctx->msg_length] = '\0';
|
|
}
|
|
}
|
|
else if (type == WS_FR_OP_BIN) {
|
|
struct _Stream * pt = (struct _Stream *)msg;
|
|
int count = 0;
|
|
float re, im;
|
|
int channels = TxChrono.channels;
|
|
int two_channels = channels == 2;
|
|
int header = 16 * sizeof(uint32_t);
|
|
const unsigned char * vpt = msg + header;
|
|
const unsigned char * vpt_end = vpt + (size - header);
|
|
const unsigned char * vpt2;
|
|
const unsigned char * vpt2_end;
|
|
int count2;
|
|
|
|
if (verbose >= 3) {
|
|
QuiskPrintf("\n\nMSG size %ld length %d\n 0 ", size, pt->length);
|
|
vpt2 = msg + header;
|
|
vpt2_end = vpt2 + (size - header);
|
|
count2 = 0;
|
|
while (vpt2 < vpt2_end) {
|
|
re = *(float *)vpt2;
|
|
vpt2 += sizeof(float);
|
|
im = *(float *)vpt2;
|
|
vpt2 += sizeof(float);
|
|
if (fabsf(re) > 1.1)
|
|
re = 9;
|
|
if (fabsf(im) > 1.1)
|
|
im = 9;
|
|
count2++;
|
|
QuiskPrintf("%8.4f %8.4f ", re, im);
|
|
if (count2 % 5 == 0)
|
|
QuiskPrintf("\n%4d ", count2);
|
|
}
|
|
QuiskPrintf("\n");
|
|
}
|
|
|
|
if (pt->type == TX_AUDIO_STREAM) {
|
|
n = pt->length * 7 + 100; // Make room for 6X interpolation
|
|
if (cCopy_size < n) {
|
|
if (cCopy)
|
|
free(cCopy);
|
|
cCopy_size = n;
|
|
cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double));
|
|
}
|
|
if (pt->length > 0 && client == tci_tx_audio_client) {
|
|
n = 0;
|
|
switch (TxChrono.format) {
|
|
case TCI_INT16:
|
|
break;
|
|
case TCI_FLOAT32:
|
|
// pt->length is the number of floats
|
|
while (vpt < vpt_end && count < pt->length) {
|
|
re = *(float *)vpt;
|
|
vpt += sizeof(float);
|
|
count++;
|
|
if (two_channels) {
|
|
im = *(float *)vpt;
|
|
vpt += sizeof(float);
|
|
count++;
|
|
}
|
|
else {
|
|
im = re;
|
|
}
|
|
cCopy[n++] = (re + I * im) * (CLIP32 / 2);
|
|
}
|
|
if (verbose && count != pt->length)
|
|
QuiskPrintf("TCI *TX count %ld %d %d\n", size, count, pt->length);
|
|
n = interpolate_tx_audio_stream(cCopy, n, ctx);
|
|
pthread_mutex_lock(&tx_buffer_mutex);
|
|
CircularBuffer(0, cCopy, 0, n);
|
|
pthread_mutex_unlock(&tx_buffer_mutex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (verbose)
|
|
PrintStream((const char *)msg, size, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
static void tci_startup(void)
|
|
{
|
|
struct ws_server tci = {
|
|
.host = NULL,
|
|
.port = 0,
|
|
.thread_loop = 1,
|
|
.timeout_ms = 1000,
|
|
.evs.onopen = &onopen,
|
|
.evs.onclose = &onclose,
|
|
.evs.onmessage = &onmessage
|
|
};
|
|
char host32[32];
|
|
|
|
tci_clients_count = 0;
|
|
tci_port = QuiskGetConfigInt("tci_port", 40001);
|
|
strncpy(host32, QuiskGetConfigString("tci_ip", "127.0.0.1"), 31);
|
|
tci.host = host32;
|
|
tci.port = tci_port;
|
|
ws_socket(&tci);
|
|
if (verbose)
|
|
QuiskPrintf("TCI Start server on host %s port %d\n", tci.host, tci.port);
|
|
return;
|
|
}
|
|
|
|
static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx)
|
|
{
|
|
switch(ctx->audio_stream_samplerate) { // Starting audio rate is 48000
|
|
case 48000:
|
|
break;
|
|
case 24000:
|
|
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2);
|
|
break;
|
|
case 12000:
|
|
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1);
|
|
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2);
|
|
break;
|
|
case 8000:
|
|
nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1);
|
|
nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim3, 3);
|
|
break;
|
|
default:
|
|
nSamples = 0;
|
|
break;
|
|
}
|
|
return nSamples;
|
|
}
|
|
|
|
static int interpolate_tx_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx)
|
|
{
|
|
switch(tci_tx_audio_rate) { // Ending audio rate is 48000
|
|
case 48000:
|
|
break;
|
|
case 24000:
|
|
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
|
|
break;
|
|
case 12000:
|
|
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
|
|
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx2);
|
|
break;
|
|
case 8000:
|
|
nSamples = quisk_cInterpolate(cSamples, nSamples, &ctx->filtTxInterp3, 3);
|
|
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &ctx->HalfBandTx1);
|
|
break;
|
|
default:
|
|
nSamples = 0;
|
|
break;
|
|
}
|
|
return nSamples;
|
|
}
|
|
|
|
void tci_send_audio(complex double * cSamples, int nSamples) // called from the sound thread
|
|
{
|
|
int i, n;
|
|
struct _Stream stream;
|
|
struct ClientData * ctx;
|
|
static complex double * cCopy = NULL;
|
|
static int cCopy_size = 0;
|
|
complex double * cAudio;
|
|
|
|
if (tci_clients_count <= 0)
|
|
return;
|
|
|
|
if (nSamples <= 0)
|
|
return;
|
|
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (n = 0; n < tci_clients_count; n++) {
|
|
ctx = ws_get_connection_context(tci_clients_list[n]);
|
|
if (ctx->send_Rx_audio_stream == 0)
|
|
continue;
|
|
memset(&stream, 0, 16 * sizeof(uint32_t));
|
|
stream.sample_rate = ctx->audio_stream_samplerate;
|
|
stream.format = ctx->audio_stream_sample_type;
|
|
stream.type = RX_AUDIO_STREAM;
|
|
stream.channels = ctx->audio_stream_channels;
|
|
int two_channels = stream.channels == 2;
|
|
int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
|
|
void * vpt = &stream.data;
|
|
void * vpt_end = vpt + TCI_STREAM_DATA_BYTES;
|
|
if (ctx->audio_stream_samplerate != 48000) { // decimation must not change cSamples
|
|
if (cCopy_size < nSamples) {
|
|
if (cCopy)
|
|
free(cCopy);
|
|
cCopy_size = nSamples * 2;
|
|
cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double));
|
|
}
|
|
memcpy(cCopy, cSamples, nSamples * sizeof(complex double));
|
|
nSamples = decimate_audio_stream(cCopy, nSamples, ctx);
|
|
cAudio = cCopy;
|
|
}
|
|
else {
|
|
cAudio = cSamples;
|
|
}
|
|
switch (ctx->audio_stream_sample_type) {
|
|
case TCI_FLOAT32:
|
|
stream.length = 0; // make a frame
|
|
for (i = 0; i < nSamples; i++) {
|
|
*(float *)vpt = (float)(creal(cAudio[i]) * (1.0 / 2147483648.0 / 2));
|
|
vpt += bytes_per_sample;
|
|
stream.length++;
|
|
if (two_channels) {
|
|
*(float *)vpt = (float)(cimag(cAudio[i]) * (1.0 / 2147483648.0 / 2));
|
|
vpt += bytes_per_sample;
|
|
stream.length++;
|
|
}
|
|
if (vpt >= vpt_end || i == nSamples - 1) {
|
|
sendframe_bin(tci_clients_list[n], (const char *)&stream,
|
|
(16 * sizeof(uint32_t) + stream.length * bytes_per_sample));
|
|
stream.length = 0;
|
|
vpt = &stream.data;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
}
|
|
|
|
void tci_send_iq(complex double * cSamples, int nSamples) // called from the sound thread
|
|
{
|
|
int i, n;
|
|
struct _Stream stream;
|
|
struct ClientData * ctx;
|
|
|
|
if (tci_clients_count <= 0)
|
|
return;
|
|
|
|
if (nSamples <= 0)
|
|
return;
|
|
|
|
pthread_mutex_lock(&clients_list_mutex);
|
|
for (n = 0; n < tci_clients_count; n++) {
|
|
ctx = ws_get_connection_context(tci_clients_list[n]);
|
|
if (ctx->send_IQ_stream == 0)
|
|
continue;
|
|
memset(&stream, 0, 16 * sizeof(uint32_t));
|
|
stream.sample_rate = ctx->IQ_stream_samplerate;
|
|
stream.format = ctx->audio_stream_sample_type;
|
|
stream.type = IQ_STREAM;
|
|
stream.channels = 2;
|
|
int bytes_per_sample = ctx->audio_stream_bytes_per_sample;
|
|
void * vpt = &stream.data;
|
|
void * vpt_end = vpt + TCI_STREAM_DATA_BYTES;
|
|
int original_length = nSamples;
|
|
switch (ctx->audio_stream_sample_type) {
|
|
case TCI_FLOAT32:
|
|
stream.length = 0; // make a frame
|
|
for (i = 0; i < nSamples; i++) {
|
|
*(float *)vpt = (float)(creal(cSamples[i]) * (1.0 / 2147483648.0 / 2));
|
|
vpt += bytes_per_sample;
|
|
stream.length++;
|
|
*(float *)vpt = (float)(cimag(cSamples[i]) * (1.0 / 2147483648.0 / 2));
|
|
vpt += bytes_per_sample;
|
|
stream.length++;
|
|
if (vpt >= vpt_end || i == nSamples - 1) {
|
|
sendframe_bin(tci_clients_list[n], (const char *)&stream,
|
|
(16 * sizeof(uint32_t) + stream.length * bytes_per_sample));
|
|
original_length -= stream.length / 2;
|
|
stream.length = 0;
|
|
vpt = &stream.data;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (verbose && original_length != 0)
|
|
QuiskPrintf("Error in tci_send_iq: Samples remain %d\n", original_length);
|
|
}
|
|
pthread_mutex_unlock(&clients_list_mutex);
|
|
}
|
|
|
|
int tci_get_mic(complex double * cSamples, int mic_count) // called from the sound thread
|
|
{
|
|
if (tci_tx_audio_client) {
|
|
pthread_mutex_lock(&tx_buffer_mutex); // Copy samples from the Tx buffer
|
|
int count = CircularBuffer(0, cSamples, mic_count, 0);
|
|
pthread_mutex_unlock(&tx_buffer_mutex);
|
|
if (count < mic_count) {
|
|
if (verbose >= 2)
|
|
QuiskPrintf("TCI *TX replace mic %d %d\n", count, mic_count);
|
|
while (count < mic_count)
|
|
cSamples[count++] = 0;
|
|
}
|
|
// Ask for more samples
|
|
double dtime = QuiskTimeSec();
|
|
if (tci_tx_audio_samples < (dtime - tci_tx_audio_time) * tci_tx_audio_rate) {
|
|
TxChrono.length = tci_tx_audio_request;
|
|
sendframe_bin(tci_tx_audio_client, (const char *)&TxChrono, 16 * sizeof(uint32_t));
|
|
//tci_tx_audio_samples += tci_tx_audio_request;
|
|
tci_tx_audio_samples += tci_tx_audio_request / 2; // Stream.length is floats, not samples
|
|
if (verbose >= 3)
|
|
QuiskPrintf("TCI Request more Tx samples\n");
|
|
}
|
|
}
|
|
return mic_count;
|
|
}
|
|
|
|
PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds) // called from the GUI thread
|
|
{ /* Call with keyword arguments ONLY.
|
|
Sent from Quisk when parameters change. Broadcast the change to clients.*/
|
|
static char * kwlist[] = {"start", "close", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable",
|
|
"tci_modulation", NULL} ;
|
|
long long new_vfo=-1;
|
|
long long new_dds=-1;
|
|
int start=-1, close=-1, new_trx=-1, new_split_enable=-1;
|
|
int new_if = BAD_TCI_IF;
|
|
char * mode=NULL;
|
|
char char_buf[TCI_COMMAND_SIZE];
|
|
|
|
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiLiLiis", kwlist,
|
|
&start, &close, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable,
|
|
&mode))
|
|
return NULL;
|
|
if (start == 1) {
|
|
tci_started = 1;
|
|
tci_startup(); // start the TCP server for TCI
|
|
}
|
|
if (tci_started == 0) {
|
|
Py_INCREF (Py_None);
|
|
return Py_None;
|
|
}
|
|
if (close != -1) {
|
|
sendframe_txt_bcast(tci_port, "stop;");
|
|
}
|
|
if (new_dds != -1) {
|
|
quisk_dds = new_dds;
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds);
|
|
sendframe_txt_bcast(tci_port, char_buf);
|
|
}
|
|
if (new_if != BAD_TCI_IF) {
|
|
quisk_if = new_if;
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if);
|
|
sendframe_txt_bcast(tci_port, char_buf);
|
|
}
|
|
if (new_vfo != -1) {
|
|
quisk_vfo = new_vfo;
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo);
|
|
sendframe_txt_bcast(tci_port, char_buf);
|
|
}
|
|
if (new_split_enable != -1) {
|
|
quisk_split_enable = new_split_enable;
|
|
if (quisk_split_enable)
|
|
sendframe_txt_bcast(tci_port, "split_enable:0,true;");
|
|
else
|
|
sendframe_txt_bcast(tci_port, "split_enable:0,false;");
|
|
}
|
|
if (new_trx != -1) {
|
|
quisk_trx = new_trx;
|
|
if (quisk_trx) {
|
|
sendframe_txt_bcast(tci_port, "trx:0,true;");
|
|
}
|
|
else {
|
|
tci_tx_audio_client = 0;
|
|
sendframe_txt_bcast(tci_port, "trx:0,false;");
|
|
}
|
|
}
|
|
if (mode) {
|
|
if (strcmp(mode, "DGT-U") == 0)
|
|
strcpy(quisk_modulation, "digu");
|
|
else if (strcmp(mode, "DGT-L") == 0)
|
|
strcpy(quisk_modulation, "digl");
|
|
else if (strcmp(mode, "CWL") == 0 || strcmp(mode, "CWU") == 0)
|
|
strcpy(quisk_modulation, "cw");
|
|
else if (strcmp(mode, "LSB") == 0)
|
|
strcpy(quisk_modulation, "lsb");
|
|
else if (strcmp(mode, "USB") == 0)
|
|
strcpy(quisk_modulation, "usb");
|
|
else if (strcmp(mode, "AM") == 0)
|
|
strcpy(quisk_modulation, "am");
|
|
else if (strcmp(mode, "FM") == 0)
|
|
strcpy(quisk_modulation, "fm");
|
|
else
|
|
strncpy(quisk_modulation, mode, TCI_COMMAND_SIZE - 1);
|
|
snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s", quisk_modulation);
|
|
sendframe_txt_bcast(tci_port, char_buf);
|
|
}
|
|
Py_INCREF (Py_None);
|
|
return Py_None;
|
|
}
|
|
|
|
PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from the GUI thread
|
|
{ /* Return client request for a parameter change */
|
|
const char * name;
|
|
int ii;
|
|
long long ll;
|
|
char char_buf[TCI_COMMAND_SIZE];
|
|
|
|
if (!PyArg_ParseTuple (args, "s", &name))
|
|
return NULL;
|
|
|
|
if (strcmp(name, "tci_clients_count") == 0)
|
|
return PyLong_FromLong(tci_clients_count);
|
|
|
|
if (tci_started == 0) {
|
|
}
|
|
else if (strcmp(name, "tci_vfo") == 0 && client_vfo >= 0) {
|
|
ll = client_vfo;
|
|
client_vfo = -1;
|
|
return PyLong_FromLongLong(ll);
|
|
}
|
|
else if (strcmp(name, "tci_dds") == 0 && client_dds >= 0) {
|
|
ll = client_dds;
|
|
client_dds = -1;
|
|
return PyLong_FromLongLong(ll);
|
|
}
|
|
else if (strcmp(name, "tci_if") == 0 && client_if != BAD_TCI_IF) {
|
|
ii = client_if;
|
|
client_if = BAD_TCI_IF;
|
|
return PyLong_FromLong(ii);
|
|
}
|
|
else if (strcmp(name, "tci_split_enable") == 0 && client_split_enable >= 0) {
|
|
ii = client_split_enable;
|
|
client_split_enable = -1;
|
|
return PyLong_FromLong(ii);
|
|
}
|
|
else if (strcmp(name, "tci_trx") == 0 && client_trx >= 0) {
|
|
ii = client_trx;
|
|
client_trx = -1;
|
|
return PyLong_FromLong(ii);
|
|
}
|
|
else if (strcmp(name, "tci_modulation") == 0 && client_modulation[0] != 0) {
|
|
char_buf[0] = 0;
|
|
if (strcmp(client_modulation, "digu") == 0)
|
|
strcpy(char_buf, "DGT-U");
|
|
else if (strcmp(client_modulation, "digl") == 0)
|
|
strcpy(char_buf, "DGT-L");
|
|
else if (strcmp(client_modulation, "cw") == 0)
|
|
strcpy(char_buf, "CWU");
|
|
else if (strcmp(client_modulation, "lsb") == 0)
|
|
strcpy(char_buf, "LSB");
|
|
else if (strcmp(client_modulation, "usb") == 0)
|
|
strcpy(char_buf, "USB");
|
|
else if (strcmp(client_modulation, "am") == 0)
|
|
strcpy(char_buf, "AM");
|
|
else if (strcmp(client_modulation, "fm") == 0)
|
|
strcpy(char_buf, "FM");
|
|
client_modulation[0] = 0;
|
|
if (char_buf[0])
|
|
return PyUnicode_FromString(char_buf);
|
|
}
|
|
Py_INCREF (Py_None);
|
|
return Py_None;
|
|
}
|