/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2019 Edouard Griffiths, F4EXB // // Copyright (C) 2023 Jon Beniston, M7RCE // // // // 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 . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include "dsp/basebandsamplesink.h" #include "dsp/datafifo.h" #include "rttymod.h" #include "rttymodsource.h" #include "util/messagequeue.h" #include "maincore.h" RttyModSource::RttyModSource() : m_channelSampleRate(48000), m_channelFrequencyOffset(0), m_spectrumRate(2000), m_fmPhase(0.0), m_spectrumSink(nullptr), m_specSampleBufferIndex(0), m_magsq(0.0), m_levelCalcCount(0), m_peakLevel(0.0f), m_levelSum(0.0f), m_byteIdx(0), m_bitIdx(0), m_bitCount(0) { m_bits.append(0); m_lowpass.create(301, m_channelSampleRate, 400.0 / 2.0); m_pulseShape.create(0.5, 6, m_channelSampleRate / 45.45, true); m_demodBuffer.resize(1<<12); m_demodBufferFill = 0; m_specSampleBuffer.resize(m_specSampleBufferSize); m_interpolatorDistanceRemain = 0; m_interpolatorConsumed = false; m_interpolatorDistance = (Real)m_channelSampleRate / (Real)m_spectrumRate; m_interpolator.create(48, m_spectrumRate, m_spectrumRate / 2.2, 3.0); applySettings(m_settings, true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); } RttyModSource::~RttyModSource() { } void RttyModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) { std::for_each( begin, begin + nbSamples, [this](Sample& s) { pullOne(s); } ); } void RttyModSource::pullOne(Sample& sample) { if (m_settings.m_channelMute) { sample.m_real = 0.0f; sample.m_imag = 0.0f; return; } // Calculate next sample modulateSample(); // Shift to carrier frequency Complex ci = m_modSample; ci *= m_carrierNco.nextIQ(); // Calculate power double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); m_movingAverage(magsq); m_magsq = m_movingAverage.asDouble(); // Convert from float to fixed point sample.m_real = (FixReal) (ci.real() * SDR_TX_SCALEF); sample.m_imag = (FixReal) (ci.imag() * SDR_TX_SCALEF); } void RttyModSource::sampleToSpectrum(Complex sample) { if (m_spectrumSink) { Complex out; if (m_interpolator.decimate(&m_interpolatorDistanceRemain, sample, &out)) { m_interpolatorDistanceRemain += m_interpolatorDistance; Real r = std::real(out) * SDR_TX_SCALEF; Real i = std::imag(out) * SDR_TX_SCALEF; m_specSampleBuffer[m_specSampleBufferIndex++] = Sample(r, i); if (m_specSampleBufferIndex == m_specSampleBufferSize) { m_spectrumSink->feed(m_specSampleBuffer.begin(), m_specSampleBuffer.end(), false); m_specSampleBufferIndex = 0; } } } } void RttyModSource::modulateSample() { Real mod; if (m_sampleIdx == 0) { if (m_bitCount == 0) { if (!m_textToTransmit.isEmpty()) { // Encode a character at a time, so we get a TxReport after each character QString s = m_textToTransmit.left(1); m_textToTransmit = m_textToTransmit.mid(1); encodeText(s); } else { // Transmit "diddle" encodeText(">"); } initTX(); } m_bit = getBit(); } m_sampleIdx++; if (m_sampleIdx >= m_samplesPerSymbol) { m_sampleIdx = 0; } // FSK if (m_settings.m_pulseShaping) { if (m_sampleIdx == 1) { mod = m_pulseShape.filter(m_bit ? 1.0f : -1.0f); } else { mod = m_pulseShape.filter(0.0f); } } else { mod = m_bit ? 1.0f : -1.0f; } // FM m_fmPhase += m_phaseSensitivity * mod * (m_settings.m_spaceHigh ? -1.0f : 1.0f); // Keep phase in range -pi,pi if (m_fmPhase > M_PI) { m_fmPhase -= 2.0f * M_PI; } else if (m_fmPhase < -M_PI) { m_fmPhase += 2.0f * M_PI; } if (!m_settings.m_rfNoise) { m_modSample.real(m_linearGain * cos(m_fmPhase)); m_modSample.imag(m_linearGain * sin(m_fmPhase)); } else { // Noise to test filter frequency response m_modSample.real(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); m_modSample.imag(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); } // Apply low pass filter to limit RF BW m_modSample = m_lowpass.filter(m_modSample); // Display in spectrum analyser sampleToSpectrum(m_modSample); Real s = std::real(m_modSample); calculateLevel(s); // Send to demod analyser m_demodBuffer[m_demodBufferFill] = mod * std::numeric_limits::max(); ++m_demodBufferFill; if (m_demodBufferFill >= m_demodBuffer.size()) { QList dataPipes; MainCore::instance()->getDataPipes().getDataPipes(m_channel, "demod", dataPipes); if (dataPipes.size() > 0) { QList::iterator it = dataPipes.begin(); for (; it != dataPipes.end(); ++it) { DataFifo *fifo = qobject_cast((*it)->m_element); if (fifo) { fifo->write((quint8*) &m_demodBuffer[0], m_demodBuffer.size() * sizeof(qint16), DataFifo::DataTypeI16); } } } m_demodBufferFill = 0; } } void RttyModSource::calculateLevel(Real& sample) { if (m_levelCalcCount < m_levelNbSamples) { m_peakLevel = std::max(std::fabs(m_peakLevel), sample); m_levelSum += sample * sample; m_levelCalcCount++; } else { m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); m_peakLevelOut = m_peakLevel; m_peakLevel = 0.0f; m_levelSum = 0.0f; m_levelCalcCount = 0; } } void RttyModSource::applySettings(const RttyModSettings& settings, bool force) { if ((settings.m_baud != m_settings.m_baud) || force) { m_samplesPerSymbol = m_channelSampleRate / settings.m_baud; qDebug() << "m_samplesPerSymbol: " << m_samplesPerSymbol << " (" << m_channelSampleRate << "/" << settings.m_baud << ")"; } if ((settings.m_lpfTaps != m_settings.m_lpfTaps) || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { qDebug() << "RttyModSource::applySettings: Creating new lpf with taps " << settings.m_lpfTaps << " rfBW " << settings.m_rfBandwidth; m_lowpass.create(settings.m_lpfTaps, m_channelSampleRate, settings.m_rfBandwidth / 2.0); } if ((settings.m_beta != m_settings.m_beta) || (settings.m_symbolSpan != m_settings.m_symbolSpan) || (settings.m_baud != m_settings.m_baud) || force) { qDebug() << "RttyModSource::applySettings: Recreating pulse shaping filter: " << " beta: " << settings.m_beta << " symbolSpan: " << settings.m_symbolSpan << " channelSampleRate:" << m_channelSampleRate << " baud:" << settings.m_baud; m_pulseShape.create(settings.m_beta, settings.m_symbolSpan, m_channelSampleRate/settings.m_baud, true); } if ((settings.m_characterSet != m_settings.m_characterSet) || force) { m_rttyEncoder.setCharacterSet(settings.m_characterSet); } if ((settings.m_unshiftOnSpace != m_settings.m_unshiftOnSpace) || force) { m_rttyEncoder.setUnshiftOnSpace(settings.m_unshiftOnSpace); } if ((settings.m_msbFirst != m_settings.m_msbFirst) || force) { m_rttyEncoder.setMsbFirst(settings.m_msbFirst); } m_settings = settings; // Precalculate FM sensensity and linear gain to save doing it in the loop m_phaseSensitivity = 2.0f * M_PI * (m_settings.m_frequencyShift/2.0f) / (double)m_channelSampleRate; m_linearGain = powf(10.0f, m_settings.m_gain/20.0f); } void RttyModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) { qDebug() << "RttyModSource::applyChannelSettings:" << " channelSampleRate: " << channelSampleRate << " channelFrequencyOffset: " << channelFrequencyOffset << " rfBandwidth: " << m_settings.m_rfBandwidth; if ((channelFrequencyOffset != m_channelFrequencyOffset) || (channelSampleRate != m_channelSampleRate) || force) { m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); } if ((m_channelSampleRate != channelSampleRate) || force) { qDebug() << "RttyModSource::applyChannelSettings: Recreating filters"; m_lowpass.create(m_settings.m_lpfTaps, channelSampleRate, m_settings.m_rfBandwidth / 2.0); qDebug() << "RttyModSource::applyChannelSettings: Recreating bandpass filter: " << " channelSampleRate:" << channelSampleRate; qDebug() << "RttyModSource::applyChannelSettings: Recreating pulse shaping filter: " << " beta: " << m_settings.m_beta << " symbolSpan: " << m_settings.m_symbolSpan << " channelSampleRate:" << m_channelSampleRate << " baud:" << m_settings.m_baud; m_pulseShape.create(m_settings.m_beta, m_settings.m_symbolSpan, channelSampleRate/m_settings.m_baud, true); } if ((m_channelSampleRate != channelSampleRate) || force) { m_interpolatorDistanceRemain = 0; m_interpolatorConsumed = false; m_interpolatorDistance = (Real) channelSampleRate / (Real) m_spectrumRate; m_interpolator.create(48, m_spectrumRate, m_spectrumRate / 2.2, 3.0); } m_channelSampleRate = channelSampleRate; m_channelFrequencyOffset = channelFrequencyOffset; m_samplesPerSymbol = m_channelSampleRate / m_settings.m_baud; qDebug() << "m_samplesPerSymbol: " << m_samplesPerSymbol << " (" << m_channelSampleRate << "/" << m_settings.m_baud << ")"; // Precalculate FM sensensity to save doing it in the loop m_phaseSensitivity = 2.0f * M_PI * (m_settings.m_frequencyShift/2.0f) / (double)m_channelSampleRate; QList pipes; MainCore::instance()->getMessagePipes().getMessagePipes(m_channel, "reportdemod", pipes); if (pipes.size() > 0) { for (const auto& pipe : pipes) { MessageQueue* messageQueue = qobject_cast(pipe->m_element); MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create(m_channel, m_channelSampleRate); messageQueue->push(msg); } } } int RttyModSource::getBit() { int bit; if (m_bitCount > 0) { bit = (m_bits[m_byteIdx] >> m_bitIdx) & 1; m_bitIdx++; m_bitCount--; if (m_bitIdx == 8) { m_byteIdx++; m_bitIdx = 0; } } else { qDebug() << "RttyModSource::getBit: Called when empty"; bit = 1; } return bit; } void RttyModSource::addBit(int bit) { m_bits[m_byteIdx] |= bit << m_bitIdx; m_bitIdx++; m_bitCount++; m_bitCountTotal++; if (m_bitIdx == 8) { m_byteIdx++; if (m_bits.size() <= m_byteIdx) { m_bits.append(0); } m_bitIdx = 0; } } void RttyModSource::initTX() { m_byteIdx = 0; m_bitIdx = 0; m_bitCount = m_bitCountTotal; // Reset to allow retransmission m_bit = 0; } void RttyModSource::addTXText(QString text) { int count = m_settings.m_repeat ? m_settings.m_repeatCount : 1; for (int i = 0; i < count; i++) { QString s = text; if (m_settings.m_prefixCRLF) { s.prepend("\r\r\n>"); // '>' switches to letters } if (m_settings.m_postfixCRLF) { s.append("\r\r\n"); } m_textToTransmit.append(s); } } void RttyModSource::encodeText(const QString& text) { // RTTY encoding m_byteIdx = 0; m_bitIdx = 0; m_bitCount = 0; m_bitCountTotal = 0; for (int i = 0; i < m_bits.size(); i++) { m_bits[i] = 0; } QString s = text.toUpper(); // RTTY only supports upper case for (int i = 0; i < s.size(); i++) { unsigned bits; unsigned bitCount; bool error; error = m_rttyEncoder.encode(s[i], bits, bitCount); for (int j = 0; j < bitCount; j++) { int txBit = (bits >> j) & 1; addBit(txBit); } } if (getMessageQueueToGUI()) { RttyMod::MsgReportTx* msg = RttyMod::MsgReportTx::create(s, m_textToTransmit.size()); getMessageQueueToGUI()->push(msg); } }