First working cw sidetone

Still needs better timing and volume
qcpfix
Phil Taylor 2023-02-17 13:58:02 +00:00
rodzic fd6d132fd3
commit 9eb89a9cb1
7 zmienionych plików z 525 dodań i 40 usunięć

Wyświetl plik

@ -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:

Wyświetl plik

@ -6,11 +6,10 @@
#include <QFont>
#include <QInputDialog>
#include <QMessageBox>
#include <QThread>
#include <math.h>
#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

Wyświetl plik

@ -192,11 +192,56 @@
<number>0</number>
</property>
<item>
<widget class="QLabel" name="spacerLabel">
<property name="text">
<string/>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="stopBtn">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Stop sending CW</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Local Sidetone Level</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="sidetoneLevelSlider">
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="sidetoneEnableChk">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
@ -214,6 +259,17 @@
</property>
<item>
<widget class="QPushButton" name="sendBtn">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Send</string>
</property>
@ -225,16 +281,6 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopBtn">
<property name="toolTip">
<string>Stop sending CW</string>
</property>
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="textToSendEdit">
<property name="sizePolicy">

254
cwsidetone.cpp 100644
Wyświetl plik

@ -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;i<morse.size();i++)
{
QChar c = morse.at(i);
if (c == '-')
{
buffer.append(generateData(dah,this->frequency));
} else if (c == '.')
{
buffer.append(generateData(dit,this->frequency));
} else // Space char
{
buffer.append(generateData(dah+dit,0));
}
if (i+1<morse.size())
{
buffer.append(generateData(dit,0));
}
else
{
buffer.append(generateData(dah,0)); // inter-character space
}
}
return;
}
QByteArray cwSidetone::generateData(qint64 len, qint64 freq)
{
QByteArray data;
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
const int channels = format.channels();
const int channelBytes = format.sampleSize() / 8;
#else
const int channels = format.channelCount();
const int channelBytes = format.bytesPerSample();
#endif
const int sampleRate = format.sampleRate();
qint64 length = (sampleRate * channels * channelBytes) * len / 100000;
const int sampleBytes = channels * channelBytes;
length -= length % sampleBytes;
Q_UNUSED(sampleBytes) // suppress warning in release builds
data.resize(length);
unsigned char *ptr = reinterpret_cast<unsigned char *>(data.data());
int sampleIndex = 0;
while (length) {
const qreal x = qSin(2 * M_PI * freq * qreal(sampleIndex % sampleRate) / sampleRate);
for (int i=0; i<channels; ++i) {
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
if (format.sampleSize() == 8 && format.sampleType() == QAudioFormat::UnSignedInt)
#else
if (format.sampleFormat() == QAudioFormat::UInt8)
#endif
{
const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255);
*reinterpret_cast<quint8*>(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<qint16>(x * 32767);
qToLittleEndian<qint16>(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);
}
}

62
cwsidetone.h 100644
Wyświetl plik

@ -0,0 +1,62 @@
#ifndef CWSIDETONE_H
#define CWSIDETONE_H
#include <QApplication>
#include <QAudioOutput>
#include <QMap>
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
#include <QAudioDeviceInfo>
#include <QAudioOutput>
#else
#include <QAudioDevice>
#include <QAudioSink>
#include <QMediaDevices>
#endif
#include <QtMath>
#include <QtEndian>
#include <QTimer>
#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

Wyświetl plik

@ -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);

Wyświetl plik

@ -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 \