// // ebsynth.c // // Copyright (c) 2016 Paul Nicholson // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // #define VERSION "0.8h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef TRUE #define FALSE 0 #define TRUE (!FALSE) #endif /////////////////////////////////////////////////////////////////////////////// // Utilities // /////////////////////////////////////////////////////////////////////////////// // // Messages to stderr. // static int loglevel = 0; // Incremented by -v options static void logd( int level, char const *format, ...) { if (loglevel < level) return; char temp[300]; va_list ap; va_start( ap, format); vsnprintf( temp, 300, format, ap); va_end( ap); fprintf( stderr, "ebsynth: %s\n", temp); fflush( stderr); } // // Screen attributes: AB highlights variable data, AG for constant values. // #define AB 1 #define AG 2 static int nocolour = FALSE; // Set TRUE by -x nocolor static void __attribute__ ((format (printf, 4, 5))) xyprintf( int a, int x, int y, char const *format, ...) { va_list ap; char temp[200]; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); attrset( nocolour ? A_NORMAL : COLOR_PAIR( a)); for (int i = 0; temp[i]; i++) mvaddch( y, x+i, temp[i]); attrset( A_NORMAL); } static void curses_init( void) { initscr(); if (!nocolour) start_color(); scrollok( stdscr, FALSE); clearok( stdscr, TRUE); nonl(); curs_set( 0); if (!nocolour) { init_pair( AB, COLOR_YELLOW, COLOR_BLACK); init_pair( AG, COLOR_GREEN, COLOR_BLACK); } for (int i = 0; i < LINES; i++) xyprintf( AB, 0, i, "%*s", COLS, ""); } static void curses_end( void) { curs_set( 2); endwin(); } static void __attribute__ ((format (printf, 1, 2))) bailout( char const *format, ...) { curses_end(); va_list ap; char temp[ 200]; va_start( ap, format); vsprintf( temp, format, ap); va_end( ap); logd( 0, "terminating: %s", temp); exit( 1); } static void signal_trap( int sig) { bailout( "signal %d, %s", sig, strsignal( sig)); } static void setup_signal_handling( void) { struct sigaction sa; sa.sa_handler = signal_trap; sigemptyset( &sa.sa_mask); sa.sa_flags = 0; sigaction( SIGINT, &sa, NULL); sigaction( SIGHUP, &sa, NULL); sigaction( SIGQUIT, &sa, NULL); sigaction( SIGTERM, &sa, NULL); sigaction( SIGFPE, &sa, NULL); sigaction( SIGBUS, &sa, NULL); sigaction( SIGSEGV, &sa, NULL); sigaction( SIGURG, &sa, NULL); sigaction( SIGSYS, &sa, NULL); } static void *safe_malloc( int nbytes, int zero) { void *p = malloc( nbytes); if (!p) bailout( "out of memory"); if (zero) memset( p, 0, nbytes); return p; } static char *format_hhmmss( double secs, int prec) { static char temp[50]; int isecs = (int) secs; sprintf( temp, "%02d:%02d:%0*.*f", isecs/3600, (isecs % 3600)/60, prec + 3, prec, secs - isecs/60 * 60 ); return temp; } static char *format_date( time_t when) { static char temp[100]; struct tm *tm = gmtime( &when); sprintf( temp, "%04d-%02d-%02d %02d:%02d:%02d", tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); return temp; } static char status_msg[100] = ""; // Keyer status message static int status_change = FALSE; static void __attribute__ ((format (printf, 1, 2))) show_status( char const *format, ...) { va_list ap; va_start( ap, format); vsnprintf( status_msg, 100, format, ap); va_end( ap); status_change = TRUE; } static int refresh_message_times = FALSE; static void show_message_times( time_t t_start, double duration) { time_t t_end = t_start + ceil( duration); xyprintf( AB, 2, 18, "Start at: %s", format_date( t_start)); xyprintf( AB, 42, 18, "End by: %s", format_date( t_end)); refresh(); } /////////////////////////////////////////////////////////////////////////////// // Timestamps // /////////////////////////////////////////////////////////////////////////////// typedef struct timestamp { int32_t secs; double frac; } timestamp; static inline double timestamp_diff( timestamp T1, timestamp T2) { return T1.secs - T2.secs + T1.frac - T2.frac; } static inline timestamp timestamp_normalise( timestamp t) { if (t.secs < 0) t.secs = 0; if (t.secs == 0 && t.frac < 0) t.frac = 0; int n = floor( t.frac); timestamp r = { t.secs + n, t.frac - n}; return r; } static inline timestamp timestamp_add( timestamp t, long double f) { return timestamp_normalise( (timestamp){ t.secs + (int) f, t.frac + f - (int) f}); } // // Return absolute phase of frequency F at time T, in cycles 0..1 // static inline double phase( timestamp T, double F) { double c = T.secs * (F - (int)F) + T.frac * F; return c - (unsigned) c; } /////////////////////////////////////////////////////////////////////////////// // Soundcard // /////////////////////////////////////////////////////////////////////////////// static unsigned int sample_rate = 0; // Set by -r option and may be // modified during card setup static snd_pcm_t *capture_handle; static snd_pcm_t *playback_handle; static int nread = 0; // Number of stereo sample pairs to read at a time static int obufsize = 0; // Output buffer size, frames static int obufdel = 0; // Output buffer delay, uS static char *device = "hw:0,0"; // Sound device, set by -d option #define NOMINAL_PERIOD 2.5e-3 // Desirable soundcard period size, seconds static snd_pcm_format_t pcm_format = 0; static int pcm_bytes = 0; // Bytes per sample static int nchans_in = 0; static int nchans_out = 0; static int hack_octo = FALSE; // Set TRUE by -x octo which activates // work-around for Octo channel shifting bug static int octo_offset = 0; static void setup_input( void) { int err; snd_pcm_hw_params_t *hw_params; logd( 1, "using ALSA device %s", device); // // Open the card for capture, allocate and initialise hw_params structure. // if ((err = snd_pcm_open( &capture_handle, device, SND_PCM_STREAM_CAPTURE, 0)) < 0) bailout( "cannot open audio device %s (%s)", device, snd_strerror( err)); if ((err = snd_pcm_hw_params_malloc( &hw_params)) < 0 || (err = snd_pcm_hw_params_any( capture_handle, hw_params)) < 0) bailout( "cannot init hardware params struct (%s)", snd_strerror( err)); // // Report some info about the card/driver capability. // unsigned int rate_min, rate_max; snd_pcm_hw_params_get_rate_min( hw_params, &rate_min, 0); snd_pcm_hw_params_get_rate_max( hw_params, &rate_max, 0); logd( 1, "rate min %d max %d", rate_min, rate_max); // // Set up the access mode and sample format. Look for the sample format // which has the lowest overhead. // if ((err = snd_pcm_hw_params_set_access( capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) bailout("cannot set access type (%s)", snd_strerror(err)); pcm_format = SND_PCM_FORMAT_S16_LE; if ((err = snd_pcm_hw_params_set_format( capture_handle, hw_params, pcm_format)) < 0) { pcm_format = SND_PCM_FORMAT_S32_LE; if ((err = snd_pcm_hw_params_set_format( capture_handle, hw_params, pcm_format)) < 0) { pcm_format = SND_PCM_FORMAT_S24_LE; if ((err = snd_pcm_hw_params_set_format( capture_handle, hw_params, pcm_format)) < 0) { pcm_format = SND_PCM_FORMAT_S24_3LE; if ((err = snd_pcm_hw_params_set_format( capture_handle, hw_params, pcm_format)) < 0) bailout( "cannot set input sample format (%s)", snd_strerror(err)); } } } switch( pcm_format) { case SND_PCM_FORMAT_S16_LE: logd( 0, "format 16 bits, 2 bytes"); pcm_bytes = 2; break; case SND_PCM_FORMAT_S24_3LE: logd( 0, "format 24 bits, 3 bytes"); pcm_bytes = 3; break; case SND_PCM_FORMAT_S24_LE: logd( 0, "format 24 bits, 4 bytes"); pcm_bytes = 4; break; case SND_PCM_FORMAT_S32_LE: logd( 0, "format 32 bits, 4 bytes"); pcm_bytes = 4; break; default: bailout( "pcm format not handled"); } // // Set up the sample rate and channel count. If -r not given, set to the // highest available rate. // if (!sample_rate) sample_rate = rate_max; if ((err = snd_pcm_hw_params_set_rate_near( capture_handle, hw_params, &sample_rate, 0)) < 0) bailout( "cannot set input sample rate (%s)", snd_strerror(err)); logd( 0, "sample rate set: %d", sample_rate); xyprintf( AG, 2, 3, "Device: %s at %d/sec", device, sample_rate); // // Set the lowest available channel count, but at least two. // nchans_in = 2; while (nchans_in <= 8) if ((err = snd_pcm_hw_params_set_channels( capture_handle, hw_params, nchans_in)) == 0) break; else nchans_in++; if (nchans_in > 8) bailout( "cannot set input channel count (%s)", snd_strerror(err)); logd( 0, "input channels: %d", nchans_in); // // Set up the period and buffer sizes. // snd_pcm_uframes_t period_size = 0; snd_pcm_uframes_t buffer_size = 0; if (!period_size) { int aim = sample_rate * NOMINAL_PERIOD; for (period_size = 32; aim/(double)period_size > 1.5; period_size *= 2);; } if (period_size) { if ((err = snd_pcm_hw_params_set_period_size_near( capture_handle, hw_params, &period_size, 0)) < 0) bailout( "cannot set period size (%s)", snd_strerror( err)); } if (buffer_size) { if ((err = snd_pcm_hw_params_set_buffer_size_near( capture_handle, hw_params, &buffer_size)) < 0) bailout( "cannot set input buffer size (%s)", snd_strerror( err)); } if ((err = snd_pcm_hw_params( capture_handle, hw_params)) < 0) bailout( "cannot set input parameters (%s)", snd_strerror( err)); // // Report more info, now that the parameters are set. // unsigned int count; snd_pcm_uframes_t frames; snd_pcm_hw_params_get_period_time( hw_params, &count, 0); snd_pcm_hw_params_get_period_size( hw_params, &frames, 0); logd( 1, "period size: %d frames, %u uS", (int) frames, count); snd_pcm_hw_params_get_buffer_time( hw_params, &count, 0); snd_pcm_hw_params_get_buffer_size( hw_params, &frames); logd( 1, "input buffer size: %d frames, %u uS", (int) frames, count); xyprintf( AG, 42, 3, " In buffer: %d frames, %.1f mS", (int) frames, count/1000.0); snd_pcm_hw_params_get_periods( hw_params, &count, 0); logd( 1, "periods per buffer: %d", count); // // Set the read size to two periods. // if (!nread) { snd_pcm_hw_params_get_period_size( hw_params, &frames, 0); nread = 1 * frames; } logd( 1, "nread: %d", nread); xyprintf( AG, 42, 4, " Read size: %d frames, %.2f mS", nread, nread/(double) sample_rate * 1e3); snd_pcm_hw_params_free( hw_params); } static void setup_output( void) { int err; snd_pcm_hw_params_t *hw_params; // // Open the card for playback, allocate and initialise hw_params structure. // if ((err = snd_pcm_open( &playback_handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) bailout( "cannot open audio device %s (%s)", device, snd_strerror( err)); if ((err = snd_pcm_hw_params_malloc( &hw_params)) < 0 || (err = snd_pcm_hw_params_any( playback_handle, hw_params)) < 0) bailout( "cannot init hardware params struct (%s)", snd_strerror( err)); // // Set up the access mode and sample format. Assumes the output side can // use the same PCM format as we set for the input. // if ((err = snd_pcm_hw_params_set_access (playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) bailout( "cannot set access type (%s)", snd_strerror( err)); if ((err = snd_pcm_hw_params_set_format( playback_handle, hw_params, pcm_format)) < 0) bailout( "cannot set output sample format (%s)", snd_strerror(err)); // // Set up the sample rate and channel count. Sample rate must be same // as the input rate, must have at least two channels. // if ((err = snd_pcm_hw_params_set_rate( playback_handle, hw_params, sample_rate, 0)) < 0) bailout( "cannot set output sample rate (%s)", snd_strerror(err)); nchans_out = 2; while (nchans_out <= 8) if ((err = snd_pcm_hw_params_set_channels( playback_handle, hw_params, nchans_out)) == 0) break; else nchans_out++; if (nchans_out > 8) bailout( "cannot set output channel count (%s)", snd_strerror(err)); logd( 0, "output channels: %d", nchans_out); if ((err = snd_pcm_hw_params( playback_handle, hw_params)) < 0) bailout( "cannot set output parameters (%s)", snd_strerror( err)); // // Get info about the output buffer, to work out the output buffer delay // which we need for accurate symbol timing. Buffer delays can be several // seconds (so that many symbols are queued in the buffer). // unsigned int count; snd_pcm_uframes_t frames; snd_pcm_hw_params_get_buffer_time( hw_params, &count, 0); snd_pcm_hw_params_get_buffer_size( hw_params, &frames); obufsize = frames; obufdel = count; // uS logd( 1, "output buffer size: %d frames, %d uS", (int) frames, obufdel); xyprintf( AG, 2, 4, "Out buffer: %d frames, %.1f mS", (int) frames, obufdel/1000.0); } static int read_soundcard( char *buf) { int ne; while ((ne = snd_pcm_readi( capture_handle, buf, nread)) <= 0) { if (ne == -EPIPE) { logd( 0, "soundcard read overrun"); snd_pcm_prepare( capture_handle); continue; } if (ne != -EAGAIN && ne != EINTR) bailout( "soundcard read failed: %s", snd_strerror( ne)); } return ne; // Number of frames read } static void write_soundcard( char const *buf, int n) { int ne; while ((ne = snd_pcm_writei( playback_handle, buf, nread)) != n) { if (ne == -EPIPE) { logd( 0, "soundcard write underrun"); snd_pcm_prepare( playback_handle); continue; } if (ne != -EAGAIN) bailout( "soundcard write failed: %s", snd_strerror( ne)); } } // // Audioinjector Octo has a bug which, more often than not, shifts the // input channels. Six input channels should be at offsets 0..5 in the // frame and the two unused channels should be offsets 6 and 7. // Unused channels should have values -1 so look for the pair and set // octo_offset accordingly. // static void remap_octo( char const *rbuf, int n) { static int done = FALSE; if (done) return; if (pcm_format == SND_PCM_FORMAT_S16_LE) { int16_t *rp = (int16_t *) rbuf; int offset = -1; while (offset < 0 && n--) { for (int k = 0; k < 8; k++) if (rp[k] == -1 && rp[(k + 1)%8] == -1) { int q = 0; for (int j = 0; j < 8; j++) if (j != k && j != (k+1)%8 && rp[j] != -1) q++; if (q == 6) { offset = k; break; } } // fprintf( stderr, "%d %d %d %d %d %d %d %d\n", // rp[0], rp[1], rp[2], rp[3], rp[4], rp[5], rp[6], rp[7]); rp += 8; } if (offset >= 0) { octo_offset = (offset + 2) % 8; logd( 0, "octo channel offset: %d", octo_offset); done = TRUE; } } if (pcm_format == SND_PCM_FORMAT_S32_LE) { int32_t *rp = (int32_t *) rbuf; int offset = -1; while (offset < 0 && n--) { for (int k = 0; k < 8; k++) if (rp[k] == -256 && rp[(k + 1)%8] == -256) { int q = 0; for (int j = 0; j < 8; j++) if (j != k && j != (k+1)%8 && rp[j] != -256) q++; if (q == 6) { offset = k; break; } } // fprintf( stderr, "%d %d %d %d %d %d %d %d\n", // rp[0], rp[1], rp[2], rp[3], rp[4], rp[5], rp[6], rp[7]); rp += 8; } if (offset >= 0) { octo_offset = (offset + 2) % 8; logd( 0, "octo channel offset: %d", octo_offset); done = TRUE; } } } /////////////////////////////////////////////////////////////////////////////// // BPSK Modulation Buffer // /////////////////////////////////////////////////////////////////////////////// // // Buffer for modulation phase when BPSK is in use. Length is equal to the // output buffer delay. No compensation for input delay because that is // small and variable. // // This buffer enables the feedback phase monitor to allow for the BPSK phase // that was active when the signal was generated. // // The value stored is the modulation phase in cycles. // static double *kbuf; static int kbuflen = 0; static int kp = 0; static inline void add_kbuf( double v) { if (!kbuflen) return; kbuf[kp++] = v; kp %= kbuflen; } static inline double get_kbuf( void) { return kbuf[(kp + 1) % kbuflen]; } static void init_kbuf( int frames) { kbuflen = frames; kbuf = safe_malloc( kbuflen * sizeof( double), TRUE); kp = 0; } /////////////////////////////////////////////////////////////////////////////// // Smoothing Functions // /////////////////////////////////////////////////////////////////////////////// // // Rate smoothing. Two stages of exponential moving average. // static double rstc = 10; // Set by -x rstc= static double rate_smooth( double in) { static double s1 = 0, s2 = 0; static double alpha = 0; static int once = FALSE; if (!once) { s1 = s2 = in; alpha = 1 - exp( -1/rstc); logd( 3, "rs alpha %.3e", alpha); once = TRUE; return in; } s1 += alpha * (in - s1); s2 += alpha * (s1 - s2); return s2; } // // Time smoothing. Two stages of exponential moving average. // static double tbtc = 10; // Set by -x tbtc= static double time_smooth( double in) { static double s1 = 0, s2 = 0; static double alpha = 0; static int once = FALSE; if (!once) { s1 = s2 = in; alpha = 1 - exp( -1/tbtc); logd( 3, "td alpha %.3e", alpha); once = TRUE; return in; } s1 += alpha * (in - s1); s2 += alpha * (s1 - s2); return s2; } // // Phase smoothing. Single stage of exponential moving average, smoothing // the two components independently. // static double phtc = 10; // Set by -x phtc= static double phase_smooth( double in) { static double s1_c = 0, s1_s = 0; static int once = FALSE; static double alpha = 0; double in_cos = cos( in); double in_sin = sin( in); if (!once) { alpha = 1 - exp( -1/phtc); logd( 3, "ph alpha %.3e", alpha); s1_c = in_cos; s1_s = in_sin; once = TRUE; return in; } s1_c += alpha * (in_cos - s1_c); s1_s += alpha * (in_sin - s1_s); return atan2( s1_s, s1_c); } /////////////////////////////////////////////////////////////////////////////// // Synthesizer // /////////////////////////////////////////////////////////////////////////////// static double Tfreq = 0; // Target output frequency static double Rfreq = 0; // Reference frequency static double Afreq = 0; // Apparent reference frequency static double gperiod = 0.05; // Period of the Goertzels, set by -x gperiod= static int USE_FEEDBACK = FALSE; // Set TRUE by -f option static double rphlast = 0; // Previous reference phase static int nperiod = 0; // Period counter static int ng = 0; // Number of samples accumulated static double opcyc = 0; // Output cycle static double dopcyc = 0; // Output cycle increment static double opcyc_adj = 0; static double phgain = 0.1; // Phase correction servo gain, set by -x phgain= static double xrate = 0; // Sample rate measured from the reference static double ref_rms = 0; // Reference signal RMS amplitude static double fb_rms = 0; // Feedback RMS amplitude static double df = 0; // Reference frequency loop error static int64_t incnt = 0; // Input frame counter static int64_t incnt_this = 0; // Sample number of start of capture buffer static int64_t incnt_goertzel = 0; // Sample number of start of Goertzels static snd_pcm_sframes_t odelay_frames = 0; // // Goertzel for reference signal // static double ref_d1 = 0, ref_d2 = 0; static double ref_realW = 0; static double ref_imagW = 0; static double ref_cyc = 0; // // Goertzel for feedback signal // static double fb_d1 = 0, fb_d2 = 0; static double fb_realW = 0; static double fb_imagW = 0; static double fb_cyc = 0; static complex double fb_val = 0; static double fb_phase = 0; // // Initialise Goertzels // static void setup_goertzels( void) { double p = 2 * M_PI * Afreq/sample_rate; ref_realW = 2 * cos( p); ref_imagW = sin( p); p = 2 * M_PI * Tfreq/xrate; fb_realW = 2 * cos( p); fb_imagW = sin( p); ref_d1 = ref_d2 = 0; fb_d1 = fb_d2 = 0; } // // Frequency and phase control when a reference frequency is in use. // static void update_synth_ref( void) { // Complex amplitude of the reference signal complex double a = 0.5 * ref_realW * ref_d1 - ref_d2 + I * ref_imagW * ref_d1; // Convert from the local phase of the Goertzel to the global phase by // which we track the reference. ref_cyc -= (int) ref_cyc; // Discard whole cycles to avoid eventual // loss of precision. ref_cyc += ng/xrate * Rfreq; // Add the number of cycles that // went into the Goertzel a *= cos(ref_cyc * 2 * M_PI) - I*sin(ref_cyc * 2 * M_PI); a /= ng; ref_rms = cabs( a); // Do the same for the feedback signal fb_cyc -= (int) fb_cyc; fb_cyc += ng/xrate * Tfreq; fb_val *= cos( fb_cyc * 2 * M_PI) - I * sin( fb_cyc * 2 * M_PI); // // Work out the phase difference between the current and previous // reference phase measurements. If our estimate of the sample rate is // perfect then dph will be zero. // double rph = carg(a); // Reference phase in radians double dph = rph - rphlast; // Reference phase change in this period rphlast = rph; if (dph > M_PI) dph -= 2 * M_PI; if (dph < -M_PI) dph += 2 * M_PI; if (nperiod) // Not the first? { // Revise the apparent frequency of the reference df = dph/(2*M_PI) * gperiod; // Frequency error in cycles per second Afreq += df; // // New sample rate and output phase increment. // double prev_xrate = xrate; xrate = sample_rate * Rfreq/Afreq; dopcyc = Tfreq/xrate; // Cycles per sample period // Correct the output phase, pro rata with the change of ref phase opcyc += dph/(2*M_PI) * Tfreq/Rfreq; // Calculate the change in output buffer delay resulting from the // change of sample rate. obc is the buffer delay change in units // of reference frequency cycles. double obc = odelay_frames * Rfreq * (1/prev_xrate - 1/xrate); // Adjust the phase to compensate for the changed output buffer delay opcyc -= obc * Tfreq/Rfreq;; // // Apply feedback phase correction if used. Adjust the output phase // by one tenth (phgain) of the phase error per second, // if (USE_FEEDBACK) { double mcyc = get_kbuf(); fb_phase = phase_smooth( carg( fb_val) - mcyc * 2 * M_PI); opcyc -= fb_phase/(2*M_PI) * phgain * gperiod; } // Set up new parameters for the reference and feedback Goertzels setup_goertzels(); } // Discard whole output cycles to avoid loss of precision opcyc -= (int) opcyc; } // // Timing, frequency and phase control when 1PPS is in use. // static int USE_PPS = TRUE; // Set false by -R option static double *cbuf = NULL; // Capture buffer static int cbuflen = 0; // Capture buffer length, samples static double pps_peak = 0; // Peak value of PPS channel static int64_t incnt_prev = 0; // incnt_this from previous capture static double secmark_prev = 0; // The secmark_index of the previous pulse static timestamp timebase_T; // Current timestamp static uint64_t timebase_incnt = 0; // Input sample number at timebase_T static double timebase_err = 0; static double tbgain = 0.05; // Timebase servo gain, set by -x tbgain= static int cmp_double( const void *p1, const void *p2) { double v1 = *(double *)p1; double v2 = *(double *)p2; if (v1 < v2) return -1; if (v1 > v2) return 1; return 0; } static double ppsmad = 0; // Mean absolute difference betweeen PPS intervals static int pps_missed = 0; // Count of missed PPS pulses static time_t pps_sec = 0; // UT second at start of capture buffer, one less // than the second we are waiting for static int ppsdump = FALSE; // TRUE with -x ppsdump static void update_pps_ref( double v) { static int fftwid = 128; static int capture_on = FALSE; static int cbufp = 0; // Capture buffer load index static double last_interval = 0; if (!cbuflen) // First time through? Do some initialisation. { // Half second PPS buffer cbuflen = 0.5 * sample_rate; cbuf = safe_malloc( sizeof( double) * cbuflen, FALSE); } // // As we reach within 0.25 seconds of the second mark, start capturing // into cbuf. // if (!capture_on) { struct timespec ts; clock_gettime( CLOCK_REALTIME, &ts); if (ts.tv_nsec / 1e9 > 0.75) { cbufp = 0; // Capture buffer load index incnt_this = incnt; // Input sample number of start of capture buffer pps_sec = ts.tv_sec; capture_on = TRUE; } } if (!capture_on) return; cbuf[cbufp++] = v; // Load this sample into the capture buffer if (cbufp < cbuflen) return; // Capture buffer not yet full? capture_on = FALSE; // End of capture, buffer is full // // Find the max absolute value of the buffer. // double absmax = 0; for (int i = 0; i < cbuflen; i++) { double v = fabs( cbuf[i]); if ( v > absmax) absmax = v; } if (absmax < 0.01) { logd( 1, "PPS missing"); pps_peak = 0; pps_missed++; return; } // // Find the leading edge of the pulse in the capture buffer, looking for // the earliest sample that exceeds 50% of the absolute amplitude. // // cbuf will be copied to the Fourier input buffer so that the sample at // vstart is centered. // int vstart; for (vstart = 0; vstart < cbuflen && fabs( cbuf[vstart]) < 0.5 * absmax; vstart++) ;;; int pps_polarity = cbuf[vstart] > 0 ? +1 : -1; if (vstart < fftwid/2) { // Pulse must have started before the buffer capture logd( 1, "pulse starts too early - skipped"); pps_missed++; if (ppsdump) { FILE *f = fopen( "/run/ebsynth-pps.tmp", "w"); if (f) { for (int i = 0; i < cbuflen; i++) fprintf( f, "%d %.3e\n", i, cbuf[i]); fclose( f); rename( "/run/ebsynth-pps.tmp", "/run/ebsynth-pps-early"); } } return; } if (vstart > cbuflen - fftwid/2 - 1) { logd( 1, "pulse starts too late - skipped"); pps_missed++; if (ppsdump) { FILE *f = fopen( "/run/ebsynth-pps.tmp", "w"); if (f) { for (int i = 0; i < cbuflen; i++) fprintf( f, "%d %.3e\n", i, cbuf[i]); fclose( f); rename( "/run/ebsynth-pps.tmp", "/run/ebsynth-pps-late"); } } return; } // // A bit of extra work to find the peak amplitude, only used for the // PPS amplitude display. // int peak_pos = vstart; // Offset into capture buffer of peak location pps_peak = cbuf[peak_pos]; for (peak_pos = vstart; peak_pos < vstart + fftwid/2; peak_pos++) if ( cbuf[peak_pos] * pps_polarity > pps_peak * pps_polarity) pps_peak = cbuf[peak_pos]; // // Fourier transform. // static fftw_plan ffp; static complex *X = NULL; static double *inbuf = NULL; static int b1, b2; // First and last bins to use for phase slope static double *tsbuf = NULL; static double *window = NULL; if (!X) // First time through initialisation { X = safe_malloc( sizeof( fftw_complex) * (fftwid/2 + 1), FALSE); inbuf = safe_malloc( sizeof( double) * fftwid, FALSE); ffp = fftw_plan_dft_r2c_1d( fftwid, inbuf, X, FFTW_ESTIMATE); b1 = fftwid/2 * 0.1; b2 = fftwid/2 * 0.6; tsbuf = safe_malloc( sizeof( double) * fftwid/2, FALSE); window = safe_malloc( sizeof( double) * fftwid, FALSE); for (int i = 0; i < fftwid; i++) window[i] = pow( sin(M_PI * i/fftwid), 8); } if (ppsdump) { FILE *f = fopen( "/run/ebsynth-pps.tmp", "w"); if (f) { for (int i = 0; i < fftwid; i++) { fprintf( f, "%d %.3e %.3e\n", i, cbuf[vstart - fftwid/2 + i], cbuf[vstart - fftwid/2 + i] * window[i]); } fclose( f); rename( "/run/ebsynth-pps.tmp", "/run/ebsynth-pps-td"); } } // // Copy part of cbuf into inbuf, positioning so that offset vstart in // cbuf ends up at the center of inbuf. // for (int i = 0; i < fftwid; i++) inbuf[i] = cbuf[vstart - fftwid/2 + i] * window[i]; fftw_execute( ffp); for (int i = 0; i < fftwid/2; i += 2) X[i] = -X[i]; if (ppsdump) { FILE *f = fopen( "/run/ebsynth-pps.tmp", "w"); if (f) { for (int i = 0; i < fftwid/2; i++) { fprintf( f, "%d %.1f %.3e %.1f\n", i, i * sample_rate/(double) fftwid, cabs( X[i]), carg( X[i]) * 180/M_PI); } fclose( f); rename( "/run/ebsynth-pps.tmp", "/run/ebsynth-pps-fd"); } } // // Median phase slope. // int n = 0; // Number of phase slope samples into tsbuf for (int i = b1; i < b2; i++) { double dp = carg( X[i+1]/X[i]); tsbuf[n++] = dp; } qsort( tsbuf, n, sizeof( double), cmp_double); double slope = tsbuf[n/2]/(2 * M_PI); // Cycles per bin // // secmark_index: precise location of the phase center in the FT inbuf. // double offset = -slope * fftwid; // Offset of the phase center from vstart logd( 3, "slope %.1f", offset); if (isnan( offset)) { logd( 1, "nan secmark"); pps_missed++; return; } if (fabs( offset) > 4) { logd( 1, "invalid phase slope - skipped, offset %.1f samples", offset); pps_missed++; return; } double secmark_index = vstart + offset; // // Interval between this pulse center and previous, units of samples. // static int first = TRUE; double interval = incnt_this - incnt_prev + secmark_index - secmark_prev; secmark_prev = secmark_index; incnt_prev = incnt_this; if (first) { first = FALSE; return; } // // Only use the raw interval if it is reasonable, say within 0.1% of the // nominal sample rate. A seriously bad soundcard crystal could // permanently fail this sanity check. // // If some pulses have been missed, we routinely get one of these wild // intervals when the next good pulse arrives. // if (interval > sample_rate * 1.001 || interval < sample_rate * 0.999) { logd( 1, "wild PPS interval %.6f - skipped", interval); pps_missed++; return; } // // Track the mean absolute difference between consecutive PPS intervals. // This is only for info display, ppsmad not used for regulation. // #define SRTC 40 // Time constant (seconds) for MAD interval smoothing double smfac = exp(-1.0/SRTC); // MAD smoothing factor double ad = 0; double new_ppsmad = 0; if (last_interval) { ad = fabs(interval - last_interval); if (!ppsmad) new_ppsmad = ad; else new_ppsmad = ppsmad * smfac + ad * (1-smfac); } // // PPS interval looks good, accept the interval and the new mean absolute // difference. // ppsmad = new_ppsmad; last_interval = interval; // // Update our sample rate estimate. This is just a moving average of // recent intervals. rate_smooth() implements a low pass filter // which provides the averaging. // xrate = rate_smooth( interval); logd( 1, "interval %.6f %.6f mad %.3f", interval, xrate, 1e6 * ppsmad/sample_rate); // // Update the timebase. One twentieth (tbgain) of the timebase error // is corrected per second. // if (timebase_incnt) { timestamp P; P.secs = pps_sec; P.frac = 1 - secmark_index/xrate; timestamp X = timestamp_add( timebase_T, (incnt_this - timebase_incnt)/xrate); double diff = timestamp_diff( P, X); timebase_err = time_smooth( diff); timebase_incnt = incnt_this; timebase_T = timestamp_add( X, timebase_err * tbgain); } else { timebase_incnt = incnt_this; timebase_T.secs = pps_sec; timebase_T.frac = 1 - secmark_index/xrate; } // // Update the output phase using the new timebase. // dopcyc = Tfreq/xrate; timestamp t = timestamp_add( timebase_T, (incnt - timebase_incnt)/xrate); opcyc = phase( t, Tfreq) + opcyc_adj; } /////////////////////////////////////////////////////////////////////////////// // PTT // /////////////////////////////////////////////////////////////////////////////// // // PTT control via RS232 DTR/RTS or via RPi GPIO // static char *ptt_device = NULL; // Set by -p dev= static int ptt_gpio = -1; // Set by -p gpio= #define USE_PTT (ptt_device != NULL || ptt_gpio >= 0) static int ptt_fd = -1; #define PTT_TOGETHER 1 #define PTT_OPPOSITE 2 static int ptt_mode = PTT_TOGETHER; // Set by -p mcr= static double ptt_pre = 2; // Set by -p pre= static double ptt_post = 2; // Set by -p post= static int ptt_state = 0; // // Turn on or off the PTT via the modem control pins. // static void drive_ptt_rs232( int state) { int mcr; // Modem control register value if (ioctl( ptt_fd, TIOCMGET, &mcr) < 0) bailout( "TIOCMGET failed on %s: %s", ptt_device, strerror( errno)); if (state) switch (ptt_mode) { case PTT_TOGETHER: mcr |= TIOCM_DTR; mcr |= TIOCM_RTS; break; case PTT_OPPOSITE: mcr |= TIOCM_DTR; mcr &= ~TIOCM_RTS; break; } else switch (ptt_mode) { case PTT_TOGETHER: mcr &= ~TIOCM_DTR; mcr &= ~TIOCM_RTS; break; case PTT_OPPOSITE: mcr &= ~TIOCM_DTR; mcr |= TIOCM_RTS; break; } if (ioctl( ptt_fd, TIOCMSET, &mcr) < 0) bailout( "TIOCMSET failed on %s: %s", ptt_device, strerror( errno)); } // // Turn on or off the PTT via a GPIO pin. // static void drive_ptt_gpio( int state) { if( write( ptt_fd, state ? "1" : "0", 1) < 0) bailout( "i/o error gpio%d: %s", ptt_gpio, strerror( errno)); } // // Drive the PPT. // static void drive_ptt( int state) { if (ptt_device) drive_ptt_rs232( state); else if (ptt_gpio >= 0) drive_ptt_gpio( state); ptt_state = state; } // // Initialise the PTT control for RS232 MCR. // static void setup_ptt_rs232( void) { if ((ptt_fd = open( ptt_device, O_RDWR | O_NOCTTY | O_NONBLOCK)) < 0) bailout( "cannot open %s: %s", ptt_device, strerror( errno)); drive_ptt( FALSE); // Turn off PTT } // // Initialise a GPIO channel for PTT control. // static void setup_ptt_gpio( void) { logd( 1, "ptt: RP gpio %d", ptt_gpio); FILE *f; // Try to close device in case already active, ignore errors if( (f = fopen( "/sys/class/gpio/unexport", "w")) != NULL) { fprintf( f, "%d", ptt_gpio); fclose( f); } // Open the GPIO port if( (f = fopen( "/sys/class/gpio/export", "w")) == NULL || fprintf( f, "%d", ptt_gpio) < 0 || fclose( f) < 0) bailout( "unable to export GPIO: %s", strerror( errno)); // Set direction to "out" char filename[100]; sprintf( filename, "/sys/class/gpio/gpio%d/direction", ptt_gpio); if( (f = fopen( filename, "w")) == NULL || fprintf( f, "out") < 0 || fclose( f) < 0) bailout( "unable to initialise GPIO: %s", strerror( errno)); // Prepare output handle for PTT sprintf( filename, "/sys/class/gpio/gpio%d/value", ptt_gpio); if( (ptt_fd = open( filename, O_WRONLY)) < 0) bailout( "unable to open GPIO: %s", strerror( errno)); } static void setup_ptt( void) { if (ptt_device) setup_ptt_rs232(); else if (ptt_gpio >= 0) setup_ptt_gpio(); } // // A time-ordered queue is necessary to cope with the case where one or more // short messages reside within a long output buffer. // static struct PTTQ { long double T; int state; struct PTTQ *next; } *pttq = NULL; // Linked list to queue PTT events // // Discard a PTT queue entry. // static void discard_ptt( struct PTTQ **p) { struct PTTQ *t = (*p)->next; free( *p); *p = t; } // // Add an event to the PTT queue. // static void queue_ptt( long double T, int state) { if (!USE_PTT) return; // PTT not in use? // Find where to insert the event struct PTTQ **pp = &pttq; while (*pp) { if (T < (*pp)->T) break; pp = &(*pp)->next; } // An ON event must cancel a later OFF event. An OFF event will replace // a later OFF event. while (*pp && !(*pp)->state) discard_ptt( pp); // Insert new event into the queue struct PTTQ *t = *pp; *pp = safe_malloc( sizeof( struct PTTQ), TRUE); (*pp)->T = T; (*pp)->state = state; (*pp)->next = t; } // // Check the PTT queue head and drive PTT if an event is ready. // static void update_ptt( long double Tutc) { if (!USE_PTT || !pttq || Tutc < pttq->T) return; drive_ptt( pttq->state); discard_ptt( &pttq); } /////////////////////////////////////////////////////////////////////////////// // Keyer // /////////////////////////////////////////////////////////////////////////////// static int MFLAG = FALSE; // Set by -m to enable modulation static time_t Tstart = 0; // Transmit start time static int Kstate = 0; // Keyer state variable static int repeat = 0; // Seconds if -n given static int repeat_type = FALSE; // FALSE if repeats are UT mod repeat // TRUE if repeat is increment to start time static int Nsymbols = 0; // Number of output symbols (transmitted bits) static double sym_period = 0; // Symbol period seconds, set by -S option static int nk = 0; // Sample counter for keyer static int kout = -1; // Current keyer output symbol -1 or +1 static int kcnt = 0; // Sample counter for phase slew static int kslew = 0; // Max count for phase slew static double *slewtable = NULL; // Table of slewing coefficients -1 .. +1 #define KEYR 0.01 // Keyer period, seconds #define SLEW_HARD 1 // Hard keying #define SLEW_SIGMOID 2 // Sigmoid function #define SLEW_LINEAR 3 // Linear ramp static double slew_factor = 0.05; // Fraction of symbol period for phase slew static int slew_mode = SLEW_SIGMOID; #define MAXSYM 10000 // Max number of symbols allowed static int symbols[MAXSYM]; // Array of symbols to send, read from stdin // // Initialise the keyer. // static void setup_keyer( void) { kslew = sample_rate * sym_period * slew_factor; slewtable = safe_malloc( sizeof( double) * kslew, TRUE); switch (slew_mode) { case SLEW_SIGMOID: for (int i = 0; i < kslew; i++) slewtable[i] = erf(4 * (i/(double) kslew - 0.5)); break; case SLEW_LINEAR: for (int i = 0; i < kslew; i++) slewtable[i] = -1 + 2.0 * i/(double) kslew; break; } } // // Begin a new output bit. // static void set_output( int val) { if (kout != val) { kcnt = kslew - 1; // Set up for a slew kout = val; // The new output state } } static void update_keyer( void) { static int cbit = -1; // Current message bit static long double last_status = 0; // Time of last screen update // of keyer info // // Get UTC and add the buffer delay so that the message is timed // at the output of the soundcard. Add a further half slew time so // that mid slew occurs at the symbol start time. // // Tutc is the current UTC from the system clock. // struct timespec ts; clock_gettime( CLOCK_REALTIME, &ts); long double Tutc = ts.tv_sec + ts.tv_nsec/1e9; // // Tsym adjusts for the output buffer delay so that symbol generation is // ahead of Tutc by the right amount for the symbols to reach the D/A at // their proper time. If phase slewing is in use, add half the slew time // so that the symbols are timed mid-slew. // long double Tsym = Tutc + obufdel/1e6; if (slew_mode != SLEW_HARD) Tsym += sym_period * slew_factor * 0.5; // // Tutc is used for PTT timing and for setting up the next repeat start // time. Tsym is used only for symbol timing. // update_ptt( Tutc); // // State 0: idle state. // if (Kstate == 0) return; // // State 1: waiting for start time to arrive. // if (Kstate == 1) { double remains = Tstart - Tsym; if (remains > 0) { if (Tutc - last_status >= 0.2) { show_status( "Transmit starts in: %s", format_hhmmss( remains, 1)); last_status = Tutc; } return; } // Start time has arrived cbit = -1; Kstate = 2; } // // State 2: sending. // if (Kstate == 2) { long double elapsed = Tsym - Tstart; int bit = elapsed/sym_period; // Current bit number, 0 .. Nsymbols - 1 if (bit >= Nsymbols) // Send completed? { set_output( -1); show_status( "Message completed"); if (repeat) // Repeat requested? If so, prepare the next start time { while (Tstart - ptt_pre < Tutc) if (repeat_type) Tstart += repeat; else Tstart = (Tstart / repeat + 1) * repeat; queue_ptt( Tstart - ptt_pre, 1); queue_ptt( Tstart + Nsymbols * sym_period + ptt_post, 0); refresh_message_times = TRUE; Kstate = 1; return; } Kstate = 3; return; } if (bit != cbit) // Time to send next output bit? { cbit = bit; set_output( symbols[cbit] == '1' ? 1 : -1); show_status( "Sending bit %d/%d symbol=%c", cbit+1, Nsymbols, symbols[cbit]); } } // // State 3: sending complete with no repeat requested. // if (Kstate == 3) { return; } // // State 10: test mode, toggle the output every sym_period. // if (Kstate == 10) { long double elapsed = Tsym - Tstart; int bit = elapsed/sym_period; if (bit != cbit) { cbit = bit; set_output( bit & 1 ? 1 : -1); show_status( "Test mode symbol: %d", kout); } } } /////////////////////////////////////////////////////////////////////////////// // Frame Processing // /////////////////////////////////////////////////////////////////////////////// static int harmonic = 1; // From -h option, harmonic in use // // Output options. // static int OFLAG_BRIDGE = FALSE; // Set TRUE by -o bridge static int OFLAG_SQUARE = FALSE; // Set TRUE by -o square static int OFLAG_REF = FALSE; // Set TRUE by -o ref static int OFLAG_MUTE = FALSE; // Set TRUE by -o mute static int OFLAG_IQ = FALSE; // Set TRUE by -o iq // // Sine waveform table. // #define TABLESIZE (1 << 15) #define TABLEMASK (TABLESIZE - 1) static float sinetable[TABLESIZE]; static void init_sinetable( void) { for (int i = 0; i < TABLESIZE; i++) sinetable[i] = sin( i * 2 * M_PI/TABLESIZE); } static inline float wave( double cyc) { int n = (int) ((cyc - (int) cyc) * TABLESIZE) + TABLESIZE; n &= TABLEMASK; if (OFLAG_SQUARE) return n < TABLEMASK/2 ? 1 : -1; return sinetable[n]; } // // process_frame() is called for each frame of input from the soundcard, // generating one output frame. // static void process_frame( double in1, double in2, double *out1, double *out2) { double vref = in1; double vfb = in2; // // Insert samples for reference and feedback into the Goertzels. // double y; y = vref + ref_realW * ref_d1 - ref_d2; ref_d2 = ref_d1; ref_d1 = y; y = vfb + fb_realW * fb_d1 - fb_d2; fb_d2 = fb_d1; fb_d1 = y; ng++; // Number of samples into the Goertzels if (ng >= sample_rate * gperiod) // Goertzels complete? { if (USE_FEEDBACK) { // Finalise the feedback goertzel fb_val = 0.5 * fb_realW * fb_d1 - fb_d2 + I * fb_imagW * fb_d1; fb_val /= ng; fb_rms = cabs( fb_val); } if (!USE_PPS) update_synth_ref(); if (USE_PPS && USE_FEEDBACK) { // // Calculate absolute phase of the feedback, with reference to our // timebase. // double offset; if (incnt_goertzel > timebase_incnt) offset = (incnt_goertzel - timebase_incnt + 1)/xrate; else offset = (timebase_incnt - incnt_goertzel - 1)/-xrate; offset += gperiod * sample_rate/xrate; timestamp T = timestamp_add( timebase_T, offset); double ph = phase( T, Tfreq) * 2 * M_PI; complex double a = fb_val * (cos( ph) - I*sin( ph)); // // Compensate for the modulation phase and calculate the phase // error to be corrected. // double mcyc = get_kbuf(); fb_phase = phase_smooth( carg( a) - mcyc * 2 * M_PI); // // Adjust the output phase by one tenth (phgain) of the error per // second, // opcyc_adj -= fb_phase/(2*M_PI) * phgain * gperiod; } ng = 0; incnt_goertzel = incnt; setup_goertzels(); if (nperiod < 2) fb_cyc = opcyc; nperiod++; } if (USE_PPS) update_pps_ref( vref); if (MFLAG && ++nk > sample_rate * KEYR) { update_keyer(); nk = 0; } incnt++; if (nperiod < 2 || OFLAG_MUTE) return; // // Generate the output. kout is the current modulation symbol, +1 or -1. // double mcyc; // Modulation phase in cycles if (!kcnt || slew_mode == SLEW_HARD) { // Offset by +/- quarter cycle according to the current output symbol mcyc = kout * 0.25/harmonic; } else { // Slew to kout over kslew frames as kcnt counts down double v = slewtable[kslew - kcnt - 1]; // Ranges -1 to 1 as kcnt // counts down mcyc = kout * v * 0.25/harmonic; kcnt--; } mcyc += 0.25/harmonic; // // Generate the output. // if (OFLAG_REF) { *out2 = in1; // Channel 2 is copy of reference incoming on channel 1 *out1 = wave( opcyc + mcyc); } else if (OFLAG_BRIDGE) { *out1 = wave( opcyc + mcyc); *out2 = -*out1; } else if (OFLAG_IQ) { *out1 = wave( opcyc + mcyc); *out2 = wave( opcyc + mcyc - 0.25/harmonic); } else *out1 = *out2 = wave( opcyc + mcyc); add_kbuf( mcyc); opcyc += dopcyc; } // // Functions to unpack soundcard input buffers, process each frame, and pack // into soundcard output buffers. // static void process_buffer_S16_LE_octo( char const *rbuf, char *sbuf, int n) { int16_t *rp = (int16_t *) rbuf; int16_t *sp = (int16_t *) sbuf; for (int i = 0; i < n; i++) // For each frame { double s1 = 0, s2 = 0; process_frame( rp[i * nchans_in + (0 + octo_offset)%8]/(double)INT16_MAX, rp[i * nchans_in + (1 + octo_offset)%8]/(double)INT16_MAX, &s1, &s2); for (int j = 0; j < 8; j++) sp[i * nchans_out + j] = s1 * INT16_MAX; } } static void process_buffer_S16_LE( char const *rbuf, char *sbuf, int n) { int16_t *rp = (int16_t *) rbuf; int16_t *sp = (int16_t *) sbuf; for (int i = 0; i < n; i++) // For each frame { double s1 = 0, s2 = 0; process_frame( rp[i * nchans_in + 0]/(double)INT16_MAX, rp[i * nchans_in + 1]/(double)INT16_MAX, &s1, &s2); sp[i * nchans_out + 0] = s1 * INT16_MAX; sp[i * nchans_out + 1] = s2 * INT16_MAX; } } static void process_buffer_S32_LE( char const *rbuf, char *sbuf, int n) { int32_t *rp = (int32_t *) rbuf; int32_t *sp = (int32_t *) sbuf; for (int i = 0; i < n; i++) // For each frame { double s1 = 0, s2 = 0; process_frame( rp[i * nchans_in + 0]/(double)INT32_MAX, rp[i * nchans_in + 1]/(double)INT32_MAX, &s1, &s2); sp[i * nchans_out + 0] = s1 * INT32_MAX; sp[i * nchans_out + 1] = s2 * INT32_MAX; } } static void process_buffer_S32_LE_octo( char const *rbuf, char *sbuf, int n) { int32_t *rp = (int32_t *) rbuf; int32_t *sp = (int32_t *) sbuf; for (int i = 0; i < n; i++) // For each frame { double s1 = 0, s2 = 0; process_frame( rp[i * nchans_in + (0 + octo_offset)%8]/(double)INT32_MAX, rp[i * nchans_in + (1 + octo_offset)%8]/(double)INT32_MAX, &s1, &s2); for (int j = 0; j < 8; j++) sp[i * nchans_out + j] = s1 * INT32_MAX; } } static void process_buffer_S24_3LE( char const *rbuf, char *sbuf, int n) { for (int i = 0; i < n; i++) // For each frame { uint8_t *rp = (uint8_t *) rbuf + i * nchans_in * 3; uint32_t t; // Put the 3 bytes into the upper 3 of a 32 bit word t = (rp[0] << 8) | (rp[1] << 16) | (rp[2] << 24); double r1 = * (int32_t *) &t; rp += 3; t = (rp[0] << 8) | (rp[1] << 16) | (rp[2] << 24); double r2 = * (int32_t *) &t; double s1 = 0, s2 = 0; process_frame( r1/INT32_MAX, r2/INT32_MAX, &s1, &s2); int32_t s; char *p = (char *) &s; s = s1 * INT32_MAX; uint8_t *sp = (uint8_t *) sbuf + i * nchans_out * 3; sp[0] = p[1]; sp[1] = p[2]; sp[2] = p[3]; sp += 3; s = s2 * INT32_MAX; sp[0] = p[1]; sp[1] = p[2]; sp[2] = p[3]; } } // // Synthesiser task. This is run as a separate real time thread so that // it will not be blocked by screen or disk I/O. // static void *task_synth( void *arg) { // Send and receive buffers, each holding nread frames char *sbuf = safe_malloc( nread * pcm_bytes * nchans_out, TRUE); char *rbuf = safe_malloc( nread * pcm_bytes * nchans_in, TRUE); // // Set a real-time scheduling policy at high priority. // int pri = sched_get_priority_max( SCHED_FIFO); struct sched_param pa; pa.sched_priority = pri; if (sched_setscheduler( 0, SCHED_FIFO, &pa)) logd( 1, "unable to set real-time scheduling priority"); int err; if ((err = snd_pcm_prepare( capture_handle)) < 0) bailout( "cannot prepare soundcard input (%s)", snd_strerror( err)); // // Output pre-load to almost a full buffer. Then measure the actual // buffer delay. The output delay remains fairly constant so a one-off // measurement is sufficient. // if (!OFLAG_MUTE) { if ((err = snd_pcm_prepare( playback_handle)) < 0) bailout( "cannot prepare soundcard output (%s)", snd_strerror( err)); for (int i = 0; i < obufsize / nread - 1; i++) write_soundcard( sbuf, nread); if (snd_pcm_delay( playback_handle, &odelay_frames) < 0) { logd( 0, "cannot get odelay"); odelay_frames = obufdel/1e6 * sample_rate; } else logd( 1, "output delay %d frames", odelay_frames); init_kbuf( odelay_frames); } // // Main loop. // while (1) { int n = read_soundcard( rbuf); // Returns number of frames read if (hack_octo) remap_octo( rbuf, n); switch( pcm_format) { case SND_PCM_FORMAT_S16_LE: if (hack_octo) process_buffer_S16_LE_octo( rbuf, sbuf, n); else process_buffer_S16_LE( rbuf, sbuf, n); break; case SND_PCM_FORMAT_S24_3LE: process_buffer_S24_3LE( rbuf, sbuf, n); break; case SND_PCM_FORMAT_S32_LE: if (hack_octo) process_buffer_S32_LE_octo( rbuf, sbuf, n); else process_buffer_S32_LE( rbuf, sbuf, n); break; default: bailout( "pcm format not handled"); } if (!OFLAG_MUTE) write_soundcard( sbuf, n); } return 0; } /////////////////////////////////////////////////////////////////////////////// // Command Line // /////////////////////////////////////////////////////////////////////////////// // // Convert up to n digits of ASCII into decimal int. // static int atoin( char const *s, int n) { int val = 0; while (n-- && isdigit( *s)) val = val * 10 + (*s++ - '0'); return val; } static time_t parse_datetime( char const *stamp) { time_t now = time( NULL); char const *s = stamp; if (!strcasecmp( s, "now")) return now; // // Test for integer unix time. // if (strlen( s) == 10) { char const *t = s; while (*t && isdigit( *t)) t++; if (!*t) return atol( s); } // // Test for time step. // if (s[0] == '+') { int step = atoi( s+1); return (now / step + 1) * step; } struct tm *tm = gmtime( &now); // // Parse an optional date // yyyy-mm-dd or yyyymmdd // 0123456789 01234567 // int has_date = FALSE; if (isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && (s[4] == '-' || s[4] == '/') && isdigit( s[5]) && isdigit( s[6]) && (s[7] == '-' || s[7] == '/') && isdigit( s[8]) && isdigit( s[9])) { tm->tm_year = atoin( s, 4) - 1900; tm->tm_mon = atoin( s+5, 2) - 1; tm->tm_mday = atoin( s+8, 2); s += 10; has_date = TRUE; } else if (isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && isdigit( s[4]) && isdigit( s[5]) && isdigit( s[6]) && isdigit( s[7])) { tm->tm_year = atoin( s, 4) - 1900; tm->tm_mon = atoin( s+4, 2) - 1; tm->tm_mday = atoin( s+6, 2); s += 8; has_date = TRUE; } // // Separator between date and time. // if (has_date) { if (*s == ' ' || *s == '_' || *s == 'T' || *s == 't') s++; else if (!strncasecmp( s, "UT", 2)) s += 2; else bailout( "invalid date/time [%s]", stamp); } // // Parse a time // hh:mm:ss or hhmmss or hh:mm or hhmm // 01234567 012345 01234 or 0123 // if (!*s) bailout( "missing time"); if (isdigit( s[0]) && isdigit( s[1]) && s[2] == ':' && isdigit( s[3]) && isdigit( s[4]) && s[5] == ':' && isdigit( s[6]) && isdigit( s[7])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+3, 2); tm->tm_sec = atoin( s+6, 2); s += 8; } else if (isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3]) && isdigit( s[4]) && isdigit( s[5])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+2, 2); tm->tm_sec = atoin( s+4, 2); s += 6; } else if (isdigit( s[0]) && isdigit( s[1]) && s[2] == ':' && isdigit( s[3]) && isdigit( s[4])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+3, 2); tm->tm_sec = 0; s += 5; } else if (isdigit( s[0]) && isdigit( s[1]) && isdigit( s[2]) && isdigit( s[3])) { tm->tm_hour = atoin( s+0, 2); tm->tm_min = atoin( s+2, 2); tm->tm_sec = 0; s += 4; } else bailout( "invalid time [%s]", s); if (*s) logd( 0, "junk [%s] following date/time ignored", s); time_t secs = mktime( tm); if (secs < 0) bailout( "invalid date/time [%s]", stamp); // // If the start time has already been passed, the user probably means // tomorrow. // if (secs < now) { if (has_date) bailout( "start time has already passed"); secs += 86400; } return secs; } static void parse_output_options( char *s) { while (s && *s) { char *p = strchr( s, ','); if (p) *p++ = 0; if (!strcmp( s, "bridge")) OFLAG_BRIDGE = TRUE; else if (!strcmp( s, "square")) OFLAG_SQUARE = TRUE; else if (!strcmp( s, "ref")) OFLAG_REF = TRUE; else if (!strcmp( s, "mute")) OFLAG_MUTE = TRUE; else if (!strcmp( s, "iq")) OFLAG_IQ = TRUE; else bailout( "unrecognised output option [%s]", s); s = p; } } static void parse_slew( char const *s) { if (!strcmp( s, "hard")) { slew_mode = SLEW_HARD; return; } if (!strcmp( s, "sigmoid")) { slew_mode = SLEW_SIGMOID; return; } if (!strcmp( s, "linear")) { slew_mode = SLEW_LINEAR; return; } if (!strncmp( s, "sigmoid=", 8)) { slew_mode = SLEW_SIGMOID; slew_factor = atof( s+8); return; } if (!strncmp( s, "linear=", 7)) { slew_mode = SLEW_LINEAR; slew_factor = atof( s+7); return; } bailout( "unrecognised slewing mode [%s]", s); } static void parse_ptt( char *s) { while (s && *s) { char *p = strchr( s, ','); if (p) *p++ = 0; if (!strncmp( s, "dev=", 4)) ptt_device = strdup( s+4); else if (!strncmp( s, "gpio=", 5)) ptt_gpio = atoi( s+5); else if (!strcmp( s, "mcr=dtr+rts")) ptt_mode = PTT_TOGETHER; else if (!strcmp( s, "mcr=dtr-rts")) ptt_mode = PTT_OPPOSITE; else if (!strncmp( s, "pre=", 4)) ptt_pre = atof( s+4); else if (!strncmp( s, "post=", 5)) ptt_post = atof( s+5); else bailout( "unrecognised PTT option [%s]", s); s = p; } } static void usage( void) { printf( "EbSynth version %s\n\n" "usage: ebsynth [options] 2> logfile\n" "\n" "General options:\n" " -d device ALSA device (default hw:0,0)\n" " -r rate Sample rate (default max rate of the device)\n" " -F freq Output frequency in Hz\n" " -R freq Reference frequency in Hz (default expect 1PPS)\n" " -f Enable phase feedback\n" " -v Increase log detail\n" "\n" "Modulation options\n" " -m Enable modulation (read symbols from stdin)\n" " -S secs Symbol period (seconds)\n" " -n secs Repeat period (seconds)\n" " -h num Harmonic used (default 1)\n" " -T start Start time\n" " -s hard Hard keying\n" " -s sigmoid Sigmoid phase slew keying (default)\n" " -s linear Linear phase ramp keying\n" "\n" "Output options (can be comma separated)\n" " -o bridge Complimentary output phase\n" " -o square Square wave output (default sine)\n" " -o ref Output the reference on channel 2\n" " -o iq Output complex signal\n" " -o mute Turn off output altogether\n" "\n" "PTT control via serial port (can be comma separated)\n" " -p dev=device Serial device (example dev=/dev/ttyS0)\n" " -p mcr=dtr+rts Drive DTR and RTS together (default)\n" " -p mcr=dtr-rts Drive DTR and RTS opposite\n" " -p gpio=channel Drive GPIO for PTT\n" " -p pre=seconds Activate PTT seconds before message (default 2)\n" " -p post=seconds Drop PTT seconds after message (default 2)\n" "\nTweaks and hacks\n" " -x rstc=seconds Rate smoothing time constant (default 10)\n" " -x tbtc=seconds Time smoothing time constant (default 10)\n" " -x phtc=seconds Phase smoothing time constant (default 10)\n" " -x tbgain= Timebase servo loop gain (default 0.05)\n" " -x phgain= Phase feedback servo loop gain (default 0.1)\n" " -x gperiod=seconds Phase measurement time (default 0.05)\n" " -x octo Work around octo channel shift bug\n" " -x nocolour Don't assume a colour terminal\n" " -x ppsdump Dump PPS waveform and transform\n" "", VERSION ); exit( 1); } // // Option x to invoke hacks. // static void parse_experimental( char const *arg) { if (!strcmp( arg, "octo")) hack_octo = TRUE; else if (!strncmp( arg, "tbtc=", 5)) tbtc = atof( arg + 5); else if (!strncmp( arg, "rstc=", 5)) rstc = atof( arg + 5); else if (!strncmp( arg, "phtc=", 5)) phtc = atof( arg + 5); else if (!strncmp( arg, "gperiod=", 8)) gperiod = atof( arg + 8); else if (!strncmp( arg, "tbgain=", 7)) tbgain = atof( arg + 7); else if (!strncmp( arg, "phgain=", 7)) phgain = atof( arg + 7); else if (!strcmp( arg, "nocolour") || !strcmp( arg, "nocolor")) nocolour = TRUE; else if (!strcmp( arg, "ppsdump")) ppsdump = TRUE; else bailout( "unrecognised -x arg [%s]", arg); } int main( int argc, char *argv[]) { int test_mode = FALSE; // TRUE if -t given while (1) { int c = getopt( argc, argv, "vd:r:F:R:S:T:n:o:h:s:p:x:mtf?"); if (c == 'v') loglevel++; else if (c == 'd') device = strdup( optarg); else if (c == 'F') Tfreq = atof( optarg); else if (c == 'R') { Rfreq = atof( optarg); USE_PPS = FALSE; } else if (c == 'r') sample_rate = atoi( optarg); else if (c == 'S') sym_period = atof( optarg); else if (c == 'T') Tstart = parse_datetime( optarg); else if (c == 'm') MFLAG = TRUE; else if (c == 'n') { repeat_type = optarg[0] == '+'; repeat = atoi( optarg); } else if (c == 't') test_mode = TRUE; else if (c == 'f') USE_FEEDBACK = TRUE; else if (c == 'o') parse_output_options( strdup( optarg)); else if (c == 'h') harmonic = atoi( optarg); else if (c == 's') parse_slew( optarg); else if (c == 'p') parse_ptt( strdup( optarg)); else if (c == 'x') parse_experimental( optarg); else if (c == -1) break; else usage(); } // // Check options. More checks later after sample rate is chosen. // if (sample_rate < 0) bailout( "invalid sample rate"); if (Tfreq <= 0) bailout( "requested frequency missing or invalid"); if (harmonic < 1) bailout( "invalid harmonic specified"); if (repeat < 0) bailout( "invalid repeat time"); if (ptt_pre < 0) bailout( "invalid -p pre= value"); if (ptt_post < 0) bailout( "invalid -p post= value"); if (ptt_device && ptt_gpio >= 0) bailout( "cannot have both RS232 and GPIO ptt"); if (MFLAG) { if (!Tstart && !test_mode && (!repeat || repeat_type)) bailout( "no start time given, needs -T"); if (sym_period <= 0) bailout( "missing or invalid symbol period"); } if (OFLAG_BRIDGE && (OFLAG_REF || OFLAG_IQ)) bailout( "incompatible output options"); if (OFLAG_REF && OFLAG_IQ) bailout( "incompatible output options"); if (hack_octo) if (OFLAG_REF || OFLAG_IQ || OFLAG_BRIDGE) bailout( "output options incompatible with -x octo"); // // If no start time given, but we have a UT modulo repeat, then we can // default to the next available start time. // if (MFLAG && !Tstart & !test_mode && repeat && !repeat_type) { struct timespec ts; clock_gettime( CLOCK_REALTIME, &ts); uint64_t t = ceill( ts.tv_sec + ts.tv_nsec/ (long double) 1e9); Tstart = t; while (Tstart - ptt_pre < t) Tstart = (Tstart / repeat + 1) * repeat; } // // Read BPSK symbols from stdin if -m was given. Input file should be // ASCII '0' or '1' and can span multiple lines. // if (MFLAG && !test_mode) { int c; while ((c = getc( stdin)) != EOF) if (c == '0' || c == '1') { if (Nsymbols == MAXSYM) bailout( "max symbols %d", MAXSYM); symbols[Nsymbols++] = c; } else if (c != '\n' && c != '\r') bailout( "bad character [%c] in input", c); if (!Nsymbols) bailout( "no symbols read from stdin"); } // // Display and signals initialisation. // setup_signal_handling(); curses_init(); xyprintf( AG, 2, 1, "EbSynth: Version %s", VERSION); if (!USE_FEEDBACK) xyprintf( AG, 2, 10, "Feedback amplitude: Internal"); if (!USE_PPS) xyprintf( AG, 2, 6, "Reference frequency: %13.6f Hz", Rfreq); if (!MFLAG) { xyprintf( AG, 2, 14, "Modulation: None"); } else { if (test_mode) { xyprintf( AG, 2, 14, "Modulation: Test mode Period: %.2f secs", sym_period); } else { xyprintf( AG, 2, 14, "Modulation: %d symbols Period: %.2f secs", Nsymbols, sym_period); if (repeat > 0 && !repeat_type) xyprintf( AG, 53, 14, "Repeat: UT mod %d secs", repeat); else if (repeat > 0) xyprintf( AG, 53, 14, "Repeat: start+%d secs", repeat); else xyprintf( AG, 53, 14, "Repeat: none"); show_message_times( Tstart, Nsymbols * sym_period); } switch (slew_mode) { case SLEW_HARD: xyprintf( AG, 2, 15, "Keying: Hard"); break; case SLEW_SIGMOID: xyprintf( AG, 2, 15, "Keying: Sigmoid, factor %.2f", slew_factor); break; case SLEW_LINEAR: xyprintf( AG, 2, 15, "Keying: Linear, factor %.2f", slew_factor); break; } if (harmonic > 1) xyprintf( AG, 53, 15, "Harmonic: %d", harmonic); if (USE_PTT) { if (ptt_device) xyprintf( AG, 32, 15, "PTT: %s", ptt_device); else if (ptt_gpio >= 0) xyprintf( AG, 32, 15, "PTT: gpio%d", ptt_gpio); } } Kstate = test_mode ? 10 : 1; refresh(); // // Prepare the soundcard. Then we know the sample rate and can sanity // check the given frequencies. // setup_input(); if (Tfreq >= sample_rate/2.0) bailout( "sample rate too low for output frequency"); if (Rfreq >= sample_rate/2.0) bailout( "sample rate too low for reference frequency"); if (!OFLAG_MUTE) setup_output(); setup_keyer(); // // Not using modulation? Then leave the PTT permanently on. // setup_ptt(); if (!MFLAG) drive_ptt( TRUE); if (Tstart) { queue_ptt( Tstart - ptt_pre, 1); queue_ptt( Tstart + Nsymbols * sym_period + ptt_post, 0); } // // Initialise and start the synthesizer thread. // init_sinetable(); Afreq = Rfreq; xrate = sample_rate; setup_goertzels(); pthread_t tmp; pthread_create( &tmp, NULL, task_synth, NULL); // // Main loop to service the display. // int lastsec = 0; int lastptt = -1; while (1) { if (USE_PTT && ptt_state != lastptt) { xyprintf( AB, 2, 20, "PTT: %3s", ptt_state ? "ON" : "OFF"); refresh(); lastptt = ptt_state; } if (status_change) { status_change = FALSE; xyprintf( AB, 2, 19, "Status: %-35.35s", status_msg); refresh(); } // // Display update each second. // struct timespec ts; clock_gettime( CLOCK_REALTIME, &ts); if (ts.tv_sec != lastsec) { lastsec = ts.tv_sec; xyprintf( AB, 42, 1, "UT: %s", format_date( ts.tv_sec)); if (!USE_PPS) { xyprintf( AB, 2, 7, " Apparent frequency: %13.6f Hz", Afreq); xyprintf( AB, 42, 6, "Reference amplitude: %5.2f RMS", ref_rms); xyprintf( AB, 2, 8, " Sample rate: %13.6f Hz", xrate); } else { if (fabs( timebase_err) >= 1e-3) xyprintf( AB, 2, 6, "Timebase error: %+7.1f mS", timebase_err * 1e3); else if (fabs( timebase_err) >= 1e-6) xyprintf( AB, 2, 6, "Timebase error: %+7.1f uS", timebase_err * 1e6); else xyprintf( AB, 2, 6, "Timebase error: %+7.1f nS", timebase_err * 1e9); xyprintf( AB, 2, 7, " Sample rate: %13.6f Hz", xrate); xyprintf( AB, 42, 6, "PPS amplitude: %+6.2f peak", pps_peak); xyprintf( AB, 42, 7, " PPS jitter: %8.3f uS", ppsmad/sample_rate * 1e6); xyprintf( AB, 42, 8, "Pulses missed: %d", pps_missed); } if (USE_FEEDBACK) { xyprintf( AB, 2, 10, "Feedback amplitude: %6.2f RMS", fb_rms); xyprintf( AB, 2, 11, "Feeback phase: %+6.1f deg", fb_phase * 180/M_PI); } xyprintf( AG, 42, 10, "Target frequency: %13.6f Hz", Tfreq); if (!USE_PPS) xyprintf( AB, 42, 11, " Output error: %+9.2f uHz", -df * Tfreq/Rfreq * 1e6); if (refresh_message_times) { refresh_message_times = FALSE; show_message_times( Tstart, Nsymbols * sym_period); } refresh(); } usleep( 50000); } return 0; }