/* fl_fdmdv.cxx Created 14 June 2012 David Rowe Fltk 1.3 based GUI program to prototype FDMDV & Codec 2 integration issues such as: + spectrum, waterfall, and other FDMDV GUI displays + integration with real time audio I/O using portaudio + what we do with audio when out of sync */ #include #include #include #include #include #include #include #include #include #include #include "portaudio.h" #include "fdmdv.h" #include "codec2.h" #define MIN_DB -40.0 #define MAX_DB 0.0 #define BETA 0.1 // constant for time averageing spectrum data #define MIN_HZ 0 #define MAX_HZ 4000 #define WATERFALL_SECS_Y 5 // number of seconds respresented by y axis of waterfall #define DT 0.02 // time between samples #define FS 8000 // FDMDV modem sample rate #define SCATTER_MEM (FDMDV_NSYM)*50 #define SCATTER_X_MAX 3.0 #define SCATTER_Y_MAX 3.0 // main window params #define W 1200 #define W3 (W/3) #define H 600 #define H2 (H/2) #define SP 20 // sound card #define SAMPLE_RATE 48000 /* 48 kHz sampling rate rec. as we can trust accuracy of sound card */ #define N8 FDMDV_NOM_SAMPLES_PER_FRAME /* processing buffer size at 8 kHz */ #define MEM8 (FDMDV_OS_TAPS/FDMDV_OS) #define N48 (N8*FDMDV_OS) /* processing buffer size at 48 kHz */ #define NUM_CHANNELS 2 /* I think most sound cards prefer stereo, we will convert to mono */ #define BITS_PER_CODEC_FRAME (2*FDMDV_BITS_PER_FRAME) #define BYTES_PER_CODEC_FRAME (BITS_PER_CODEC_FRAME/8) // forward class declarations class Spectrum; class Waterfall; class Scatter; class Scalar; // Globals -------------------------------------- char *fin_name = NULL; char *fout_name = NULL; char *sound_dev_name = NULL; FILE *fin = NULL; FILE *fout = NULL; struct FDMDV *fdmdv; struct CODEC2 *codec2; float av_mag[FDMDV_NSPEC]; // shared between a few classes // GUI variables -------------------------------- Fl_Group *agroup; Fl_Window *window; Fl_Window *zoomSpectrumWindow = NULL; Fl_Window *zoomWaterfallWindow = NULL; Spectrum *aSpectrum; Spectrum *aZoomedSpectrum; Waterfall *aWaterfall; Waterfall *aZoomedWaterfall; Scatter *aScatter; Scalar *aTimingEst; Scalar *aFreqEst; Scalar *aSNR; int zoom_spectrum = 0; // Main processing loop states ------------------ float Ts = 0.0; short input_buf[2*FDMDV_NOM_SAMPLES_PER_FRAME]; int n_input_buf = 0; int nin = FDMDV_NOM_SAMPLES_PER_FRAME; short *output_buf; int n_output_buf = 0; int codec_bits[2*FDMDV_BITS_PER_FRAME]; int state = 0; // Portaudio states ----------------------------- PaStream *stream = NULL; PaError err; typedef struct { float in48k[FDMDV_OS_TAPS + N48]; float in8k[MEM8 + N8]; } paCallBackData; // Class for each window type ------------------ class Spectrum: public Fl_Box { protected: int handle(int event) { // detect a left mouse down if inside the spectrum window if ((event == FL_NO_EVENT) && (Fl::event_button() == 1)) { if ((Fl::event_x() > x()) && (Fl::event_x() < (x() + w())) && (Fl::event_y() > y()) && (Fl::event_y() < (y() + h()))) { // show zoomed spectrum window zoomSpectrumWindow->show(); } } return 0; } void draw() { float x_px_per_point = 0.0; float y_px_per_dB = 0.0; int i, x1, y1, x2, y2; float mag1, mag2; char label[20]; float px_per_hz; Fl_Box::draw(); fl_color(FL_BLACK); fl_rectf(x(),y(),w(),h()); fl_color(FL_GREEN); fl_line_style(FL_SOLID); fl_push_clip(x(),y(),w(),h()); //printf("%d %d\n", w(), h()); x_px_per_point = (float)w()/FDMDV_NSPEC; y_px_per_dB = (float)h()/(MAX_DB - MIN_DB); // plot spectrum for(i=0; i x()) && (Fl::event_x() < (x() + w())) && (Fl::event_y() > y()) && (Fl::event_y() < (y() + h()))) { // show zoomed spectrum window zoomWaterfallWindow->show(); } } return 0; } // map val to a rgb colour // from http://eddiema.ca/2011/01/21/c-sharp-heatmaps/ unsigned heatmap(float val, float min, float max) { unsigned r = 0; unsigned g = 0; unsigned b = 0; val = (val - min) / (max - min); if(val <= 0.2) { b = (unsigned)((val / 0.2) * 255); } else if(val > 0.2 && val <= 0.7) { b = (unsigned)((1.0 - ((val - 0.2) / 0.5)) * 255); } if(val >= 0.2 && val <= 0.6) { g = (unsigned)(((val - 0.2) / 0.4) * 255); } else if(val > 0.6 && val <= 0.9) { g = (unsigned)((1.0 - ((val - 0.6) / 0.3)) * 255); } if(val >= 0.5) { r = (unsigned)(((val - 0.5) / 0.5) * 255); } //printf("%f %x %x %x\n", val, r, g, b); return (b << 16) + (g << 8) + r; } void draw() { float spec_index_per_px, intensity_per_dB; int px_per_sec; int index, dy, dy_blocks, bytes_in_row_of_blocks, b; int px, py, intensity; unsigned *last_row, *pdest, *psrc; /* detect resizing of window */ if ((h() != prev_h) || (w() != prev_w)) { delete pixel_buf; new_pixel_buf(w(), h()); } Fl_Box::draw(); // determine dy, the height of one "block" px_per_sec = (float)h()/WATERFALL_SECS_Y; dy = DT*px_per_sec; // number of dy high blocks in spectrogram dy_blocks = h()/dy; // shift previous bit map bytes_in_row_of_blocks = dy*w()*sizeof(unsigned); for(b=0; b 255) intensity = 255; if (intensity < 0) intensity = 0; if (greyscale) { for(py=0; py (h()/2 - 10)) y1 = h()/2 - 10; if (y1 < -(h()/2 - 10)) y1 = -(h()/2 - 10); return y1; } void draw() { float x_scale; float y_scale; int i, x1, y1, x2, y2; char label[100]; Fl_Box::draw(); /* detect resizing of window */ if ((h() != prev_h) || (w() != prev_w) || (x() != prev_x) || (y() != prev_y)) { fl_color(FL_BLACK); fl_rectf(x(),y(),w(),h()); prev_h = h(); prev_w = w(); prev_x = x(); prev_y = y(); } fl_push_clip(x(),y(),w(),h()); x_scale = (float)w()/x_max; y_scale = (float)h()/(2.0*y_max); // erase last sample fl_color(FL_BLACK); x1 = x_scale * index + x(); y1 = y_scale * mem[index]; y1 = clip(y1); y1 = y() + h()/2 - y1; fl_point(x1, y1); // draw new sample fl_color(FL_GREEN); x1 = x_scale * index + x(); y1 = y_scale * new_sample; y1 = clip(y1); y1 = y() + h()/2 - y1; fl_point(x1, y1); mem[index] = new_sample; index++; if (index >= x_max) index = 0; // y axis graticule step = 10; while ((2.0*y_max/step) > 10) step *= 2.0; while ((2.0*y_max/step) < 4) step /= 2.0; fl_color(FL_DARK_GREEN); fl_line_style(FL_DOT); for(i=-y_max; i= *nin) { // demod per frame processing for(i=0; i<*nin; i++) rx_fdm[i] = (float)input_buf[i]/FDMDV_SCALE; nin_prev = *nin; fdmdv_demod(fdmdv, rx_bits, &sync_bit, rx_fdm, nin); *n_input_buf -= nin_prev; assert(*n_input_buf >= 0); // shift input buffer for(i=0; i<*n_input_buf; i++) input_buf[i] = input_buf[i+nin_prev]; // compute rx spectrum & get demod stats, and update GUI plot data fdmdv_get_rx_spectrum(fdmdv, rx_spec, rx_fdm, nin_prev); fdmdv_get_demod_stats(fdmdv, &stats); new_data(rx_spec); aScatter->add_new_samples(stats.rx_symbols); aTimingEst->add_new_sample(stats.rx_timing); aFreqEst->add_new_sample(stats.foff); aSNR->add_new_sample(stats.snr_est); /* State machine to: + Mute decoded audio when out of sync. The demod is synced when we are using the fine freq estimate and SNR is above a thresh. + Decode codec bits only if we have a 0,1 sync bit sequence. Collects two frames of demod bits to decode one frame of codec bits. */ next_state = *state; switch (*state) { case 0: /* mute output audio when out of sync */ if (*n_output_buf < 2*codec2_samples_per_frame(c2) - N8) { for(i=0; i 3.0)) next_state = 1; break; case 1: if (sync_bit == 0) { next_state = 2; /* first half of frame of codec bits */ memcpy(codec_bits, rx_bits, FDMDV_BITS_PER_FRAME*sizeof(int)); } else next_state = 1; if (stats.fest_coarse_fine == 0) next_state = 0; break; case 2: next_state = 1; if (stats.fest_coarse_fine == 0) next_state = 0; if (sync_bit == 1) { /* second half of frame of codec bits */ memcpy(&codec_bits[FDMDV_BITS_PER_FRAME], rx_bits, FDMDV_BITS_PER_FRAME*sizeof(int)); /* pack bits, MSB received first */ bit = 7; byte = 0; memset(packed_bits, 0, BYTES_PER_CODEC_FRAME); for(i=0; i= DT) { *Ts -= DT; if (!zoomSpectrumWindow->shown() && !zoomWaterfallWindow->shown()) { aSpectrum->redraw(); aWaterfall->redraw(); aScatter->redraw(); aTimingEst->redraw(); aFreqEst->redraw(); aSNR->redraw(); } if (zoomSpectrumWindow->shown()) aZoomedSpectrum->redraw(); if (zoomWaterfallWindow->shown()) aZoomedWaterfall->redraw(); } } /* idle() is the FLTK function that gets continusouly called when FLTK is not doing GUI work. We use this function for providing file input to update the GUI when simulating real time operation. */ void idle(void*) { int ret, i; if (fin_name != NULL) { ret = fread(&input_buf[n_input_buf], sizeof(short), FDMDV_NOM_SAMPLES_PER_FRAME, fin); n_input_buf += FDMDV_NOM_SAMPLES_PER_FRAME; per_frame_rx_processing(output_buf, &n_output_buf, codec_bits, input_buf, &n_input_buf, &nin, &state, codec2); if (fout_name != NULL) { if (n_output_buf >= N8) { ret = fwrite(output_buf, sizeof(short), N8, fout); n_output_buf -= N8; assert(n_output_buf >= 0); /* shift speech sample output buffer */ for(i=0; iin8k; float *in48k = cbData->in48k; float out8k[N8]; float out48k[N48]; short out48k_short[N48]; (void) timeInfo; (void) statusFlags; assert(inputBuffer != NULL); /* Convert input model samples from 48 to 8 kHz ------------ */ /* just use left channel */ for(i=0; i= N8) { if (state == 0) { for(i=0; i= 0); /* shift speech samples in output buffer */ for(i=0; i<(uint)n_output_buf; i++) output_buf[i] = output_buf[i+N8]; /* Convert output speech to 48 kHz sample rate ------------- */ /* upsample and update filter memory */ fdmdv_8_to_48(out48k, &in8k[MEM8], N8); for(i=0; i= argc) return 0; fin_name = argv[i+1]; i += 2; return 2; } if (argv[i][1] == 'o') { if ((i+1) >= argc) return 0; fout_name = argv[i+1]; i += 2; return 2; } if (argv[i][1] == 's') { if ((i+1) >= argc) return 0; sound_dev_name = argv[i+1]; i += 2; return 2; } return 0; } /*------------------------------------------------------------*\ MAIN \*------------------------------------------------------------*/ int main(int argc, char **argv) { int ret; int i; PaStreamParameters inputParameters, outputParameters; paCallBackData cbData; i = 1; Fl::args(argc,argv,i,arg_callback); if (argc == 1) { printf("usage: %s [-i inputFdmdvRawFile] [-o outputRawSoundFile] [-s inputSoundDevice]\n", argv[0]); exit(0); } if (fin_name != NULL) { fin = fopen(fin_name,"rb"); if (fin == NULL) { fprintf(stderr, "Error opening input fdmdv raw file %s\n", fin_name); exit(1); } } if (fout_name != NULL) { fout = fopen(fout_name,"wb"); if (fout == NULL) { fprintf(stderr, "Error opening output speech raw file %s\n", fout_name); exit(1); } } for(i=0; idefaultLowInputLatency; inputParameters.hostApiSpecificStreamInfo = NULL; outputParameters.device = Pa_GetDefaultOutputDevice(); /* default output device */ if (outputParameters.device == paNoDevice) { fprintf(stderr,"Error: No default output device.\n"); goto pa_error; } outputParameters.channelCount = NUM_CHANNELS; /* stereo output */ outputParameters.sampleFormat = paInt16; outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputParameters.device )->defaultLowOutputLatency; outputParameters.hostApiSpecificStreamInfo = NULL; err = Pa_OpenStream( &stream, &inputParameters, &outputParameters, SAMPLE_RATE, N48, paClipOff, callback, &cbData); if( err != paNoError ) goto pa_error; } /*------------------------------------------------------------------------*\ Init GUI \*------------------------------------------------------------------------*/ // recommended to prevent dithering and stopped display being // covered by black flickering squares Fl::visual(FL_RGB); // set up main window window = new Fl_Window(W, SP+H2+SP+SP+H2+SP, "fl_fmdv"); //window->size_range(100, 100); //window->resizable(); aSpectrum = new Spectrum(SP, SP, W3-2*SP, H2); aWaterfall = new Waterfall(SP, SP+H2+SP+SP, W3-2*SP, H2); aScatter = new Scatter(W3+SP, SP, W3-2*SP, H2); aTimingEst = new Scalar(W3+SP, SP+H2+SP+SP, W3-2*SP, H2, 100, 80, "Timing Est"); aFreqEst = new Scalar(2*W3+SP, SP, W3-2*SP, H2, 100, 100, "Frequency Est"); aSNR = new Scalar(2*W3+SP, SP+H2+SP+SP, W3-2*SP, H2, 100, 20, "SNR"); Fl::add_idle(idle); window->end(); // set up zoomed spectrum window zoomSpectrumWindow = new Fl_Window(W, H, "Spectrum"); aZoomedSpectrum = new Spectrum(SP, SP, W-2*SP, H-2*SP); zoomSpectrumWindow->end(); // set up zoomed waterfall window zoomWaterfallWindow = new Fl_Window(W, H, "Waterfall"); aZoomedWaterfall = new Waterfall(SP, SP, W-2*SP, H-2*SP); zoomWaterfallWindow->end(); if (sound_dev_name != NULL) { err = Pa_StartStream( stream ); if( err != paNoError ) goto pa_error; } // show the main window and start running window->show(argc, argv); Fl::run(); if (sound_dev_name != NULL) { err = Pa_StopStream( stream ); if( err != paNoError ) goto pa_error; Pa_CloseStream( stream ); Pa_Terminate(); } fdmdv_destroy(fdmdv); codec2_destroy(codec2); free(output_buf); if (fin_name != NULL) fclose(fin); if (fout_name != NULL) fclose(fout); return ret; // Portaudio error handling pa_error: if( stream ) { Pa_AbortStream( stream ); Pa_CloseStream( stream ); } Pa_Terminate(); fprintf( stderr, "An error occured while using the portaudio stream\n" ); fprintf( stderr, "Error number: %d\n", err ); fprintf( stderr, "Error message: %s\n", Pa_GetErrorText( err ) ); return -1; }