//========================================================================== // Name: main.cpp // // Purpose: FreeDV main() // Created: Apr. 9, 2012 // Authors: David Rowe, David Witten // // License: // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2.1, // as published by the Free Software Foundation. This program is // distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public // License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, see . // //========================================================================== #include #include #include #include #include #include #include #include #include #include #include "version.h" #include "main.h" #include "os/os_interface.h" #include "freedv_interface.h" #include "audio/AudioEngineFactory.h" #include "codec2_fdmdv.h" #include "pipeline/TxRxThread.h" #include "reporting/pskreporter.h" #include "reporting/FreeDVReporter.h" #include "gui/dialogs/dlg_options.h" #include "gui/dialogs/dlg_filter.h" #include "gui/dialogs/dlg_easy_setup.h" #include "gui/dialogs/freedv_reporter.h" #define wxUSE_FILEDLG 1 #define wxUSE_LIBPNG 1 #define wxUSE_LIBJPEG 1 #define wxUSE_GIF 1 #define wxUSE_PCX 1 #define wxUSE_LIBTIFF 1 extern "C" { extern void golay23_init(void); } //------------------------------------------------------------------- // Bunch of globals used for communication with sound card call // back functions // ------------------------------------------------------------------ // freedv states int g_verbose; int g_Nc; int g_mode; FreeDVInterface freedvInterface; float g_pwr_scale; int g_clip; int g_freedv_verbose; bool g_queueResync; // test Frames int g_testFrames; int g_test_frame_sync_state; int g_test_frame_count; int g_channel_noise; int g_resyncs; float g_sig_pwr_av = 0.0; short *g_error_hist, *g_error_histn; float g_tone_phase; // time averaged magnitude spectrum used for waterfall and spectrum display float g_avmag[MODEM_STATS_NSPEC]; // TX level for attenuation int g_txLevel = 0; // GUI controls that affect rx and tx processes int g_SquelchActive; float g_SquelchLevel; int g_analog; int g_tx; float g_snr; bool g_half_duplex; bool g_voice_keyer_tx; SRC_STATE *g_spec_src; // sample rate converter for spectrum // sending and receiving Call Sign data struct FIFO *g_txDataInFifo; struct FIFO *g_rxDataOutFifo; // tx/rx processing states int g_State, g_prev_State; paCallBackData *g_rxUserdata; int g_dump_timing; int g_dump_fifo_state; time_t g_sync_time; // FIFOs used for plotting waveforms struct FIFO *g_plotDemodInFifo; struct FIFO *g_plotSpeechOutFifo; struct FIFO *g_plotSpeechInFifo; // Soundcard config int g_nSoundCards; // PortAudio over/underflow counters int g_infifo1_full; int g_outfifo1_empty; int g_infifo2_full; int g_outfifo2_empty; int g_AEstatus1[4]; int g_AEstatus2[4]; // playing and recording from sound files extern SNDFILE *g_sfPlayFile; extern bool g_playFileToMicIn; extern bool g_loopPlayFileToMicIn; extern int g_playFileToMicInEventId; extern SNDFILE *g_sfRecFile; extern bool g_recFileFromRadio; extern unsigned int g_recFromRadioSamples; extern int g_recFileFromRadioEventId; extern SNDFILE *g_sfPlayFileFromRadio; extern bool g_playFileFromRadio; extern int g_sfFs; extern int g_sfTxFs; extern bool g_loopPlayFileFromRadio; extern int g_playFileFromRadioEventId; extern SNDFILE *g_sfRecFileFromModulator; extern bool g_recFileFromModulator; extern int g_recFileFromModulatorEventId; extern SNDFILE *g_sfRecMicFile; extern bool g_recFileFromMic; extern bool g_recVoiceKeyerFile; wxWindow *g_parent; // Click to tune rx and tx frequency offset states float g_RxFreqOffsetHz; float g_TxFreqOffsetHz; // experimental mutex to make sound card callbacks mutually exclusive // TODO: review code and see if we need this any more, as fifos should // now be thread safe wxMutex g_mutexProtectingCallbackData(wxMUTEX_RECURSIVE); // TX mode change mutex wxMutex txModeChangeMutex; // End of TX state control bool endingTx; // Option test file to log samples FILE *ftest; // Config file management wxConfigBase *pConfig = NULL; // WxWidgets - initialize the application IMPLEMENT_APP(MainApp); void MainApp::OnInitCmdLine(wxCmdLineParser& parser) { wxApp::OnInitCmdLine(parser); parser.AddOption("f", "config", "Use different configuration file instead of the default."); } bool MainApp::OnCmdLineParsed(wxCmdLineParser& parser) { if (!wxApp::OnCmdLineParsed(parser)) { return false; } wxString configPath; pConfig = wxConfigBase::Get(); if (parser.Found("f", &configPath)) { fprintf(stderr, "Loading configuration from %s\n", (const char*)configPath.ToUTF8()); pConfig = new wxFileConfig(wxT("FreeDV"), wxT("CODEC2-Project"), configPath, configPath, wxCONFIG_USE_LOCAL_FILE); wxConfigBase::Set(pConfig); } pConfig->SetRecordDefaults(); return true; } //------------------------------------------------------------------------- // OnInit() //------------------------------------------------------------------------- bool MainApp::OnInit() { // Initialize locale. m_locale.Init(); m_reporters.clear(); if(!wxApp::OnInit()) { return false; } SetVendorName(wxT("CODEC2-Project")); SetAppName(wxT("FreeDV")); // not needed, it's the default value golay23_init(); m_rTopWindow = wxRect(0, 0, 0, 0); // Create the main application window frame = new MainFrame(NULL); SetTopWindow(frame); // Should guarantee that the first plot tab defined is the one // displayed. But it doesn't when built from command line. Why? frame->m_auiNbookCtrl->ChangeSelection(0); frame->Layout(); frame->Show(); g_parent =frame; return true; } //------------------------------------------------------------------------- // OnExit() //------------------------------------------------------------------------- int MainApp::OnExit() { return 0; } #if defined(FREEDV_MODE_2020) bool MainFrame::test2020HWAllowed_() { bool allowed = true; #if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) // AVX checking code on x86 is here due to LPCNet in binary builds being // compiled to use it. Running the sanity check below could potentially // cause crashes. uint32_t eax, ebx, ecx, edx; eax = ebx = ecx = edx = 0; __cpuid(1, eax, ebx, ecx, edx); if (ecx & (1<<27) && ecx & (1<<28)) { // CPU supports XSAVE and AVX uint32_t xcr0, xcr0_high; asm("xgetbv" : "=a" (xcr0), "=d" (xcr0_high) : "c" (0)); allowed = (xcr0 & 6) == 6; // AVX state saving enabled? } else { allowed = false; } #endif // defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) return allowed; } //------------------------------------------------------------------------- // test2020Mode_(): Makes sure that 2020 mode will work //------------------------------------------------------------------------- void MainFrame::test2020Mode_() { printf("Making sure your machine can handle 2020 mode:\n"); bool allowed = false; #if !defined(LPCNET_DISABLED) allowed = test2020HWAllowed_(); wxGetApp().appConfiguration.freedvAVXSupported = allowed; if (!allowed) { std::cout << "Warning: AVX support not found!" << std::endl; } else { // Sanity check: encode 1 second of 16 kHz white noise and then try to // decode it. If it takes longer than 0.5 seconds, it's unlikely that // 2020/2020B will work properly on this machine. printf("Generating test audio...\n"); struct FIFO* inFifo = codec2_fifo_create(24000); assert(inFifo != nullptr); struct freedv* fdv = freedv_open(FREEDV_MODE_2020); assert(fdv != nullptr); int numInSamples = 0; int samplesToGenerate = freedv_get_n_speech_samples(fdv); int samplesGenerated = freedv_get_n_nom_modem_samples(fdv); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> distrib(SHRT_MIN, SHRT_MAX); while (numInSamples < 16000) { short inSamples[samplesToGenerate]; COMP outSamples[samplesGenerated]; for (int index = 0; index < samplesToGenerate; index++) { inSamples[index] = distrib(gen); } freedv_comptx(fdv, outSamples, inSamples); for (int index = 0; index < samplesGenerated; index++) { short realVal = outSamples[index].real; codec2_fifo_write(inFifo, &realVal, 1); } numInSamples += samplesToGenerate; } printf("Decoding modulated audio...\n"); std::chrono::high_resolution_clock systemClock; auto startTime = systemClock.now(); int nin = freedv_nin(fdv); short inputBuf[freedv_get_n_max_modem_samples(fdv)]; short outputBuf[freedv_get_n_speech_samples(fdv)]; COMP rx_fdm[freedv_get_n_max_modem_samples(fdv)]; while(codec2_fifo_read(inFifo, inputBuf, nin) == 0) { for(int i=0; i(endTime - startTime); if (timeTaken > std::chrono::milliseconds(600)) { allowed = false; } std::cout << "One second of 2020 decoded in " << timeTaken.count() << " ms" << std::endl; } #endif // !defined(LPCNET_DISABLED) std::cout << "2020 allowed: " << allowed << std::endl; // Save results to configuration. wxGetApp().appConfiguration.freedv2020Allowed = allowed; } #endif // defined(FREEDV_MODE_2020) //------------------------------------------------------------------------- // loadConfiguration_(): Loads or sets default configuration options. //------------------------------------------------------------------------- void MainFrame::loadConfiguration_() { wxGetApp().appConfiguration.load(pConfig); // restore frame position and size int x = wxGetApp().appConfiguration.mainWindowLeft; int y = wxGetApp().appConfiguration.mainWindowTop; int w = wxGetApp().appConfiguration.mainWindowWidth; int h = wxGetApp().appConfiguration.mainWindowHeight; // sanitise frame position as a first pass at Win32 registry bug if (x < 0 || x > 2048) x = 20; if (y < 0 || y > 2048) y = 20; if (w < 0 || w > 2048) w = 800; if (h < 0 || h > 2048) h = 780; g_SquelchActive = wxGetApp().appConfiguration.squelchActive; g_SquelchLevel = wxGetApp().appConfiguration.squelchLevel; g_SquelchLevel /= 2.0; Move(x, y); wxSize size = GetMinSize(); if (w < size.GetWidth()) w = size.GetWidth(); if (h < size.GetHeight()) h = size.GetHeight(); // XXX - with really short windows, wxWidgets sometimes doesn't size // the components properly until the user resizes the window (even if only // by a pixel or two). As a really hacky workaround, we emulate this behavior // when restoring window sizing. These resize events also happen after configuration // is restored but I'm not sure this is necessary. CallAfter([=]() { SetSize(w, h); }); CallAfter([=]() { SetSize(w, h - 1); }); CallAfter([=]() { SetSize(w, h); }); g_txLevel = wxGetApp().appConfiguration.transmitLevel; char fmt[15]; m_sliderTxLevel->SetValue(g_txLevel); snprintf(fmt, 15, "%0.1f dB", (double)g_txLevel / 10.0); wxString fmtString(fmt); m_txtTxLevelNum->SetLabel(fmtString); // Adjust frequency entry labels if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { m_freqBox->SetLabel(_("Report Freq. (kHz)")); } else { m_freqBox->SetLabel(_("Report Freq. (MHz)")); } // PTT ------------------------------------------------------------------- // Note: we're no longer using RigName but we need to bring over the old data // for backwards compatibility. if (wxGetApp().appConfiguration.rigControlConfiguration.hamlibRigName == wxT("")) { wxGetApp().m_intHamlibRig = pConfig->ReadLong("/Hamlib/RigName", 0); wxGetApp().appConfiguration.rigControlConfiguration.hamlibRigName = HamlibRigController::RigIndexToName(wxGetApp().m_intHamlibRig); } else { wxGetApp().m_intHamlibRig = HamlibRigController::RigNameToIndex(std::string(wxGetApp().appConfiguration.rigControlConfiguration.hamlibRigName->ToUTF8())); } // ----------------------------------------------------------------------- wxGetApp().m_FreeDV700Combine = 1; g_verbose = wxGetApp().appConfiguration.debugVerbose; g_freedv_verbose = wxGetApp().appConfiguration.apiVerbose; wxGetApp().m_attn_carrier_en = 0; wxGetApp().m_attn_carrier = 0; wxGetApp().m_tone = 0; wxGetApp().m_tone_freq_hz = 1000; wxGetApp().m_tone_amplitude = 500; // General reporting parameters // wxString::Format() doesn't respect locale but C++ iomanip should. Use the latter instead. if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency > 0) { double freqFactor = 1000.0; if (!wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { freqFactor *= 1000.0; } double freq = ((double)wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency) / freqFactor; std::stringstream ss; std::locale loc(""); ss.imbue(loc); if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { ss << std::fixed << std::setprecision(1) << freq; } else { ss << std::fixed << std::setprecision(4) << freq; } std::string sVal = ss.str(); m_cboReportFrequency->SetValue(sVal); } int defaultMode = wxGetApp().appConfiguration.currentFreeDVMode.getDefaultVal(); int mode = wxGetApp().appConfiguration.currentFreeDVMode; setDefaultMode: if (mode == 0) m_rb1600->SetValue(1); if (mode == 3) m_rb700c->SetValue(1); if (mode == 4) m_rb700d->SetValue(1); if (mode == 5) m_rb700e->SetValue(1); if (mode == 6) m_rb800xa->SetValue(1); // mode 7 was the former 2400B mode, now removed. if ((mode == 9) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported) m_rb2020->SetValue(1); else if (mode == 9) { // Default to 700D otherwise mode = defaultMode; goto setDefaultMode; } #if defined(FREEDV_MODE_2020B) if ((mode == 10) && wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported) m_rb2020b->SetValue(1); else if (mode == 10) { // Default to 700D otherwise mode = defaultMode; goto setDefaultMode; } #endif // defined(FREEDV_MODE_2020B) pConfig->SetPath(wxT("/")); // Set initial state of additional modes. switch(mode) { case 0: case 4: case 5: // 700D/E and 1600; don't expand additional modes break; default: m_collpane->Collapse(false); wxCollapsiblePaneEvent evt; OnChangeCollapseState(evt); break; } m_togBtnAnalog->Disable(); m_btnTogPTT->Disable(); m_togBtnVoiceKeyer->Disable(); // squelch settings char sqsnr[15]; m_sliderSQ->SetValue((int)((g_SquelchLevel+5.0)*2.0)); snprintf(sqsnr, 15, "%4.1f dB", g_SquelchLevel); wxString sqsnr_string(sqsnr); m_textSQ->SetLabel(sqsnr_string); m_ckboxSQ->SetValue(g_SquelchActive); // SNR settings m_ckboxSNR->SetValue(wxGetApp().appConfiguration.snrSlow); setsnrBeta(wxGetApp().appConfiguration.snrSlow); // Show/hide frequency box based on reporting enablement m_freqBox->Show(wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled); // Show/hide callsign combo box based on reporting enablement if (wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { m_cboLastReportedCallsigns->Show(); m_txtCtrlCallSign->Hide(); m_cboLastReportedCallsigns->Enable(m_lastReportedCallsignListView->GetItemCount() > 0); } else { m_cboLastReportedCallsigns->Hide(); m_txtCtrlCallSign->Show(); } // Ensure that sound card count is correct. Otherwise the Audio Options won't show // the correct devices prior to start. bool hasSoundCard1InDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName != "none"; bool hasSoundCard1OutDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName != "none"; bool hasSoundCard2InDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName != "none"; bool hasSoundCard2OutDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName != "none"; g_nSoundCards = 0; if (hasSoundCard1InDevice && hasSoundCard1OutDevice) { g_nSoundCards = 1; if (hasSoundCard2InDevice && hasSoundCard2OutDevice) g_nSoundCards = 2; } // Update the reporting list as needed. updateReportingFreqList_(); // Relayout window so that the changes can take effect. auto currentSizer = m_panel->GetSizer(); m_panel->SetSizerAndFit(currentSizer, false); m_panel->Layout(); // Load default voice keyer file as current. if (wxGetApp().appConfiguration.voiceKeyerWaveFile != "") { wxFileName fullVKPath(wxGetApp().appConfiguration.voiceKeyerWaveFilePath, wxGetApp().appConfiguration.voiceKeyerWaveFile); vkFileName_ = fullVKPath.GetFullPath().mb_str(); m_togBtnVoiceKeyer->SetToolTip(_("Toggle Voice Keyer using file ") + wxGetApp().appConfiguration.voiceKeyerWaveFile + _(". Right-click for additional options.")); wxString fileNameWithoutExt; wxFileName::SplitPath(wxGetApp().appConfiguration.voiceKeyerWaveFile, nullptr, &fileNameWithoutExt, nullptr); setVoiceKeyerButtonLabel_(fileNameWithoutExt); } else { vkFileName_ = ""; } if (wxGetApp().appConfiguration.experimentalFeatures && wxGetApp().appConfiguration.tabLayout != "") { ((TabFreeAuiNotebook*)m_auiNbookCtrl)->LoadPerspective(wxGetApp().appConfiguration.tabLayout); const_cast(m_auiNbookCtrl->GetAuiManager()).Update(); } // Initialize FreeDV Reporter as required initializeFreeDVReporter_(); // If the FreeDV Reporter window was open on last execution, reopen it now. CallAfter([&]() { if (wxGetApp().appConfiguration.reporterWindowVisible) { wxCommandEvent event; OnToolsFreeDVReporter(event); } }); } //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= // Class MainFrame(wxFrame* pa->ent) : TopFrame(parent) //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-= MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent, wxID_ANY, _("FreeDV ") + _(FREEDV_VERSION)) { #if defined(__linux__) pthread_setname_np(pthread_self(), "FreeDV GUI"); #endif // defined(__linux__) m_reporterDialog = nullptr; m_filterDialog = nullptr; m_zoom = 1.; suppressFreqModeUpdates_ = false; tools->AppendSeparator(); wxMenuItem* m_menuItemToolsConfigDelete; m_menuItemToolsConfigDelete = new wxMenuItem(tools, wxID_ANY, wxString(_("&Restore defaults")) , wxT("Delete config file/keys and restore defaults"), wxITEM_NORMAL); this->Connect(m_menuItemToolsConfigDelete->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainFrame::OnDeleteConfig)); this->Connect(m_menuItemToolsConfigDelete->GetId(), wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnDeleteConfigUI)); tools->Append(m_menuItemToolsConfigDelete); // Add Waterfall Plot window m_panelWaterfall = new PlotWaterfall((wxFrame*) m_auiNbookCtrl, false, 0); m_panelWaterfall->SetToolTip(_("Double-click to tune")); m_auiNbookCtrl->AddPage(m_panelWaterfall, _("Waterfall"), true, wxNullBitmap); // Add Spectrum Plot window wxPanel* spectrumPanel = new wxPanel(m_auiNbookCtrl); wxFlexGridSizer* spectrumPanelSizer = new wxFlexGridSizer(2, 1, 5, 5); wxBoxSizer* spectrumPanelControlSizer = new wxBoxSizer(wxHORIZONTAL); spectrumPanelSizer->AddGrowableRow(0); spectrumPanelSizer->AddGrowableCol(0); // Actual Spectrum plot m_panelSpectrum = new PlotSpectrum(spectrumPanel, g_avmag, MODEM_STATS_NSPEC*((float)MAX_F_HZ/MODEM_STATS_MAX_F_HZ)); m_panelSpectrum->SetToolTip(_("Double-click to tune")); spectrumPanelSizer->Add(m_panelSpectrum, 0, wxALL | wxEXPAND, 5); // Spectrum plot control interface wxStaticText* labelAveraging = new wxStaticText(spectrumPanel, wxID_ANY, wxT("Average across"), wxDefaultPosition, wxDefaultSize, 0); spectrumPanelControlSizer->Add(labelAveraging, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); wxString samplingChoices[] = { "1", "2", "3" }; m_cbxNumSpectrumAveraging = new wxComboBox(spectrumPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 3, samplingChoices, wxCB_DROPDOWN | wxCB_READONLY); m_cbxNumSpectrumAveraging->SetSelection(wxGetApp().appConfiguration.currentSpectrumAveraging); spectrumPanelControlSizer->Add(m_cbxNumSpectrumAveraging, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); m_cbxNumSpectrumAveraging->Connect(wxEVT_TEXT, wxCommandEventHandler(MainFrame::OnAveragingChange), NULL, this); wxStaticText* labelSamples = new wxStaticText(spectrumPanel, wxID_ANY, wxT("sample(s)"), wxDefaultPosition, wxDefaultSize, 0); spectrumPanelControlSizer->Add(labelSamples, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); spectrumPanelSizer->Add(spectrumPanelControlSizer, 0, wxALL | wxEXPAND, 5); spectrumPanel->SetSizerAndFit(spectrumPanelSizer); m_auiNbookCtrl->AddPage(spectrumPanel, _("Spectrum"), true, wxNullBitmap); // Add Scatter Plot window m_panelScatter = new PlotScatter((wxFrame*) m_auiNbookCtrl); m_auiNbookCtrl->AddPage(m_panelScatter, _("Scatter"), true, wxNullBitmap); // Add Demod Input window m_panelDemodIn = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelDemodIn, _("Frm Radio"), true, wxNullBitmap); g_plotDemodInFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); // Add Speech Input window m_panelSpeechIn = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelSpeechIn, _("Frm Mic"), true, wxNullBitmap); g_plotSpeechInFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); // Add Speech Output window m_panelSpeechOut = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, WAVEFORM_PLOT_TIME, 1.0/WAVEFORM_PLOT_FS, -1, 1, 1, 0.2, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelSpeechOut, _("To Spkr/Hdphns"), true, wxNullBitmap); g_plotSpeechOutFifo = codec2_fifo_create(4*WAVEFORM_PLOT_BUF); // Add Timing Offset window m_panelTimeOffset = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, 5.0, DT, -0.5, 0.5, 1, 0.1, "%2.1f", 0); m_auiNbookCtrl->AddPage(m_panelTimeOffset, L"Timing \u0394", true, wxNullBitmap); // Add Frequency Offset window m_panelFreqOffset = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, 5.0, DT, -200, 200, 1, 50, "%3.0fHz", 0); m_auiNbookCtrl->AddPage(m_panelFreqOffset, L"Frequency \u0394", true, wxNullBitmap); // Add Test Frame Errors window m_panelTestFrameErrors = new PlotScalar((wxFrame*) m_auiNbookCtrl, 2*MODEM_STATS_NC_MAX, 30.0, DT, 0, 2*MODEM_STATS_NC_MAX+2, 1, 1, "", 1); m_auiNbookCtrl->AddPage(m_panelTestFrameErrors, L"Test Frame Errors", true, wxNullBitmap); // Add Test Frame Histogram window. 1 column for every bit, 2 bits per carrier m_panelTestFrameErrorsHist = new PlotScalar((wxFrame*) m_auiNbookCtrl, 1, 1.0, 1.0/(2*MODEM_STATS_NC_MAX), 0.001, 0.1, 1.0/MODEM_STATS_NC_MAX, 0.1, "%0.0E", 0); m_auiNbookCtrl->AddPage(m_panelTestFrameErrorsHist, L"Test Frame Histogram", true, wxNullBitmap); m_panelTestFrameErrorsHist->setBarGraph(1); m_panelTestFrameErrorsHist->setLogY(1); // this->Connect(m_menuItemHelpUpdates->GetId(), wxEVT_UPDATE_UI, wxUpdateUIEventHandler(TopFrame::OnHelpCheckUpdatesUI)); m_togBtnOnOff->Connect(wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnTogBtnOnOffUI), NULL, this); m_togBtnAnalog->Connect(wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnTogBtnAnalogClickUI), NULL, this); // m_btnTogPTT->Connect(wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnTogBtnPTT_UI), NULL, this); loadConfiguration_(); #ifdef _USE_TIMER Bind(wxEVT_TIMER, &MainFrame::OnTimer, this); // ID_MY_WINDOW); m_plotTimer.SetOwner(this, ID_TIMER_WATERFALL); m_pskReporterTimer.SetOwner(this, ID_TIMER_PSKREPORTER); m_updFreqStatusTimer.SetOwner(this,ID_TIMER_UPD_FREQ); //[UP] //m_panelWaterfall->Refresh(); #endif // Create voice keyer popup menu. voiceKeyerPopupMenu_ = new wxMenu(); assert(voiceKeyerPopupMenu_ != nullptr); auto chooseVKFileMenuItem = voiceKeyerPopupMenu_->Append(wxID_ANY, _("&Use another voice keyer file...")); voiceKeyerPopupMenu_->Connect( chooseVKFileMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainFrame::OnChooseAlternateVoiceKeyerFile), NULL, this); auto recordNewVoiceKeyerFileMenuItem = voiceKeyerPopupMenu_->Append(wxID_ANY, _("&Record new voice keyer file...")); voiceKeyerPopupMenu_->Connect( recordNewVoiceKeyerFileMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainFrame::OnRecordNewVoiceKeyerFile), NULL, this); voiceKeyerPopupMenu_->AppendSeparator(); auto monitorVKMenuItem = voiceKeyerPopupMenu_->AppendCheckItem(wxID_ANY, _("Monitor transmitted audio")); voiceKeyerPopupMenu_->Check(monitorVKMenuItem->GetId(), wxGetApp().appConfiguration.monitorVoiceKeyerAudio); voiceKeyerPopupMenu_->Connect( monitorVKMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainFrame::OnSetMonitorVKAudio), NULL, this); // Create PTT popup menu pttPopupMenu_ = new wxMenu(); assert(pttPopupMenu_ != nullptr); auto monitorMenuItem = pttPopupMenu_->AppendCheckItem(wxID_ANY, _("Monitor transmitted audio")); pttPopupMenu_->Check(monitorMenuItem->GetId(), wxGetApp().appConfiguration.monitorTxAudio); pttPopupMenu_->Connect( monitorMenuItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainFrame::OnSetMonitorTxAudio), NULL, this); m_RxRunning = false; m_txThread = nullptr; m_rxThread = nullptr; wxGetApp().linkStep = nullptr; #ifdef _USE_ONIDLE Connect(wxEVT_IDLE, wxIdleEventHandler(MainFrame::OnIdle), NULL, this); #endif //_USE_ONIDLE g_sfPlayFile = NULL; g_playFileToMicIn = false; g_loopPlayFileToMicIn = false; g_sfRecFile = NULL; g_recFileFromRadio = false; g_sfPlayFileFromRadio = NULL; g_playFileFromRadio = false; g_loopPlayFileFromRadio = false; g_sfRecFileFromModulator = NULL; g_recFileFromModulator = false; g_sfRecMicFile = nullptr; g_recFileFromMic = false; g_recVoiceKeyerFile = false; // init click-tune states g_RxFreqOffsetHz = 0.0; m_panelWaterfall->setRxFreq(FDMDV_FCENTRE - g_RxFreqOffsetHz); m_panelSpectrum->setRxFreq(FDMDV_FCENTRE - g_RxFreqOffsetHz); g_TxFreqOffsetHz = 0.0; g_tx = 0; // data states g_txDataInFifo = codec2_fifo_create(MAX_CALLSIGN*FREEDV_VARICODE_MAX_BITS); g_rxDataOutFifo = codec2_fifo_create(MAX_CALLSIGN*FREEDV_VARICODE_MAX_BITS); sox_biquad_start(); g_testFrames = 0; g_test_frame_sync_state = 0; g_resyncs = 0; wxGetApp().m_testFrames = false; wxGetApp().m_channel_noise = false; g_tone_phase = 0.0; optionsDlg = new OptionsDlg(NULL); m_schedule_restore = false; vk_state = VK_IDLE; m_timeSinceSyncLoss = 0; // Init optional Windows debug console so we can see all those printfs #ifdef __WXMSW__ if (wxGetApp().appConfiguration.debugConsoleEnabled || wxGetApp().appConfiguration.firstTimeUse) { // somewhere to send printfs while developing int ret = AllocConsole(); freopen("CONOUT$", "w", stdout); freopen("CONOUT$", "w", stderr); fprintf(stderr, "AllocConsole: %d m_debug_console: %d\n", ret, wxGetApp().appConfiguration.debugConsoleEnabled.get()); } #endif #if defined(FREEDV_MODE_2020) && !defined(LPCNET_DISABLED) // First time use: make sure 2020 mode will actually work on this machine. if (wxGetApp().appConfiguration.firstTimeUse) { test2020Mode_(); } else { wxGetApp().appConfiguration.freedvAVXSupported = test2020HWAllowed_(); } #else // Disable LPCNet if not compiled in. wxGetApp().appConfiguration.freedv2020Allowed = false; #endif // defined(FREEDV_MODE_2020) && !defined(LPCNET_DISABLED) if(!wxGetApp().appConfiguration.freedv2020Allowed || !wxGetApp().appConfiguration.freedvAVXSupported) { m_rb2020->Disable(); #if defined(FREEDV_MODE_2020B) m_rb2020b->Disable(); #endif // FREEDV_MODE_2020B } if (wxGetApp().appConfiguration.firstTimeUse) { // Initial setup. Display Easy Setup dialog. CallAfter([&]() { EasySetupDialog* dlg = new EasySetupDialog(this); if (dlg->ShowModal() == wxOK) { // Show/hide frequency box based on PSK Reporter status. m_freqBox->Show(wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled); // Show/hide callsign combo box based on PSK Reporter Status if (wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { m_cboLastReportedCallsigns->Show(); m_txtCtrlCallSign->Hide(); } else { m_cboLastReportedCallsigns->Hide(); m_txtCtrlCallSign->Show(); } // Relayout window so that the changes can take effect. m_panel->Layout(); } }); } wxGetApp().appConfiguration.firstTimeUse = false; //#define FTEST #ifdef FTEST ftest = fopen("ftest.raw", "wb"); assert(ftest != NULL); #endif /* experimental checkbox control of thread priority, used to helpo debug 700D windows sound break up */ wxGetApp().m_txRxThreadHighPriority = true; g_dump_timing = g_dump_fifo_state = 0; } //------------------------------------------------------------------------- // ~MainFrame() //------------------------------------------------------------------------- MainFrame::~MainFrame() { delete voiceKeyerPopupMenu_; int x; int y; int w; int h; if (m_filterDialog != nullptr) { m_filterDialog->Close(); } if (m_reporterDialog != nullptr) { // wxWidgets doesn't fire wxEVT_MOVE events on Linux for some // reason, so we need to grab and save the current position again. auto pos = m_reporterDialog->GetPosition(); wxGetApp().appConfiguration.reporterWindowLeft = pos.x; wxGetApp().appConfiguration.reporterWindowTop = pos.y; m_reporterDialog->setReporter(nullptr); m_reporterDialog->Close(); m_reporterDialog->Destroy(); m_reporterDialog = nullptr; } //fprintf(stderr, "MainFrame::~MainFrame()\n"); #ifdef FTEST fclose(ftest); #endif wxGetApp().rigPttController = nullptr; wxGetApp().rigFrequencyController = nullptr; wxGetApp().m_pttInSerialPort = nullptr; if (!IsIconized()) { GetSize(&w, &h); GetPosition(&x, &y); wxGetApp().appConfiguration.mainWindowLeft = x; wxGetApp().appConfiguration.mainWindowTop = y; wxGetApp().appConfiguration.mainWindowWidth = w; wxGetApp().appConfiguration.mainWindowHeight = h; } if (wxGetApp().appConfiguration.experimentalFeatures) { wxGetApp().appConfiguration.tabLayout = ((TabFreeAuiNotebook*)m_auiNbookCtrl)->SavePerspective(); } wxGetApp().appConfiguration.squelchActive = g_SquelchActive; wxGetApp().appConfiguration.squelchLevel = (int)(g_SquelchLevel*2.0); wxGetApp().appConfiguration.transmitLevel = g_txLevel; int mode; if (m_rb1600->GetValue()) mode = 0; if (m_rb700c->GetValue()) mode = 3; if (m_rb700d->GetValue()) mode = 4; if (m_rb700e->GetValue()) mode = 5; if (m_rb800xa->GetValue()) mode = 6; if (m_rb2020->GetValue()) mode = 9; #if defined(FREEDV_MODE_2020B) if (m_rb2020b->GetValue()) mode = 10; #endif // defined(FREEDV_MODE_2020B) wxGetApp().appConfiguration.currentFreeDVMode = mode; wxGetApp().appConfiguration.save(pConfig); m_cbxNumSpectrumAveraging->Disconnect(wxEVT_TEXT, wxCommandEventHandler(MainFrame::OnAveragingChange), NULL, this); m_togBtnOnOff->Disconnect(wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnTogBtnOnOffUI), NULL, this); m_togBtnAnalog->Disconnect(wxEVT_UPDATE_UI, wxUpdateUIEventHandler(MainFrame::OnTogBtnAnalogClickUI), NULL, this); if (m_RxRunning) { stopRxStream(); } sox_biquad_finish(); if (g_sfPlayFile != NULL) { sf_close(g_sfPlayFile); g_sfPlayFile = NULL; } if (g_sfRecFile != NULL) { sf_close(g_sfRecFile); g_sfRecFile = NULL; } if (g_sfRecFileFromModulator != NULL) { sf_close(g_sfRecFileFromModulator); g_sfRecFileFromModulator = NULL; } #ifdef _USE_TIMER if(m_pskReporterTimer.IsRunning()) { m_pskReporterTimer.Stop(); } if(m_plotTimer.IsRunning()) { m_plotTimer.Stop(); Unbind(wxEVT_TIMER, &MainFrame::OnTimer, this); } #endif //_USE_TIMER #ifdef _USE_ONIDLE Disconnect(wxEVT_IDLE, wxIdleEventHandler(MainFrame::OnIdle), NULL, this); #endif // _USE_ONIDLE if (optionsDlg != NULL) { delete optionsDlg; optionsDlg = NULL; } wxGetApp().rigFrequencyController = nullptr; wxGetApp().rigPttController = nullptr; wxGetApp().m_reporters.clear(); } #ifdef _USE_ONIDLE void MainFrame::OnIdle(wxIdleEvent &evt) { } #endif void MainFrame::OnAveragingChange(wxCommandEvent& event) { wxGetApp().appConfiguration.currentSpectrumAveraging = m_cbxNumSpectrumAveraging->GetSelection(); } #ifdef _USE_TIMER //---------------------------------------------------------------- // OnTimer() // // when the timer fires every DT seconds we update the GUI displays. // the tabs only the plot that is visible actually gets updated, this // keeps CPU load reasonable //---------------------------------------------------------------- void MainFrame::OnTimer(wxTimerEvent &evt) { if (!m_RxRunning) { return; } if (evt.GetTimer().GetId() == ID_TIMER_PSKREPORTER) { // Reporter timer fired; send in-progress packet. for (auto& obj : wxGetApp().m_reporters) { obj->send(); } } else if (evt.GetTimer().GetId() == ID_TIMER_UPD_FREQ) { // show freq. and mode [UP] if (wxGetApp().rigFrequencyController && wxGetApp().rigFrequencyController->isConnected()) { if (g_verbose) fprintf(stderr, "update freq and mode ....\n"); wxGetApp().rigFrequencyController->requestCurrentFrequencyMode(); } } else { int r,c; if (m_panelWaterfall->checkDT()) { m_panelWaterfall->setRxFreq(FDMDV_FCENTRE - g_RxFreqOffsetHz); m_panelWaterfall->m_newdata = true; m_panelWaterfall->setColor(wxGetApp().appConfiguration.waterfallColor); m_panelWaterfall->addOffset(freedvInterface.getCurrentRxModemStats()->foff); m_panelWaterfall->setSync(freedvInterface.getSync() ? true : false); m_panelWaterfall->Refresh(); } m_panelSpectrum->setRxFreq(FDMDV_FCENTRE - g_RxFreqOffsetHz); // Note: each element in this combo box is a numeric value starting from 1, // so just incrementing the selected index should get us the correct results. m_panelSpectrum->setNumAveraging(m_cbxNumSpectrumAveraging->GetSelection() + 1); m_panelSpectrum->addOffset(freedvInterface.getCurrentRxModemStats()->foff); m_panelSpectrum->setSync(freedvInterface.getSync() ? true : false); m_panelSpectrum->m_newdata = true; m_panelSpectrum->Refresh(); /* update scatter/eye plot ------------------------------------------------------------*/ if (freedvInterface.isRunning()) { int currentMode = freedvInterface.getCurrentMode(); if (currentMode != wxGetApp().m_prevMode) { // Force recreation of EQ filters. m_newMicInFilter = true; m_newSpkOutFilter = true; // The receive mode changed, so the previous samples are no longer valid. m_panelScatter->clearCurrentSamples(); } wxGetApp().m_prevMode = currentMode; if (currentMode == FREEDV_MODE_800XA) { /* FSK Mode - eye diagram ---------------------------------------------------------*/ /* add samples row by row */ int i; for (i=0; ineyetr; i++) { m_panelScatter->add_new_samples_eye(&freedvInterface.getCurrentRxModemStats()->rx_eye[i][0], freedvInterface.getCurrentRxModemStats()->neyesamp); } } else { // Reset g_Nc accordingly. switch(currentMode) { case FREEDV_MODE_1600: g_Nc = 16; m_panelScatter->setNc(g_Nc+1); /* +1 for BPSK pilot */ break; case FREEDV_MODE_700C: /* m_FreeDV700Combine may have changed at run time */ g_Nc = 14; if (wxGetApp().m_FreeDV700Combine) { m_panelScatter->setNc(g_Nc/2); /* diversity combnation */ } else { m_panelScatter->setNc(g_Nc); } break; case FREEDV_MODE_700D: case FREEDV_MODE_700E: g_Nc = 17; m_panelScatter->setNc(g_Nc); break; case FREEDV_MODE_2020: #if defined(FREEDV_MODE_2020B) case FREEDV_MODE_2020B: #endif // FREEDV_MODE_2020B g_Nc = 31; m_panelScatter->setNc(g_Nc); break; } /* PSK Modes - scatter plot -------------------------------------------------------*/ for (r=0; rnr; r++) { if ((currentMode == FREEDV_MODE_1600) || (currentMode == FREEDV_MODE_700D) || (currentMode == FREEDV_MODE_700E) || (currentMode == FREEDV_MODE_2020) #if defined(FREEDV_MODE_2020B) || (currentMode == FREEDV_MODE_2020B) #endif // FREEDV_MODE_2020B ) { m_panelScatter->add_new_samples_scatter(&freedvInterface.getCurrentRxModemStats()->rx_symbols[r][0]); } else if (currentMode == FREEDV_MODE_700C) { if (wxGetApp().m_FreeDV700Combine) { /* FreeDV 700C uses diversity, so optionally combine symbols for scatter plot, as combined symbols are used for demodulation. Note we need to use a copy of the symbols, as we are not sure when the stats will be updated. */ COMP rx_symbols_copy[g_Nc/2]; for(c=0; crx_symbols[r][c], freedvInterface.getCurrentRxModemStats()->rx_symbols[r][c+g_Nc/2])); m_panelScatter->add_new_samples_scatter(rx_symbols_copy); } else { /* Sometimes useful to plot carriers separately, e.g. to determine if tx carrier power is constant across carriers. */ m_panelScatter->add_new_samples_scatter(&freedvInterface.getCurrentRxModemStats()->rx_symbols[r][0]); } } } } } m_panelScatter->Refresh(); // Oscilloscope type speech plots ------------------------------------------------------- short speechInPlotSamples[WAVEFORM_PLOT_BUF]; if (codec2_fifo_read(g_plotSpeechInFifo, speechInPlotSamples, WAVEFORM_PLOT_BUF)) { memset(speechInPlotSamples, 0, WAVEFORM_PLOT_BUF*sizeof(short)); //fprintf(stderr, "empty!\n"); } m_panelSpeechIn->add_new_short_samples(0, speechInPlotSamples, WAVEFORM_PLOT_BUF, 32767); m_panelSpeechIn->Refresh(); short speechOutPlotSamples[WAVEFORM_PLOT_BUF]; if (codec2_fifo_read(g_plotSpeechOutFifo, speechOutPlotSamples, WAVEFORM_PLOT_BUF)) memset(speechOutPlotSamples, 0, WAVEFORM_PLOT_BUF*sizeof(short)); m_panelSpeechOut->add_new_short_samples(0, speechOutPlotSamples, WAVEFORM_PLOT_BUF, 32767); m_panelSpeechOut->Refresh(); short demodInPlotSamples[WAVEFORM_PLOT_BUF]; if (codec2_fifo_read(g_plotDemodInFifo, demodInPlotSamples, WAVEFORM_PLOT_BUF)) { memset(demodInPlotSamples, 0, WAVEFORM_PLOT_BUF*sizeof(short)); } m_panelDemodIn->add_new_short_samples(0,demodInPlotSamples, WAVEFORM_PLOT_BUF, 32767); m_panelDemodIn->Refresh(); // Demod states ----------------------------------------------------------------------- m_panelTimeOffset->add_new_sample(0, (float)freedvInterface.getCurrentRxModemStats()->rx_timing/FDMDV_NOM_SAMPLES_PER_FRAME); m_panelTimeOffset->Refresh(); m_panelFreqOffset->add_new_sample(0, freedvInterface.getCurrentRxModemStats()->foff); m_panelFreqOffset->Refresh(); // SNR text box and gauge ------------------------------------------------------------ // LP filter freedvInterface.getCurrentRxModemStats()->snr_est some more to stabilise the // display. freedvInterface.getCurrentRxModemStats()->snr_est already has some low pass filtering // but we need it fairly fast to activate squelch. So we // optionally perform some further filtering for the display // version of SNR. The "Slow" checkbox controls the amount of // filtering. The filtered snr also controls the squelch float snr_limited; // some APIs pass us invalid values, so lets trap it rather than bombing if (!(isnan(freedvInterface.getCurrentRxModemStats()->snr_est) || isinf(freedvInterface.getCurrentRxModemStats()->snr_est))) { g_snr = m_snrBeta*g_snr + (1.0 - m_snrBeta)*freedvInterface.getCurrentRxModemStats()->snr_est; } snr_limited = g_snr; if (snr_limited < -5.0) snr_limited = -5.0; if (snr_limited > 20.0) snr_limited = 20.0; char snr[15]; snprintf(snr, 15, "%4.1f dB", g_snr); //fprintf(stderr, "g_mode: %d snr_est: %f m_snrBeta: %f g_snr: %f snr_limited: %f\n", g_mode, g_stats.snr_est, m_snrBeta, g_snr, snr_limited); wxString snr_string(snr); m_textSNR->SetLabel(snr_string); m_gaugeSNR->SetValue((int)(snr_limited+5)); // Level Gauge ----------------------------------------------------------------------- float tooHighThresh; if (!g_tx && m_RxRunning) { // receive mode - display From Radio peaks // peak from this DT sampling period int maxDemodIn = 0; for(int i=0; i m_maxLevel) m_maxLevel = maxDemodIn; tooHighThresh = FROM_RADIO_MAX; } else { // transmit mode - display From Mic peaks // peak from this DT sampling period int maxSpeechIn = 0; for(int i=0; i m_maxLevel) m_maxLevel = maxSpeechIn; tooHighThresh = FROM_MIC_MAX; } // Peak Reading meter: updates peaks immediately, then slowly decays int maxScaled = (int)(100.0 * ((float)m_maxLevel/32767.0)); m_gaugeLevel->SetValue(maxScaled); //printf("maxScaled: %d\n", maxScaled); if (((float)maxScaled/100) > tooHighThresh) m_textLevel->SetLabel("Too High"); else m_textLevel->SetLabel(""); m_maxLevel *= LEVEL_BETA; // sync LED (Colours don't work on Windows) ------------------------ //fprintf(stderr, "g_State: %d m_rbSync->GetValue(): %d\n", g_State, m_rbSync->GetValue()); if (m_textSync->IsEnabled()) { auto oldColor = m_textSync->GetForegroundColour(); wxColour newColor = g_State ? wxColour( 0, 255, 0 ) : wxColour( 255, 0, 0 ); // green if sync, red otherwise if (g_State) { if (g_prev_State == 0) { g_resyncs++; // Auto-reset stats if we've gone long enough since losing sync. // NOTE: m_timeSinceSyncLoss is in milliseconds. if (m_timeSinceSyncLoss >= wxGetApp().appConfiguration.statsResetTimeSecs * 1000) { resetStats_(); // Clear RX text to reduce the incidence of incorrect callsigns extracted with // the PSK Reporter callsign extraction logic. m_txtCtrlCallSign->SetValue(wxT("")); m_cboLastReportedCallsigns->SetValue(wxT("")); m_cboLastReportedCallsigns->Enable(m_lastReportedCallsignListView->GetItemCount() > 0); memset(m_callsign, 0, MAX_CALLSIGN); m_pcallsign = m_callsign; // Get current time to enforce minimum sync time requirement for PSK Reporter. g_sync_time = time(0); freedvInterface.resetReliableText(); } } m_timeSinceSyncLoss = 0; } else { // Counts the amount of time since losing sync. Once we exceed // wxGetApp().appConfiguration.statsResetTimeSecs, we will reset the stats. m_timeSinceSyncLoss += _REFRESH_TIMER_PERIOD; } if (oldColor != newColor) { m_textSync->SetForegroundColour(newColor); m_textSync->SetLabel("Modem"); m_textSync->Refresh(); } } g_prev_State = g_State; // send Callsign ---------------------------------------------------- char callsign[MAX_CALLSIGN]; memset(callsign, 0, MAX_CALLSIGN); if (!wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { strncpy(callsign, (const char*) wxGetApp().appConfiguration.reportingConfiguration.reportingFreeTextString->mb_str(wxConvUTF8), MAX_CALLSIGN - 2); if (strlen(callsign) < MAX_CALLSIGN - 1) { strncat(callsign, "\r", 2); } // buffer 1 txt message to ensure tx data fifo doesn't "run dry" char* sendBuffer = &callsign[0]; if ((unsigned)codec2_fifo_used(g_txDataInFifo) < strlen(sendBuffer)) { unsigned int i; // write chars to tx data fifo for(i = 0; i < strlen(sendBuffer); i++) { short ashort = (unsigned char)sendBuffer[i]; codec2_fifo_write(g_txDataInFifo, &ashort, 1); } } // See if any Callsign info received -------------------------------- short ashort; while (codec2_fifo_read(g_rxDataOutFifo, &ashort, 1) == 0) { unsigned char incomingChar = (unsigned char)ashort; // Pre-1.5.1 behavior, where text is handled as-is. if (incomingChar == '\r' || incomingChar == '\n' || incomingChar == 0 || ((m_pcallsign - m_callsign) > MAX_CALLSIGN-1)) { // CR completes line. Fill in remaining positions with zeroes. if ((m_pcallsign - m_callsign) <= MAX_CALLSIGN-1) { memset(m_pcallsign, 0, MAX_CALLSIGN - (m_pcallsign - m_callsign)); } // Reset to the beginning. m_pcallsign = m_callsign; } else { *m_pcallsign++ = incomingChar; } m_txtCtrlCallSign->SetValue(m_callsign); } } // We should only report to reporters when all of the following are true: // a) The callsign encoder indicates a valid callsign has been received. // b) We detect a valid format callsign in the text (see https://en.wikipedia.org/wiki/Amateur_radio_call_signs). // c) We don't currently have a pending report to add to the outbound list for the active callsign. // When the above is true, capture the callsign and current SNR and add to the PSK Reporter object's outbound list. if (wxGetApp().m_reporters.size() > 0 && wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { const char* text = freedvInterface.getReliableText(); assert(text != nullptr); wxString wxCallsign = text; delete[] text; if (wxCallsign.Length() > 0) { freedvInterface.resetReliableText(); wxRegEx callsignFormat("(([A-Za-z0-9]+/)?[A-Za-z0-9]{1,3}[0-9][A-Za-z0-9]*[A-Za-z](/[A-Za-z0-9]+)?)"); if (callsignFormat.Matches(wxCallsign)) { wxString rxCallsign = callsignFormat.GetMatch(wxCallsign, 1); std::string pendingCallsign = rxCallsign.ToStdString(); auto pendingSnr = (int)(g_snr + 0.5); wxString freqString; if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { double freq = wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency.get() / 1000.0; freqString = wxString::Format("%.01f", freq); } else { double freq = wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency.get() / 1000000.0; freqString = wxString::Format("%.04f", freq); } if (m_lastReportedCallsignListView->GetItemCount() == 0 || m_lastReportedCallsignListView->GetItemText(0, 0) != rxCallsign || m_lastReportedCallsignListView->GetItemText(0, 1) != freqString) { auto currentTime = wxDateTime::Now(); wxString currentTimeAsString = ""; if (wxGetApp().appConfiguration.reportingConfiguration.useUTCForReporting) { currentTime = currentTime.ToUTC(); } currentTimeAsString.Printf(wxT("%s %s"), currentTime.FormatISODate(), currentTime.FormatISOTime()); auto index = m_lastReportedCallsignListView->InsertItem(0, rxCallsign, 0); m_lastReportedCallsignListView->SetItem(index, 1, freqString); m_lastReportedCallsignListView->SetItem(index, 2, currentTimeAsString); } wxString snrAsString; snrAsString.Printf(wxT("%0.1f"), g_snr); auto index = m_lastReportedCallsignListView->GetTopItem(); m_lastReportedCallsignListView->SetItem(index, 3, snrAsString); m_cboLastReportedCallsigns->SetText(rxCallsign); m_cboLastReportedCallsigns->Enable(m_lastReportedCallsignListView->GetItemCount() > 0); if (wxGetApp().appConfiguration.rigControlConfiguration.hamlibUseForPTT) { wxGetApp().rigFrequencyController->requestCurrentFrequencyMode(); } int64_t freq = wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency; // Only report if there's a valid reporting frequency and if we're not playing // a recording through ourselves (to avoid false reports). if (freq > 0) { long long freqLongLong = freq; fprintf( stderr, "Adding callsign %s @ SNR %d, freq %lld to PSK Reporter.\n", pendingCallsign.c_str(), pendingSnr, freqLongLong); if (!g_playFileFromRadio) { for (auto& obj : wxGetApp().m_reporters) { obj->addReceiveRecord( pendingCallsign, freedvInterface.getCurrentModeStr(), freq, pendingSnr); } } } } } } // Run time update of EQ filters ----------------------------------- g_mutexProtectingCallbackData.Lock(); bool micEqEnableOld = g_rxUserdata->micInEQEnable; bool spkrEqEnableOld = g_rxUserdata->spkOutEQEnable; if (m_newMicInFilter || m_newSpkOutFilter || micEqEnableOld != wxGetApp().appConfiguration.filterConfiguration.micInChannel.eqEnable || spkrEqEnableOld != wxGetApp().appConfiguration.filterConfiguration.spkOutChannel.eqEnable) { deleteEQFilters(g_rxUserdata); g_rxUserdata->micInEQEnable = wxGetApp().appConfiguration.filterConfiguration.micInChannel.eqEnable; g_rxUserdata->spkOutEQEnable = wxGetApp().appConfiguration.filterConfiguration.spkOutChannel.eqEnable; if (m_newMicInFilter || m_newSpkOutFilter) { if (g_nSoundCards == 1) { designEQFilters(g_rxUserdata, wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate, 0); } else { designEQFilters(g_rxUserdata, wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate, wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate); } } m_newMicInFilter = m_newSpkOutFilter = false; } g_mutexProtectingCallbackData.Unlock(); // set some run time options (if applicable) freedvInterface.setRunTimeOptions( (int)wxGetApp().appConfiguration.freedv700Clip, (int)wxGetApp().appConfiguration.freedv700TxBPF); // Test Frame Bit Error Updates ------------------------------------ // Toggle test frame mode at run time if (!freedvInterface.usingTestFrames() && wxGetApp().m_testFrames) { // reset stats on check box off to on transition freedvInterface.resetTestFrameStats(); } freedvInterface.setTestFrames(wxGetApp().m_testFrames, wxGetApp().m_FreeDV700Combine); g_channel_noise = wxGetApp().m_channel_noise; // update stats on main page const int STR_LENGTH = 80; char mode[STR_LENGTH], bits[STR_LENGTH], errors[STR_LENGTH], ber[STR_LENGTH], resyncs[STR_LENGTH], clockoffset[STR_LENGTH], freqoffset[STR_LENGTH], syncmetric[STR_LENGTH]; snprintf(mode, STR_LENGTH, "Mode: %s", freedvInterface.getCurrentModeStr()); wxString modeString(mode); m_textCurrentDecodeMode->SetLabel(modeString); snprintf(bits, STR_LENGTH, "Bits: %d", freedvInterface.getTotalBits()); wxString bits_string(bits); m_textBits->SetLabel(bits_string); snprintf(errors, STR_LENGTH, "Errs: %d", freedvInterface.getTotalBitErrors()); wxString errors_string(errors); m_textErrors->SetLabel(errors_string); float b = (float)freedvInterface.getTotalBitErrors()/(1E-6+freedvInterface.getTotalBits()); snprintf(ber, STR_LENGTH, "BER: %4.3f", b); wxString ber_string(ber); m_textBER->SetLabel(ber_string); snprintf(resyncs, STR_LENGTH, "Resyncs: %d", g_resyncs); wxString resyncs_string(resyncs); m_textResyncs->SetLabel(resyncs_string); snprintf(freqoffset, STR_LENGTH, "FrqOff: %3.1f", freedvInterface.getCurrentRxModemStats()->foff); wxString freqoffset_string(freqoffset); m_textFreqOffset->SetLabel(freqoffset_string); snprintf(syncmetric, STR_LENGTH, "Sync: %3.2f", freedvInterface.getCurrentRxModemStats()->sync_metric); wxString syncmetric_string(syncmetric); m_textSyncMetric->SetLabel(syncmetric_string); // Codec 2 700C/D/E & 800XA VQ "auto EQ" equaliser variance auto var = freedvInterface.getVariance(); char var_str[STR_LENGTH]; snprintf(var_str, STR_LENGTH, "Var: %4.1f", var); wxString var_string(var_str); m_textCodec2Var->SetLabel(var_string); if (g_State) { snprintf(clockoffset, STR_LENGTH, "ClkOff: %+-d", (int)round(freedvInterface.getCurrentRxModemStats()->clock_offset*1E6) % 10000); wxString clockoffset_string(clockoffset); m_textClockOffset->SetLabel(clockoffset_string); // update error pattern plots if supported short* error_pattern = nullptr; int sz_error_pattern = freedvInterface.getErrorPattern(&error_pattern); if (sz_error_pattern) { int i,b; /* both modes map IQ to alternate bits, but on same carrier */ if (freedvInterface.getCurrentMode() == FREEDV_MODE_1600) { /* FreeDV 1600 mapping from error pattern to two bits on each carrier */ for(b=0; badd_new_sample(b, b + 0.8*error_pattern[i]); g_error_hist[b] += error_pattern[i]; g_error_histn[b]++; } //if (b%2) // printf("g_error_hist[%d]: %d\n", b/2, g_error_hist[b/2]); } /* calculate BERs and send to plot */ float ber[2*MODEM_STATS_NC_MAX]; for(b=0; b<2*MODEM_STATS_NC_MAX; b++) { ber[b] = 0.0; } for(b=0; badd_new_samples(0, ber, 2*MODEM_STATS_NC_MAX); } if ((freedvInterface.getCurrentMode() == FREEDV_MODE_700C)) { int c; //fprintf(stderr, "after g_error_pattern_fifo read 2\n"); /* FreeDV 700 mapping from error pattern to bit on each carrier, see data bit to carrier mapping in: codec2-dev/octave/cohpsk_frame_design.ods We can plot a histogram of the errors/carrier before or after diversity recombination. Actually one bar for each IQ bit in carrier order. */ int hist_Nc = sz_error_pattern/4; //fprintf(stderr, "hist_Nc: %d\n", hist_Nc); for(i=0; iadd_new_sample(c, c + 0.8*error_pattern[i]); g_error_hist[c] += error_pattern[i]; g_error_histn[c]++; //printf("i: %d c: %d\n", i, c); } for(; i<2*MODEM_STATS_NC_MAX*4; i++) { c = floor(i/4); m_panelTestFrameErrors->add_new_sample(c, c); //printf("i: %d c: %d\n", i, c); } /* calculate BERs and send to plot */ float ber[2*MODEM_STATS_NC_MAX]; for(b=0; b<2*MODEM_STATS_NC_MAX; b++) { ber[b] = 0.0; } for(b=0; badd_new_samples(0, ber, 2*MODEM_STATS_NC_MAX); } m_panelTestFrameErrors->Refresh(); m_panelTestFrameErrorsHist->Refresh(); delete[] error_pattern; } } /* FIFO and PortAudio under/overflow debug counters */ optionsDlg->DisplayFifoPACounters(); // command from UDP thread that is best processed in main thread to avoid seg faults if (m_schedule_restore) { if (IsIconized()) Restore(); m_schedule_restore = false; } // Voice Keyer state machine VoiceKeyerProcessEvent(VK_DT); // Detect Sync state machine DetectSyncProcessEvent(); } } #endif //------------------------------------------------------------------------- // OnExit() //------------------------------------------------------------------------- void MainFrame::OnExit(wxCommandEvent& event) { if (wxGetApp().rigFrequencyController) { wxGetApp().rigFrequencyController->disconnect(); wxGetApp().rigFrequencyController = nullptr; } if (wxGetApp().rigPttController) { wxGetApp().rigPttController->disconnect(); wxGetApp().rigPttController = nullptr; } wxGetApp().m_reporters.clear(); //fprintf(stderr, "MainFrame::OnExit\n"); wxUnusedVar(event); #ifdef _USE_TIMER m_plotTimer.Stop(); m_pskReporterTimer.Stop(); #endif // _USE_TIMER if(g_sfPlayFile != NULL) { sf_close(g_sfPlayFile); g_sfPlayFile = NULL; } if(g_sfRecFile != NULL) { sf_close(g_sfRecFile); g_sfRecFile = NULL; } if(m_RxRunning) { stopRxStream(); } m_togBtnAnalog->Disable(); auto engine = AudioEngineFactory::GetAudioEngine(); engine->stop(); engine->setOnEngineError(nullptr, nullptr); Destroy(); } void MainFrame::OnChangeTxMode( wxCommandEvent& event ) { wxRadioButton* hiddenModeToSet = nullptr; std::vector buttonsToClear { m_hiddenMode1, m_hiddenMode2, m_rb700c, m_rb700d, m_rb700e, m_rb800xa, m_rb1600, m_rb2020, #if defined(FREEDV_MODE_2020B) m_rb2020b, #endif // FREEDV_MODE_2020B }; auto eventObject = (wxRadioButton*)event.GetEventObject(); if (eventObject != nullptr) { std::string label = (const char*)eventObject->GetLabel().ToUTF8(); if (label == "700D" || label == "700E" || label == "1600") { hiddenModeToSet = m_hiddenMode2; } else { hiddenModeToSet = m_hiddenMode1; } buttonsToClear.erase(std::find(buttonsToClear.begin(), buttonsToClear.end(), (wxRadioButton*)eventObject)); } txModeChangeMutex.Lock(); if (eventObject == m_rb1600 || (eventObject == nullptr && m_rb1600->GetValue())) { g_mode = FREEDV_MODE_1600; } else if (eventObject == m_rb700c || (eventObject == nullptr && m_rb700c->GetValue())) { g_mode = FREEDV_MODE_700C; } else if (eventObject == m_rb700d || (eventObject == nullptr && m_rb700d->GetValue())) { g_mode = FREEDV_MODE_700D; } else if (eventObject == m_rb700e || (eventObject == nullptr && m_rb700e->GetValue())) { g_mode = FREEDV_MODE_700E; } else if (eventObject == m_rb800xa || (eventObject == nullptr && m_rb800xa->GetValue())) { g_mode = FREEDV_MODE_800XA; } else if (eventObject == m_rb2020 || (eventObject == nullptr && m_rb2020->GetValue())) { assert(wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported); g_mode = FREEDV_MODE_2020; } #if defined(FREEDV_MODE_2020B) else if (eventObject == m_rb2020b || (eventObject == nullptr && m_rb2020b->GetValue())) { assert(wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported); g_mode = FREEDV_MODE_2020B; } #endif // FREEDV_MODE_2020B if (freedvInterface.isRunning()) { // Need to change the TX interface live. freedvInterface.changeTxMode(g_mode); } // Force recreation of EQ filters. m_newMicInFilter = true; m_newSpkOutFilter = true; txModeChangeMutex.Unlock(); // Manually implement mutually exclusive behavior as // we can't rely on wxWidgets doing it on account of // how we're splitting the modes. if (eventObject != nullptr) { buttonsToClear.erase(std::find(buttonsToClear.begin(), buttonsToClear.end(), hiddenModeToSet)); for (auto& var : buttonsToClear) { var->SetValue(false); } hiddenModeToSet->SetValue(true); } // Report TX change to registered reporters for (auto& obj : wxGetApp().m_reporters) { obj->transmit(freedvInterface.getCurrentTxModeStr(), g_tx); } } void MainFrame::performFreeDVOn_() { if (g_verbose) fprintf(stderr, "Start .....\n"); g_queueResync = false; endingTx = false; m_timeSinceSyncLoss = 0; executeOnUiThreadAndWait_([&]() { m_txtCtrlCallSign->SetValue(wxT("")); m_lastReportedCallsignListView->DeleteAllItems(); m_cboLastReportedCallsigns->Enable(false); m_cboLastReportedCallsigns->SetText(wxT("")); }); memset(m_callsign, 0, MAX_CALLSIGN); m_pcallsign = m_callsign; freedvInterface.resetReliableText(); // // Start Running ------------------------------------------------- // vk_state = VK_IDLE; // modify some button states when running executeOnUiThreadAndWait_([&]() { m_textSync->Enable(); m_textCurrentDecodeMode->Enable(); // determine what mode we are using wxCommandEvent tmpEvent; OnChangeTxMode(tmpEvent); if (!wxGetApp().appConfiguration.multipleReceiveEnabled) { m_rb1600->Disable(); m_rb700c->Disable(); m_rb700d->Disable(); m_rb700e->Disable(); m_rb800xa->Disable(); m_rb2020->Disable(); #if defined(FREEDV_MODE_2020B) m_rb2020b->Disable(); #endif // FREEDV_MODE_2020B freedvInterface.addRxMode(g_mode); } else { if(wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported) { freedvInterface.addRxMode(FREEDV_MODE_2020); #if defined(FREEDV_MODE_2020B) freedvInterface.addRxMode(FREEDV_MODE_2020B); #endif // FREEDV_MODE_2020B } int rxModes[] = { FREEDV_MODE_1600, FREEDV_MODE_700E, FREEDV_MODE_700C, FREEDV_MODE_700D, FREEDV_MODE_800XA }; for (auto& mode : rxModes) { freedvInterface.addRxMode(mode); } // If we're receive-only, it doesn't make sense to be able to change TX mode. if (g_nSoundCards <= 1) { m_rb1600->Disable(); m_rb700c->Disable(); m_rb700d->Disable(); m_rb700e->Disable(); m_rb800xa->Disable(); m_rb2020->Disable(); #if defined(FREEDV_MODE_2020B) m_rb2020b->Disable(); #endif // FREEDV_MODE_2020B } } // Default voice keyer sample rate to 8K. The exact voice keyer // sample rate will be determined when the .wav file is loaded. g_sfTxFs = FS; wxGetApp().m_prevMode = g_mode; freedvInterface.start(g_mode, wxGetApp().appConfiguration.fifoSizeMs, !wxGetApp().appConfiguration.multipleReceiveEnabled || wxGetApp().appConfiguration.multipleReceiveOnSingleThread, wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled); // Codec 2 VQ Equaliser freedvInterface.setEq(wxGetApp().appConfiguration.filterConfiguration.enable700CEqualizer); // Codec2 verbosity setting freedvInterface.setVerbose(g_freedv_verbose); // Text field/callsign callbacks. if (!wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { freedvInterface.setTextCallbackFn(&my_put_next_rx_char, &my_get_next_tx_char); } else { char temp[9]; memset(temp, 0, 9); strncpy(temp, wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign->ToUTF8(), 8); // One less than the size of temp to ensure we don't overwrite the null. fprintf(stderr, "Setting callsign to %s\n", temp); freedvInterface.setReliableText(temp); } g_error_hist = new short[MODEM_STATS_NC_MAX*2]; g_error_histn = new short[MODEM_STATS_NC_MAX*2]; int i; for(i=0; i<2*MODEM_STATS_NC_MAX; i++) { g_error_hist[i] = 0; g_error_histn[i] = 0; } // init Codec 2 LPC Post Filter (FreeDV 1600) freedvInterface.setLpcPostFilter( wxGetApp().appConfiguration.filterConfiguration.codec2LPCPostFilterEnable, wxGetApp().appConfiguration.filterConfiguration.codec2LPCPostFilterBassBoost, wxGetApp().appConfiguration.filterConfiguration.codec2LPCPostFilterBeta, wxGetApp().appConfiguration.filterConfiguration.codec2LPCPostFilterGamma); // Init Speex pre-processor states // by inspecting Speex source it seems that only denoiser is on by default if (g_verbose) fprintf(stderr, "freedv_get_n_speech_samples(tx): %d\n", freedvInterface.getTxNumSpeechSamples()); if (g_verbose) fprintf(stderr, "freedv_get_speech_sample_rate(tx): %d\n", freedvInterface.getTxSpeechSampleRate()); // adjust spectrum and waterfall freq scaling base on mode m_panelSpectrum->setFreqScale(MODEM_STATS_NSPEC*((float)MAX_F_HZ/(freedvInterface.getTxModemSampleRate()/2))); m_panelWaterfall->setFs(freedvInterface.getTxModemSampleRate()); // Init text msg decoding if (!wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) freedvInterface.setTextVaricodeNum(1); // scatter plot (PSK) or Eye (FSK) mode if (g_mode == FREEDV_MODE_800XA) { m_panelScatter->setEyeScatter(PLOT_SCATTER_MODE_EYE); } else { m_panelScatter->setEyeScatter(PLOT_SCATTER_MODE_SCATTER); } }); g_State = g_prev_State = 0; g_snr = 0.0; g_half_duplex = wxGetApp().appConfiguration.halfDuplexMode; m_pcallsign = m_callsign; memset(m_callsign, 0, sizeof(m_callsign)); m_maxLevel = 0; executeOnUiThreadAndWait_([&]() { m_textLevel->SetLabel(wxT("")); m_gaugeLevel->SetValue(0); }); // attempt to start sound cards and tx/rx processing std::promise tmpPromise; std::future tmpFuture = tmpPromise.get_future(); // Note: this executes on the UI thread as macOS may need to display popups // to process this request. CallAfter([&]() { VerifyMicrophonePermissions(tmpPromise); }); tmpFuture.wait(); if (tmpFuture.get()) { bool soundCardSetupValid = false; executeOnUiThreadAndWait_([&]() { soundCardSetupValid = validateSoundCardSetup(); }); if (soundCardSetupValid) { wxGetApp().m_reporters.clear(); startRxStream(); if (m_RxRunning) { // attempt to start PTT ...... if (wxGetApp().appConfiguration.rigControlConfiguration.hamlibUseForPTT) { OpenHamlibRig(); } else if (wxGetApp().appConfiguration.rigControlConfiguration.useSerialPTT) { OpenSerialPort(); } else { wxGetApp().rigPttController = nullptr; } #if defined(WIN32) if (wxGetApp().appConfiguration.rigControlConfiguration.useOmniRig) { // OmniRig can be anbled along with serial port PTT. // The logic below will ensure we don't overwrite the serial PTT // handler. OpenOmniRig(); } #endif // defined(WIN32) // Initialize PSK Reporter reporting. if (wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { if (wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign->ToStdString() == "" || wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare->ToStdString() == "") { executeOnUiThreadAndWait_([&]() { wxMessageBox("Reporting requires a valid callsign and grid square in Tools->Options. Reporting will be disabled.", wxT("Error"), wxOK | wxICON_ERROR, this); }); } else { if (wxGetApp().appConfiguration.reportingConfiguration.pskReporterEnabled) { auto pskReporter = std::make_shared( wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign->ToStdString(), wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare->ToStdString(), std::string("FreeDV ") + FREEDV_VERSION); assert(pskReporter != nullptr); wxGetApp().m_reporters.push_back(pskReporter); } if (wxGetApp().appConfiguration.reportingConfiguration.freedvReporterEnabled) { wxGetApp().m_reporters.push_back(wxGetApp().m_sharedReporterObject); wxGetApp().m_sharedReporterObject->showOurselves(); } // Enable FreeDV Reporter timer (every 5 minutes). executeOnUiThreadAndWait_([&]() { m_pskReporterTimer.Start(5 * 60 * 1000); }); // Make sure QSY button becomes enabled after start. executeOnUiThreadAndWait_([&]() { if (m_reporterDialog != nullptr) { m_reporterDialog->refreshQSYButtonState(); } }); // Immediately transmit selected TX mode and frequency to avoid UI glitches. for (auto& obj : wxGetApp().m_reporters) { obj->transmit(freedvInterface.getCurrentTxModeStr(), g_tx); obj->freqChange(wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency); } } } else { wxGetApp().m_reporters.clear(); } if (wxGetApp().appConfiguration.rigControlConfiguration.useSerialPTTInput) { OpenPTTInPort(); } executeOnUiThreadAndWait_([&]() { #ifdef _USE_TIMER m_plotTimer.Start(_REFRESH_TIMER_PERIOD, wxTIMER_CONTINUOUS); m_updFreqStatusTimer.Start(5*1000); // every 5 seconds[UP] #endif // _USE_TIMER }); } } } else { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString("Microphone permissions must be granted to FreeDV for it to function properly."), wxT("Error"), wxOK | wxICON_ERROR, this); }); } // Clear existing TX text, if any. codec2_fifo_destroy(g_txDataInFifo); g_txDataInFifo = codec2_fifo_create(MAX_CALLSIGN*FREEDV_VARICODE_MAX_BITS); } void MainFrame::performFreeDVOff_() { if (g_verbose) fprintf(stderr, "Stop .....\n"); // // Stop Running ------------------------------------------------- // #ifdef _USE_TIMER executeOnUiThreadAndWait_([&]() { m_plotTimer.Stop(); m_pskReporterTimer.Stop(); m_updFreqStatusTimer.Stop(); // [UP] }); #endif // _USE_TIMER // always end with PTT in rx state if (wxGetApp().rigPttController != nullptr && wxGetApp().rigPttController->isConnected()) { wxGetApp().rigPttController->ptt(false); wxGetApp().rigPttController->disconnect(); } wxGetApp().rigPttController = nullptr; if (wxGetApp().rigFrequencyController != nullptr && wxGetApp().rigFrequencyController->isConnected()) { wxGetApp().rigFrequencyController->disconnect(); } wxGetApp().rigFrequencyController = nullptr; if (wxGetApp().appConfiguration.rigControlConfiguration.useSerialPTTInput) { ClosePTTInPort(); } executeOnUiThreadAndWait_([&]() { m_btnTogPTT->SetValue(false); VoiceKeyerProcessEvent(VK_SPACE_BAR); }); stopRxStream(); wxGetApp().m_reporters.clear(); if (wxGetApp().m_sharedReporterObject) { wxGetApp().m_sharedReporterObject->hideFromView(); } // FreeDV clean up delete[] g_error_hist; delete[] g_error_histn; freedvInterface.stop(); m_newMicInFilter = m_newSpkOutFilter = true; executeOnUiThreadAndWait_([&]() { m_textSync->Disable(); m_textCurrentDecodeMode->Disable(); m_togBtnAnalog->Disable(); m_btnTogPTT->Disable(); m_togBtnVoiceKeyer->Disable(); m_rb1600->Enable(); m_rb700c->Enable(); m_rb700d->Enable(); m_rb700e->Enable(); m_rb800xa->Enable(); if(wxGetApp().appConfiguration.freedv2020Allowed && wxGetApp().appConfiguration.freedvAVXSupported) { m_rb2020->Enable(); #if defined(FREEDV_MODE_2020B) m_rb2020b->Enable(); #endif // FREEDV_MODE_2020B } // Make sure QSY button becomes disabled after stop. if (m_reporterDialog != nullptr) { m_reporterDialog->refreshQSYButtonState(); } }); } //------------------------------------------------------------------------- // OnTogBtnOnOff() //------------------------------------------------------------------------- void MainFrame::OnTogBtnOnOff(wxCommandEvent& event) { if (!m_togBtnOnOff->IsEnabled()) return; m_togBtnOnOff->SetFocus(); // Disable buttons while on/off is occurring m_togBtnOnOff->Enable(false); m_togBtnAnalog->Enable(false); m_togBtnVoiceKeyer->Enable(false); m_btnTogPTT->Enable(false); // we are attempting to start if (m_togBtnOnOff->GetValue()) { std::thread onOffExec([this]() { #if defined(__linux__) pthread_setname_np(pthread_self(), "FreeDV TurningOn"); #endif // defined(__linux__) performFreeDVOn_(); if (!m_RxRunning) { // Startup failed. performFreeDVOff_(); } // On/Off actions complete, re-enable button. executeOnUiThreadAndWait_([&]() { bool txEnabled = m_RxRunning && !wxGetApp().appConfiguration.reportingConfiguration.freedvReporterForceReceiveOnly && (g_nSoundCards == 2); m_togBtnAnalog->Enable(m_RxRunning); m_togBtnVoiceKeyer->Enable(txEnabled); m_btnTogPTT->Enable(txEnabled); optionsDlg->setSessionActive(m_RxRunning); if (m_RxRunning) { m_togBtnOnOff->SetLabel(wxT("&Stop")); } m_togBtnOnOff->SetValue(m_RxRunning); m_togBtnOnOff->Enable(true); // On some systems the Report Frequency box ends up getting // focus after clicking on Start. This causes the frequency // to never update. To avoid this, we force focus to be elsewhere // in the window. m_auiNbookCtrl->SetFocus(); }); }); onOffExec.detach(); } else { std::thread onOffExec([this]() { #if defined(__linux__) pthread_setname_np(pthread_self(), "FreeDV TurningOff"); #endif // defined(__linux__) performFreeDVOff_(); // On/Off actions complete, re-enable button. executeOnUiThreadAndWait_([&]() { m_togBtnAnalog->Enable(m_RxRunning); m_togBtnVoiceKeyer->Enable(m_RxRunning); m_btnTogPTT->Enable(m_RxRunning); optionsDlg->setSessionActive(m_RxRunning); m_togBtnOnOff->SetValue(m_RxRunning); m_togBtnOnOff->SetLabel(wxT("&Start")); m_togBtnOnOff->Enable(true); }); }); onOffExec.detach(); } } static std::mutex stoppingMutex; //------------------------------------------------------------------------- // stopRxStream() //------------------------------------------------------------------------- void MainFrame::stopRxStream() { std::unique_lock lk(stoppingMutex); if(m_RxRunning) { m_RxRunning = false; //fprintf(stderr, "waiting for thread to stop\n"); if (m_txThread) { if (txInSoundDevice) { txInSoundDevice->stop(); txInSoundDevice.reset(); } if (txOutSoundDevice) { txOutSoundDevice->stop(); txOutSoundDevice.reset(); } m_txThread->terminateThread(); m_txThread->Wait(); delete m_txThread; m_txThread = nullptr; } if (m_rxThread) { if (rxInSoundDevice) { rxInSoundDevice->stop(); rxInSoundDevice.reset(); } if (rxOutSoundDevice) { rxOutSoundDevice->stop(); rxOutSoundDevice.reset(); } m_rxThread->terminateThread(); m_rxThread->Wait(); delete m_txThread; m_rxThread = nullptr; } wxGetApp().linkStep = nullptr; destroy_fifos(); // Free memory allocated for filters. m_newMicInFilter = true; m_newSpkOutFilter = true; deleteEQFilters(g_rxUserdata); delete g_rxUserdata; auto engine = AudioEngineFactory::GetAudioEngine(); engine->stop(); engine->setOnEngineError(nullptr, nullptr); } } void MainFrame::destroy_fifos(void) { codec2_fifo_destroy(g_rxUserdata->infifo1); codec2_fifo_destroy(g_rxUserdata->outfifo1); if (g_rxUserdata->infifo2) codec2_fifo_destroy(g_rxUserdata->infifo2); if (g_rxUserdata->outfifo2) codec2_fifo_destroy(g_rxUserdata->outfifo2); codec2_fifo_destroy(g_rxUserdata->rxinfifo); codec2_fifo_destroy(g_rxUserdata->rxoutfifo); } //------------------------------------------------------------------------- // startRxStream() //------------------------------------------------------------------------- void MainFrame::startRxStream() { if (g_verbose) fprintf(stderr, "startRxStream .....\n"); if(!m_RxRunning) { m_RxRunning = true; auto engine = AudioEngineFactory::GetAudioEngine(); engine->setOnEngineError([&](IAudioEngine&, std::string error, void* state) { executeOnUiThreadAndWait_([&, error]() { wxMessageBox(wxString::Format( "Error encountered while initializing the audio engine: %s.", error), wxT("Error"), wxOK, this); }); }, nullptr); engine->start(); if (g_nSoundCards == 0) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxT("No Sound Cards configured, use Tools - Audio Config to configure"), wxT("Error"), wxOK); }); m_RxRunning = false; engine->stop(); engine->setOnEngineError(nullptr, nullptr); return; } else if (g_nSoundCards == 1) { // RX-only setup. // Note: we assume 2 channels, but IAudioEngine will automatically downgrade to 1 channel if needed. rxInSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName, IAudioEngine::AUDIO_ENGINE_IN, wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); rxOutSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName, IAudioEngine::AUDIO_ENGINE_OUT, wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); bool failed = false; if (!rxInSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find RX input sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (!rxOutSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find RX output sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (failed) { if (rxInSoundDevice) { rxInSoundDevice.reset(); } if (rxOutSoundDevice) { rxOutSoundDevice.reset(); } m_RxRunning = false; engine->stop(); engine->setOnEngineError(nullptr, nullptr); return; } else { // Re-save sample rates in case they were somehow invalid before // device creation. wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate = rxInSoundDevice->getSampleRate(); wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = rxOutSoundDevice->getSampleRate(); } } else { // RX + TX setup // Same note as above re: number of channels. rxInSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName, IAudioEngine::AUDIO_ENGINE_IN, wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate, 2); rxInSoundDevice->setDescription("Radio to FreeDV"); rxInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); rxOutSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName, IAudioEngine::AUDIO_ENGINE_OUT, wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate, 2); rxOutSoundDevice->setDescription("FreeDV to Speaker"); rxOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); txInSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName, IAudioEngine::AUDIO_ENGINE_IN, wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate, 2); txInSoundDevice->setDescription("Mic to FreeDV"); txInSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); txOutSoundDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName, IAudioEngine::AUDIO_ENGINE_OUT, wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate, 2); txOutSoundDevice->setDescription("FreeDV to Radio"); txOutSoundDevice->setOnAudioDeviceChanged([&](IAudioDevice&, std::string newDeviceName, void*) { CallAfter([&, newDeviceName]() { wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = wxString::FromUTF8(newDeviceName.c_str()); wxGetApp().appConfiguration.save(pConfig); }); }, nullptr); bool failed = false; if (!rxInSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find RX input sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (!rxOutSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find RX output sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (!txInSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find TX input sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (!txOutSoundDevice) { executeOnUiThreadAndWait_([&]() { wxMessageBox(wxString::Format("Could not find TX output sound device '%s'. Please check settings and try again.", wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName.get()), wxT("Error"), wxOK); }); failed = true; } if (failed) { if (rxInSoundDevice) { rxInSoundDevice.reset(); } if (rxOutSoundDevice) { rxOutSoundDevice.reset(); } if (txInSoundDevice) { txInSoundDevice.reset(); } if (txOutSoundDevice) { txOutSoundDevice.reset(); } m_RxRunning = false; engine->stop(); engine->setOnEngineError(nullptr, nullptr); return; } else { // Re-save sample rates in case they were somehow invalid before // device creation. wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate = rxInSoundDevice->getSampleRate(); wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = rxOutSoundDevice->getSampleRate(); wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = txInSoundDevice->getSampleRate(); wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = txOutSoundDevice->getSampleRate(); } } // Init call back data structure ---------------------------------------------- g_rxUserdata = new paCallBackData; // create FIFOs used to interface between IAudioEngine and txRx // processing loop, which iterates about once every 20ms. // Sample rate conversion, stats for spectral plots, and // transmit processng are all performed in the tx/rxProcessing // loop. int m_fifoSize_ms = wxGetApp().appConfiguration.fifoSizeMs; int soundCard1InFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate/1000; int soundCard1OutFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate/1000; g_rxUserdata->infifo1 = codec2_fifo_create(soundCard1InFifoSizeSamples); g_rxUserdata->outfifo1 = codec2_fifo_create(soundCard1OutFifoSizeSamples); if (txInSoundDevice && txOutSoundDevice) { int soundCard2InFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate/1000; int soundCard2OutFifoSizeSamples = m_fifoSize_ms*wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate/1000; g_rxUserdata->outfifo2 = codec2_fifo_create(soundCard2OutFifoSizeSamples); g_rxUserdata->infifo2 = codec2_fifo_create(soundCard2InFifoSizeSamples); if (g_verbose) fprintf(stderr, "fifoSize_ms: %d infifo2: %d/outfilo2: %d\n", wxGetApp().appConfiguration.fifoSizeMs.get(), soundCard2InFifoSizeSamples, soundCard2OutFifoSizeSamples); } if (g_verbose) fprintf(stderr, "fifoSize_ms: %d infifo1: %d/outfilo1 %d\n", wxGetApp().appConfiguration.fifoSizeMs.get(), soundCard1InFifoSizeSamples, soundCard1OutFifoSizeSamples); // reset debug stats for FIFOs g_infifo1_full = g_outfifo1_empty = g_infifo2_full = g_outfifo2_empty = 0; g_infifo1_full = g_outfifo1_empty = g_infifo2_full = g_outfifo2_empty = 0; for (int i=0; i<4; i++) { g_AEstatus1[i] = g_AEstatus2[i] = 0; } // These FIFOs interface between the 20ms tx/rxProcessing() // loop and the demodulator, which requires a variable number // of input samples to adjust for timing clock differences // between remote tx and rx. These FIFOs also help with the // different processing block size of different FreeDV modes. // TODO: might be able to tune these on a per waveform basis, or refactor // to a neater design with less layers of FIFOs int modem_samplerate, rxInFifoSizeSamples, rxOutFifoSizeSamples; modem_samplerate = freedvInterface.getRxModemSampleRate(); rxInFifoSizeSamples = freedvInterface.getRxNumModemSamples(); rxOutFifoSizeSamples = freedvInterface.getRxNumSpeechSamples(); // add an extra 40ms to give a bit of headroom for processing loop adding samples // which operates on 20ms buffers rxInFifoSizeSamples += 0.04*modem_samplerate; rxOutFifoSizeSamples += 0.04*modem_samplerate; g_rxUserdata->rxinfifo = codec2_fifo_create(rxInFifoSizeSamples); g_rxUserdata->rxoutfifo = codec2_fifo_create(rxOutFifoSizeSamples); if (g_verbose) fprintf(stderr, "rxInFifoSizeSamples: %d rxOutFifoSizeSamples: %d\n", rxInFifoSizeSamples, rxOutFifoSizeSamples); // Init Equaliser Filters ------------------------------------------------------ m_newMicInFilter = m_newSpkOutFilter = true; g_mutexProtectingCallbackData.Lock(); g_rxUserdata->micInEQEnable = wxGetApp().appConfiguration.filterConfiguration.micInChannel.eqEnable; g_rxUserdata->spkOutEQEnable = wxGetApp().appConfiguration.filterConfiguration.spkOutChannel.eqEnable; if ( wxGetApp().appConfiguration.filterConfiguration.micInChannel.eqEnable || wxGetApp().appConfiguration.filterConfiguration.spkOutChannel.eqEnable) { if (g_nSoundCards == 1) { designEQFilters( g_rxUserdata, wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate, 0); } else { designEQFilters( g_rxUserdata, wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate, wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate); } } m_newMicInFilter = m_newSpkOutFilter = false; g_mutexProtectingCallbackData.Unlock(); // optional tone in left channel to reliably trigger vox g_rxUserdata->leftChannelVoxTone = wxGetApp().appConfiguration.rigControlConfiguration.leftChannelVoxTone; g_rxUserdata->voxTonePhase = 0; // Set sound card callbacks auto errorCallback = [&](IAudioDevice&, std::string error, void*) { CallAfter([&, error]() { wxMessageBox(wxString::Format("Error encountered while processing audio: %s", error), wxT("Error"), wxOK); }); }; rxInSoundDevice->setOnAudioData([&](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short indata[size]; for (size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { indata[i] = audioData[0]; } if (codec2_fifo_write(cbData->infifo1, indata, size)) { g_infifo1_full++; } m_rxThread->notify(); }, g_rxUserdata); rxInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { g_AEstatus1[1]++; }, nullptr); rxInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { g_AEstatus1[0]++; }, nullptr); rxInSoundDevice->setOnAudioError(errorCallback, nullptr); rxOutSoundDevice->setOnAudioError(errorCallback, nullptr); if (txInSoundDevice && txOutSoundDevice) { rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; int result = codec2_fifo_read(cbData->outfifo2, outdata, size); if (result == 0) { for (size_t i = 0; i < size; i++) { for (int j = 0; j < dev.getNumChannels(); j++) { *audioData++ = outdata[i]; } } } else { g_outfifo2_empty++; } }, g_rxUserdata); rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { g_AEstatus2[3]++; }, nullptr); rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { g_AEstatus2[2]++; }, nullptr); txInSoundDevice->setOnAudioData([&](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short indata[size]; if (!endingTx) { for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { indata[i] = audioData[0]; } if (codec2_fifo_write(cbData->infifo2, indata, size)) { g_infifo2_full++; } } m_txThread->notify(); }, g_rxUserdata); txInSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { g_AEstatus2[1]++; }, nullptr); txInSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { g_AEstatus2[0]++; }, nullptr); txOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { // write signal to all channels if the device can support 2+ channels. // Otherwise, we assume we're only dealing with one channel and write // only to that channel. if (dev.getNumChannels() >= 2) { for(size_t i = 0; i < size; i++, audioData += dev.getNumChannels()) { if (cbData->leftChannelVoxTone) { cbData->voxTonePhase += 2.0*M_PI*VOX_TONE_FREQ/wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate; cbData->voxTonePhase -= 2.0*M_PI*floor(cbData->voxTonePhase/(2.0*M_PI)); audioData[0] = VOX_TONE_AMP*cos(cbData->voxTonePhase); } else audioData[0] = outdata[i]; for (auto j = 1; j < dev.getNumChannels(); j++) { audioData[j] = outdata[i]; } } } else { for(size_t i = 0; i < size; i++, audioData++) { audioData[0] = outdata[i]; } } } else { g_outfifo1_empty++; } }, g_rxUserdata); txOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { g_AEstatus1[3]++; }, nullptr); txOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { g_AEstatus1[2]++; }, nullptr); txInSoundDevice->setOnAudioError(errorCallback, nullptr); txOutSoundDevice->setOnAudioError(errorCallback, nullptr); } else { rxOutSoundDevice->setOnAudioData([](IAudioDevice& dev, void* data, size_t size, void* state) { paCallBackData* cbData = static_cast(state); short* audioData = static_cast(data); short outdata[size]; int result = codec2_fifo_read(cbData->outfifo1, outdata, size); if (result == 0) { for (size_t i = 0; i < size; i++) { for (int j = 0; j < dev.getNumChannels(); j++) { *audioData++ = outdata[i]; } } } else { g_outfifo1_empty++; } }, g_rxUserdata); rxOutSoundDevice->setOnAudioOverflow([](IAudioDevice& dev, void* state) { g_AEstatus1[3]++; }, nullptr); rxOutSoundDevice->setOnAudioUnderflow([](IAudioDevice& dev, void* state) { g_AEstatus1[2]++; }, nullptr); } // Create link to allow monitoring TX/VK audio wxGetApp().linkStep = std::make_shared(rxOutSoundDevice->getSampleRate()); // start tx/rx processing thread if (txInSoundDevice && txOutSoundDevice) { m_txThread = new TxRxThread(true, txInSoundDevice->getSampleRate(), txOutSoundDevice->getSampleRate(), wxGetApp().linkStep.get()); if ( m_txThread->Create() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't create TX thread!")); } if (wxGetApp().m_txRxThreadHighPriority) { m_txThread->SetPriority(WXTHREAD_MAX_PRIORITY); } txInSoundDevice->start(); if (!txInSoundDevice->isRunning()) { rxInSoundDevice.reset(); rxOutSoundDevice.reset(); txInSoundDevice.reset(); txOutSoundDevice.reset(); m_RxRunning = false; return; } txOutSoundDevice->start(); if (!txOutSoundDevice->isRunning()) { txInSoundDevice->stop(); rxInSoundDevice.reset(); rxOutSoundDevice.reset(); txInSoundDevice.reset(); txOutSoundDevice.reset(); m_RxRunning = false; return; } if ( m_txThread->Run() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't start TX thread!")); } } m_rxThread = new TxRxThread(false, rxInSoundDevice->getSampleRate(), rxOutSoundDevice->getSampleRate(), wxGetApp().linkStep.get()); if ( m_rxThread->Create() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't create RX thread!")); } if (wxGetApp().m_txRxThreadHighPriority) { m_rxThread->SetPriority(WXTHREAD_MAX_PRIORITY); } rxInSoundDevice->start(); if (!rxInSoundDevice->isRunning()) { if (txInSoundDevice) txInSoundDevice->stop(); if (txOutSoundDevice) txOutSoundDevice->stop(); rxInSoundDevice.reset(); rxOutSoundDevice.reset(); txInSoundDevice.reset(); txOutSoundDevice.reset(); m_RxRunning = false; return; } rxOutSoundDevice->start(); if (!rxOutSoundDevice->isRunning()) { if (txInSoundDevice) txInSoundDevice->stop(); if (txOutSoundDevice) txOutSoundDevice->stop(); rxInSoundDevice->stop(); rxInSoundDevice.reset(); rxOutSoundDevice.reset(); txInSoundDevice.reset(); txOutSoundDevice.reset(); m_RxRunning = false; return; } if ( m_rxThread->Run() != wxTHREAD_NO_ERROR ) { wxLogError(wxT("Can't start RX thread!")); } if (g_verbose) fprintf(stderr, "starting tx/rx processing thread\n"); // Work around an issue where the buttons stay disabled even if there // is an error opening one or more audio device(s). bool txDevicesRunning = (!txInSoundDevice || txInSoundDevice->isRunning()) && (!txOutSoundDevice || txOutSoundDevice->isRunning()); bool rxDevicesRunning = (rxInSoundDevice && rxInSoundDevice->isRunning()) && (rxOutSoundDevice && rxOutSoundDevice->isRunning()); m_RxRunning = txDevicesRunning && rxDevicesRunning; } } bool MainFrame::validateSoundCardSetup() { bool canRun = true; // Translate device names to IDs auto engine = AudioEngineFactory::GetAudioEngine(); engine->setOnEngineError([&](IAudioEngine&, std::string error, void* state) { CallAfter([&]() { wxMessageBox(wxString::Format( "Error encountered while initializing the audio engine: %s.", error), wxT("Error"), wxOK, this); }); }, nullptr); engine->start(); auto defaultInputDevice = engine->getDefaultAudioDevice(IAudioEngine::AUDIO_ENGINE_IN); auto defaultOutputDevice = engine->getDefaultAudioDevice(IAudioEngine::AUDIO_ENGINE_OUT); bool hasSoundCard1InDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName != "none"; bool hasSoundCard1OutDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName != "none"; bool hasSoundCard2InDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName != "none"; bool hasSoundCard2OutDevice = wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName != "none"; g_nSoundCards = 0; if (hasSoundCard1InDevice && hasSoundCard1OutDevice) { g_nSoundCards = 1; if (hasSoundCard2InDevice && hasSoundCard2OutDevice) g_nSoundCards = 2; } // For the purposes of validation, number of channels isn't necessary. auto soundCard1InDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName, IAudioEngine::AUDIO_ENGINE_IN, wxGetApp().appConfiguration.audioConfiguration.soundCard1In.sampleRate, 1); auto soundCard1OutDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName, IAudioEngine::AUDIO_ENGINE_OUT, wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate, 1); auto soundCard2InDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName, IAudioEngine::AUDIO_ENGINE_IN, wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate, 1); auto soundCard2OutDevice = engine->getAudioDevice(wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName, IAudioEngine::AUDIO_ENGINE_OUT, wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate, 1); if (wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName != "none" && !soundCard1InDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName.get()), wxT("Sound Device Removed"), wxOK, this); canRun = false; } else if (canRun && wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName != "none" && !soundCard1OutDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName.get()), wxT("Sound Device Removed"), wxOK, this); canRun = false; } else if (canRun && wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName != "none" && !soundCard2InDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName.get()), wxT("Sound Device Removed"), wxOK, this); canRun = false; } else if (canRun && wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName != "none" && !soundCard2OutDevice) { wxMessageBox(wxString::Format( "Your %s device cannot be found and may have been removed from your system. Please go to Tools->Audio Config... to confirm your audio setup.", wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName.get()), wxT("Sound Device Removed"), wxOK, this); canRun = false; } if (!canRun) { if (g_nSoundCards == 1) { if (!soundCard1OutDevice && defaultOutputDevice.isValid()) { wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = defaultOutputDevice.name; wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = defaultOutputDevice.defaultSampleRate; } else { wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName = "none"; wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.sampleRate = 0; } } else if (g_nSoundCards == 2) { if (!soundCard2InDevice && defaultInputDevice.isValid()) { // If we're not already using the default input device as the radio input device, use that instead. if (defaultInputDevice.name != wxGetApp().appConfiguration.audioConfiguration.soundCard1In.deviceName) { wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = defaultInputDevice.name; wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = defaultInputDevice.defaultSampleRate; } else { wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = "none"; wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = 0; } } else { wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName = "none"; wxGetApp().appConfiguration.audioConfiguration.soundCard2In.sampleRate = 0; } if (!soundCard2OutDevice && defaultOutputDevice.isValid()) { // If we're not already using the default output device as the radio input device, use that instead. if (defaultOutputDevice.name != wxGetApp().appConfiguration.audioConfiguration.soundCard1Out.deviceName) { wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = defaultOutputDevice.name; wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = defaultOutputDevice.defaultSampleRate; } else { wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = "none"; wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = 0; } } else { wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName = "none"; wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.sampleRate = 0; } if (wxGetApp().appConfiguration.audioConfiguration.soundCard2In.deviceName == "none" && wxGetApp().appConfiguration.audioConfiguration.soundCard2Out.deviceName == "none") { g_nSoundCards = 1; } } } if (canRun && g_nSoundCards == 0) { // Initial setup. Display Easy Setup dialog. CallAfter([&]() { EasySetupDialog* dlg = new EasySetupDialog(this); if (dlg->ShowModal() == wxOK) { // Show/hide frequency box based on PSK Reporter status. m_freqBox->Show(wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled); // Show/hide callsign combo box based on PSK Reporter Status if (wxGetApp().appConfiguration.reportingConfiguration.reportingEnabled) { m_cboLastReportedCallsigns->Show(); m_txtCtrlCallSign->Hide(); } else { m_cboLastReportedCallsigns->Hide(); m_txtCtrlCallSign->Show(); } // Relayout window so that the changes can take effect. m_panel->Layout(); } }); canRun = false; } engine->stop(); engine->setOnEngineError(nullptr, nullptr); return canRun; } void MainFrame::initializeFreeDVReporter_() { bool receiveOnly = isReceiveOnly(); auto oldReporterObject = wxGetApp().m_sharedReporterObject; wxGetApp().m_sharedReporterObject = std::make_shared( wxGetApp().appConfiguration.reportingConfiguration.freedvReporterHostname->ToStdString(), wxGetApp().appConfiguration.reportingConfiguration.reportingCallsign->ToStdString(), wxGetApp().appConfiguration.reportingConfiguration.reportingGridSquare->ToStdString(), std::string("FreeDV ") + FREEDV_VERSION, receiveOnly); assert(wxGetApp().m_sharedReporterObject); // If we're running, remove any existing reporter object. if (oldReporterObject != nullptr) { for (auto& ptr : wxGetApp().m_reporters) { if (ptr == oldReporterObject) { oldReporterObject = nullptr; ptr = wxGetApp().m_sharedReporterObject; break; } } } // Make built in FreeDV Reporter client available. if (m_reporterDialog == nullptr) { m_reporterDialog = new FreeDVReporterDialog(this); } m_reporterDialog->setReporter(wxGetApp().m_sharedReporterObject); m_reporterDialog->refreshLayout(); // Set up QSY request handler wxGetApp().m_sharedReporterObject->setOnQSYRequestFn([&](std::string callsign, uint64_t freqHz, std::string message) { double freqFactor = 1000.0; std::string fmtMsg = "%s has requested that you QSY to %.01f kHz."; if (!wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { freqFactor *= 1000.0; fmtMsg = "%s has requested that you QSY to %.04f MHz."; } double frequencyReadable = freqHz / freqFactor; wxString fullMessage = wxString::Format(wxString(fmtMsg), callsign, frequencyReadable); int dialogStyle = wxOK | wxICON_INFORMATION | wxCENTRE; if (wxGetApp().rigFrequencyController != nullptr && wxGetApp().appConfiguration.rigControlConfiguration.hamlibEnableFreqModeChanges) { fullMessage = wxString::Format(_("%s Would you like to change to that frequency now?"), fullMessage); dialogStyle = wxYES_NO | wxICON_QUESTION | wxCENTRE; } CallAfter([&, fullMessage, dialogStyle, frequencyReadable]() { wxMessageDialog messageDialog(this, fullMessage, wxT("FreeDV Reporter"), dialogStyle); if (dialogStyle & wxYES_NO) { messageDialog.SetYesNoLabels(_("Change Frequency"), _("Cancel")); } auto answer = messageDialog.ShowModal(); if (answer == wxID_YES) { // This will implicitly cause Hamlib to change the frequecy and mode. if (wxGetApp().appConfiguration.reportingConfiguration.reportingFrequencyAsKhz) { m_cboReportFrequency->SetValue(wxString::Format("%.1f", frequencyReadable)); } else { m_cboReportFrequency->SetValue(wxString::Format("%.4f", frequencyReadable)); } } }); }); wxGetApp().m_sharedReporterObject->connect(); if (!freedvInterface.isRunning()) { wxGetApp().m_sharedReporterObject->hideFromView(); } else { wxGetApp().m_sharedReporterObject->transmit(freedvInterface.getCurrentTxModeStr(), g_tx); wxGetApp().m_sharedReporterObject->freqChange(wxGetApp().appConfiguration.reportingConfiguration.reportingFrequency); } }