satdump/plugins/analog_support/sstv/module_sstv_decoder.cpp
2025-09-25 17:25:13 +01:00

271 lines
No EOL
9.7 KiB
C++

#include "module_sstv_decoder.h"
#include "common/dsp/filter/firdes.h"
#include "common/utils.h"
#include "common/wav.h"
#include "core/exception.h"
#include "core/resources.h"
#include "dsp/filter/fir.h"
#include "dsp/sstv_linesync.h"
#include "dsp/utils/hilbert.h"
#include "dsp/utils/quadrature_demod.h"
#include "image/image.h"
#include "image/io.h"
#include "imgui/imgui.h"
#include "imgui/imgui_image.h"
#include "logger.h"
#include "nlohmann/json_utils.h"
#include "lineproc.h"
namespace sstv
{
SSTVDecoderModule::SSTVDecoderModule(std::string input_file, std::string output_file_hint, nlohmann::json parameters) : ProcessingModule(input_file, output_file_hint, parameters)
{
if (parameters.count("audio_samplerate") > 0)
d_audio_samplerate = parameters["audio_samplerate"].get<long>();
else
throw satdump_exception("Audio samplerate parameter must be present!");
if (parameters.count("sstv_mode") > 0)
d_sstv_mode = parameters["sstv_mode"].get<std::string>();
else
throw satdump_exception("SSTV Mode parameter must be present!");
}
SSTVDecoderModule::~SSTVDecoderModule() {}
void SSTVDecoderModule::process()
{
if (input_data_type == satdump::pipeline::DATA_FILE)
filesize = getFilesize(d_input_file);
else
filesize = 0;
std::string main_dir = d_output_file_hint.substr(0, d_output_file_hint.rfind('/'));
bool is_stereo = false;
std::ifstream data_in;
if (input_data_type == satdump::pipeline::DATA_FILE)
{
wav::WavHeader hdr = wav::parseHeaderFromFileWav(d_input_file);
if (!wav::isValidWav(hdr))
throw satdump_exception("File is not WAV!");
if (hdr.bits_per_sample != 16)
throw satdump_exception("Only 16-bits WAV are supported!");
d_audio_samplerate = hdr.samplerate;
is_stereo = hdr.channel_cnt == 2;
if (is_stereo)
logger->info("File is stereo. Ignoring second channel!");
wav::FileMetadata md = wav::tryParseFilenameMetadata(d_input_file, true);
if (md.timestamp != 0 && (d_parameters.contains("start_timestamp") ? d_parameters["start_timestamp"] == -1 : 1))
{
d_parameters["start_timestamp"] = md.timestamp;
logger->trace("Has timestamp %d", md.timestamp);
}
else
{
logger->warn("Couldn't automatically determine the timestamp, in case of unexpected results, please verify you have specified the correct timestamp manually");
}
data_in = std::ifstream(d_input_file, std::ios::binary);
}
logger->info("Using input wav " + d_input_file);
// SSTV Blocks
satdump::ndsp::FIRBlock<float> bpf;
satdump::ndsp::HilbertBlock hilb;
satdump::ndsp::QuadratureDemodBlock quad;
satdump::ndsp::SSTVLineSyncBlock lsync;
// Configure blocks
auto sstv_modes = loadJsonFile(resources::getResourcePath("sstv.json"));
if (!sstv_modes.contains(d_sstv_mode))
throw satdump_exception("Invalid SSTV Mode : " + d_sstv_mode);
nlohmann::json sstv_cfg = sstv_modes[d_sstv_mode];
double sstv_samplerate = sstv_cfg["samplerate"]; // TODO resample!
double freq_sync = getValueOrDefault(sstv_cfg["sync_freq"], 1200);
double freq_img_black = getValueOrDefault(sstv_cfg["freq_img_black"], 1488); // 1500); should be 1500?
double freq_img_white = getValueOrDefault(sstv_cfg["freq_img_white"], 2300);
bpf.set_cfg("taps", dsp::firdes::band_pass(1, sstv_samplerate, freq_sync, freq_img_white, 500, dsp::fft::window::WIN_KAISER));
hilb.set_cfg("ntaps", 651);
lsync.set_cfg("sync_time", sstv_cfg["sync_time"]);
lsync.set_cfg("line_time", sstv_cfg["line_time"]);
// Link them
satdump::ndsp::BlockIO io_in;
io_in.name = "in";
io_in.type = satdump::ndsp::DSP_SAMPLE_TYPE_F32;
io_in.fifo = std::make_shared<satdump::ndsp::DSPStream>(2);
bpf.set_input(io_in, 0);
hilb.link(&bpf, 0, 0, 2);
quad.link(&hilb, 0, 0, 2);
lsync.link(&quad, 0, 0, 2);
auto io_out = lsync.get_output(0, 4);
// Start everything
auto feeder_func = [this, &io_in, &data_in, is_stereo]()
{
const int buffer_size = 1024;
int16_t *s16_buf = new int16_t[buffer_size];
time_t lastTime = 0;
while (input_data_type == satdump::pipeline::DATA_FILE ? !data_in.eof() : input_active.load())
{
if (input_data_type == satdump::pipeline::DATA_FILE)
data_in.read((char *)s16_buf, buffer_size * sizeof(int16_t));
else
input_fifo->read((uint8_t *)s16_buf, buffer_size * sizeof(int16_t));
auto nblk = io_in.fifo->newBufferSamples(buffer_size, sizeof(float));
if (is_stereo)
{
for (int i = 0; i < buffer_size / 2; i++)
nblk.getSamples<float>()[i] = s16_buf[i * 2] / 32767.0f;
nblk.size = buffer_size / 2;
io_in.fifo->wait_enqueue(nblk);
}
else
{
volk_16i_s32f_convert_32f_u((&nblk)->getSamples<float>(), (const int16_t *)s16_buf, 32767, buffer_size);
nblk.size = buffer_size;
io_in.fifo->wait_enqueue(nblk);
}
if (input_data_type == satdump::pipeline::DATA_FILE)
progress = data_in.tellg();
if (time(NULL) % 10 == 0 && lastTime != time(NULL))
{
lastTime = time(NULL);
logger->info("Progress " + std::to_string(round(((double)progress / (double)filesize) * 1000.0) / 10.0) + "%%");
}
}
delete[] s16_buf;
io_in.fifo->wait_enqueue(io_in.fifo->newBufferTerminator());
};
std::thread feeder_thread(feeder_func);
bpf.start();
hilb.start();
quad.start();
lsync.start();
std::vector<uint32_t> all_lines;
SSTVLineProc lproc;
lproc.setCfg(sstv_cfg);
auto reader_func = [&]()
{
while (true)
{
satdump::ndsp::DSPBuffer iblk = io_out.fifo->wait_dequeue();
if (iblk.isTerminator())
{
io_out.fifo->free(iblk);
break;
}
// logger->info("Got Something!");
auto ll = lproc.lineProc(std::vector<float>(iblk.getSamples<float>(), iblk.getSamples<float>() + iblk.size));
all_lines.insert(all_lines.end(), ll.begin(), ll.end());
if (textureID != 0 && all_lines.size() > 0)
{
auto img = rgbaToImg(all_lines, lproc.output_width);
img.resize_bilinear(512, img.height() * (512.0 / img.width()));
image::Image preview(8, 512, 512, 3);
int offset = img.height() > 512 ? (img.height() - 512) : 0;
for (int ch = 0; ch < img.channels(); ch++)
for (int i = 0; i < std::min<int>(512, img.height()); i++)
image::imemcpy(preview, ch * preview.height() * preview.width() + i * 512, img, ch * img.height() * img.width() + (i + offset) * 512, 512);
image::image_to_rgba(preview, textureBuffer);
has_to_update = true;
}
io_out.fifo->free(iblk);
}
logger->info("BYE!");
};
std::thread read_thread(reader_func);
// Stop everything
if (feeder_thread.joinable())
feeder_thread.join();
bpf.stop();
hilb.stop();
quad.stop();
lsync.stop();
if (read_thread.joinable())
read_thread.join();
auto img = rgbaToImg(all_lines, lproc.output_width);
image::save_img(img, d_output_file_hint + ".png");
}
void SSTVDecoderModule::drawUI(bool window)
{
ImGui::Begin("SSTV Decoder", NULL, window ? 0 : NOWINDOW_FLAGS);
ImGui::BeginGroup();
{
if (textureID == 0)
{
textureID = makeImageTexture();
textureBuffer = new uint32_t[262144]; // 512x512
memset(textureBuffer, 0, sizeof(uint32_t) * 262144);
has_to_update = true;
}
if (has_to_update)
{
updateImageTexture(textureID, textureBuffer, 512, 512);
has_to_update = false;
}
ImGui::Image((void *)(intptr_t)textureID, {200 * ui_scale, 200 * ui_scale});
}
ImGui::EndGroup();
ImGui::SameLine();
ImGui::BeginGroup();
{
ImGui::Button("Status", {200 * ui_scale, 20 * ui_scale});
drawStatus(sstv_status);
}
ImGui::EndGroup();
if (input_data_type == satdump::pipeline::DATA_FILE)
ImGui::ProgressBar((double)progress / (double)filesize, ImVec2(ImGui::GetContentRegionAvail().x, 20 * ui_scale));
ImGui::End();
}
std::string SSTVDecoderModule::getID() { return "sstv_decoder"; }
std::shared_ptr<satdump::pipeline::ProcessingModule> SSTVDecoderModule::getInstance(std::string input_file, std::string output_file_hint, nlohmann::json parameters)
{
return std::make_shared<SSTVDecoderModule>(input_file, output_file_hint, parameters);
}
} // namespace sstv