2022-12-04 02:10:34 +01:00
|
|
|
#include <config.h>
|
|
|
|
|
#include <core.h>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <gui/gui.h>
|
|
|
|
|
#include <gui/style.h>
|
|
|
|
|
#include <gui/widgets/image.h>
|
|
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <module.h>
|
|
|
|
|
#include <signal_path/signal_path.h>
|
|
|
|
|
|
|
|
|
|
#include <dsp/demod/quadrature.h>
|
|
|
|
|
#include <dsp/sink/handler_sink.h>
|
2023-12-13 23:25:18 +01:00
|
|
|
#include "linesync.h"
|
|
|
|
|
#include <dsp/loop/pll.h>
|
|
|
|
|
#include <dsp/convert/real_to_complex.h>
|
|
|
|
|
#include <dsp/filter/fir.h>
|
|
|
|
|
#include <dsp/taps/from_array.h>
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
#include "amplitude.h"
|
|
|
|
|
#include <dsp/demod/am.h>
|
|
|
|
|
#include <dsp/loop/fast_agc.h>
|
|
|
|
|
|
2025-04-23 04:49:45 +02:00
|
|
|
#include "filters.h"
|
|
|
|
|
#include <dsp/math/normalize_phase.h>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
|
2022-12-04 02:10:34 +01:00
|
|
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
|
|
|
|
|
|
|
|
|
SDRPP_MOD_INFO{/* Name: */ "atv_decoder",
|
|
|
|
|
/* Description: */ "ATV decoder for SDR++",
|
|
|
|
|
/* Author: */ "Ryzerth",
|
|
|
|
|
/* Version: */ 0, 1, 0,
|
2023-12-13 23:25:18 +01:00
|
|
|
/* Max instances */ -1
|
|
|
|
|
};
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
#define SAMPLE_RATE (625.0f * (float)LINE_SIZE * 25.0f)
|
2022-12-04 02:10:34 +01:00
|
|
|
|
|
|
|
|
class ATVDecoderModule : public ModuleManager::Instance {
|
|
|
|
|
public:
|
2025-03-10 17:47:56 +01:00
|
|
|
ATVDecoderModule(std::string name) : img(768, 576) {
|
2022-12-04 02:10:34 +01:00
|
|
|
this->name = name;
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 7000000.0f, SAMPLE_RATE, SAMPLE_RATE, SAMPLE_RATE, true);
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
agc.init(vfo->output, 1.0f, 1e6, 0.001f, 1.0f);
|
|
|
|
|
demod.init(&agc.out, SAMPLE_RATE, SAMPLE_RATE / 2.0f);
|
2025-10-09 00:59:23 -04:00
|
|
|
// demod.init(vfo->output, dsp::demod::AM<float>::CARRIER, 8000000.0f, 50.0 / SAMPLE_RATE, 50.0 / SAMPLE_RATE, 0.0f, SAMPLE_RATE);
|
|
|
|
|
// demod.init(vfo->output, SAMPLE_RATE, SAMPLE_RATE / 2.0f);
|
2023-12-13 23:25:18 +01:00
|
|
|
sync.init(&demod.out, 1.0f, 1e-6, 1.0, 0.05);
|
|
|
|
|
sink.init(&sync.out, handler, this);
|
|
|
|
|
|
|
|
|
|
r2c.init(NULL);
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
agc.start();
|
2022-12-04 02:10:34 +01:00
|
|
|
demod.start();
|
2023-12-13 23:25:18 +01:00
|
|
|
sync.start();
|
2022-12-04 02:10:34 +01:00
|
|
|
sink.start();
|
|
|
|
|
|
|
|
|
|
gui::menu.registerEntry(name, menuHandler, this, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~ATVDecoderModule() {
|
|
|
|
|
if (vfo) {
|
|
|
|
|
sigpath::vfoManager.deleteVFO(vfo);
|
|
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
agc.stop();
|
2022-12-04 02:10:34 +01:00
|
|
|
demod.stop();
|
2025-03-10 17:47:56 +01:00
|
|
|
sync.stop();
|
|
|
|
|
sink.stop();
|
2022-12-04 02:10:34 +01:00
|
|
|
gui::menu.removeEntry(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void postInit() {}
|
|
|
|
|
|
2023-12-13 23:25:18 +01:00
|
|
|
void enable() {
|
|
|
|
|
enabled = true;
|
|
|
|
|
}
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2023-12-13 23:25:18 +01:00
|
|
|
void disable() {
|
|
|
|
|
enabled = false;
|
|
|
|
|
}
|
2022-12-04 02:10:34 +01:00
|
|
|
|
|
|
|
|
bool isEnabled() { return enabled; }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
static void menuHandler(void *ctx) {
|
|
|
|
|
ATVDecoderModule *_this = (ATVDecoderModule *)ctx;
|
|
|
|
|
|
|
|
|
|
if (!_this->enabled) {
|
|
|
|
|
style::beginDisabled();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::FillWidth();
|
|
|
|
|
_this->img.draw();
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
ImGui::TextUnformatted("Horizontal Sync:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (_this->sync.locked > 750) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Locked");
|
2023-12-13 23:25:18 +01:00
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
else {
|
|
|
|
|
ImGui::TextUnformatted("Not locked");
|
2023-12-13 23:25:18 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
ImGui::TextUnformatted("Vertical Sync:");
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (_this->vlock > 15) {
|
2023-12-13 23:25:18 +01:00
|
|
|
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Locked");
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
ImGui::TextUnformatted("Not locked");
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
ImGui::Checkbox("Fast Lock", &_this->sync.fastLock);
|
|
|
|
|
ImGui::Checkbox("Color Mode", &_this->colorMode);
|
2023-12-13 23:25:18 +01:00
|
|
|
|
2022-12-04 02:10:34 +01:00
|
|
|
if (!_this->enabled) {
|
|
|
|
|
style::endDisabled();
|
|
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
|
|
|
|
|
ImGui::Text("Gain: %f", _this->gain);
|
|
|
|
|
ImGui::Text("Offset: %f", _this->offset);
|
2025-04-23 04:49:45 +02:00
|
|
|
ImGui::Text("Subcarrier: %f", _this->subcarrierFreq);
|
2022-12-04 02:10:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-04-23 04:49:45 +02:00
|
|
|
uint32_t pp = 0;
|
|
|
|
|
|
2022-12-04 02:10:34 +01:00
|
|
|
static void handler(float *data, int count, void *ctx) {
|
|
|
|
|
ATVDecoderModule *_this = (ATVDecoderModule *)ctx;
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Correct the offset
|
2025-12-12 01:53:37 -05:00
|
|
|
#if VOLK_VERSION_MAJOR > 2 || (VOLK_VERSION_MAJOR == 2 && VOLK_VERSION_MINOR >= 3)
|
2025-03-10 17:47:56 +01:00
|
|
|
volk_32f_s32f_add_32f(data, data, _this->offset, count);
|
2025-12-12 01:53:37 -05:00
|
|
|
#else
|
|
|
|
|
const float ofs = _this->offset;
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
data[i] += ofs;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-03-10 17:47:56 +01:00
|
|
|
|
|
|
|
|
// Correct the gain
|
|
|
|
|
volk_32f_s32f_multiply_32f(data, data, _this->gain, count);
|
|
|
|
|
|
|
|
|
|
// Compute the sync levels
|
|
|
|
|
float syncLLevel = 0.0f;
|
|
|
|
|
float syncRLevel = 0.0f;
|
|
|
|
|
volk_32f_accumulator_s32f(&syncLLevel, data, EQUAL_LEN);
|
|
|
|
|
volk_32f_accumulator_s32f(&syncRLevel, &data[EQUAL_LEN], SYNC_LEN - EQUAL_LEN);
|
|
|
|
|
syncLLevel *= 1.0f / EQUAL_LEN;
|
|
|
|
|
syncRLevel *= 1.0f / (SYNC_LEN - EQUAL_LEN);
|
|
|
|
|
float syncLevel = (syncLLevel + syncRLevel) * 0.5f; // TODO: It's technically correct but if the sizes were different it wouldn't be
|
|
|
|
|
|
|
|
|
|
// Compute the blanking level
|
|
|
|
|
float blankLevel = 0.0f;
|
|
|
|
|
volk_32f_accumulator_s32f(&blankLevel, &data[HBLANK_START], HBLANK_LEN);
|
|
|
|
|
blankLevel /= (float)HBLANK_LEN;
|
|
|
|
|
|
|
|
|
|
// Run the offset control loop
|
|
|
|
|
_this->offset -= (blankLevel / _this->gain)*0.001;
|
|
|
|
|
_this->offset = std::clamp<float>(_this->offset, -1.0f, 1.0f);
|
|
|
|
|
_this->gain -= (blankLevel - syncLevel + SYNC_LEVEL)*0.01f;
|
|
|
|
|
_this->gain = std::clamp<float>(_this->gain, 0.1f, 10.0f);
|
|
|
|
|
|
|
|
|
|
// Detect the sync type
|
|
|
|
|
uint16_t shortSync = (syncLLevel < 0.5f*SYNC_LEVEL) && (syncRLevel > 0.5f*SYNC_LEVEL) && (blankLevel > 0.5f*SYNC_LEVEL);
|
|
|
|
|
uint16_t longSync = (syncLLevel < 0.5f*SYNC_LEVEL) && (syncRLevel < 0.5f*SYNC_LEVEL) && (blankLevel < 0.5f*SYNC_LEVEL);
|
|
|
|
|
|
|
|
|
|
// Save sync type to history
|
|
|
|
|
_this->syncHistory = (_this->syncHistory << 2) | (longSync << 1) | shortSync;
|
|
|
|
|
|
2025-10-09 00:59:23 -04:00
|
|
|
// // If the line has a colorburst, decode it
|
|
|
|
|
// dsp::complex_t* buf1 = _this->r2c.out.readBuf;
|
|
|
|
|
// dsp::complex_t* buf2 = _this->r2c.out.writeBuf;
|
|
|
|
|
// if (true) {
|
|
|
|
|
// // Convert the line into complex
|
|
|
|
|
// _this->r2c.process(count, data, buf1);
|
|
|
|
|
|
|
|
|
|
// // Extract the chroma subcarrier (TODO: Optimise by running only where needed)
|
|
|
|
|
// for (int i = COLORBURST_START; i < count-(CHROMA_BANDPASS_DELAY+1); i++) {
|
|
|
|
|
// volk_32fc_x2_dot_prod_32fc((lv_32fc_t*)&buf2[i], (lv_32fc_t*)&buf1[i - CHROMA_BANDPASS_DELAY], (lv_32fc_t*)CHROMA_BANDPASS, CHROMA_BANDPASS_SIZE);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // Down convert the chroma subcarrier (TODO: Optimise by running only where needed)
|
|
|
|
|
// lv_32fc_t startPhase = { 1.0f, 0.0f };
|
|
|
|
|
// lv_32fc_t phaseDelta = { sinf(_this->subcarrierFreq), cosf(_this->subcarrierFreq) };
|
|
|
|
|
// #if VOLK_VERSION >= 030100
|
|
|
|
|
// volk_32fc_s32fc_x2_rotator2_32fc((lv_32fc_t*)&buf2[COLORBURST_START], (lv_32fc_t*)&buf2[COLORBURST_START], &phaseDelta, &startPhase, count - COLORBURST_START);
|
|
|
|
|
// #else
|
|
|
|
|
// volk_32fc_s32fc_x2_rotator_32fc((lv_32fc_t*)&buf2[COLORBURST_START], (lv_32fc_t*)&buf2[COLORBURST_START], phaseDelta, &startPhase, count - COLORBURST_START);
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
// // Compute the phase of the burst
|
|
|
|
|
// dsp::complex_t burstAvg = { 0.0f, 0.0f };
|
|
|
|
|
// volk_32fc_accumulator_s32fc((lv_32fc_t*)&burstAvg, (lv_32fc_t*)&buf2[COLORBURST_START], COLORBURST_LEN);
|
|
|
|
|
// float burstAmp = burstAvg.amplitude();
|
|
|
|
|
// if (burstAmp*(1.0f/(float)COLORBURST_LEN) < 0.02f) {
|
|
|
|
|
// printf("%d\n", _this->line);
|
|
|
|
|
// }
|
|
|
|
|
// burstAvg *= (1.0f / (burstAmp*burstAmp));
|
|
|
|
|
// burstAvg = burstAvg.conj();
|
|
|
|
|
|
|
|
|
|
// // Normalize the chroma data (TODO: Optimise by running only where needed)
|
|
|
|
|
// volk_32fc_s32fc_multiply_32fc((lv_32fc_t*)&buf2[COLORBURST_START], (lv_32fc_t*)&buf2[COLORBURST_START], *((lv_32fc_t*)&burstAvg), count - COLORBURST_START);
|
|
|
|
|
|
|
|
|
|
// // Compute the frequency error of the burst
|
|
|
|
|
// float phase = buf2[COLORBURST_START].phase();
|
|
|
|
|
// float error = 0.0f;
|
|
|
|
|
// for (int i = COLORBURST_START+1; i < COLORBURST_START+COLORBURST_LEN; i++) {
|
|
|
|
|
// float cphase = buf2[i].phase();
|
|
|
|
|
// error += dsp::math::normalizePhase(cphase - phase);
|
|
|
|
|
// phase = cphase;
|
|
|
|
|
// }
|
|
|
|
|
// error *= (1.0f / (float)(COLORBURST_LEN-1));
|
|
|
|
|
|
|
|
|
|
// // Update the subcarrier freq
|
|
|
|
|
// _this->subcarrierFreq += error*0.0001f;
|
|
|
|
|
// }
|
2025-04-23 04:49:45 +02:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Render the line if it's visible
|
|
|
|
|
if (_this->ypos >= 34 && _this->ypos <= 34+576-1) {
|
|
|
|
|
uint32_t* currentLine = &((uint32_t *)_this->img.buffer)[(_this->ypos - 34)*768];
|
2025-04-23 04:49:45 +02:00
|
|
|
if (_this->colorMode) {
|
2025-10-09 00:59:23 -04:00
|
|
|
// for (int i = 155; i < (155+768); i++) {
|
|
|
|
|
// int imval1 = std::clamp<float>(fabsf(buf2[i-155+COLORBURST_START].re*5.0f) * 255.0f, 0, 255);
|
|
|
|
|
// int imval2 = std::clamp<float>(fabsf(buf2[i-155+COLORBURST_START].im*5.0f) * 255.0f, 0, 255);
|
|
|
|
|
// currentLine[i-155] = 0xFF000000 | (imval2 << 8) | imval1;
|
|
|
|
|
// }
|
2025-04-23 04:49:45 +02:00
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
for (int i = 155; i < (155+768); i++) {
|
2025-10-09 00:59:23 -04:00
|
|
|
int imval = std::clamp<float>(data[i] * 255.0f, 0, 255);
|
2025-04-23 04:49:45 +02:00
|
|
|
currentLine[i-155] = 0xFF000000 | (imval << 16) | (imval << 8) | imval;
|
|
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Compute whether to rollover
|
|
|
|
|
bool rollToOdd = (_this->ypos == 624);
|
|
|
|
|
bool rollToEven = (_this->ypos == 623);
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Compute the field sync
|
|
|
|
|
bool syncToOdd = (_this->syncHistory == 0b0101011010010101);
|
|
|
|
|
bool syncToEven = (_this->syncHistory == 0b0001011010100101);
|
2023-12-13 23:25:18 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Process the sync (NOTE: should start with 0b01, but for some reason I don't see a sync?)
|
|
|
|
|
if (rollToOdd || syncToOdd) {
|
|
|
|
|
// Update the vertical lock state
|
|
|
|
|
bool disagree = (rollToOdd ^ syncToOdd);
|
|
|
|
|
if (disagree && _this->vlock > 0) {
|
|
|
|
|
_this->vlock--;
|
|
|
|
|
}
|
|
|
|
|
else if (!disagree && _this->vlock < 20) {
|
|
|
|
|
_this->vlock++;
|
2022-12-04 02:10:34 +01:00
|
|
|
}
|
2023-12-13 23:25:18 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// Start the odd field
|
|
|
|
|
_this->ypos = 1;
|
2025-04-23 04:49:45 +02:00
|
|
|
_this->line++;
|
2023-12-13 23:25:18 +01:00
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
else if (rollToEven || syncToEven) {
|
|
|
|
|
// Update the vertical lock state
|
|
|
|
|
bool disagree = (rollToEven ^ syncToEven);
|
|
|
|
|
if (disagree && _this->vlock > 0) {
|
|
|
|
|
_this->vlock--;
|
|
|
|
|
}
|
|
|
|
|
else if (!disagree && _this->vlock < 20) {
|
|
|
|
|
_this->vlock++;
|
2022-12-04 02:10:34 +01:00
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
|
|
|
|
|
// Start the even field
|
2023-12-13 23:25:18 +01:00
|
|
|
_this->ypos = 0;
|
2025-04-23 04:49:45 +02:00
|
|
|
_this->line = 0;
|
2025-03-10 17:47:56 +01:00
|
|
|
|
|
|
|
|
// Swap the video buffer
|
2023-12-13 23:25:18 +01:00
|
|
|
_this->img.swap();
|
2022-12-04 02:10:34 +01:00
|
|
|
}
|
2025-03-10 17:47:56 +01:00
|
|
|
else {
|
|
|
|
|
_this->ypos += 2;
|
2025-04-23 04:49:45 +02:00
|
|
|
_this->line++;
|
2025-03-10 17:47:56 +01:00
|
|
|
}
|
2022-12-04 02:10:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
// NEW SYNC:
|
|
|
|
|
float offset = 0.0f;
|
|
|
|
|
float gain = 1.0f;
|
|
|
|
|
uint16_t syncHistory = 0;
|
2025-04-23 04:49:45 +02:00
|
|
|
int line = 0;
|
2025-03-10 17:47:56 +01:00
|
|
|
int ypos = 0;
|
|
|
|
|
int vlock = 0;
|
2025-04-23 04:49:45 +02:00
|
|
|
float subcarrierFreq = 0.0f;
|
2025-03-10 17:47:56 +01:00
|
|
|
|
2022-12-04 02:10:34 +01:00
|
|
|
std::string name;
|
|
|
|
|
bool enabled = true;
|
|
|
|
|
|
|
|
|
|
VFOManager::VFO *vfo = NULL;
|
2025-10-09 00:59:23 -04:00
|
|
|
// dsp::demod::Quadrature demod;
|
2025-03-10 17:47:56 +01:00
|
|
|
dsp::loop::FastAGC<dsp::complex_t> agc;
|
|
|
|
|
dsp::demod::Amplitude demod;
|
|
|
|
|
//dsp::demod::AM<float> demod;
|
2023-12-13 23:25:18 +01:00
|
|
|
LineSync sync;
|
2022-12-04 02:10:34 +01:00
|
|
|
dsp::sink::Handler<float> sink;
|
2023-12-13 23:25:18 +01:00
|
|
|
dsp::convert::RealToComplex r2c;
|
2022-12-04 02:10:34 +01:00
|
|
|
|
2025-03-10 17:47:56 +01:00
|
|
|
bool colorMode = false;
|
2023-12-13 23:25:18 +01:00
|
|
|
|
2022-12-04 02:10:34 +01:00
|
|
|
ImGui::ImageDisplay img;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
MOD_EXPORT void _INIT_() {}
|
|
|
|
|
|
|
|
|
|
MOD_EXPORT ModuleManager::Instance *_CREATE_INSTANCE_(std::string name) { return new ATVDecoderModule(name); }
|
|
|
|
|
|
|
|
|
|
MOD_EXPORT void _DELETE_INSTANCE_(void *instance) { delete (ATVDecoderModule *)instance; }
|
|
|
|
|
|
|
|
|
|
MOD_EXPORT void _END_() {}
|