kopia lustrzana https://github.com/jamescoxon/dl-fldigi
916 wiersze
26 KiB
C++
916 wiersze
26 KiB
C++
// ----------------------------------------------------------------------------
|
|
// cw.cxx -- morse code modem
|
|
//
|
|
// Copyright (C) 2006-2010
|
|
// Dave Freese, W1HKJ
|
|
//
|
|
// This file is part of fldigi. Adapted from code contained in gmfsk source code
|
|
// distribution.
|
|
// gmfsk Copyright (C) 2001, 2002, 2003
|
|
// Tomi Manninen (oh2bns@sral.fi)
|
|
// Copyright (C) 2004
|
|
// Lawrence Glaister (ve7it@shaw.ca)
|
|
//
|
|
// Fldigi is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Fldigi is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with fldigi. If not, see <http://www.gnu.org/licenses/>.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
|
#include <config.h>
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "digiscope.h"
|
|
#include "waterfall.h"
|
|
#include "fl_digi.h"
|
|
|
|
#include "cw.h"
|
|
#include "misc.h"
|
|
//#include "modeIO.h"
|
|
#include "configuration.h"
|
|
#include "status.h"
|
|
#include "debug.h"
|
|
#include "FTextRXTX.h"
|
|
|
|
#define DEC_RATIO 16
|
|
#define CW_FIRLEN 64
|
|
|
|
using namespace std;
|
|
|
|
static bool use_paren;
|
|
string prosigns;
|
|
|
|
void cw::tx_init(SoundBase *sc)
|
|
{
|
|
scard = sc;
|
|
phaseacc = 0;
|
|
lastsym = 0;
|
|
qskphase = 0;
|
|
}
|
|
|
|
void cw::rx_init()
|
|
{
|
|
cw_receive_state = RS_IDLE;
|
|
smpl_ctr = 0;
|
|
cw_rr_current = 0;
|
|
agc_peak = 0;
|
|
set_scope_mode(Digiscope::SCOPE);
|
|
update_Status();
|
|
usedefaultWPM = false;
|
|
scope_clear = true;
|
|
}
|
|
|
|
void cw::init()
|
|
{
|
|
bool wfrev = wf->Reverse();
|
|
bool wfsb = wf->USB();
|
|
reverse = wfrev ^ !wfsb;
|
|
|
|
if (progdefaults.StartAtSweetSpot)
|
|
set_freq(progdefaults.CWsweetspot);
|
|
else if (progStatus.carrier != 0) {
|
|
set_freq(progStatus.carrier);
|
|
#if !BENCHMARK_MODE
|
|
progStatus.carrier = 0;
|
|
#endif
|
|
} else
|
|
set_freq(wf->Carrier());
|
|
|
|
trackingfilter->reset();
|
|
cw_adaptive_receive_threshold = (long int)trackingfilter->run(2 * cw_send_dot_length);
|
|
put_cwRcvWPM(cw_send_speed);
|
|
for (int i = 0; i < OUTBUFSIZE; i++)
|
|
outbuf[i] = qskbuf[i] = 0.0;
|
|
rx_init();
|
|
use_paren = progdefaults.CW_use_paren;
|
|
prosigns = progdefaults.CW_prosigns;
|
|
stopflag = false;
|
|
}
|
|
|
|
cw::~cw() {
|
|
if (cwfilter) delete cwfilter;
|
|
if (bitfilter) delete bitfilter;
|
|
if (trackingfilter) delete trackingfilter;
|
|
}
|
|
|
|
|
|
//cw::cw() : morse(), modem()
|
|
cw::cw() : modem()
|
|
{
|
|
double lp;
|
|
|
|
cap |= CAP_BW;
|
|
|
|
mode = MODE_CW;
|
|
freqlock = false;
|
|
usedefaultWPM = false;
|
|
frequency = progdefaults.CWsweetspot;
|
|
tx_frequency = get_txfreq_woffset();
|
|
risetime = progdefaults.CWrisetime;
|
|
QSKshape = progdefaults.QSKshape;
|
|
|
|
samplerate = CWSampleRate;
|
|
fragmentsize = CWMaxSymLen;
|
|
|
|
cw_speed = progdefaults.CWspeed;
|
|
bandwidth = progdefaults.CWbandwidth;
|
|
|
|
cw_send_speed = cw_speed;
|
|
cw_receive_speed = cw_speed;
|
|
cw_adaptive_receive_threshold = 2 * DOT_MAGIC / cw_speed;
|
|
cw_noise_spike_threshold = cw_adaptive_receive_threshold / 4;
|
|
cw_send_dot_length = DOT_MAGIC / cw_send_speed;
|
|
cw_send_dash_length = 3 * cw_send_dot_length;
|
|
symbollen = (int)(samplerate * 1.2 / progdefaults.CWspeed);
|
|
fsymlen = (int)((50*(samplerate * 1.2 / progdefaults.CWfarnsworth) - 41*symbollen)/9);
|
|
|
|
memset(rx_rep_buf, 0, sizeof(rx_rep_buf));
|
|
|
|
// block of variables that get updated each time speed changes
|
|
pipesize = 512;
|
|
cwTrack = true;
|
|
phaseacc = 0.0;
|
|
pipeptr = 0;
|
|
agc_peak = 1.0;
|
|
|
|
lp = 0.5 * bandwidth / samplerate;
|
|
cwfilter = new C_FIR_filter();
|
|
cwfilter->init_lowpass (CW_FIRLEN, DEC_RATIO, lp);
|
|
|
|
bitfilter = new Cmovavg(8);
|
|
bitfilterlen = (int)(samplerate / frequency / 4);
|
|
bitfilterlen = bitfilterlen < 2 ? 2 : bitfilterlen;
|
|
bitfilter->setLength(bitfilterlen);
|
|
|
|
trackingfilter = new Cmovavg(TRACKING_FILTER_SIZE);
|
|
|
|
makeshape();
|
|
sync_parameters();
|
|
wf->Bandwidth ((int)bandwidth);
|
|
update_Status();
|
|
//printf("%s\n", progdefaults.CW_prosigns.c_str());
|
|
}
|
|
|
|
// sync_parameters()
|
|
// Synchronize the dot, dash, end of element, end of character, and end
|
|
// of word timings and ranges to new values of Morse speed, or receive tolerance.
|
|
|
|
void cw::sync_parameters()
|
|
{
|
|
int lowerwpm, upperwpm, nusymbollen, nufsymlen;
|
|
|
|
int wpm = usedefaultWPM ? progdefaults.defCWspeed : progdefaults.CWspeed;
|
|
int fwpm = usedefaultWPM ? progdefaults.defCWspeed : progdefaults.CWfarnsworth;
|
|
|
|
cw_send_dot_length = DOT_MAGIC / progdefaults.CWspeed;
|
|
|
|
// if (usedefaultWPM == false)
|
|
// cw_send_dot_length = DOT_MAGIC / progdefaults.CWspeed;
|
|
// else
|
|
// cw_send_dot_length = DOT_MAGIC / progdefaults.defCWspeed;
|
|
|
|
cw_send_dash_length = 3 * cw_send_dot_length;
|
|
|
|
nusymbollen = (int)(samplerate * 1.2 / wpm);
|
|
nufsymlen = (int)((50*(samplerate * 1.2 / fwpm) - 41*symbollen)/9);
|
|
// nusymbollen = (int)(samplerate * 1.2 / progdefaults.CWspeed);
|
|
// nufsymlen = (int)((50*(samplerate * 1.2 / progdefaults.CWfarnsworth) - 41*symbollen)/9);
|
|
|
|
if (symbollen != nusymbollen ||
|
|
nufsymlen != fsymlen ||
|
|
risetime != progdefaults.CWrisetime ||
|
|
QSKshape != progdefaults.QSKshape ) {
|
|
risetime = progdefaults.CWrisetime;
|
|
QSKshape = progdefaults.QSKshape;
|
|
symbollen = nusymbollen;
|
|
fsymlen = nufsymlen;
|
|
makeshape();
|
|
}
|
|
int len = (int)(samplerate / frequency / 4);
|
|
len = len < 2 ? 2 : len;
|
|
if (bitfilterlen != len) {
|
|
bitfilterlen = len;
|
|
bitfilter->setLength(bitfilterlen);
|
|
}
|
|
|
|
// check if user changed the tracking or the cw default speed
|
|
if ((cwTrack != progdefaults.CWtrack) ||
|
|
(cw_send_speed != progdefaults.CWspeed)) {
|
|
trackingfilter->reset();
|
|
cw_adaptive_receive_threshold = (long int)trackingfilter->run(2 * cw_send_dot_length);
|
|
put_cwRcvWPM(cw_send_speed);
|
|
}
|
|
cwTrack = progdefaults.CWtrack;
|
|
cw_send_speed = progdefaults.CWspeed;
|
|
|
|
// Receive parameters:
|
|
lowerwpm = cw_send_speed - progdefaults.CWrange;
|
|
upperwpm = cw_send_speed + progdefaults.CWrange;
|
|
if (lowerwpm < progdefaults.CWlowerlimit)
|
|
lowerwpm = progdefaults.CWlowerlimit;
|
|
if (upperwpm > progdefaults.CWupperlimit)
|
|
upperwpm = progdefaults.CWupperlimit;
|
|
cw_lower_limit = 2 * DOT_MAGIC / upperwpm;
|
|
cw_upper_limit = 2 * DOT_MAGIC / lowerwpm;
|
|
|
|
if (cwTrack)
|
|
cw_receive_speed = DOT_MAGIC / (cw_adaptive_receive_threshold / 2);
|
|
else {
|
|
cw_receive_speed = cw_send_speed;
|
|
cw_adaptive_receive_threshold = 2 * cw_send_dot_length;
|
|
}
|
|
|
|
cw_receive_dot_length = DOT_MAGIC / cw_receive_speed;
|
|
|
|
cw_receive_dash_length = 3 * cw_receive_dot_length;
|
|
|
|
cw_noise_spike_threshold = cw_receive_dot_length / 4;
|
|
}
|
|
|
|
|
|
//=======================================================================
|
|
// cw_update_tracking()
|
|
// This gets called everytime we have a dot dash sequence or a dash dot
|
|
// sequence. Since we have semi validated tone durations, we can try and
|
|
// track the cw speed by adjusting the cw_adaptive_receive_threshold variable.
|
|
// This is done with moving average filters for both dot & dash.
|
|
//=======================================================================
|
|
|
|
void cw::update_tracking(int idot, int idash)
|
|
{
|
|
int dot, dash;
|
|
if (idot > cw_lower_limit && idot < cw_upper_limit)
|
|
dot = idot;
|
|
else
|
|
dot = cw_send_dot_length;
|
|
if (idash > cw_lower_limit && idash < cw_upper_limit)
|
|
dash = idash;
|
|
else
|
|
dash = cw_send_dash_length;
|
|
|
|
cw_adaptive_receive_threshold = (long int)trackingfilter->run((dash + dot) / 2);
|
|
sync_parameters();
|
|
}
|
|
|
|
//=======================================================================
|
|
//update_syncscope()
|
|
//Routine called to update the display on the sync scope display.
|
|
//For CW this is an o scope pattern that shows the cw data stream.
|
|
//=======================================================================
|
|
|
|
void cw::update_Status()
|
|
{
|
|
put_MODEstatus("CW %s Rx %d", usedefaultWPM ? "*" : " ", cw_receive_speed);
|
|
}
|
|
|
|
//=======================================================================
|
|
//update_syncscope()
|
|
//Routine called to update the display on the sync scope display.
|
|
//For CW this is an o scope pattern that shows the cw data stream.
|
|
//=======================================================================
|
|
//
|
|
|
|
static int clrcount = 16;
|
|
void cw::update_syncscope()
|
|
{
|
|
int j;
|
|
|
|
for (int i = 0; i < pipesize; i++) {
|
|
j = (i + pipeptr) % pipesize;
|
|
scopedata[i] = 0.1 + 0.6 * pipe[j] / agc_peak;
|
|
}
|
|
set_scope(scopedata, pipesize, false);
|
|
scopedata.next(); // change buffers
|
|
clrcount = 16;
|
|
put_cwRcvWPM(cw_receive_speed);
|
|
update_Status();
|
|
}
|
|
|
|
void cw::clear_syncscope()
|
|
{
|
|
if (--clrcount) return;
|
|
for (int i = 0; i < pipesize; i++)
|
|
scopedata[i] = 0;
|
|
set_scope(scopedata, pipesize, false);
|
|
scopedata.next();
|
|
clrcount = 16;
|
|
}
|
|
|
|
|
|
|
|
//=====================================================================
|
|
// cw_rxprocess()
|
|
// Called with a block (512 samples) of audio.
|
|
//======================================================================
|
|
|
|
int cw::rx_process(const double *buf, int len)
|
|
{
|
|
complex z;
|
|
double delta;
|
|
double value;
|
|
const char *c;
|
|
|
|
if (use_paren != progdefaults.CW_use_paren ||
|
|
prosigns != progdefaults.CW_prosigns) {
|
|
use_paren = progdefaults.CW_use_paren;
|
|
prosigns = progdefaults.CW_prosigns;
|
|
morse::init();
|
|
}
|
|
|
|
// check if user changed filter bandwidth
|
|
if (bandwidth != progdefaults.CWbandwidth) {
|
|
bandwidth = progdefaults.CWbandwidth;
|
|
cwfilter->init_lowpass (CW_FIRLEN, DEC_RATIO, 0.5 * bandwidth / samplerate);
|
|
wf->Bandwidth ((int)bandwidth);
|
|
}
|
|
|
|
// compute phase increment expected at our specific rx tone freq
|
|
delta = 2.0 * M_PI * frequency / samplerate;
|
|
|
|
while (len-- > 0) {
|
|
// Mix with the internal NCO
|
|
z = complex ( *buf * cos(phaseacc), *buf * sin(phaseacc) );
|
|
buf++;
|
|
phaseacc += delta;
|
|
if (phaseacc > M_PI)
|
|
phaseacc -= 2.0 * M_PI;
|
|
if (cwfilter->run ( z, z )) {
|
|
|
|
// update the basic sample counter used for morse timing
|
|
smpl_ctr += DEC_RATIO;
|
|
// demodulate
|
|
value = z.mag();
|
|
|
|
value = bitfilter->run(value);
|
|
// Compute a variable threshold value for tone
|
|
// detection. Fast attack and slow decay.
|
|
if (value > agc_peak)
|
|
agc_peak = decayavg(agc_peak, value, 80.0);
|
|
else
|
|
agc_peak = decayavg(agc_peak, value, 800.0);
|
|
|
|
metric = clamp(agc_peak * 1000.0 , 0.0, 100.0);
|
|
|
|
// save correlation amplitude value for the sync scope
|
|
pipe[pipeptr] = value;
|
|
pipeptr = (pipeptr + 1) % pipesize;
|
|
|
|
if (!progStatus.sqlonoff || metric > progStatus.sldrSquelchValue ) {
|
|
// upward trend means tone starting
|
|
if ((value > 0.66 * agc_peak) && (cw_receive_state != RS_IN_TONE))
|
|
handle_event(CW_KEYDOWN_EVENT, NULL);
|
|
// downward trend means tone stopping
|
|
if ((value < 0.33 * agc_peak) && (cw_receive_state == RS_IN_TONE))
|
|
handle_event(CW_KEYUP_EVENT, NULL);
|
|
}
|
|
if (handle_event(CW_QUERY_EVENT, &c) == CW_SUCCESS) {
|
|
if (strlen(c) == 1)
|
|
put_rx_char(progdefaults.rx_lowercase ? tolower(*c) : *c);
|
|
else while (*c)
|
|
put_rx_char(progdefaults.rx_lowercase ? tolower(*c++) : *c++, FTextBase::CTRL);
|
|
update_syncscope();
|
|
}
|
|
}
|
|
}
|
|
clear_syncscope();
|
|
display_metric(metric);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
// Compare two timestamps, and return the difference between them in usecs.
|
|
|
|
int cw::usec_diff(unsigned int earlier, unsigned int later)
|
|
{
|
|
// Compare the timestamps.
|
|
// At 4 WPM, the dash length is 3*(1200000/4)=900,000 usecs, and
|
|
// the word gap is 2,100,000 usecs.
|
|
if (earlier >= later) {
|
|
return 0;
|
|
} else
|
|
return (int) (((double) (later - earlier) * USECS_PER_SEC) / samplerate);
|
|
}
|
|
|
|
|
|
//=======================================================================
|
|
// handle_event()
|
|
// high level cw decoder... gets called with keyup, keydown, reset and
|
|
// query commands.
|
|
// Keyup/down influences decoding logic.
|
|
// Reset starts everything out fresh.
|
|
// The query command returns CW_SUCCESS and the character that has
|
|
// been decoded (may be '*',' ' or [a-z,0-9] or a few others)
|
|
// If there is no data ready, CW_ERROR is returned.
|
|
//=======================================================================
|
|
|
|
int cw::handle_event(int cw_event, const char **c)
|
|
{
|
|
static int space_sent = true; // for word space logic
|
|
static int last_element = 0; // length of last dot/dash
|
|
int element_usec; // Time difference in usecs
|
|
|
|
switch (cw_event) {
|
|
case CW_RESET_EVENT:
|
|
sync_parameters();
|
|
cw_receive_state = RS_IDLE;
|
|
cw_rr_current = 0; // reset decoding pointer
|
|
smpl_ctr = 0; // reset audio sample counter
|
|
memset(rx_rep_buf, 0, sizeof(rx_rep_buf));
|
|
break;
|
|
case CW_KEYDOWN_EVENT:
|
|
// A receive tone start can only happen while we
|
|
// are idle, or in the middle of a character.
|
|
if (cw_receive_state == RS_IN_TONE)
|
|
return CW_ERROR;
|
|
// first tone in idle state reset audio sample counter
|
|
if (cw_receive_state == RS_IDLE) {
|
|
smpl_ctr = 0;
|
|
memset(rx_rep_buf, 0, sizeof(rx_rep_buf));
|
|
cw_rr_current = 0;
|
|
}
|
|
// save the timestamp
|
|
cw_rr_start_timestamp = smpl_ctr;
|
|
// Set state to indicate we are inside a tone.
|
|
old_cw_receive_state = cw_receive_state;
|
|
cw_receive_state = RS_IN_TONE;
|
|
return CW_ERROR;
|
|
break;
|
|
case CW_KEYUP_EVENT:
|
|
// The receive state is expected to be inside a tone.
|
|
if (cw_receive_state != RS_IN_TONE)
|
|
return CW_ERROR;
|
|
// Save the current timestamp
|
|
cw_rr_end_timestamp = smpl_ctr;
|
|
element_usec = usec_diff(cw_rr_start_timestamp, cw_rr_end_timestamp);
|
|
|
|
// make sure our timing values are up to date
|
|
sync_parameters();
|
|
// If the tone length is shorter than any noise cancelling
|
|
// threshold that has been set, then ignore this tone.
|
|
if (cw_noise_spike_threshold > 0
|
|
&& element_usec < cw_noise_spike_threshold) {
|
|
cw_receive_state = old_cw_receive_state;
|
|
return CW_ERROR;
|
|
}
|
|
|
|
// Set up to track speed on dot-dash or dash-dot pairs for this test to work, we need a dot dash pair or a
|
|
// dash dot pair to validate timing from and force the speed tracking in the right direction. This method
|
|
// is fundamentally different than the method in the unix cw project. Great ideas come from staring at the
|
|
// screen long enough!. Its kind of simple really ... when you have no idea how fast or slow the cw is...
|
|
// the only way to get a threshold is by having both code elements and setting the threshold between them
|
|
// knowing that one is supposed to be 3 times longer than the other. with straight key code... this gets
|
|
// quite variable, but with most faster cw sent with electronic keyers, this is one relationship that is
|
|
// quite reliable. Lawrence Glaister (ve7it@shaw.ca)
|
|
if (last_element > 0) {
|
|
// check for dot dash sequence (current should be 3 x last)
|
|
if ((element_usec > 2 * last_element) &&
|
|
(element_usec < 4 * last_element)) {
|
|
update_tracking(last_element, element_usec);
|
|
}
|
|
// check for dash dot sequence (last should be 3 x current)
|
|
if ((last_element > 2 * element_usec) &&
|
|
(last_element < 4 * element_usec)) {
|
|
update_tracking(element_usec, last_element);
|
|
}
|
|
}
|
|
last_element = element_usec;
|
|
// ok... do we have a dit or a dah?
|
|
// a dot is anything shorter than 2 dot times
|
|
if (element_usec <= cw_adaptive_receive_threshold) {
|
|
rx_rep_buf[cw_rr_current++] = CW_DOT_REPRESENTATION;
|
|
} else {
|
|
// a dash is anything longer than 2 dot times
|
|
rx_rep_buf[cw_rr_current++] = CW_DASH_REPRESENTATION;
|
|
}
|
|
// We just added a representation to the receive buffer.
|
|
// If it's full, then reset everything as it probably noise
|
|
if (cw_rr_current == RECEIVE_CAPACITY - 1) {
|
|
cw_receive_state = RS_IDLE;
|
|
cw_rr_current = 0; // reset decoding pointer
|
|
smpl_ctr = 0; // reset audio sample counter
|
|
return CW_ERROR;
|
|
} else
|
|
// zero terminate representation
|
|
rx_rep_buf[cw_rr_current] = 0;
|
|
// All is well. Move to the more normal after-tone state.
|
|
cw_receive_state = RS_AFTER_TONE;
|
|
return CW_ERROR;
|
|
break;
|
|
case CW_QUERY_EVENT:
|
|
// this should be called quite often (faster than inter-character gap) It looks after timing
|
|
// key up intervals and determining when a character, a word space, or an error char '*' should be returned.
|
|
// CW_SUCCESS is returned when there is a printable character. Nothing to do if we are in a tone
|
|
if (cw_receive_state == RS_IN_TONE)
|
|
return CW_ERROR;
|
|
// in this call we expect a pointer to a char to be valid
|
|
if (c == NULL) {
|
|
// else we had no place to put character...
|
|
cw_receive_state = RS_IDLE;
|
|
cw_rr_current = 0;
|
|
// reset decoding pointer
|
|
return CW_ERROR;
|
|
}
|
|
// compute length of silence so far
|
|
sync_parameters();
|
|
element_usec = usec_diff(cw_rr_end_timestamp, smpl_ctr);
|
|
|
|
// SHORT time since keyup... nothing to do yet
|
|
if (element_usec < (2 * cw_receive_dot_length))
|
|
return CW_ERROR;
|
|
// MEDIUM time since keyup... check for character space
|
|
// one shot through this code via receive state logic
|
|
// FARNSWOTH MOD HERE -->
|
|
if (element_usec >= (2 * cw_receive_dot_length) &&
|
|
element_usec <= (4 * cw_receive_dot_length) &&
|
|
cw_receive_state == RS_AFTER_TONE) {
|
|
// Look up the representation
|
|
*c = morse::rx_lookup(rx_rep_buf);
|
|
if (*c == NULL)
|
|
// invalid decode... let user see error
|
|
*c = "*";
|
|
cw_receive_state = RS_IDLE;
|
|
cw_rr_current = 0; // reset decoding pointer
|
|
space_sent = false;
|
|
return CW_SUCCESS;
|
|
}
|
|
// LONG time since keyup... check for a word space
|
|
// FARNSWOTH MOD HERE -->
|
|
if ((element_usec > (4 * cw_receive_dot_length)) && !space_sent) {
|
|
*c = " ";
|
|
space_sent = true;
|
|
return CW_SUCCESS;
|
|
}
|
|
// should never get here... catch all
|
|
return CW_ERROR;
|
|
break;
|
|
}
|
|
// should never get here... catch all
|
|
return CW_ERROR;
|
|
}
|
|
|
|
//===========================================================================
|
|
// cw transmit routines
|
|
// Define the amplitude envelop for key down events (32 samples long)
|
|
// this is 1/2 cycle of a raised cosine
|
|
//===========================================================================
|
|
|
|
double keyshape[KNUM];
|
|
|
|
void cw::makeshape()
|
|
{
|
|
for (int i = 0; i < KNUM; i++) keyshape[i] = 1.0;
|
|
knum = (int)(8 * risetime);
|
|
|
|
if (knum >= symbollen)
|
|
knum = symbollen - 1;
|
|
|
|
if (knum > KNUM)
|
|
knum = KNUM;
|
|
|
|
switch (QSKshape) {
|
|
case 1: // blackman
|
|
for (int i = 0; i < knum; i++)
|
|
keyshape[i] = (0.42 - 0.50 * cos(M_PI * i/ knum) + 0.08 * cos(2 * M_PI * i / knum));
|
|
break;
|
|
case 0: // raised cosine (hanning)
|
|
default:
|
|
for (int i = 0; i < knum; i++)
|
|
keyshape[i] = 0.5 * (1.0 - cos (M_PI * i / knum));
|
|
}
|
|
}
|
|
|
|
inline double cw::nco(double freq)
|
|
{
|
|
phaseacc += 2.0 * M_PI * freq / samplerate;
|
|
|
|
if (phaseacc > M_PI)
|
|
phaseacc -= 2.0 * M_PI;
|
|
|
|
return sin(phaseacc);
|
|
}
|
|
|
|
inline double cw::qsknco()
|
|
{
|
|
qskphase += 2.0 * M_PI * 1000 / samplerate;//1600.0 / samplerate;
|
|
|
|
if (qskphase > M_PI)
|
|
qskphase -= 2.0 * M_PI;
|
|
|
|
return sin(qskphase);
|
|
}
|
|
|
|
//=====================================================================
|
|
// send_symbol()
|
|
// Sends a part of a morse character (one dot duration) of either
|
|
// sound at the correct freq or silence. Rise and fall time is controlled
|
|
// with a raised cosine shape.
|
|
//
|
|
// Left channel contains the shaped A2 CW waveform
|
|
// Right channel contains a square wave burst of 1600 Hz that is used
|
|
// to trigger a qsk switch. Right channel has pre and post timings for
|
|
// proper switching of the qsk switch before and after the A2 element.
|
|
// If the Pre + Post timing exceeds the interelement spacing then the
|
|
// Pre and / or Post is only applied at the beginning and end of the
|
|
// character.
|
|
//=======================================================================
|
|
|
|
int q_carryover = 0, carryover = 0;
|
|
|
|
void cw::send_symbol(int bits, int len)
|
|
{
|
|
double freq;
|
|
int sample, qsample, i;
|
|
int delta = 0;
|
|
int keydown;
|
|
int keyup;
|
|
int kpre;
|
|
int kpost;
|
|
int duration = 0;
|
|
int symlen = 0;
|
|
float dsymlen = 0.0;
|
|
int currsym = bits & 1;
|
|
|
|
freq = get_txfreq_woffset();
|
|
|
|
delta = (int) (len * (progdefaults.CWweight - 50) / 100.0);
|
|
|
|
symlen = len;
|
|
if (currsym == 1) {
|
|
dsymlen = len * (progdefaults.CWdash2dot - 3.0) / (progdefaults.CWdash2dot + 1.0);
|
|
if (lastsym == 1 && currsym == 1)
|
|
symlen += (int)(3 * dsymlen);
|
|
else
|
|
symlen -= (int)dsymlen;
|
|
}
|
|
|
|
if (delta < -(symlen - knum)) delta = -(symlen - knum);
|
|
if (delta > (symlen - knum)) delta = symlen - knum;
|
|
|
|
keydown = symlen + delta ;
|
|
keyup = symlen - delta;
|
|
|
|
if (progdefaults.QSK) {
|
|
kpre = (int)(progdefaults.CWpre * 8);
|
|
if (kpre > symlen) kpre = symlen;
|
|
|
|
if (progdefaults.CWnarrow) {
|
|
if (keydown - 2*knum < 0)
|
|
kpost = knum + (int)(progdefaults.CWpost * 8);
|
|
else
|
|
kpost = keydown - knum + (int)(progdefaults.CWpost * 8);
|
|
} else
|
|
kpost = keydown + (int)(progdefaults.CWpost * 8);
|
|
|
|
if (kpost < 0) kpost = 0;
|
|
} else {
|
|
kpre = 0;
|
|
kpost = 0;
|
|
}
|
|
|
|
if (firstelement) {
|
|
firstelement = false;
|
|
return;
|
|
}
|
|
|
|
if (currsym == 1) { // keydown
|
|
sample = 0;
|
|
if (lastsym == 1) {
|
|
for (i = 0; i < keydown; i++, sample++) {
|
|
outbuf[sample] = nco(freq);
|
|
qskbuf[sample] = qsknco();
|
|
}
|
|
duration = keydown;
|
|
} else {
|
|
if (carryover) {
|
|
for (int i = carryover; i < knum; i++, sample++)
|
|
outbuf[sample] = nco(freq) * keyshape[knum - i];
|
|
while (sample < kpre)
|
|
outbuf[sample++] = 0 * nco(freq);
|
|
} else
|
|
for (int i = 0; i < kpre; i++, sample++)
|
|
outbuf[sample] = 0 * nco(freq);
|
|
sample = 0;
|
|
for (int i = 0; i < kpre; i++, sample++) {
|
|
qskbuf[sample] = qsknco();
|
|
}
|
|
for (int i = 0; i < knum; i++, sample++) {
|
|
outbuf[sample] = nco(freq) * keyshape[i];
|
|
qskbuf[sample] = qsknco();
|
|
}
|
|
duration = kpre + knum;
|
|
}
|
|
carryover = 0;
|
|
}
|
|
else { // keyup
|
|
if (lastsym == 0) {
|
|
duration = keyup;
|
|
sample = 0;
|
|
if (carryover) {
|
|
for (int i = carryover; i < knum; i++, sample++)
|
|
outbuf[sample] = nco(freq) * keyshape[knum - i];
|
|
while (sample < duration)
|
|
outbuf[sample++] = 0 * nco(freq);
|
|
} else
|
|
while (sample < duration)
|
|
outbuf[sample++] = 0 * nco(freq);
|
|
carryover = 0;
|
|
|
|
qsample = 0;
|
|
if (q_carryover) {
|
|
for (int i = 0; i < q_carryover; i++, qsample++) {
|
|
qskbuf[qsample] = qsknco();
|
|
}
|
|
while (qsample < duration)
|
|
qskbuf[qsample++] = 0 * qsknco();
|
|
} else
|
|
while (qsample < duration)
|
|
qskbuf[qsample++] = 0 * qsknco();
|
|
if (q_carryover > duration)
|
|
q_carryover = duration - q_carryover;
|
|
else
|
|
q_carryover = 0;
|
|
|
|
} else { // last symbol = 1
|
|
duration = 2 * symbollen - kpre - knum;
|
|
carryover = 0;
|
|
sample = 0;
|
|
|
|
int next = keydown - knum;
|
|
if (progdefaults.CWnarrow)
|
|
next = keydown - 2*knum;
|
|
|
|
for (int i = 0; i < next; i++, sample++)
|
|
outbuf[sample] = nco(freq);
|
|
|
|
for (int i = 0; i < knum; i++, sample++) {
|
|
if (sample == duration) {
|
|
carryover = i;
|
|
break;
|
|
}
|
|
outbuf[sample] = nco(freq) * keyshape[knum - i];
|
|
}
|
|
while (sample < duration)
|
|
outbuf[sample++] = 0 * nco(freq);
|
|
|
|
q_carryover = 0;
|
|
qsample = 0;
|
|
|
|
for (int i = 0; i < kpost; i++, qsample++) {
|
|
if (qsample == duration) {
|
|
q_carryover = kpost - duration;
|
|
break;
|
|
}
|
|
qskbuf[qsample] = qsknco();
|
|
}
|
|
while (qsample < duration)
|
|
qskbuf[qsample++] = 0 * qsknco();
|
|
}
|
|
}
|
|
|
|
if (duration > 0) {
|
|
if (progdefaults.QSK)
|
|
ModulateStereo(outbuf, qskbuf, duration);
|
|
else
|
|
ModulateXmtr(outbuf, duration);
|
|
}
|
|
|
|
lastsym = currsym;
|
|
firstelement = false;
|
|
}
|
|
|
|
//=====================================================================
|
|
// send_ch()
|
|
// sends a morse character and the space afterwards
|
|
//=======================================================================
|
|
|
|
void cw::send_ch(int ch)
|
|
{
|
|
int code;
|
|
int chout = ch;
|
|
int flen;
|
|
|
|
sync_parameters();
|
|
// handle word space separately (7 dots spacing)
|
|
// last char already had 3 elements of inter-character spacing
|
|
|
|
if ((chout == ' ') || (chout == '\n')) {
|
|
firstelement = false;
|
|
if (progdefaults.CWusefarnsworth)
|
|
flen = 4 * fsymlen;
|
|
else
|
|
flen = 4 * symbollen;
|
|
while (flen - symbollen > 0) {
|
|
send_symbol(0, symbollen);
|
|
flen -= symbollen;
|
|
}
|
|
if (flen) send_symbol(0, flen);
|
|
put_echo_char(progdefaults.rx_lowercase ? tolower(ch) : ch);
|
|
return;
|
|
}
|
|
|
|
// convert character code to a morse representation
|
|
if ((chout < 256) && (chout >= 0)) {
|
|
code = tx_lookup(chout); //cw_tx_lookup(ch);
|
|
firstelement = true;
|
|
} else {
|
|
code = 0x04; // two extra dot spaces
|
|
firstelement = false;
|
|
}
|
|
|
|
// loop sending out binary bits of cw character
|
|
while (code > 1) {
|
|
send_symbol(code, symbollen);// & 1);
|
|
code = code >> 1;
|
|
}
|
|
if (progdefaults.CWusefarnsworth)
|
|
flen = fsymlen;
|
|
else
|
|
flen = symbollen;
|
|
while(flen - symbollen > 0) {
|
|
send_symbol(0, symbollen);
|
|
flen -= symbollen;
|
|
}
|
|
if (flen) send_symbol(0, flen);
|
|
|
|
FL_AWAKE();
|
|
|
|
if (ch != -1) {
|
|
string prtstr = tx_print(ch);
|
|
if (prtstr.length() == 1)
|
|
put_echo_char(progdefaults.rx_lowercase ? tolower(prtstr[0]) : prtstr[0]);
|
|
else
|
|
for (size_t n = 0; n < prtstr.length(); n++)
|
|
put_echo_char(progdefaults.rx_lowercase ? tolower(prtstr[n]) : prtstr[n], FTextBase::CTRL);
|
|
}
|
|
}
|
|
|
|
//=====================================================================
|
|
// cw_txprocess()
|
|
// Read characters from screen and send them out the sound card.
|
|
// This is called repeatedly from a thread during tx.
|
|
//=======================================================================
|
|
|
|
int cw::tx_process()
|
|
{
|
|
int c;
|
|
|
|
if (use_paren != progdefaults.CW_use_paren ||
|
|
prosigns != progdefaults.CW_prosigns) {
|
|
use_paren = progdefaults.CW_use_paren;
|
|
prosigns = progdefaults.CW_prosigns;
|
|
morse::init();
|
|
}
|
|
|
|
c = get_tx_char();
|
|
if (c == 0x03 || stopflag) {
|
|
send_symbol(0, symbollen);
|
|
stopflag = false;
|
|
return -1;
|
|
}
|
|
send_ch(c);
|
|
return 0;
|
|
}
|
|
|
|
void cw::incWPM()
|
|
{
|
|
if (usedefaultWPM) return;
|
|
if (progdefaults.CWspeed < progdefaults.CWupperlimit) {
|
|
progdefaults.CWspeed++;
|
|
sync_parameters();
|
|
set_CWwpm();
|
|
update_Status();
|
|
}
|
|
}
|
|
|
|
void cw::decWPM()
|
|
{
|
|
if (usedefaultWPM) return;
|
|
if (progdefaults.CWspeed > progdefaults.CWlowerlimit) {
|
|
progdefaults.CWspeed--;
|
|
set_CWwpm();
|
|
sync_parameters();
|
|
update_Status();
|
|
}
|
|
}
|
|
|
|
void cw::toggleWPM()
|
|
{
|
|
usedefaultWPM = !usedefaultWPM;
|
|
sync_parameters();
|
|
update_Status();
|
|
}
|
|
|