#include "module_generic_analog_demod.h" #include "common/audio/audio_sink.h" #include "common/dsp/io/wav_writer.h" #include "core/config.h" #include "imgui/imgui.h" #include "logger.h" #include namespace generic_analog { GenericAnalogDemodModule::GenericAnalogDemodModule(std::string input_file, std::string output_file_hint, nlohmann::json parameters) : BaseDemodModule(input_file, output_file_hint, parameters) { name = "Generic Analog Demodulator (WIP)"; show_freq = false; play_audio = satdump::satdump_cfg.shouldPlayAudio(); constellation.d_hscale = 1.0; // 80.0 / 100.0; constellation.d_vscale = 0.5; // 20.0 / 100.0; MIN_SPS = 1; MAX_SPS = 1e9; upcoming_symbolrate = d_symbolrate; if (d_parameters.contains("bandwidth")) { upcoming_symbolrate = d_parameters["bandwidth"]; settings_changed = true; } if (d_parameters.contains("record_demod_audio")) { record_demod_audio = d_parameters["record_demod_audio"]; } // modulation_type if (d_parameters.contains("modulation_type")) { const std::string &mod = d_parameters["modulation_type"]; if (mod == "NFM") { modulation_type = ModulationType::NFM; } else if (mod == "AM") { modulation_type = ModulationType::AM; } } } void GenericAnalogDemodModule::init() { BaseDemodModule::initb(); // Resampler to BW // res = std::make_shared>(agc->output_stream, d_symbolrate, final_samplerate); // Quadrature demod // qua = std::make_shared(res->output_stream, dsp::hz_to_rad(d_symbolrate / 2, d_symbolrate)); } GenericAnalogDemodModule::~GenericAnalogDemodModule() {} void GenericAnalogDemodModule::process() { if (input_data_type == satdump::pipeline::DATA_FILE) filesize = file_source->getFilesize(); else filesize = 0; const bool output_to_file = output_data_type == satdump::pipeline::DATA_FILE || record_demod_audio; std::string output_file_hint = d_output_file_hint; if (output_to_file) { // append satellite NORAD number to filename if (d_parameters.contains("satellite_norad")) { output_file_hint.push_back('_'); output_file_hint.append(make_safe_string(std::to_string(d_parameters["satellite_norad"].get()))); } // append satellite name to filename if (d_parameters.contains("satellite_name") && d_parameters["satellite_name"].get().size() > 0) { output_file_hint.push_back('_'); output_file_hint.append(make_safe_string(d_parameters["satellite_name"])); } // append satellite downlink frequency to filename if (d_parameters.contains("satellite_frequency")) { output_file_hint.push_back('_'); output_file_hint.append(make_safe_string(std::to_string(d_parameters["satellite_frequency"].get()) + "Hz")); } data_out = std::ofstream(output_file_hint + ".wav", std::ios::binary); d_output_file = output_file_hint + ".wav"; } logger->info("Using input baseband " + d_input_file); logger->info("Demodulating to " + output_file_hint + ".wav"); logger->info("Buffer size : " + std::to_string(d_buffer_size)); time_t lastTime = 0; // Start BaseDemodModule::start(); // res->start(); // qua->start(); // Buffers to wav int16_t *output_wav_buffer = new int16_t[d_buffer_size * 100]; int16_t *output_wav_buffer_resamp = new int16_t[d_buffer_size * 200]; uint64_t final_data_size = 0; dsp::WavWriter wave_writer(data_out); if (output_to_file) wave_writer.write_header(audio_samplerate, 1); std::shared_ptr audio_sink; if (input_data_type != satdump::pipeline::DATA_FILE && audio::has_sink()) { enable_audio = true; audio_sink = audio::get_default_sink(); audio_sink->set_samplerate(audio_samplerate); audio_sink->start(); } ///////////// dsp::RationalResamplerBlock input_resamp(nullptr, d_symbolrate, final_samplerate); dsp::QuadratureDemodBlock quad_demod(nullptr, dsp::hz_to_rad(d_symbolrate / 2, d_symbolrate)); complex_t *work_buffer_complex = dsp::create_volk_buffer(d_buffer_size); float *work_buffer_float = dsp::create_volk_buffer(d_buffer_size); int dat_size = 0; while (demod_should_run()) { dat_size = agc->output_stream->read(); if (dat_size <= 0) { agc->output_stream->flush(); continue; } #if 1 proc_mtx.lock(); if (settings_changed) { if (upcoming_symbolrate > 0) { d_symbolrate = upcoming_symbolrate; input_resamp.set_ratio(d_symbolrate, final_samplerate); quad_demod.set_gain(dsp::hz_to_rad(d_symbolrate / 2, d_symbolrate)); } settings_changed = false; } int nout = input_resamp.process(agc->output_stream->readBuf, dat_size, work_buffer_complex); if (modulation_type == ModulationType::NFM) nout = quad_demod.process(work_buffer_complex, nout, work_buffer_float); else if (modulation_type == ModulationType::AM) volk_32fc_magnitude_32f((float *)work_buffer_float, (lv_32fc_t *)work_buffer_complex, nout); // Into const constellation.pushFloatAndGaussian(work_buffer_float, nout); for (int i = 0; i < nout; i++) { if (work_buffer_float[i] > 1.0f) work_buffer_float[i] = 1.0f; if (work_buffer_float[i] < -1.0f) work_buffer_float[i] = -1.0f; } volk_32f_s32f_convert_16i(output_wav_buffer, (float *)work_buffer_float, 32767, nout); int final_out = audio::AudioSink::resample_s16(output_wav_buffer, output_wav_buffer_resamp, d_symbolrate, audio_samplerate, nout, 1); if (enable_audio && play_audio) audio_sink->push_samples(output_wav_buffer_resamp, final_out); if (output_to_file) { data_out.write((char *)output_wav_buffer_resamp, final_out * sizeof(int16_t)); final_data_size += final_out * sizeof(int16_t); } else { output_fifo->write((uint8_t *)output_wav_buffer_resamp, final_out * sizeof(int16_t)); } proc_mtx.unlock(); #else // Into const constellation.pushFloatAndGaussian(qua->output_stream->readBuf, qua->output_stream->getDataSize()); for (int i = 0; i < dat_size; i++) { if (qua->output_stream->readBuf[i] > 1.0f) qua->output_stream->readBuf[i] = 1.0f; if (qua->output_stream->readBuf[i] < -1.0f) qua->output_stream->readBuf[i] = -1.0f; } volk_32f_s32f_convert_16i(output_wav_buffer, (float *)qua->output_stream->readBuf, 32767, dat_size); int final_out = audio::AudioSink::resample_s16(output_wav_buffer, output_wav_buffer_resamp, d_symbolrate, audio_samplerate, dat_size, 1); if (enable_audio && play_audio) audio_sink->push_samples(output_wav_buffer_resamp, final_out); if (output_data_type == DATA_FILE || record_live_audio) { data_out.write((char *)output_wav_buffer_resamp, final_out * sizeof(int16_t)); final_data_size += final_out * sizeof(int16_t); } else { output_fifo->write((uint8_t *)output_wav_buffer_resamp, final_out * sizeof(int16_t)); } #endif agc->output_stream->flush(); if (input_data_type == satdump::pipeline::DATA_FILE) progress = file_source->getPosition(); 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) + "%%"); } } if (enable_audio) audio_sink->stop(); // Finish up WAV if (output_to_file) { wave_writer.finish_header(final_data_size); data_out.close(); } delete[] output_wav_buffer; delete[] output_wav_buffer_resamp; logger->info("Demodulation finished"); if (input_data_type == satdump::pipeline::DATA_FILE) stop(); } void GenericAnalogDemodModule::stop() { // Stop BaseDemodModule::stop(); // res->stop(); // qua->stop(); agc->output_stream->stopReader(); } void GenericAnalogDemodModule::drawUI(bool window) { ImGui::Begin(name.c_str(), NULL, window ? 0 : NOWINDOW_FLAGS); ImGui::BeginGroup(); constellation.draw(); // Constellation ImGui::EndGroup(); ImGui::SameLine(); ImGui::BeginGroup(); { ImGui::Button("Settings", {200 * ui_scale, 20 * ui_scale}); proc_mtx.lock(); ImGui::SetNextItemWidth(200 * ui_scale); ImGui::InputInt("Bandwidth##bandwidthsetting", &upcoming_symbolrate); ImGui::Checkbox("Record Audio##analogoption", &record_demod_audio); ImGui::RadioButton("NFM##analogoption", reinterpret_cast(&modulation_type), ModulationType::NFM); ImGui::SameLine(); ImGui::RadioButton("AM##analogoption", reinterpret_cast(&modulation_type), ModulationType::AM); style::beginDisabled(); ImGui::RadioButton("WFM##analogoption", false); ImGui::SameLine(); ImGui::RadioButton("USB##analogoption", false); ImGui::RadioButton("LSB##analogoption", false); // ImGui::SameLine(); ImGui::SameLine(); ImGui::RadioButton("CW##analogoption", false); style::endDisabled(); if (ImGui::Button("Set###analogset")) settings_changed = true; proc_mtx.unlock(); ImGui::Button("Signal", {200 * ui_scale, 20 * ui_scale}); /* if (show_freq) { ImGui::Text("Freq : "); ImGui::SameLine(); ImGui::TextColored(style::theme.orange, "%.0f Hz", display_freq); } snr_plot.draw(snr, peak_snr); */ if (!d_is_streaming_input) if (ImGui::Checkbox("Show FFT", &show_fft)) fft_splitter->set_enabled("fft", show_fft); if (enable_audio) { const char *btn_icon, *label; ImVec4 color; if (play_audio) { color = style::theme.green.Value; btn_icon = u8"\uF028##aptaudio"; label = "Audio Playing"; } else { color = style::theme.red.Value; btn_icon = u8"\uF026##aptaudio"; label = "Audio Muted"; } ImGui::PushStyleColor(ImGuiCol_Text, color); if (ImGui::Button(btn_icon)) play_audio = !play_audio; ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextUnformatted(label); } } ImGui::EndGroup(); if (!d_is_streaming_input) ImGui::ProgressBar((double)progress / (double)filesize, ImVec2(ImGui::GetContentRegionAvail().x, 20 * ui_scale)); drawStopButton(); ImGui::End(); drawFFT(); } std::string GenericAnalogDemodModule::make_safe_string(const std::string &str) { std::string safe_str; for (const char c : str) { if (std::isalnum(c)) // character is alphanumeric { safe_str.push_back(c); } else if (safe_str.empty() || safe_str.back() != '_') // replace other characters with underscore { safe_str.push_back('_'); // but only once in a row } } if (!safe_str.empty() && safe_str.back() == '_') // remove trailing underscore { safe_str.pop_back(); } return safe_str; } std::string GenericAnalogDemodModule::getID() { return "generic_analog_demod"; } std::shared_ptr GenericAnalogDemodModule::getInstance(std::string input_file, std::string output_file_hint, nlohmann::json parameters) { return std::make_shared(input_file, output_file_hint, parameters); } } // namespace generic_analog