js8call/JS8Submode.cpp
2025-01-25 17:20:45 -08:00

232 lines
9.7 KiB
C++

#include "JS8Submode.hpp"
#include "commons.h"
#include "varicode.h"
#include <type_traits>
/******************************************************************************/
// Private Implementation
/******************************************************************************/
namespace JS8::Submode
{
namespace
{
// std::floor doesn't become constexpr until C++23; until then, our own
// implementation. We only use this for computation of the number of
// frames needed for a submode, so it doesn't need to be complicated.
//
// If we move to a compiler requirement later than C++17, then concepts
// would make for a nicer syntax than what we've used here.
template <typename T,
typename = std::enable_if_t<std::is_floating_point_v<T>>>
constexpr int floor(T const v)
{
auto const i = static_cast<int>(v);
return v < i ? i - 1 : i;
}
// Ensure that our implementation works as expected.
static_assert(floor(0.0) == 0);
static_assert(floor(0.499999) == 0);
static_assert(floor(0.5) == 0);
static_assert(floor(0.999999) == 0);
static_assert(floor(1.0) == 1);
static_assert(floor(123.0) == 123);
static_assert(floor(123.4) == 123);
static_assert(floor(-0.499999) == -1);
static_assert(floor(-0.5) == -1);
static_assert(floor(-0.999999) == -1);
static_assert(floor(-1.0) == -1);
static_assert(floor(-123.0) == -123);
static_assert(floor(-123.4) == -124);
// Data that describes a JS8 submode. Anything here should be able to be
// completely determined at compile time, i.e., any instance of this is
// just constant data.
class Data
{
public:
// Constructor; in addition to the basics provided by the constructor
// parameters, we'll determine various convenience constants in order
// to simplify calling code. These derived values depend only on the
// JS8_NUM_SYMBOLS and RX_SAMPLE_RATE definitions, and therefore can
// be entirely computed at compile time.
constexpr
Data(const char * const name,
unsigned int const symbolSamples,
unsigned int const startDelayMS,
unsigned int const txSeconds,
Costas::Type const costas,
int const rxSNRThreshold,
int const rxThreshold = 10)
: m_name (name),
m_symbolSamples (symbolSamples),
m_startDelayMS (startDelayMS),
m_period (txSeconds),
m_costas (costas),
m_rxSNRThreshold (rxSNRThreshold),
m_rxThreshold (rxThreshold),
m_framesForSymbols( JS8_NUM_SYMBOLS * symbolSamples),
m_bandwidth (8 * JS8_RX_SAMPLE_RATE / symbolSamples),
m_framesPerCycle ( JS8_RX_SAMPLE_RATE * txSeconds),
m_toneSpacing ( JS8_RX_SAMPLE_RATE / (double)symbolSamples),
m_framesNeeded (floor(m_framesForSymbols + (0.5 + startDelayMS / 1000.0) * JS8_RX_SAMPLE_RATE)),
m_ratio ( m_framesForSymbols / (double)JS8_RX_SAMPLE_RATE),
m_txDuration ( m_ratio + startDelayMS / 1000.0)
{}
// Inline accessors
constexpr auto name() const { return m_name; }
constexpr auto symbolSamples() const { return m_symbolSamples; }
constexpr auto startDelayMS() const { return m_startDelayMS; }
constexpr auto period() const { return m_period; }
constexpr auto costas() const { return m_costas; }
constexpr auto rxSNRThreshold() const { return m_rxSNRThreshold; }
constexpr auto rxThreshold() const { return m_rxThreshold; }
constexpr auto framesForSymbols() const { return m_framesForSymbols; }
constexpr auto bandwidth() const { return m_bandwidth; }
constexpr auto framesPerCycle() const { return m_framesPerCycle; }
constexpr auto framesNeeded() const { return m_framesNeeded; }
constexpr auto ratio() const { return m_ratio; }
constexpr auto toneSpacing() const { return m_toneSpacing; }
constexpr auto txDuration() const { return m_txDuration; }
private:
// Data members ** ORDER DEPENDENCY **
const char * m_name;
unsigned int m_symbolSamples;
unsigned int m_startDelayMS;
unsigned int m_period;
Costas::Type m_costas;
int m_rxSNRThreshold;
int m_rxThreshold;
int m_framesForSymbols;
int m_bandwidth;
int m_framesPerCycle;
double m_toneSpacing;
int m_framesNeeded;
double m_ratio;
double m_txDuration;
};
// Data for known submodes. Normal mode uses the old Costas Array
// definition; all other modes use the new one. Note that as of this
// writing, Ultra is a known, but unused, submode; we handle it here
// nevertheless, but it's in general disabled in the calling code.
constexpr Data Normal = {"NORMAL", JS8A_SYMBOL_SAMPLES, JS8A_START_DELAY_MS, JS8A_TX_SECONDS, Costas::Type::ORIGINAL, -24};
constexpr Data Fast = {"FAST", JS8B_SYMBOL_SAMPLES, JS8B_START_DELAY_MS, JS8B_TX_SECONDS, Costas::Type::MODIFIED, -22, 16};
constexpr Data Turbo = {"TURBO", JS8C_SYMBOL_SAMPLES, JS8C_START_DELAY_MS, JS8C_TX_SECONDS, Costas::Type::MODIFIED, -20, 32};
constexpr Data Slow = {"SLOW", JS8E_SYMBOL_SAMPLES, JS8E_START_DELAY_MS, JS8E_TX_SECONDS, Costas::Type::MODIFIED, -28};
constexpr Data Ultra = {"ULTRA", JS8I_SYMBOL_SAMPLES, JS8I_START_DELAY_MS, JS8I_TX_SECONDS, Costas::Type::MODIFIED, -18, 50};
// Given a submode, return data for it, or, if we don't have any idea
// what the caller is talking about, throw.
//
// Note that the original code in all cases did its best to just carry
// on in the event of an invalid submode, e.g., by returning 0, etc.,
// but that approach will in general lead to things like division by
// zero in computeCycleForDecode(), below, so either way we're going
// to end up with a runtime error, and it seems preferable that it's
// an informative one.
//
// Note that the Varicode::SubModeType enum is not dense, so we can't
// just do direct indexed access here.
constexpr Data const &
data(int const submode)
{
switch (submode)
{
case Varicode::JS8CallNormal: return Normal;
case Varicode::JS8CallFast: return Fast;
case Varicode::JS8CallTurbo: return Turbo;
case Varicode::JS8CallSlow: return Slow;
case Varicode::JS8CallUltra: return Ultra;
default:
{
throw error {QObject::tr("Invalid JS8 submode %1").arg(submode)};
}
}
}
}
}
/******************************************************************************/
// Public Implementation
/******************************************************************************/
namespace JS8::Submode
{
// Submode name inquiry function; return a translated value, if there is
// a translated value, otherwise, the untranslated mode name.
QString
name(int const submode)
{
return QObject::tr(data(submode).name());
}
// Basic submode numeric inquiry functions, i.e., parameterized only by
// the submode, returning constant data.
unsigned int bandwidth (int const submode) { return data(submode).bandwidth(); }
Costas::Type costas (int const submode) { return data(submode).costas(); }
unsigned int framesPerCycle (int const submode) { return data(submode).framesPerCycle(); }
unsigned int framesForSymbols(int const submode) { return data(submode).framesForSymbols(); }
unsigned int framesNeeded (int const submode) { return data(submode).framesNeeded(); }
unsigned int period (int const submode) { return data(submode).period(); }
int rxSNRThreshold (int const submode) { return data(submode).rxSNRThreshold(); }
int rxThreshold (int const submode) { return data(submode).rxThreshold(); }
unsigned int startDelayMS (int const submode) { return data(submode).startDelayMS(); }
unsigned int symbolSamples (int const submode) { return data(submode).symbolSamples(); }
double toneSpacing (int const submode) { return data(submode).toneSpacing(); }
double txDuration (int const submode) { return data(submode).txDuration(); }
// Compute which cycle we are currently in based on submode frames per cycle
// and current k position.
int
computeCycleForDecode(int const submode,
int const k)
{
int const maxFrames = JS8_NTMAX * JS8_RX_SAMPLE_RATE;
int const cycleFrames = framesPerCycle(submode);
return (k / cycleFrames) % // we mod here so we loop
(maxFrames / cycleFrames); // back to zero correctly
}
// Compute an alternate cycle offset by a specific number of frames e.g.,
// if we want the 0 cycle to start at second 5, we'd provide an offset of
// (5 * RX_SAMPLE_RATE).
int
computeAltCycleForDecode(int const submode,
int const k,
int const offsetFrames)
{
int const altK = k - offsetFrames;
return computeCycleForDecode(submode, altK < 0
? altK + JS8_NTMAX * JS8_RX_SAMPLE_RATE
: altK);
}
double
computeRatio(int const submode,
double const period)
{
return (period - data(submode).ratio()) / period;
}
}
/******************************************************************************/