#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(); else throw satdump_exception("Audio samplerate parameter must be present!"); if (parameters.count("sstv_mode") > 0) d_sstv_mode = parameters["sstv_mode"].get(); 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 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(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()[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(), (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 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(iblk.getSamples(), iblk.getSamples() + 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(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 SSTVDecoderModule::getInstance(std::string input_file, std::string output_file_hint, nlohmann::json parameters) { return std::make_shared(input_file, output_file_hint, parameters); } } // namespace sstv