--- /dev/null
+/*
+ freebeacon.c
+ David Rowe
+ Created Dec 2015
+
+ FreeDV beacon daemon. Listens for freedv signals, then transmits a
+ reply. Saves received signals as a rotating log of wave files so
+ they can be published on a web site.
+
+ If stereo audio device we use left channel only.
+
+ A whole lot of code was lifted from freedv-dev for this program.
+
+ TODO:
+
+ [X] 48 to 8 kHz sample rate conversion
+ [X] Port Audio list devices
+ [X] command line processing framework
+ [ ] alsa loop to test with wave files
+ [X] beacon state machine
+ [ ] rotating log
+ [ ] RS232 tx code
+ [ ] writing to wave files
+ [ ] basic SM1000 version
+ + has audio interfaces
+
+ Building:
+
+ gcc freebeacon.c -o freebeacon -lsamplerate -lportaudio -lsndfile -I/home/david/codec2-dev/src /home/david/codec2-dev/build_linux/src/libcodec2.so
+
+ Running:
+ LD_LIBRARY_PATH=/home/david/codec2-dev/build_linux/src/ ./freebeacon
+
+ (note I really should "make install" Codec 2 and have a proper cmake file)
+*/
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <samplerate.h>
+#include <getopt.h>
+
+#include "sndfile.h"
+#include "portaudio.h"
+
+#include "codec2_fifo.h"
+#include "modem_stats.h"
+#include "freedv_api.h"
+
+#define MAX_CHAR 80
+#define FS 8000 // 8 kHz sampling rate used for modem
+#define SAMPLE_RATE 48000 // 48 kHz sampling rate rec. as we can trust accuracy of sound card
+#define N8 160 // processing buffer size at 8 kHz
+#define N48 (N8*SAMPLE_RATE/FS) // processing buffer size at 48 kHz
+
+/* globals used to communicate with async events */
+
+volatile int keepRunning;
+char txtMsg[MAX_CHAR], *ptxtMsg, triggerString[MAX_CHAR];
+int triggered;
+
+/* state machine defines */
+
+#define SRX_IDLE 0 /* listening but no FreeDV signal */
+#define SRX_SYNC 1 /* We have sync on a valid FreeDV signal */
+#define SRX_TRIGGERED 2 /* the magic trigger text string has been received, but still in RX */
+#define STX 3 /* transmitting reply */
+
+char *state_str[] = {
+ "Rx Idle",
+ "Rx Sync",
+ "Rx Triggered",
+ "Tx"
+};
+
+
+/* Called on Ctrl-C */
+
+void intHandler(int dummy) {
+ keepRunning = 0;
+}
+
+/* returns number of output samples generated by resampling */
+
+int resample(SRC_STATE *src,
+ short output_short[],
+ short input_short[],
+ int output_sample_rate,
+ int input_sample_rate,
+ int length_output_short, // maximum output array length in samples
+ int length_input_short
+ )
+{
+ SRC_DATA src_data;
+ float input[N48*4];
+ float output[N48*4];
+ int ret;
+
+ assert(src != NULL);
+ assert(length_input_short <= N48*4);
+ assert(length_output_short <= N48*4);
+
+ src_short_to_float_array(input_short, input, length_input_short);
+
+ src_data.data_in = input;
+ src_data.data_out = output;
+ src_data.input_frames = length_input_short;
+ src_data.output_frames = length_output_short;
+ src_data.end_of_input = 0;
+ src_data.src_ratio = (float)output_sample_rate/input_sample_rate;
+
+ ret = src_process(src, &src_data);
+ assert(ret == 0);
+
+ assert(src_data.output_frames_gen <= length_output_short);
+ src_float_to_short_array(output, output_short, src_data.output_frames_gen);
+
+ return src_data.output_frames_gen;
+}
+
+
+void listAudioDevices(void) {
+ const PaDeviceInfo *deviceInfo = NULL;
+ PaError err;
+ int numDevices, devn;
+
+ numDevices = Pa_GetDeviceCount();
+ printf("Num Name API InCh OutCh DefFs\n");
+ printf("==========================================================================\n");
+ for (devn = 0; devn<numDevices; devn++) {
+ deviceInfo = Pa_GetDeviceInfo(devn);
+ if (deviceInfo == NULL) {
+ fprintf(stderr, "Couldn't open devNum: %d\n", devn);
+ return;
+ }
+ printf(" %2d %40s %8s %6d %6d %6d\n",
+ devn,
+ deviceInfo->name,
+ Pa_GetHostApiInfo(deviceInfo->hostApi)->name,
+ deviceInfo->maxInputChannels,
+ deviceInfo->maxOutputChannels,
+ (int)deviceInfo->defaultSampleRate);
+ }
+}
+
+
+void printHelp(const struct option* long_options, int num_opts, char* argv[])
+{
+ int i;
+ char *option_parameters;
+
+ fprintf(stderr, "\nFreeBeacon - FreeDV Beacon\n"
+ "usage: %s [OPTIONS]\n\n"
+ "Options:\n"
+ "\t-l list audio devices\n", argv[0]);
+ for(i=0; i<num_opts-1; i++) {
+ if(long_options[i].has_arg == no_argument) {
+ option_parameters="";
+ } else if (strcmp("dev", long_options[i].name) == 0) {
+ option_parameters = " Audio Device Number";
+ } else if (strcmp("trigger", long_options[i].name) == 0) {
+ option_parameters = " text string used to trigger beacon";
+ } else if (strcmp("callsign", long_options[i].name) == 0) {
+ option_parameters = " callsign to return on tx";
+ } else if (strcmp("txfilename", long_options[i].name) == 0) {
+ option_parameters = " wavefile to use for source audio on tramsmit";
+ }
+ fprintf(stderr, "\t--%s%s\n", long_options[i].name, option_parameters);
+ }
+
+ exit(1);
+}
+
+
+/* text message callbacks */
+
+void callbackNextRxChar(void *callback_state, char c) {
+
+ /* if we hit end of buffer wrap around to start */
+
+ if ((ptxtMsg - txtMsg) < (MAX_CHAR-1))
+ *ptxtMsg++ = c;
+ else
+ ptxtMsg = txtMsg;
+
+ /* if end of string let see if we have a match for the trigger
+ string. Note tx may send trigger string many times. We only
+ need to receive it once to trigger a beacon tx cycle. */
+
+ if (c == 13) {
+ *ptxtMsg++ = c;
+ *ptxtMsg = 0;
+ ptxtMsg = txtMsg;
+ if (strstr(txtMsg, triggerString) != NULL)
+ triggered = 1;
+ }
+}
+
+char callbackNextTxChar(void *callback_state) {
+ if ((*ptxtMsg == 0) || ((ptxtMsg - txtMsg) >= MAX_CHAR))
+ ptxtMsg == txtMsg;
+
+ return *ptxtMsg++;
+}
+
+
+SNDFILE *openPlayFile(char fileName[], int *sfFs)
+{
+ SF_INFO sfInfo;
+ SNDFILE *sfPlayFile;
+
+ sfInfo.format = 0;
+
+ sfPlayFile = sf_open(fileName, SFM_READ, &sfInfo);
+ if(sfPlayFile == NULL) {
+ const char *strErr = sf_strerror(NULL);
+ fprintf(stderr, " %s couldn't open: %s", strErr, fileName);
+ }
+ *sfFs = sfInfo.samplerate;
+
+ return sfPlayFile;
+}
+
+
+int main(int argc, char *argv[]) {
+ struct freedv *f;
+ PaStreamParameters inputParameters;
+ const PaDeviceInfo *deviceInfo = NULL;
+ PaStream *stream = NULL;
+ PaError err;
+ short stereo_short[2*N48];
+ short in48k_short[N48], out48k_short[N48];
+ short in8k_short[N48];
+ int numDevices, nBufs, n8k, i, j, src_error, inputChannels, nin, devNum;
+ int outputChannels;
+ int state, next_state;
+ SRC_STATE *rxsrc, *txsrc;
+ SRC_STATE *playsrc;
+ struct FIFO *fifo;
+ char txFileName[MAX_CHAR];
+ SNDFILE *sfPlayFile;
+ int sfFs;
+ int triggerf, txfilenamef, callsignf;
+ int sync;
+ float snr_est;
+ char callsign[MAX_CHAR];
+
+ /* Defaults -------------------------------------------------------------------------------*/
+
+ devNum = 0;
+ sprintf(triggerString, "FreeBeacon");
+ sprintf(txFileName, "txaudio.wav");
+ sprintf(callsign, "FreeBeacon");
+
+ /* Process command line options -----------------------------------------------------------*/
+
+ char* opt_string = "hl:";
+ struct option long_options[] = {
+ { "dev", required_argument, &devNum, 1 },
+ { "trigger", required_argument, &triggerf, 1 },
+ { "txfilename", required_argument, &txfilenamef, 1 },
+ { "callsign", required_argument, &callsignf, 1 },
+ { NULL, no_argument, NULL, 0 }
+ };
+ int num_opts=sizeof(long_options)/sizeof(struct option);
+
+ if (argc < 2) {
+ printHelp(long_options, num_opts, argv);
+ }
+
+ while(1) {
+ int option_index = 0;
+ int opt = getopt_long(argc, argv, opt_string,
+ long_options, &option_index);
+ if (opt == -1)
+ break;
+
+ switch (opt) {
+ case 0:
+ if (strcmp(long_options[option_index].name, "dev") == 0) {
+ devNum = atoi(optarg);
+ } else if(strcmp(long_options[option_index].name, "trigger") == 0) {
+ strcpy(triggerString, optarg);
+ } else if(strcmp(long_options[option_index].name, "txfilename") == 0) {
+ strcpy(txFileName, optarg);
+ } else if(strcmp(long_options[option_index].name, "callsign") == 0) {
+ strcpy(callsign, optarg);
+ }
+ break;
+
+ case 'h':
+ printHelp(long_options, num_opts, argv);
+ break;
+
+ case 'l':
+ listAudioDevices();
+ exit(0);
+ break;
+
+ default:
+ /* This will never be reached */
+ break;
+ }
+ }
+
+
+ /* Open Sound Device and start processing --------------------------------------------------------------*/
+
+ if (Pa_Initialize()) {
+ fprintf(stderr, "Port Audio failed to initialize");
+ exit(1);
+ }
+
+ f = freedv_open(FREEDV_MODE_1600); assert(f != NULL);
+ assert(freedv_get_modem_sample_rate(f) == FS); /* just in case modem FS every changes */
+ freedv_set_callback_txt(f, callbackNextRxChar, callbackNextTxChar, NULL);
+
+ fifo = fifo_create(N48); assert(fifo != NULL);
+ rxsrc = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(rxsrc != NULL);
+ txsrc = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(txsrc != NULL);
+ playsrc = src_new(SRC_SINC_FASTEST, 1, &src_error); assert(playsrc != NULL);
+
+ /* work out how many input channels this device supports */
+
+ deviceInfo = Pa_GetDeviceInfo(devNum);
+ if (deviceInfo == NULL) {
+ fprintf(stderr, "Couldn't get device info from Port Audio for device: %d\n", devNum);
+ exit(1);
+ }
+ if (deviceInfo->maxInputChannels == 1)
+ inputChannels = 1;
+ else
+ inputChannels = 2;
+
+ /* open device */
+
+ inputParameters.device = devNum;
+ inputParameters.channelCount = inputChannels;
+ inputParameters.sampleFormat = paInt16;
+ inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputParameters.device )->defaultHighInputLatency;
+ inputParameters.hostApiSpecificStreamInfo = NULL;
+
+ fprintf(stderr, "Ctrl-C to exit\n");
+ fprintf(stderr, "trigger string: %s txFileName: %s\n", triggerString, txFileName);
+ fprintf(stderr, "Starting PortAudio on devNum: %d\n", devNum);
+
+ err = Pa_OpenStream(
+ &stream,
+ &inputParameters,
+ NULL,
+ SAMPLE_RATE,
+ 0, /* let the driver decide */
+ paClipOff,
+ NULL, /* no callback, use blocking API */
+ NULL );
+
+ if (err != paNoError) {
+ fprintf(stderr, "Couldn't initialise sound device\n");
+ exit(1);
+ }
+
+ err = Pa_StartStream(stream);
+ if (err != paNoError) {
+ fprintf(stderr, "Couldn't start sound device\n");
+ return;
+ }
+
+ signal(SIGINT, intHandler); /* ctrl-C to exit gracefully */
+
+ /* init for main loop */
+
+ state = SRX_IDLE;
+ keepRunning = 1;
+ *txtMsg = 0;
+ ptxtMsg = txtMsg;
+ triggered = 0;
+
+ while(keepRunning) {
+
+ next_state = state;
+
+ if ((state == SRX_IDLE) || (state == SRX_SYNC)) {
+ short demod_in[freedv_get_n_max_modem_samples(f)];
+ short speech_out[freedv_get_n_speech_samples(f)];
+
+ /* Read samples, resample to modem sample rate */
+
+ Pa_ReadStream(stream, stereo_short, N48);
+
+ if (inputChannels == 2) {
+ for(j=0; j<N48; j++)
+ in48k_short[j] = stereo_short[2*j]; /* left channel only */
+ }
+ else {
+ for(j=0; j<N48; j++)
+ in48k_short[j] = stereo_short[j];
+ }
+ int n8k = resample(rxsrc, in8k_short, in48k_short, FS, SAMPLE_RATE, N48, N48);
+ fifo_write(fifo, in8k_short, n8k);
+
+ /* demodulate to decoded speech samples */
+
+ nin = freedv_nin(f);
+ if (fifo_read(fifo, demod_in, nin) == nin) {
+ freedv_rx(f, speech_out, demod_in);
+ freedv_get_modem_stats(f, &sync, &snr_est);
+ }
+ }
+
+ if (state == STX) {
+ short mod_out[freedv_get_n_max_modem_samples(f)];
+ short speech_in[freedv_get_n_speech_samples(f)];
+
+ /* TODO: assert PTT, e.g. via RS232 */
+
+ /* resample sound file as can't guarantee 8KHz sample rate */
+
+ unsigned int nsf = freedv_get_n_speech_samples(f)*sfFs/FS;
+ short insf_short[nsf];
+ unsigned int n = sf_read_short(sfPlayFile, insf_short, nsf);
+ n8k = resample(playsrc, speech_in, insf_short, SAMPLE_RATE, sfFs, freedv_get_n_speech_samples(f), nsf);
+
+ if (n != nsf) {
+ /* end of file - this signals state machine we've finished */
+ sf_close(sfPlayFile);
+ sfPlayFile = NULL;
+ }
+
+ freedv_tx(f, mod_out, speech_in);
+
+ int n48k = resample(txsrc, out48k_short, mod_out, SAMPLE_RATE, FS, N48, freedv_get_n_nom_modem_samples(f));
+ for(j=0; j<n48k; j++) {
+ if (outputChannels == 2) {
+ stereo_short[2*j] = out48k_short[j]; // left channel
+ stereo_short[2*j+1] = out48k_short[j]; // right channel
+ }
+ else {
+ stereo_short[j] = out48k_short[j]; // mono
+ }
+ }
+
+ Pa_WriteStream(stream, stereo_short, n48k);
+ }
+
+ /* state machine processing */
+
+ switch(state) {
+ case SRX_IDLE:
+ if (sync) {
+ next_state = SRX_SYNC;
+ *txtMsg = 0;
+ ptxtMsg = txtMsg;
+ triggered = 0;
+ freedv_set_total_bit_errors(f, 0);
+ freedv_set_total_bits(f, 0);
+ }
+ break;
+ case SRX_SYNC:
+ if (!sync) {
+ if (triggered) {
+ float ber = (float)freedv_get_total_bit_errors(f)/freedv_get_total_bits(f);
+ char tmpStr[MAX_CHAR];
+
+ sprintf(tmpStr, "SNR: %3.1f BER: %3.2f de %s\n",
+ snr_est, ber, callsign);
+ strcpy(txtMsg, tmpStr);
+ ptxtMsg = txtMsg;
+ sfPlayFile = openPlayFile(txFileName, &sfFs);
+
+ next_state = STX;
+ }
+ }
+ else
+ next_state = SRX_IDLE;
+ break;
+ case STX:
+ if (sfPlayFile == NULL)
+ next_state = SRX_IDLE;
+ break;
+ }
+
+ if (next_state != state)
+ fprintf(stderr, "state: %s next_state %s\n", state_str[state], state_str[next_state]);
+ state = next_state;
+ }
+
+ /* Attempt to shut down gracefully */
+
+ err = Pa_StopStream(stream);
+ if (err != paNoError) {
+ fprintf(stderr, "Couldn't stop sound device\n");
+ exit(1);
+ }
+ Pa_CloseStream(stream);
+ Pa_Terminate();
+ fifo_destroy(fifo);
+ src_delete(rxsrc);
+ src_delete(txsrc);
+ src_delete(playsrc);
+ freedv_close(f);
+}