diff --git a/doc/img/NFMMod_plugin.png b/doc/img/NFMMod_plugin.png index ef15850d4..a40e1473f 100644 Binary files a/doc/img/NFMMod_plugin.png and b/doc/img/NFMMod_plugin.png differ diff --git a/doc/img/NFMMod_plugin.xcf b/doc/img/NFMMod_plugin.xcf index ff9d5c343..a7a9d0e84 100644 Binary files a/doc/img/NFMMod_plugin.xcf and b/doc/img/NFMMod_plugin.xcf differ diff --git a/plugins/channeltx/modnfm/CMakeLists.txt b/plugins/channeltx/modnfm/CMakeLists.txt index a0907f12e..0c4901273 100644 --- a/plugins/channeltx/modnfm/CMakeLists.txt +++ b/plugins/channeltx/modnfm/CMakeLists.txt @@ -3,6 +3,7 @@ project(modnfm) set(modnfm_SOURCES nfmmod.cpp nfmmodbaseband.cpp + nfmmoddcs.cpp nfmmodsource.cpp nfmmodplugin.cpp nfmmodsettings.cpp @@ -12,6 +13,7 @@ set(modnfm_SOURCES set(modnfm_HEADERS nfmmod.h nfmmodbaseband.h + nfmmoddcs.h nfmmodsource.h nfmmodplugin.h nfmmodsettings.h diff --git a/plugins/channeltx/modnfm/nfmmod.cpp b/plugins/channeltx/modnfm/nfmmod.cpp index 4d362b6d0..510602c80 100644 --- a/plugins/channeltx/modnfm/nfmmod.cpp +++ b/plugins/channeltx/modnfm/nfmmod.cpp @@ -260,6 +260,9 @@ void NFMMod::applySettings(const NFMModSettings& settings, bool force) << " m_toneFrequency: " << settings.m_toneFrequency << " m_ctcssIndex: " << settings.m_ctcssIndex << " m_ctcssOn: " << settings.m_ctcssOn + << " m_dcsOn: " << settings.m_dcsOn + << " m_dcsCode: " << settings.m_dcsCode + << " m_dcsPositive: " << settings.m_dcsPositive << " m_channelMute: " << settings.m_channelMute << " m_playLoop: " << settings.m_playLoop << " m_modAFInput " << settings.m_modAFInput diff --git a/plugins/channeltx/modnfm/nfmmoddcs.cpp b/plugins/channeltx/modnfm/nfmmoddcs.cpp new file mode 100644 index 000000000..e54037063 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmoddcs.cpp @@ -0,0 +1,97 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Edouard Griffiths, F4EXB // +// // +// This program 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +// // +// Source: https://mmi-comm.tripod.com/dcs.html // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "nfmmoddcs.h" + +const float NFMModDCS::m_codeRate = 134.3; //!< bits per second + +NFMModDCS::NFMModDCS() +{ + reset(); +} + +NFMModDCS::~NFMModDCS() +{ +} + +void NFMModDCS::reset() +{ + m_step = 0; +} + +void NFMModDCS::setDCS(int code) +{ + unsigned int dcsCode = code < 0 ? 0 : code > 511 ? 511 : code; // trim invalid values + // Parity bits calculation: + unsigned int p[11]; + p[0] = std::bitset<32>(dcsCode & 0b10011111).count() % 2; // P0 = C0 + C1 + C2 + C3 + C4 + C7 (modulo two addition) + p[1] = (std::bitset<32>(dcsCode & 0b100111110).count()+1) % 2; // P1 = NOT ( C1 + C2 + C3 + C4 + C5 + C8 ) + p[2] = std::bitset<32>(dcsCode & 0b11100011).count() % 2; // P2 = C0 + C1 + C5 + C6 + C7 + p[3] = (std::bitset<32>(dcsCode & 0b111000110).count()+1) % 2; // P3 = NOT ( C1 + C2 + C6 + C7 + C8 ) + p[4] = (std::bitset<32>(dcsCode & 0b100010011).count()+1) % 2; // P4 = NOT ( C0 + C1 + C4 + C8 ) + p[5] = (std::bitset<32>(dcsCode & 0b10111001).count()+1) % 2; // P5 = NOT ( C0 + C3 + C4 + C5 + C7 ) + p[6] = std::bitset<32>(dcsCode & 0b111101101).count() % 2; // P6 = C0 + C2 + C3 + C5 + C6 + C7 + C8 + p[7] = std::bitset<32>(dcsCode & 0b111011010).count() % 2; // P7 = C1 + C3 + C4 + C6 + C7 + C8 + p[8] = std::bitset<32>(dcsCode & 0b110110100).count() % 2; // P8 = C2 + C4 + C5 + C7 + C8 + p[9] = (std::bitset<32>(dcsCode & 0b101101000).count()+1) % 2; // P9 = NOT ( C3 + C5 + C6 + C8 ) + p[10] = (std::bitset<32>(dcsCode & 0b1001111).count()+1) % 2; // P10 = NOT ( C0 + C1 + C2 + C3 + C6 ) + int dcsIndex = 0; + // code: + for (int i = 0; i < 9; i++, dcsIndex++) { + m_dcsWord[dcsIndex] = (dcsCode >> i) & 1; + } + // filler (0x04) + m_dcsWord[dcsIndex++] = 0; + m_dcsWord[dcsIndex++] = 0; + m_dcsWord[dcsIndex++] = 1; + // parity + for (int i = 0; i < 11; i++, dcsIndex++) { + m_dcsWord[dcsIndex] = p[i]; + } + + m_step = 0; +} + +void NFMModDCS::setPositive(bool positive) +{ + m_positive = positive; + m_step = 0; +} + +void NFMModDCS::setSampleRate(int sampleRate) +{ + m_bitPerStep = m_codeRate / sampleRate; + m_step = 0; +} + +int NFMModDCS::next() +{ + int dcsIndex = (int) m_step; + int carrier = (m_dcsWord[dcsIndex] == 1) ? (m_positive ? 1 : -1) : (m_positive ? -1 : 1); + + if (m_step + m_bitPerStep < 23) { + m_step += m_bitPerStep; + } else { + m_step = m_step + m_bitPerStep - 23; + } + + return carrier; +} diff --git a/plugins/channeltx/modnfm/nfmmoddcs.h b/plugins/channeltx/modnfm/nfmmoddcs.h new file mode 100644 index 000000000..10dae1346 --- /dev/null +++ b/plugins/channeltx/modnfm/nfmmoddcs.h @@ -0,0 +1,43 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Edouard Griffiths, F4EXB // +// // +// This program 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODNFM_NFMMODDCS_H_ +#define PLUGINS_CHANNELTX_MODNFM_NFMMODDCS_H_ + +class NFMModDCS +{ +public: + NFMModDCS(); + ~NFMModDCS(); + + void reset(); + void setDCS(int code); + void setPositive(bool positive); + void setSampleRate(int sampleRate); + int next(); //!< +1/-1 sample + +private: + int m_shift; //!< current frequency shift: -1 or 1 + int m_dcsWord[23]; //!< current DCS word in transmit order including parity and filler 11 + 3 + 9 + float m_step; + bool m_positive; + float m_bitPerStep; + static const float m_codeRate; +}; + + +#endif // PLUGINS_CHANNELTX_MODNFM_NFMMODDCS_H_ diff --git a/plugins/channeltx/modnfm/nfmmodgui.cpp b/plugins/channeltx/modnfm/nfmmodgui.cpp index 6b22a2589..d17da4c02 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.cpp +++ b/plugins/channeltx/modnfm/nfmmodgui.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "device/deviceuiset.h" #include "plugin/pluginapi.h" @@ -299,10 +300,35 @@ void NFMModGUI::on_ctcss_currentIndexChanged(int index) void NFMModGUI::on_ctcssOn_toggled(bool checked) { + ui->dcsOn->setEnabled(!checked); m_settings.m_ctcssOn = checked; applySettings(); } +void NFMModGUI::on_dcsOn_toggled(bool checked) +{ + ui->ctcssOn->setEnabled(!checked); + m_settings.m_dcsOn = checked; + applySettings(); +} + +void NFMModGUI::on_dcsCode_editingFinished() +{ + bool ok; + int dcsCode = ui->dcsCode->text().toInt(&ok, 8); + + if (ok) + { + m_settings.m_dcsCode = dcsCode; + applySettings(); + } +} + +void NFMModGUI::on_dcsPositive_toggled(bool checked) +{ + m_settings.m_dcsPositive = checked; + applySettings(); +} void NFMModGUI::configureFileName() { @@ -375,7 +401,8 @@ NFMModGUI::NFMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam m_audioSampleRate(-1), m_feedbackAudioSampleRate(-1), m_tickCount(0), - m_enableNavTime(false) + m_enableNavTime(false), + m_dcsCodeValidator(QRegExp("[0-7]{1,3}")) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose, true); @@ -431,6 +458,7 @@ NFMModGUI::NFMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSam ui->ctcss->addItem(QString("%1").arg((double) NFMModSettings::getCTCSSFreq(i), 0, 'f', 1)); } + ui->dcsCode->setValidator(&m_dcsCodeValidator); ui->cwKeyerGUI->setCWKeyer(m_nfmMod->getCWKeyer()); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); @@ -500,6 +528,11 @@ void NFMModGUI::displaySettings() ui->ctcssOn->setChecked(m_settings.m_ctcssOn); ui->ctcss->setCurrentIndex(m_settings.m_ctcssIndex); + ui->dcsOn->setChecked(m_settings.m_dcsOn); + ui->dcsOn->setEnabled(!m_settings.m_ctcssOn); + ui->dcsCode->setText(tr("%1").arg(m_settings.m_dcsCode, 3, 8, QLatin1Char('0'))); + ui->dcsPositive->setChecked(m_settings.m_dcsPositive); + ui->channelMute->setChecked(m_settings.m_channelMute); ui->playLoop->setChecked(m_settings.m_playLoop); diff --git a/plugins/channeltx/modnfm/nfmmodgui.h b/plugins/channeltx/modnfm/nfmmodgui.h index ae4afd841..0f89c78b6 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.h +++ b/plugins/channeltx/modnfm/nfmmodgui.h @@ -18,6 +18,8 @@ #ifndef PLUGINS_CHANNELTX_MODNFM_NFMMODGUI_H_ #define PLUGINS_CHANNELTX_MODNFM_NFMMODGUI_H_ +#include + #include "channel/channelgui.h" #include "dsp/channelmarker.h" #include "util/movingaverage.h" @@ -70,6 +72,7 @@ private: bool m_enableNavTime; NFMModSettings::NFMModInputAF m_modAFInput; MessageQueue m_inputMessageQueue; + QRegExpValidator m_dcsCodeValidator; explicit NFMModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent = 0); virtual ~NFMModGUI(); @@ -107,6 +110,9 @@ private slots: void on_ctcss_currentIndexChanged(int index); void on_ctcssOn_toggled(bool checked); + void on_dcsOn_toggled(bool checked); + void on_dcsCode_editingFinished(); + void on_dcsPositive_toggled(bool checked); void on_feedbackEnable_toggled(bool checked); void on_feedbackVolume_valueChanged(int value); diff --git a/plugins/channeltx/modnfm/nfmmodgui.ui b/plugins/channeltx/modnfm/nfmmodgui.ui index e1f491573..495c90b56 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.ui +++ b/plugins/channeltx/modnfm/nfmmodgui.ui @@ -673,6 +673,67 @@ + + + + DCS + + + + + + + Activate DCS + + + + + + + + + + + 40 + 0 + + + + + 40 + 16777215 + + + + Qt::ClickFocus + + + false + + + DCS code (octal) + + + 023 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + DCS code is positive (1 corresponds to positive frequency shift) + + + Qt::RightToLeft + + + + + + + diff --git a/plugins/channeltx/modnfm/nfmmodsettings.cpp b/plugins/channeltx/modnfm/nfmmodsettings.cpp index b8b245899..886488d33 100644 --- a/plugins/channeltx/modnfm/nfmmodsettings.cpp +++ b/plugins/channeltx/modnfm/nfmmodsettings.cpp @@ -62,6 +62,9 @@ void NFMModSettings::resetToDefaults() m_playLoop = false; m_ctcssOn = false; m_ctcssIndex = 0; + m_dcsOn = false; + m_dcsCode = 0023; + m_dcsPositive = false; m_rgbColor = QColor(255, 0, 0).rgb(); m_title = "NFM Modulator"; m_modAFInput = NFMModInputAF::NFMModInputNone; @@ -113,6 +116,9 @@ QByteArray NFMModSettings::serialize() const s.writeReal(21, m_feedbackVolumeFactor); s.writeBool(22, m_feedbackAudioEnable); s.writeS32(23, m_streamIndex); + s.writeBool(24, m_dcsOn); + s.writeS32(25, m_dcsCode); + s.writeBool(26, m_dcsPositive); return s.final(); } @@ -186,6 +192,10 @@ bool NFMModSettings::deserialize(const QByteArray& data) d.readReal(21, &m_feedbackVolumeFactor, 1.0); d.readBool(22, &m_feedbackAudioEnable, false); d.readS32(23, &m_streamIndex, 0); + d.readBool(24, &m_dcsOn, false); + d.readS32(25, &tmp, 0023); + m_dcsCode = tmp < 0 ? 0 : tmp > 511 ? 511 : tmp; + d.readBool(26, &m_dcsPositive, false); return true; } diff --git a/plugins/channeltx/modnfm/nfmmodsettings.h b/plugins/channeltx/modnfm/nfmmodsettings.h index 2f55f9f16..01c81812f 100644 --- a/plugins/channeltx/modnfm/nfmmodsettings.h +++ b/plugins/channeltx/modnfm/nfmmodsettings.h @@ -51,6 +51,9 @@ struct NFMModSettings bool m_playLoop; bool m_ctcssOn; int m_ctcssIndex; + bool m_dcsOn; + int m_dcsCode; + bool m_dcsPositive; quint32 m_rgbColor; QString m_title; NFMModInputAF m_modAFInput; diff --git a/plugins/channeltx/modnfm/nfmmodsource.cpp b/plugins/channeltx/modnfm/nfmmodsource.cpp index ebe4eb9db..0ab99950f 100644 --- a/plugins/channeltx/modnfm/nfmmodsource.cpp +++ b/plugins/channeltx/modnfm/nfmmodsource.cpp @@ -152,6 +152,8 @@ void NFMModSource::modulateSample() if (m_settings.m_ctcssOn) { t1 = (0.85f * m_bandpass.filter(t) + 0.15f * 0.625f * m_ctcssNco.next()) * 1.2f; + } else if (m_settings.m_dcsOn) { + t1 = (0.9f * m_bandpass.filter(t) + 0.1f * 0.625f * m_dcsMod.next()) * 1.2f; } else { t1 = m_bandpass.filter(t) * 1.2f; } @@ -344,6 +346,7 @@ void NFMModSource::applyAudioSampleRate(int sampleRate) m_bandpass.create(301, sampleRate, 300.0, m_settings.m_afBandwidth); m_toneNco.setFreq(m_settings.m_toneFrequency, sampleRate); m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(m_settings.m_ctcssIndex), sampleRate); + m_dcsMod.setSampleRate(sampleRate); m_cwKeyer.setSampleRate(sampleRate); m_cwKeyer.reset(); m_preemphasisFilter.configure(m_preemphasis*sampleRate); @@ -400,6 +403,14 @@ void NFMModSource::applySettings(const NFMModSettings& settings, bool force) m_ctcssNco.setFreq(NFMModSettings::getCTCSSFreq(settings.m_ctcssIndex), m_audioSampleRate); } + if ((settings.m_dcsCode != m_settings.m_dcsCode) || force) { + m_dcsMod.setDCS(settings.m_dcsCode); + } + + if ((settings.m_dcsPositive != m_settings.m_dcsPositive) || force) { + m_dcsMod.setPositive(settings.m_dcsPositive); + } + if ((settings.m_modAFInput != m_settings.m_modAFInput) || force) { if (settings.m_modAFInput == NFMModSettings::NFMModInputAudio) { diff --git a/plugins/channeltx/modnfm/nfmmodsource.h b/plugins/channeltx/modnfm/nfmmodsource.h index 73cf14e20..8f6a6a6c4 100644 --- a/plugins/channeltx/modnfm/nfmmodsource.h +++ b/plugins/channeltx/modnfm/nfmmodsource.h @@ -36,6 +36,7 @@ #include "audio/audiofifo.h" #include "nfmmodsettings.h" +#include "nfmmoddcs.h" class ChannelAPI; @@ -78,6 +79,7 @@ private: NCO m_carrierNco; NCOF m_toneNco; NCOF m_ctcssNco; + NFMModDCS m_dcsMod; float m_modPhasor; //!< baseband modulator phasor Complex m_modSample; diff --git a/plugins/channeltx/modnfm/readme.md b/plugins/channeltx/modnfm/readme.md index 3bf8009c6..3a317f1b5 100644 --- a/plugins/channeltx/modnfm/readme.md +++ b/plugins/channeltx/modnfm/readme.md @@ -248,3 +248,15 @@ This is the audio file play length in time units

23: Play file position slider

This slider can be used to randomly set the current position in the file when file play is in pause state (button 18.3) + +

24: DCS switch

+ +Checkbox to switch on the DCS (Digital Code Squelch) sub-audio modulation. + +

25: DCS code

+ +This is the DCS octal code (0..511 in decimal 0..777 in octal) + +

26: DCS positive modulation

+ +When checked the "1" bits are represented with positive shift frequency and "0" with negative shift. When unchecked (negative modulation) this is the opposite.