freedv-gui/src/main.cpp

3300 lines
126 KiB
C++

//==========================================================================
// 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 <http://www.gnu.org/licenses/>.
//
//==========================================================================
#include <inttypes.h>
#include <time.h>
#include <sstream>
#include <iomanip>
#include <vector>
#include <deque>
#include <random>
#include <chrono>
#include <climits>
#include <wx/cmdline.h>
#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<nin; i++)
{
rx_fdm[i].real = (float)inputBuf[i];
rx_fdm[i].imag = 0.0;
}
freedv_comprx(fdv, outputBuf, rx_fdm);
}
auto endTime = systemClock.now();
auto timeTaken = std::chrono::duration_cast<std::chrono::milliseconds>(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<wxAuiManager&>(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; i<freedvInterface.getCurrentRxModemStats()->neyetr; 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; r<freedvInterface.getCurrentRxModemStats()->nr; 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; c<g_Nc/2; c++)
rx_symbols_copy[c] = fcmult(0.5, cadd(freedvInterface.getCurrentRxModemStats()->rx_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<WAVEFORM_PLOT_BUF; i++)
if (maxDemodIn < abs(demodInPlotSamples[i]))
maxDemodIn = abs(demodInPlotSamples[i]);
// peak from last second
if (maxDemodIn > 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<WAVEFORM_PLOT_BUF; i++)
if (maxSpeechIn < abs(speechInPlotSamples[i]))
maxSpeechIn = abs(speechInPlotSamples[i]);
// peak from last second
if (maxSpeechIn > 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; b<g_Nc*2; b++) {
for(i=b; i<sz_error_pattern; i+= 2*g_Nc) {
m_panelTestFrameErrors->add_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; b<g_Nc*2; b++) {
ber[b+1] = (float)g_error_hist[b]/g_error_histn[b];
}
assert(g_Nc*2 <= 2*MODEM_STATS_NC_MAX);
m_panelTestFrameErrorsHist->add_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; i<sz_error_pattern; i++) {
/* maps to IQ bits from each symbol to a "carrier" (actually one line for each IQ bit in carrier order) */
c = floor(i/4);
/* this will clock in 4 bits/carrier to plot */
m_panelTestFrameErrors->add_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; b<hist_Nc; b++) {
ber[b+1] = (float)g_error_hist[b]/g_error_histn[b];
}
assert(hist_Nc <= 2*MODEM_STATS_NC_MAX);
m_panelTestFrameErrorsHist->add_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<wxRadioButton*> 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<bool> tmpPromise;
std::future<bool> 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<PskReporter>(
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<std::mutex> 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<paCallBackData*>(state);
short* audioData = static_cast<short*>(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<paCallBackData*>(state);
short* audioData = static_cast<short*>(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<paCallBackData*>(state);
short* audioData = static_cast<short*>(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<paCallBackData*>(state);
short* audioData = static_cast<short*>(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<paCallBackData*>(state);
short* audioData = static_cast<short*>(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<LinkStep>(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<FreeDVReporter>(
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);
}
}