diff --git a/cwsender.cpp b/cwsender.cpp index 87d91f2..bfbd024 100644 --- a/cwsender.cpp +++ b/cwsender.cpp @@ -1,6 +1,8 @@ #include "cwsender.h" #include "ui_cwsender.h" +#include "logcategories.h" + cwSender::cwSender(QWidget *parent) : QMainWindow(parent), ui(new Ui::cwSender) @@ -16,11 +18,43 @@ cwSender::cwSender(QWidget *parent) : ui->statusbar->setToolTipDuration(3000); this->setToolTipDuration(3000); connect(ui->textToSendEdit->lineEdit(), &QLineEdit::textEdited, this, &cwSender::textChanged); + + toneThread = new QThread(this); + toneThread->setObjectName("sidetone()"); + + tone = new cwSidetone(sidetoneLevel, ui->wpmSpin->value(),ui->pitchSpin->value(),ui->dashSpin->value(),this); + tone->moveToThread(toneThread); + toneThread->start(); + + connect(toneThread, &QThread::finished, + [=]() { tone->deleteLater(); }); + connect(this, &cwSender::sidetone, + [=](const QString& text) { tone->send(text); }); + connect(this, &cwSender::setKeySpeed, + [=](const unsigned char& wpm) { tone->setSpeed(wpm); }); + connect(this, &cwSender::setDashRatio, + [=](const unsigned char& ratio) { tone->setRatio(ratio); }); + connect(this, &cwSender::setPitch, + [=](const unsigned char& pitch) { tone->setFrequency(pitch); }); + connect(this, &cwSender::setLevel, + [=](const unsigned char& level) { tone->setLevel(level); }); + + connect( this, SIGNAL( pitchChanged(int) ), ui->pitchSpin, SLOT( setValue(int) ) ); + connect( this, SIGNAL( dashChanged(int) ), ui->dashSpin, SLOT( setValue(int) ) ); + connect( this, SIGNAL( wpmChanged(int) ), ui->wpmSpin, SLOT( setValue(int) ) ); } cwSender::~cwSender() { qDebug(logCW()) << "Running CW Sender destructor."; + + if (toneThread != Q_NULLPTR) { + toneThread->quit(); + toneThread->wait(); + toneThread = Q_NULLPTR; + tone = Q_NULLPTR; + } + delete ui; } @@ -33,32 +67,53 @@ void cwSender::showEvent(QShowEvent *event) void cwSender::handleKeySpeed(unsigned char wpm) { - if ((wpm >= 6) && (wpm <= 48)) + if (wpm != ui->wpmSpin->value() && (wpm >= ui->wpmSpin->minimum()) && (wpm <= ui->wpmSpin->maximum())) { ui->wpmSpin->blockSignals(true); - ui->wpmSpin->setValue(wpm); + emit wpmChanged((int)wpm); ui->wpmSpin->blockSignals(false); +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(tone, [=]() { + tone->setSpeed(wpm); + }, Qt::QueuedConnection); +#else + emit setKeySpeed(ratio); +#endif } } void cwSender::handleDashRatio(unsigned char ratio) { double calc = double(ratio/10); - if ((calc >= 2.8) && (ratio <= 4.5)) + if (calc != ui->dashSpin->value() && (calc >= ui->dashSpin->minimum()) && (ratio <= ui->dashSpin->maximum())) { ui->dashSpin->blockSignals(true); - ui->dashSpin->setValue(calc); + emit dashChanged(calc); ui->dashSpin->blockSignals(false); +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(tone, [=]() { + tone->setRatio(ratio); + }, Qt::QueuedConnection); +#else + emit setDashRatio(ratio); +#endif } } void cwSender::handlePitch(unsigned char pitch) { - quint16 cwPitch = round((((600.0 / 255.0) * pitch) + 300) / 5.0) * 5.0; - if (cwPitch >= 300 && cwPitch <= 900) + int cwPitch = round((((600.0 / 255.0) * pitch) + 300) / 5.0) * 5.0; + if (cwPitch != ui->pitchSpin->value() && cwPitch >= ui->pitchSpin->minimum() && cwPitch <= ui->pitchSpin->maximum()) { ui->pitchSpin->blockSignals(true); - ui->pitchSpin->setValue(cwPitch); + emit pitchChanged(cwPitch); ui->pitchSpin->blockSignals(false); +#if (QT_VERSION >= QT_VERSION_CHECK(5,10,0)) + QMetaObject::invokeMethod(tone, [=]() { + tone->setFrequency(pitch); + }, Qt::QueuedConnection); +#else + emit setPitch(tone); +#endif } } @@ -88,12 +143,18 @@ void cwSender::textChanged(QString text) { int toSend = text.mid(0, 30).size(); if (toSend > 0) { - emit sendCW(text.mid(0, 30)); ui->textToSendEdit->clearEditText(); - ui->transcriptText->moveCursor(QTextCursor::End); ui->transcriptText->insertPlainText(text.mid(0, 30).toUpper()); ui->transcriptText->moveCursor(QTextCursor::End); + + emit sendCW(text.mid(0, 30)); + emit sidetone(text.mid(0,30)); + + } + if( (currentMode != modeCW) && (currentMode != modeCW_R) ) + { + ui->statusbar->showMessage("Note: Mode needs to be set to CW or CW-R to send CW.", 3000); } } } @@ -103,7 +164,7 @@ void cwSender::on_sendBtn_clicked() if( (ui->textToSendEdit->currentText().length() > 0) && (ui->textToSendEdit->currentText().length() <= 30) ) { - emit sendCW(ui->textToSendEdit->currentText()); + QString text = ui->textToSendEdit->currentText(); ui->transcriptText->moveCursor(QTextCursor::End); ui->transcriptText->insertPlainText(ui->textToSendEdit->currentText().toUpper()+"\n"); @@ -122,10 +183,12 @@ void cwSender::on_sendBtn_clicked() ui->textToSendEdit->setFocus(); ui->statusbar->showMessage("Sending CW", 3000); + emit sendCW(text); + emit sidetone(text); } - if( (currentMode==modeCW) || (currentMode==modeCW_R) ) + + if( (currentMode != modeCW) && (currentMode != modeCW_R) ) { - } else { ui->statusbar->showMessage("Note: Mode needs to be set to CW or CW-R to send CW.", 3000); } } @@ -217,6 +280,18 @@ void cwSender::on_macro10btn_clicked() processMacroButton(10, ui->macro10btn); } +void cwSender::on_sidetoneEnableChk_clicked(bool clicked) +{ + ui->sidetoneLevelSlider->setEnabled(clicked); +} + +void cwSender::on_sidetoneLevelSlider_valueChanged(int val) +{ + sidetoneLevel = val; + emit setLevel(val); +} + + void cwSender::processMacroButton(int buttonNumber, QPushButton *btn) { if(ui->macroEditChk->isChecked()) @@ -250,14 +325,15 @@ void cwSender::runMacroButton(int buttonNumber) outText.replace("9", "N"); } - for (int i = 0; i < outText.size(); i = i + 30) { - emit sendCW(outText.mid(i,30)); - } - ui->transcriptText->moveCursor(QTextCursor::End); ui->transcriptText->insertPlainText(outText.toUpper()+"\n"); ui->transcriptText->moveCursor(QTextCursor::End); + for (int i = 0; i < outText.size(); i = i + 30) { + emit sendCW(outText.mid(i,30)); + emit sidetone(outText.mid(i,30)); + } + ui->textToSendEdit->setFocus(); @@ -332,6 +408,16 @@ bool cwSender::getSendImmediate() return ui->sendImmediateChk->isChecked(); } +bool cwSender::getSidetoneEnable() +{ + return ui->sidetoneEnableChk->isChecked(); +} + +int cwSender::getSidetoneLevel() +{ + return ui->sidetoneLevelSlider->value(); +} + void cwSender::setCutNumbers(bool val) { ui->cutNumbersChk->setChecked(val); @@ -342,6 +428,16 @@ void cwSender::setSendImmediate(bool val) ui->sendImmediateChk->setChecked(val); } +void cwSender::setSidetoneEnable(bool val) +{ + ui->sidetoneEnableChk->setChecked(val); +} + +void cwSender::setSidetoneLevel(int val) +{ + ui->sidetoneLevelSlider->setValue(val); +} + QStringList cwSender::getMacroText() { // This is for preference saving: diff --git a/cwsender.h b/cwsender.h index 56165e0..26d1c90 100644 --- a/cwsender.h +++ b/cwsender.h @@ -6,11 +6,10 @@ #include #include #include +#include #include +#include "cwsidetone.h" #include "wfviewtypes.h" -#include "logcategories.h" - - namespace Ui { class cwSender; @@ -27,8 +26,12 @@ public: void setMacroText(QStringList macros); void setCutNumbers(bool val); void setSendImmediate(bool val); + void setSidetoneEnable(bool val); + void setSidetoneLevel(int val); bool getCutNumbers(); bool getSendImmediate(); + bool getSidetoneEnable(); + int getSidetoneLevel(); signals: void sendCW(QString cwMessage); @@ -36,8 +39,13 @@ signals: void setKeySpeed(unsigned char wpm); void setDashRatio(unsigned char ratio); void setPitch(unsigned char pitch); + void setLevel(int level); void setBreakInMode(unsigned char b); void getCWSettings(); + void sidetone(QString text); + void pitchChanged(int val); + void dashChanged(int val); + void wpmChanged(int val); public slots: void handleKeySpeed(unsigned char wpm); @@ -86,16 +94,24 @@ private slots: void on_sequenceSpin_valueChanged(int arg1); + void on_sidetoneEnableChk_clicked(bool clicked); + + void on_sidetoneLevelSlider_valueChanged(int val); + private: Ui::cwSender *ui; QString macroText[11]; int sequenceNumber = 1; int lastSentPos = 0; mode_kind currentMode; + int sidetoneLevel=0; void processMacroButton(int buttonNumber, QPushButton *btn); void runMacroButton(int buttonNumber); void editMacroButton(int buttonNumber, QPushButton *btn); void setMacroButtonText(QString btnText, QPushButton *btn); + cwSidetone* tone=Q_NULLPTR; + QThread* toneThread = Q_NULLPTR; + }; #endif // CWSENDER_H diff --git a/cwsender.ui b/cwsender.ui index 8d72739..d2035bd 100644 --- a/cwsender.ui +++ b/cwsender.ui @@ -192,11 +192,56 @@ 0 - - - - - + + + + + + 100 + 0 + + + + + true + + + + Stop sending CW + + + false + + + Stop + + + + + + + Local Sidetone Level + + + + + + + 100 + + + Qt::Horizontal + + + + + + + Enable + + + + @@ -214,6 +259,17 @@ + + + 100 + 0 + + + + + true + + Send @@ -225,16 +281,6 @@ - - - - Stop sending CW - - - Stop - - - diff --git a/cwsidetone.cpp b/cwsidetone.cpp new file mode 100644 index 0000000..207e680 --- /dev/null +++ b/cwsidetone.cpp @@ -0,0 +1,254 @@ +#include "cwsidetone.h" + +#include "logcategories.h" +#include "qapplication.h" + +cwSidetone::cwSidetone(int level, int speed, int freq, double ratio, QWidget* parent) : + parent(parent), + volume(level), + speed(speed), + frequency(freq), + ratio(ratio) +{ + + /* + * Characters to match Icom table + * Unknown characters will return '?' + */ + cwTable.clear(); + cwTable['0'] = "-----"; + cwTable['1'] = ".----"; + cwTable['2'] = "..---"; + cwTable['3'] = "...--"; + cwTable['4'] = "....-"; + cwTable['5'] = "....."; + cwTable['6'] = "-...."; + cwTable['7'] = "--..."; + cwTable['8'] = "---.."; + cwTable['9'] = "----."; + + cwTable['A'] = ".-"; + cwTable['B'] = "-..."; + cwTable['C'] = "-.-."; + cwTable['D'] = "-.."; + cwTable['E'] = "."; + cwTable['F'] = "..-."; + cwTable['G'] = "--."; + cwTable['H'] = "...."; + cwTable['I'] = ".."; + cwTable['J'] = ".---"; + cwTable['K'] = "-.-"; + cwTable['L'] = ".-.."; + cwTable['M'] = "--"; + cwTable['N'] = "-."; + cwTable['O'] = "---"; + cwTable['P'] = ".--."; + cwTable['Q'] = "--.-"; + cwTable['R'] = ".-."; + cwTable['S'] = "..."; + cwTable['T'] = "-"; + cwTable['U'] = "..-"; + cwTable['V'] = "...-"; + cwTable['W'] = ".--"; + cwTable['X'] = "-..-"; + cwTable['Y'] = "-.--"; + cwTable['Z'] = "--.."; + + cwTable['/'] = "-..-."; + cwTable['?'] = "..--.."; + cwTable['.'] = ".-.-.-"; + cwTable['-'] = "-....-"; + cwTable[','] = "--..--"; + cwTable[':'] = "---..."; + cwTable['\''] = ".----."; + cwTable['('] = "-.--.-"; + cwTable[')'] = "-.--.-"; + cwTable['='] = "-...-"; + cwTable['+'] = ".-.-."; + cwTable['"'] = ".-..-."; + + cwTable[' '] = " "; + + init(); +} + +cwSidetone::~cwSidetone() +{ + output->stop(); + delete output; +} + +void cwSidetone::init() +{ + format.setSampleRate(44100); + format.setChannelCount(1); +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) + format.setCodec("audio/pcm"); + format.setSampleSize(16); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + QAudioDeviceInfo device(QAudioDeviceInfo::defaultOutputDevice()); +#else + format.setSampleFormat(QAudioFormat::Int16); + QAudioDevice device = QMediaDevices::defaultAudioOutput(); +#endif + + if (!device.isFormatSupported(format)) { + qWarning(logCW()) << "Default format not supported, using preferred"; + format = device.preferredFormat(); + } + +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) + output = new QAudioOutput(device,format); +#else + output = new QAudioSink(device,format); +#endif + output->setVolume((qreal)volume/100.0); +} + +void cwSidetone::send(QString text) +{ + text=text.simplified(); + buffer.clear(); + QString currentChar; + int pos = 0; + while (pos < text.size()) + { + QChar ch = text.at(pos).toUpper(); + if (ch == NULL) + { + currentChar = cwTable[' ']; + } + else if (this->cwTable.contains(ch)) + { + currentChar = cwTable[ch]; + } + else + { + currentChar=cwTable['?']; + } + generateMorse(currentChar); + pos++; + } + outputDevice = output->start(); + if (outputDevice) { + qint64 written = outputDevice->write(buffer); + while (written < buffer.size()) + { + written += outputDevice->write(buffer.data()+written, buffer.size() - written); + QApplication::processEvents(); + } + } + //qInfo(logCW()) << "Sending" << this->currentChar; + emit finished(); + return; +} + +void cwSidetone::generateMorse(QString morse) +{ + + int dit = int(double(SIDETONE_MULTIPLIER / this->speed * SIDETONE_MULTIPLIER)); + int dah = int(dit * this->ratio); + + for (int i=0;ifrequency)); + } else if (c == '.') + { + buffer.append(generateData(dit,this->frequency)); + } else // Space char + { + buffer.append(generateData(dah+dit,0)); + } + + if (i+1(data.data()); + int sampleIndex = 0; + + while (length) { + const qreal x = qSin(2 * M_PI * freq * qreal(sampleIndex % sampleRate) / sampleRate); + for (int i=0; i((1.0 + x) / 2 * 255); + *reinterpret_cast(ptr) = value; + +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) + } else if (format.sampleSize() == 16 && format.sampleType() == QAudioFormat::SignedInt) +#else + } else if (format.sampleFormat() == QAudioFormat::Int16) +#endif + { + qint16 value = static_cast(x * 32767); + qToLittleEndian(value, ptr); + } + + ptr += channelBytes; + length -= channelBytes; + } + ++sampleIndex; + } + + return data; +} + +void cwSidetone::setSpeed(unsigned char speed) +{ + this->speed = (int)speed; +} + +void cwSidetone::setFrequency(unsigned char frequency) +{ + this->frequency = round((((600.0 / 255.0) * frequency) + 300) / 5.0) * 5.0; +} + +void cwSidetone::setRatio(unsigned char ratio) +{ + this->ratio = (double)ratio/10.0; +} + +void cwSidetone::setLevel(int level) { + volume = level; + if (output != Q_NULLPTR) { + output->setVolume((qreal)level/100.0); + } + +} diff --git a/cwsidetone.h b/cwsidetone.h new file mode 100644 index 0000000..e2a416a --- /dev/null +++ b/cwsidetone.h @@ -0,0 +1,62 @@ +#ifndef CWSIDETONE_H +#define CWSIDETONE_H + +#include +#include +#include + +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) +#include +#include +#else +#include +#include +#include +#endif + +#include +#include +#include + +#define SIDETONE_MULTIPLIER 386.0 + +class cwSidetone : public QObject +{ + Q_OBJECT +public: + explicit cwSidetone(int level, int speed, int freq, double ratio, QWidget *parent = 0); + ~cwSidetone(); + +signals: + void finished(); +public slots: + void send(QString text); + void setSpeed(unsigned char speed); + void setFrequency(unsigned char frequency); + void setRatio(unsigned char ratio); + void setLevel(int level); +private: + void init(); + + void generateMorse(QString morse); + QByteArray generateData(qint64 len, qint64 freq); + QByteArray buffer; + QMap< QChar, QString> cwTable; + int position = 0; + int charSpace = 20; + int wordSpace = 60; + QWidget* parent; + int volume; + int speed; + int frequency; + double ratio; + QAudioFormat format; +#if (QT_VERSION < QT_VERSION_CHECK(6,0,0)) + QAudioOutput* output = Q_NULLPTR; +#else + QAudioSink* output = Q_NULLPTR; +#endif + QIODevice* outputDevice = Q_NULLPTR; +}; + +#endif // CWSIDETONE_H diff --git a/wfmain.cpp b/wfmain.cpp index e060f64..1a9e0b0 100644 --- a/wfmain.cpp +++ b/wfmain.cpp @@ -2439,6 +2439,8 @@ void wfmain::loadSettings() settings->beginGroup("Keyer"); cw->setCutNumbers(settings->value("CutNumbers", false).toBool()); cw->setSendImmediate(settings->value("SendImmediate", false).toBool()); + cw->setSidetoneEnable(settings->value("SidetoneEnabled", false).toBool()); + cw->setSidetoneLevel(settings->value("SidetoneLevel", 0).toInt()); int numMemories = settings->beginReadArray("macros"); if(numMemories==10) { @@ -2969,6 +2971,8 @@ void wfmain::saveSettings() settings->beginGroup("Keyer"); settings->setValue("CutNumbers", cw->getCutNumbers()); settings->setValue("SendImmediate", cw->getSendImmediate()); + settings->setValue("SidetoneEnabled", cw->getSidetoneEnable()); + settings->setValue("SidetoneLevel", cw->getSidetoneLevel()); QStringList macroList = cw->getMacroText(); if(macroList.length() == 10) { @@ -5695,11 +5699,15 @@ void wfmain::receiveMode(unsigned char mode, unsigned char filter) if (currentModeInfo.mk != (mode_kind)mode || currentModeInfo.filter != filter) { - removePeriodicRapidCmd(cmdGetCwPitch); + // Remove all "Slow" commands (they will be added later if needed) + removeSlowPeriodicCommand(cmdGetCwPitch); + removeSlowPeriodicCommand(cmdGetDashRatio); + removeSlowPeriodicCommand(cmdGetKeySpeed); removeSlowPeriodicCommand(cmdGetPassband); removeSlowPeriodicCommand(cmdGetTPBFInner); removeSlowPeriodicCommand(cmdGetTPBFOuter); + quint16 maxPassbandHz = 0; switch ((mode_kind)mode) { case modeFM: @@ -5714,8 +5722,9 @@ void wfmain::receiveMode(unsigned char mode, unsigned char filter) break; case modeCW: case modeCW_R: - insertPeriodicRapidCmdUnique(cmdGetCwPitch); - issueDelayedCommandUnique(cmdGetCwPitch); + insertSlowPeriodicCommand(cmdGetCwPitch,128); + insertSlowPeriodicCommand(cmdGetDashRatio,128); + insertSlowPeriodicCommand(cmdGetKeySpeed,128); maxPassbandHz = 3600; break; case modeAM: @@ -5770,9 +5779,9 @@ void wfmain::receiveMode(unsigned char mode, unsigned char filter) if (currentModeInfo.mk != modeFM) { - insertSlowPeriodicCommand(cmdGetPassband, 128); - insertSlowPeriodicCommand(cmdGetTPBFInner, 128); - insertSlowPeriodicCommand(cmdGetTPBFOuter, 128); + insertSlowPeriodicCommand(cmdGetPassband,128); + insertSlowPeriodicCommand(cmdGetTPBFInner,128); + insertSlowPeriodicCommand(cmdGetTPBFOuter,128); issueDelayedCommandUnique(cmdGetPassband); issueDelayedCommandUnique(cmdGetTPBFInner); issueDelayedCommandUnique(cmdGetTPBFOuter); diff --git a/wfview.pro b/wfview.pro index 704e091..01311e9 100644 --- a/wfview.pro +++ b/wfview.pro @@ -218,6 +218,7 @@ INCLUDEPATH += resampler SOURCES += main.cpp\ cwsender.cpp \ + cwsidetone.cpp \ loggingwindow.cpp \ wfmain.cpp \ commhandler.cpp \ @@ -256,6 +257,7 @@ HEADERS += wfmain.h \ colorprefs.h \ commhandler.h \ cwsender.h \ + cwsidetone.h \ loggingwindow.h \ prefs.h \ printhex.h \