js8call/Modulator.cpp

220 lines
5.6 KiB
C++
Raw Permalink Normal View History

2018-02-08 21:28:33 -05:00
#include "Modulator.hpp"
#include <limits>
#include <numbers>
2018-02-08 21:28:33 -05:00
#include <QDateTime>
#include <QLoggingCategory>
2024-10-27 16:03:31 -07:00
#include <QtMath>
2018-03-05 14:49:51 -05:00
#include "commons.h"
#include "DriftingDateTime.h"
2024-10-28 07:51:13 -07:00
#include "JS8Submode.hpp"
2024-10-27 16:03:31 -07:00
#include "mainwindow.h"
#include "soundout.h"
2018-02-08 21:28:33 -05:00
#include "moc_Modulator.cpp"
Q_DECLARE_LOGGING_CATEGORY(modulator_js8)
2024-09-11 17:48:01 -07:00
namespace
{
constexpr double TAU = 2 * std::numbers::pi;
constexpr auto FRAME_RATE = 48000;
2024-10-28 07:51:13 -07:00
constexpr auto MS_PER_DAY = 86400000;
2024-10-28 09:16:11 -07:00
constexpr auto MS_PER_SEC = 1000;
2024-09-11 17:48:01 -07:00
}
void
2024-10-28 07:51:13 -07:00
Modulator::start(double const frequency,
int const submode,
SoundOutput * const stream,
Channel const channel)
2018-02-08 21:28:33 -05:00
{
Q_ASSERT (stream);
2024-10-28 09:50:20 -07:00
if (m_state != State::Idle) stop();
2024-10-27 21:42:02 -07:00
m_quickClose = false;
m_frequency = frequency;
2024-10-28 15:46:48 -07:00
m_nsps = JS8::Submode::symbolSamples(submode);
2024-10-28 13:21:06 -07:00
m_toneSpacing = JS8::Submode::toneSpacing(submode);
2024-10-28 15:46:48 -07:00
m_isym0 = std::numeric_limits<unsigned>::max();
m_amp = std::numeric_limits<qint16>::max();
m_frequency0 = 0.0;
m_phi = 0.0;
2018-02-08 21:28:33 -05:00
m_silentFrames = 0;
m_ic = 0;
2024-10-28 08:29:02 -07:00
// If we're not tuning, then we'll need to figure out exactly when we
2024-10-28 07:51:13 -07:00
// should start transmitting; this will depend on the submode in play.
if (!m_tuning)
{
// Get the nominal transmit start time for this submode, and determine
// which millisecond of the current transmit period we're currently at.
2024-10-28 07:51:13 -07:00
auto const startDelayMS = JS8::Submode::startDelayMS(submode);
unsigned const periodOffset = (DriftingDateTime::currentMSecsSinceEpoch() % MS_PER_DAY) %
2024-10-28 09:16:11 -07:00
(JS8::Submode::period(submode) * MS_PER_SEC);
2024-10-28 07:51:13 -07:00
// If we haven't yet hit the nominal start time for the period, then we
// will need to inject some silence into the transmission; determine the
// number of silent frames required to start audio at the correct amount
// of delay into the period.
//
// If we have hit the nominal start time for the period, adjust for late
// start if we're not exactly at the nominal start time.
if (startDelayMS > periodOffset) m_silentFrames = (startDelayMS - periodOffset) * FRAME_RATE / MS_PER_SEC;
else m_ic = (periodOffset - startDelayMS) * FRAME_RATE / MS_PER_SEC;
}
initialize(QIODevice::ReadOnly, channel);
2018-02-08 21:28:33 -05:00
Q_EMIT stateChanged ((m_state = m_silentFrames
? State::Synchronizing
: State::Active));
2018-02-08 21:28:33 -05:00
m_stream = stream;
2024-10-27 16:17:26 -07:00
if (m_stream)
{
2024-10-27 16:17:26 -07:00
m_stream->restart(this);
}
else
{
qCDebug(modulator_js8) << "Modulator::start: no audio output stream assigned";
}
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
void
Modulator::tune(bool const tuning)
2018-02-08 21:28:33 -05:00
{
2024-10-27 16:03:31 -07:00
m_tuning = tuning;
if (!m_tuning) stop(true);
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
void
Modulator::stop(bool const quickClose)
2018-02-08 21:28:33 -05:00
{
2024-10-27 16:03:31 -07:00
m_quickClose = quickClose;
close();
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
void
Modulator::close()
2018-02-08 21:28:33 -05:00
{
if (m_stream)
{
2024-10-27 16:03:31 -07:00
if (m_quickClose) m_stream->reset();
else m_stream->stop();
}
2024-10-27 17:17:03 -07:00
if (m_state != State::Idle)
{
2024-10-27 17:17:03 -07:00
Q_EMIT stateChanged ((m_state = State::Idle));
}
2024-10-27 16:03:31 -07:00
AudioDevice::close();
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
qint64
Modulator::readData(char * const data,
qint64 const maxSize)
2018-02-08 21:28:33 -05:00
{
2024-10-27 16:03:31 -07:00
if (maxSize == 0) return 0;
2024-10-27 16:17:26 -07:00
Q_ASSERT (!(maxSize % qint64(bytesPerFrame()))); // no torn frames
Q_ASSERT (isOpen());
2018-02-08 21:28:33 -05:00
2024-10-28 08:29:02 -07:00
qint64 framesGenerated = 0;
qint64 const maxFrames = maxSize / bytesPerFrame();
qint16 * samples = reinterpret_cast<qint16 *>(data);
qint16 const * const samplesEnd = samples + maxFrames * (bytesPerFrame() / sizeof(qint16));
2018-02-08 21:28:33 -05:00
switch (m_state)
2024-10-27 16:03:31 -07:00
{
2024-10-27 17:17:03 -07:00
case State::Synchronizing:
2024-10-27 16:03:31 -07:00
{
if (m_silentFrames)
2018-02-08 21:28:33 -05:00
{
2024-10-27 16:03:31 -07:00
// Send silence up to end of start delay.
2024-10-28 08:29:02 -07:00
framesGenerated = qMin(m_silentFrames, maxFrames);
2024-10-27 16:03:31 -07:00
do
{
2024-10-29 05:52:19 -07:00
samples = load(0, samples);
}
while (--m_silentFrames && samples != samplesEnd);
2024-10-27 16:03:31 -07:00
if (!m_silentFrames)
{
2024-10-27 17:17:03 -07:00
Q_EMIT stateChanged ((m_state = State::Active));
2018-02-08 21:28:33 -05:00
}
}
2024-10-27 16:03:31 -07:00
}
[[fallthrough]];
2018-02-08 21:28:33 -05:00
2024-10-27 17:17:03 -07:00
case State::Active:
2024-10-27 16:03:31 -07:00
{
2024-10-28 15:19:58 -07:00
// Fade out parameters; no fade out during tuning.
2024-10-27 16:03:31 -07:00
2024-10-28 15:19:58 -07:00
unsigned int const i0 = (m_tuning ? 9999 : (JS8_NUM_SYMBOLS - 0.017) * 4.0) * m_nsps;
unsigned int const i1 = (m_tuning ? 9999 : JS8_NUM_SYMBOLS * 4.0) * m_nsps;
2018-02-08 21:28:33 -05:00
while (samples != samplesEnd && m_ic < i1)
2024-10-27 16:03:31 -07:00
{
2024-10-28 15:19:58 -07:00
unsigned int const isym = m_tuning ? 0 : m_ic / (4.0 * m_nsps);
2024-10-27 21:42:02 -07:00
2024-10-27 16:03:31 -07:00
if (isym != m_isym0 || m_frequency != m_frequency0)
{
double const toneFrequency = m_frequency + itone[isym] * m_toneSpacing;
m_dphi = TAU * toneFrequency / FRAME_RATE;
2024-10-27 16:03:31 -07:00
m_isym0 = isym;
2024-10-29 05:52:19 -07:00
m_frequency0 = m_frequency;
2024-10-27 16:03:31 -07:00
}
2018-02-08 21:28:33 -05:00
2024-10-27 16:03:31 -07:00
m_phi += m_dphi;
2024-10-27 16:03:31 -07:00
if (m_phi > TAU) m_phi -= TAU;
if (m_ic > i0) m_amp = 0.98 * m_amp;
if (m_ic > i1) m_amp = 0.0;
2024-10-28 15:19:58 -07:00
samples = load(qRound(m_amp * qSin(m_phi)), samples);
2018-02-08 21:28:33 -05:00
2024-10-27 16:03:31 -07:00
++framesGenerated;
++m_ic;
}
2024-10-27 16:03:31 -07:00
if (m_amp == 0.0)
{
2024-10-27 17:17:03 -07:00
Q_EMIT stateChanged ((m_state = State::Idle));
2024-10-27 16:03:31 -07:00
return framesGenerated * bytesPerFrame();
m_phi = 0.0;
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
m_frequency0 = m_frequency;
2024-10-29 05:52:19 -07:00
// Done for this chunk; continue on the next call. Pad the
// block with silence.
2024-10-27 16:03:31 -07:00
2024-10-29 05:52:19 -07:00
while (samples != samplesEnd)
2024-10-27 16:03:31 -07:00
{
samples = load(0, samples);
++framesGenerated;
}
return framesGenerated * bytesPerFrame();
2018-02-08 21:28:33 -05:00
}
2024-10-27 16:03:31 -07:00
[[fallthrough]];
2024-10-27 17:17:03 -07:00
case State::Idle:
2024-10-27 16:03:31 -07:00
break;
}
2018-02-08 21:28:33 -05:00
2024-10-27 17:17:03 -07:00
Q_ASSERT (State::Idle == m_state);
2018-02-08 21:28:33 -05:00
return 0;
}
Q_LOGGING_CATEGORY(modulator_js8, "modulator.js8", QtWarningMsg)