mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-25 13:48:42 -05:00
0cf14dfcc9
Fixed buffer sizes are used. Rx use s 3456 x 1st downsample rate x 5 audio frames of buffer space. On Windows this means that each chunk (periodSize()) delivered from the audio stream is our initial DSP processing chunk size, thus matching audio buffer latency exactly with WSJT-X's own front end latency. This should result in optimal resilience to high system loads that might starve the soundcard ADC of buffers to fill and case dropped audio frames. For Tx a buffer sufficient for 1 s of audio is used at present, on Windows the period size will be set to 1/40 of that which gives reasonably low latency and plenty of resilience to high system loads that might starve the soundcard DAC of audio frames to render. Note that a 1 s buffer will make the "Pwr" slider slow to respond, we may have to reduce the Tx audio buffer size if this is seen as a problem.
377 lines
12 KiB
C++
377 lines
12 KiB
C++
#include "Modulator.hpp"
|
|
#include <limits>
|
|
#include <qmath.h>
|
|
#include <QDateTime>
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
#include <QRandomGenerator>
|
|
#endif
|
|
#include <QDebug>
|
|
#include "widgets/mainwindow.h" // TODO: G4WJS - break this dependency
|
|
#include "Audio/soundout.h"
|
|
#include "commons.h"
|
|
|
|
#include "moc_Modulator.cpp"
|
|
|
|
extern float gran(); // Noise generator (for tests only)
|
|
|
|
#define RAMP_INCREMENT 64 // MUST be an integral factor of 2^16
|
|
|
|
#if defined (WSJT_SOFT_KEYING)
|
|
# define SOFT_KEYING WSJT_SOFT_KEYING
|
|
#else
|
|
# define SOFT_KEYING 1
|
|
#endif
|
|
|
|
double constexpr Modulator::m_twoPi;
|
|
|
|
// float wpm=20.0;
|
|
// unsigned m_nspd=1.2*48000.0/wpm;
|
|
// m_nspd=3072; //18.75 WPM
|
|
|
|
Modulator::Modulator (unsigned frameRate, double periodLengthInSeconds,
|
|
QObject * parent)
|
|
: AudioDevice {parent}
|
|
, m_quickClose {false}
|
|
, m_phi {0.0}
|
|
, m_toneSpacing {0.0}
|
|
, m_fSpread {0.0}
|
|
, m_period {periodLengthInSeconds}
|
|
, m_frameRate {frameRate}
|
|
, m_state {Idle}
|
|
, m_tuning {false}
|
|
, m_cwLevel {false}
|
|
, m_j0 {-1}
|
|
, m_toneFrequency0 {1500.0}
|
|
{
|
|
}
|
|
|
|
void Modulator::start (QString mode, unsigned symbolsLength, double framesPerSymbol,
|
|
double frequency, double toneSpacing,
|
|
SoundOutput * stream, Channel channel,
|
|
bool synchronize, bool fastMode, double dBSNR, double TRperiod)
|
|
{
|
|
// qDebug () << "mode:" << mode << "symbolsLength:" << symbolsLength << "framesPerSymbol:" << framesPerSymbol << "frequency:" << frequency << "toneSpacing:" << toneSpacing << "channel:" << channel << "synchronize:" << synchronize << "fastMode:" << fastMode << "dBSNR:" << dBSNR << "TRperiod:" << TRperiod;
|
|
Q_ASSERT (stream);
|
|
// Time according to this computer which becomes our base time
|
|
qint64 ms0 = QDateTime::currentMSecsSinceEpoch() % 86400000;
|
|
unsigned mstr = ms0 % int(1000.0*m_period); // ms into the nominal Tx start time
|
|
|
|
if(m_state != Idle) stop();
|
|
m_quickClose = false;
|
|
m_symbolsLength = symbolsLength;
|
|
m_isym0 = std::numeric_limits<unsigned>::max (); // big number
|
|
m_frequency0 = 0.;
|
|
m_phi = 0.;
|
|
m_addNoise = dBSNR < 0.;
|
|
m_nsps = framesPerSymbol;
|
|
m_frequency = frequency;
|
|
m_amp = std::numeric_limits<qint16>::max ();
|
|
m_toneSpacing = toneSpacing;
|
|
m_bFastMode=fastMode;
|
|
m_TRperiod=TRperiod;
|
|
unsigned delay_ms=1000;
|
|
if(mode=="FT8" or (mode=="FST4" and m_nsps==720)) delay_ms=500; //FT8, FST4-15
|
|
if(mode=="FT4") delay_ms=300; //FT4
|
|
|
|
// noise generator parameters
|
|
if (m_addNoise) {
|
|
m_snr = qPow (10.0, 0.05 * (dBSNR - 6.0));
|
|
m_fac = 3000.0;
|
|
if (m_snr > 1.0) m_fac = 3000.0 / m_snr;
|
|
}
|
|
|
|
m_silentFrames = 0;
|
|
m_ic=0;
|
|
if (!m_tuning && !m_bFastMode)
|
|
{
|
|
// calculate number of silent frames to send, so that audio will
|
|
// start at the nominal time "delay_ms" into the Tx sequence.
|
|
if (synchronize)
|
|
{
|
|
if(delay_ms > mstr) m_silentFrames = (delay_ms - mstr) * m_frameRate / 1000;
|
|
}
|
|
|
|
// adjust for late starts
|
|
if(!m_silentFrames && mstr >= delay_ms)
|
|
{
|
|
m_ic = (mstr - delay_ms) * m_frameRate / 1000;
|
|
}
|
|
}
|
|
|
|
initialize (QIODevice::ReadOnly, channel);
|
|
Q_EMIT stateChanged ((m_state = (synchronize && m_silentFrames) ?
|
|
Synchronizing : Active));
|
|
|
|
// qDebug() << "delay_ms:" << delay_ms << "mstr:" << mstr << "m_silentFrames:" << m_silentFrames << "m_ic:" << m_ic << "m_state:" << m_state;
|
|
|
|
m_stream = stream;
|
|
if (m_stream) m_stream->restart (this);
|
|
}
|
|
|
|
void Modulator::tune (bool newState)
|
|
{
|
|
m_tuning = newState;
|
|
if (!m_tuning) stop (true);
|
|
}
|
|
|
|
void Modulator::stop (bool quick)
|
|
{
|
|
m_quickClose = quick;
|
|
close ();
|
|
}
|
|
|
|
void Modulator::close ()
|
|
{
|
|
if (m_stream)
|
|
{
|
|
if (m_quickClose)
|
|
{
|
|
m_stream->reset ();
|
|
}
|
|
else
|
|
{
|
|
m_stream->stop ();
|
|
}
|
|
}
|
|
if (m_state != Idle)
|
|
{
|
|
Q_EMIT stateChanged ((m_state = Idle));
|
|
}
|
|
AudioDevice::close ();
|
|
}
|
|
|
|
qint64 Modulator::readData (char * data, qint64 maxSize)
|
|
{
|
|
//qDebug () << "readData: maxSize:" << maxSize;
|
|
|
|
double toneFrequency=1500.0;
|
|
if(m_nsps==6) {
|
|
toneFrequency=1000.0;
|
|
m_frequency=1000.0;
|
|
m_frequency0=1000.0;
|
|
}
|
|
if(maxSize==0) return 0;
|
|
Q_ASSERT (!(maxSize % qint64 (bytesPerFrame ()))); // no torn frames
|
|
Q_ASSERT (isOpen ());
|
|
|
|
qint64 numFrames (maxSize / bytesPerFrame ());
|
|
qint16 * samples (reinterpret_cast<qint16 *> (data));
|
|
qint16 * end (samples + numFrames * (bytesPerFrame () / sizeof (qint16)));
|
|
qint64 framesGenerated (0);
|
|
|
|
// if(m_ic==0) qDebug() << "aa" << 0.001*(QDateTime::currentMSecsSinceEpoch() % qint64(1000*m_TRperiod))
|
|
// << m_state << m_TRperiod << m_silentFrames << m_ic << foxcom_.wave[m_ic];
|
|
|
|
switch (m_state)
|
|
{
|
|
case Synchronizing:
|
|
{
|
|
if (m_silentFrames) { // send silence up to end of start delay
|
|
framesGenerated = qMin (m_silentFrames, numFrames);
|
|
do
|
|
{
|
|
samples = load (0, samples); // silence
|
|
} while (--m_silentFrames && samples != end);
|
|
if (!m_silentFrames)
|
|
{
|
|
Q_EMIT stateChanged ((m_state = Active));
|
|
}
|
|
}
|
|
|
|
m_cwLevel = false;
|
|
m_ramp = 0; // prepare for CW wave shaping
|
|
}
|
|
// fall through
|
|
|
|
case Active:
|
|
{
|
|
unsigned int isym=0;
|
|
|
|
if(!m_tuning) isym=m_ic/(4.0*m_nsps); // Actual fsample=48000
|
|
bool slowCwId=((isym >= m_symbolsLength) && (icw[0] > 0)) && (!m_bFastMode);
|
|
if(m_TRperiod==3.0) slowCwId=false;
|
|
bool fastCwId=false;
|
|
static bool bCwId=false;
|
|
qint64 ms = QDateTime::currentMSecsSinceEpoch();
|
|
float tsec=0.001*(ms % int(1000*m_TRperiod));
|
|
if(m_bFastMode and (icw[0]>0) and (tsec > (m_TRperiod-5.0))) fastCwId=true;
|
|
if(!m_bFastMode) m_nspd=2560; // 22.5 WPM
|
|
|
|
// qDebug() << "Mod A" << m_ic << isym << tsec;
|
|
|
|
if(slowCwId or fastCwId) { // Transmit CW ID?
|
|
m_dphi = m_twoPi*m_frequency/m_frameRate;
|
|
if(m_bFastMode and !bCwId) {
|
|
m_frequency=1500; // Set params for CW ID
|
|
m_dphi = m_twoPi*m_frequency/m_frameRate;
|
|
m_symbolsLength=126;
|
|
m_nsps=4096.0*12000.0/11025.0;
|
|
m_ic=2246949;
|
|
m_nspd=2560; // 22.5 WPM
|
|
if(icw[0]*m_nspd/48000.0 > 4.0) m_nspd=4.0*48000.0/icw[0]; //Faster CW for long calls
|
|
}
|
|
bCwId=true;
|
|
unsigned ic0 = m_symbolsLength * 4 * m_nsps;
|
|
unsigned j(0);
|
|
|
|
while (samples != end) {
|
|
j = (m_ic - ic0)/m_nspd + 1; // symbol of this sample
|
|
bool level {bool (icw[j])};
|
|
m_phi += m_dphi;
|
|
if (m_phi > m_twoPi) m_phi -= m_twoPi;
|
|
qint16 sample=0;
|
|
float amp=32767.0;
|
|
float x=0;
|
|
if(m_ramp!=0) {
|
|
x=qSin(float(m_phi));
|
|
if(SOFT_KEYING) {
|
|
amp=qAbs(qint32(m_ramp));
|
|
if(amp>32767.0) amp=32767.0;
|
|
}
|
|
sample=round(amp*x);
|
|
}
|
|
if(m_bFastMode) {
|
|
sample=0;
|
|
if(level) sample=32767.0*x;
|
|
}
|
|
if (int (j) <= icw[0] && j < NUM_CW_SYMBOLS) { // stop condition
|
|
samples = load (postProcessSample (sample), samples);
|
|
++framesGenerated;
|
|
++m_ic;
|
|
} else {
|
|
Q_EMIT stateChanged ((m_state = Idle));
|
|
return framesGenerated * bytesPerFrame ();
|
|
}
|
|
|
|
// adjust ramp
|
|
if ((m_ramp != 0 && m_ramp != std::numeric_limits<qint16>::min ()) || level != m_cwLevel) {
|
|
// either ramp has terminated at max/min or direction has changed
|
|
m_ramp += RAMP_INCREMENT; // ramp
|
|
}
|
|
m_cwLevel = level;
|
|
}
|
|
return framesGenerated * bytesPerFrame ();
|
|
} else {
|
|
bCwId=false;
|
|
} //End of code for CW ID
|
|
|
|
double const baud (12000.0 / m_nsps);
|
|
// fade out parameters (no fade out for tuning)
|
|
unsigned int i0,i1;
|
|
if(m_tuning) {
|
|
i1 = i0 = (m_bFastMode ? 999999 : 9999) * m_nsps;
|
|
} else {
|
|
i0=(m_symbolsLength - 0.017) * 4.0 * m_nsps;
|
|
i1= m_symbolsLength * 4.0 * m_nsps;
|
|
}
|
|
if(m_bFastMode and !m_tuning) {
|
|
i1=m_TRperiod*48000.0 - 24000.0;
|
|
i0=i1-816;
|
|
}
|
|
|
|
qint16 sample;
|
|
|
|
while (samples != end && m_ic <= i1) {
|
|
isym=0;
|
|
if(!m_tuning and m_TRperiod!=3.0) isym=m_ic/(4.0*m_nsps); //Actual fsample=48000
|
|
if(m_bFastMode) isym=isym%m_symbolsLength;
|
|
if (isym != m_isym0 || m_frequency != m_frequency0) {
|
|
if(itone[0]>=100) {
|
|
m_toneFrequency0=itone[0];
|
|
} else {
|
|
if(m_toneSpacing==0.0) {
|
|
m_toneFrequency0=m_frequency + itone[isym]*baud;
|
|
} else {
|
|
m_toneFrequency0=m_frequency + itone[isym]*m_toneSpacing;
|
|
}
|
|
}
|
|
m_dphi = m_twoPi * m_toneFrequency0 / m_frameRate;
|
|
m_isym0 = isym;
|
|
m_frequency0 = m_frequency; //???
|
|
}
|
|
|
|
int j=m_ic/480;
|
|
if(m_fSpread>0.0 and j!=m_j0) {
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
float x1=QRandomGenerator::global ()->generateDouble ();
|
|
float x2=QRandomGenerator::global ()->generateDouble ();
|
|
#else
|
|
float x1=(float)qrand()/RAND_MAX;
|
|
float x2=(float)qrand()/RAND_MAX;
|
|
#endif
|
|
toneFrequency = m_toneFrequency0 + 0.5*m_fSpread*(x1+x2-1.0);
|
|
m_dphi = m_twoPi * toneFrequency / m_frameRate;
|
|
m_j0=j;
|
|
}
|
|
|
|
m_phi += m_dphi;
|
|
if (m_phi > m_twoPi) m_phi -= m_twoPi;
|
|
if (m_ic > i0) m_amp = 0.98 * m_amp;
|
|
if (m_ic > i1) m_amp = 0.0;
|
|
|
|
sample=qRound(m_amp*qSin(m_phi));
|
|
|
|
//Here's where we transmit from a precomputed wave[] array:
|
|
if(!m_tuning and (m_toneSpacing < 0)) {
|
|
m_amp=32767.0;
|
|
sample=qRound(m_amp*foxcom_.wave[m_ic]);
|
|
}
|
|
/*
|
|
if((m_ic<1000 or (4*m_symbolsLength*m_nsps - m_ic) < 1000) and (m_ic%10)==0) {
|
|
qDebug() << "cc" << QDateTime::currentDateTimeUtc().toString("hh:mm:ss.zzz") << m_ic << sample;
|
|
}
|
|
*/
|
|
samples = load(postProcessSample(sample), samples);
|
|
++framesGenerated;
|
|
++m_ic;
|
|
}
|
|
|
|
// qDebug() << "dd" << QDateTime::currentDateTimeUtc().toString("hh:mm:ss.zzz")
|
|
// << m_ic << i1 << foxcom_.wave[m_ic] << framesGenerated;
|
|
|
|
if (m_amp == 0.0) { // TODO G4WJS: compare double with zero might not be wise
|
|
if (icw[0] == 0) {
|
|
// no CW ID to send
|
|
Q_EMIT stateChanged ((m_state = Idle));
|
|
return framesGenerated * bytesPerFrame ();
|
|
}
|
|
m_phi = 0.0;
|
|
}
|
|
|
|
m_frequency0 = m_frequency;
|
|
// done for this chunk - continue on next call
|
|
|
|
// qDebug() << "Mod B" << m_ic << i1 << 0.001*(QDateTime::currentMSecsSinceEpoch() % (1000*m_TRperiod));
|
|
|
|
while (samples != end) // pad block with silence
|
|
{
|
|
samples = load (0, samples);
|
|
++framesGenerated;
|
|
}
|
|
return framesGenerated * bytesPerFrame ();
|
|
}
|
|
// fall through
|
|
|
|
case Idle:
|
|
break;
|
|
}
|
|
|
|
Q_ASSERT (Idle == m_state);
|
|
return 0;
|
|
}
|
|
|
|
qint16 Modulator::postProcessSample (qint16 sample) const
|
|
{
|
|
if (m_addNoise) { // Test frame, we'll add noise
|
|
qint32 s = m_fac * (gran () + sample * m_snr / 32768.0);
|
|
if (s > std::numeric_limits<qint16>::max ()) {
|
|
s = std::numeric_limits<qint16>::max ();
|
|
}
|
|
if (s < std::numeric_limits<qint16>::min ()) {
|
|
s = std::numeric_limits<qint16>::min ();
|
|
}
|
|
sample = s;
|
|
}
|
|
return sample;
|
|
}
|