diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 36122f931..b5cad7486 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -897,9 +897,14 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes * Fix various screen reader accessibility issues. (PR #481) * Use separate maximums for each slider type on the Filter dialog. (PR #485) * Fix minor UI issues with the Easy Setup dialog. (PR #484) -2. Build system: + * Adjust waterfall settings to better visualize 2 Hz fading. (PR #487) + * Fix issue causing the waterfall to not scroll at the expected rate. (PR #487) +2. Enhancements + * Add ability to average spectrum plot across 1-3 samples. (PR #487) + * Adjust sizing of main page tabs for better readability. (PR #487) +3. Build system: * Update Codec2 to v1.2.0. (PR #483) -3. Cleanup: +4. Cleanup: * Remove 2400B mode from the UI. (PR #479) ## V1.8.12 July 2023 diff --git a/src/defines.h b/src/defines.h index 29fac716e..f458fa7bf 100644 --- a/src/defines.h +++ b/src/defines.h @@ -30,14 +30,14 @@ #define MIN_MAG_DB -40.0 // min of spectrogram/waterfall magnitude axis #define MAX_MAG_DB 0.0 // max of spectrogram/waterfall magnitude axis #define STEP_MAG_DB 5.0 // magnitude axis step -#define BETA 0.95 // constant for time averaging spectrum data +#define BETA 0.75 // constant for time averaging spectrum data #define MIN_F_HZ 0 // min freq on Waterfall and Spectrum #define MAX_F_HZ 3000 // max freq on Waterfall and Spectrum #define STEP_F_HZ 500 // major (e.g. text legend) freq step on Waterfall and Spectrum graticule #define STEP_MINOR_F_HZ 100 // minor (ticks) freq step on Waterfall and Spectrum graticule #define WATERFALL_SECS_Y 30 // number of seconds represented by y axis of waterfall #define WATERFALL_SECS_STEP 5 // graticule y axis steps of waterfall -#define DT 0.1 // time between real time graphing updates +#define DT 0.10 // time between real time graphing updates #define FS 8000 // FDMDV modem sample rate // Scatter diagram diff --git a/src/main.cpp b/src/main.cpp index b2e7f05b0..c3ad3ef5d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -587,10 +587,39 @@ MainFrame::MainFrame(wxWindow *parent) : TopFrame(parent, wxID_ANY, _("FreeDV ") m_auiNbookCtrl->AddPage(m_panelWaterfall, _("Waterfall"), true, wxNullBitmap); // Add Spectrum Plot window - m_panelSpectrum = new PlotSpectrum((wxFrame*) m_auiNbookCtrl, g_avmag, + 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")); - m_auiNbookCtrl->AddPage(m_panelSpectrum, _("Spectrum"), true, wxNullBitmap); + 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(0); + spectrumPanelControlSizer->Add(m_cbxNumSpectrumAveraging, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + 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); @@ -941,6 +970,11 @@ void MainFrame::OnTimer(wxTimerEvent &evt) } 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->m_newdata = true; m_panelSpectrum->Refresh(); diff --git a/src/main.h b/src/main.h index 11111ea42..33934c867 100644 --- a/src/main.h +++ b/src/main.h @@ -287,6 +287,7 @@ class MainFrame : public TopFrame PlotScalar* m_panelDemodIn; PlotScalar* m_panelTestFrameErrors; PlotScalar* m_panelTestFrameErrorsHist; + wxComboBox* m_cbxNumSpectrumAveraging; bool m_RxRunning; diff --git a/src/plot_spectrum.cpp b/src/plot_spectrum.cpp index d296c3228..36e977ef9 100644 --- a/src/plot_spectrum.cpp +++ b/src/plot_spectrum.cpp @@ -54,6 +54,7 @@ PlotSpectrum::PlotSpectrum(wxWindow* parent, float *magdB, int n_magdB, m_newdata = false; m_firstPass = true; m_line_color = 0; + m_numSampleAveraging = 1; SetLabelSize(10.0); m_magdB = magdB; @@ -62,6 +63,18 @@ PlotSpectrum::PlotSpectrum(wxWindow* parent, float *magdB, int n_magdB, m_min_mag_db = min_mag_db; m_rxFreq = 0.0; m_clickTune = clickTune; + + m_prevMagDB = new float[n_magdB]; + assert(m_prevMagDB != nullptr); + + m_nextPrevMagDB = new float[n_magdB]; + assert(m_nextPrevMagDB != nullptr); + + for (int index = 0; index < n_magdB; index++) + { + m_prevMagDB[index] = 0; + m_nextPrevMagDB[index] = 0; + } } //---------------------------------------------------------------- @@ -69,6 +82,11 @@ PlotSpectrum::PlotSpectrum(wxWindow* parent, float *magdB, int n_magdB, //---------------------------------------------------------------- PlotSpectrum::~PlotSpectrum() { + delete[] m_prevMagDB; + m_prevMagDB = nullptr; + + delete[] m_nextPrevMagDB; + m_nextPrevMagDB = nullptr; } //---------------------------------------------------------------- @@ -126,7 +144,26 @@ void PlotSpectrum::draw(wxGraphicsContext* ctx) for(index = 0; index < m_n_magdB; index++) { x = index*index_to_px; - mag = m_magdB[index]; + + switch(m_numSampleAveraging) + { + case 1: + mag = m_magdB[index]; + break; + case 2: + mag = (m_magdB[index] + m_prevMagDB[index]) / 2; + break; + case 3: + mag = (m_magdB[index] + m_prevMagDB[index] + m_nextPrevMagDB[index]) / 3; + break; + default: + assert(0); + break; + } + + m_nextPrevMagDB[index] = m_prevMagDB[index]; + m_prevMagDB[index] = m_magdB[index]; + if (mag > m_max_mag_db) mag = m_max_mag_db; if (mag < m_min_mag_db) mag = m_min_mag_db; y = -(mag - m_max_mag_db) * mag_dB_to_py; diff --git a/src/plot_spectrum.h b/src/plot_spectrum.h index f0b8e354d..09226f58a 100644 --- a/src/plot_spectrum.h +++ b/src/plot_spectrum.h @@ -36,6 +36,8 @@ class PlotSpectrum : public PlotPanel void setRxFreq(float rxFreq) { m_rxFreq = rxFreq; } void setFreqScale(int n_magdB) { m_n_magdB = n_magdB; } + void setNumAveraging(int n) { m_numSampleAveraging = n; } + protected: void OnSize(wxSizeEvent& event); void OnShow(wxShowEvent& event); @@ -49,8 +51,11 @@ class PlotSpectrum : public PlotPanel float m_max_mag_db; float m_min_mag_db; float *m_magdB; + float *m_prevMagDB; + float *m_nextPrevMagDB; int m_n_magdB; bool m_clickTune; + int m_numSampleAveraging; void OnDoubleClickCommon(wxMouseEvent& event); diff --git a/src/plot_waterfall.cpp b/src/plot_waterfall.cpp index 7689b1385..d50595292 100644 --- a/src/plot_waterfall.cpp +++ b/src/plot_waterfall.cpp @@ -24,6 +24,9 @@ #include "main.h" #include "osx_interface.h" +// Tweak accordingly +#define Y_PER_SECOND (30) + extern float g_avmag[]; // av mag spec passed in to draw() void clickTune(float frequency); // callback to pass new click freq @@ -236,7 +239,7 @@ void PlotWaterfall::drawGraticule(wxGraphicsContext* ctx) int x, y, text_w, text_h; char buf[STR_LENGTH]; wxString s; - float f, time, freq_hz_to_px, time_s_to_py; + float f, time, freq_hz_to_px; wxBrush ltGraphBkgBrush; wxColour foregroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); @@ -249,7 +252,6 @@ void PlotWaterfall::drawGraticule(wxGraphicsContext* ctx) ctx->SetFont(tmpFont); freq_hz_to_px = (float)m_imgWidth/(MAX_F_HZ-MIN_F_HZ); - time_s_to_py = (float)m_imgHeight/WATERFALL_SECS_Y; // upper LH coords of plot area are (PLOT_BORDER + XLEFT_OFFSET, PLOT_BORDER) // lower RH coords of plot area are (PLOT_BORDER + XLEFT_OFFSET + m_rGrid.GetWidth(), @@ -257,8 +259,8 @@ void PlotWaterfall::drawGraticule(wxGraphicsContext* ctx) // Check if small screen size means text will overlap - int textXStep = STEP_F_HZ*freq_hz_to_px; - int textYStep = WATERFALL_SECS_STEP*time_s_to_py; + int textXStep = STEP_F_HZ * freq_hz_to_px; + int textYStep = WATERFALL_SECS_STEP * Y_PER_SECOND; snprintf(buf, STR_LENGTH, "%4.0fHz", (float)MAX_F_HZ - STEP_F_HZ); GetTextExtent(buf, &text_w, &text_h); int overlappedText = (text_w > textXStep) || (text_h > textYStep); @@ -290,10 +292,10 @@ void PlotWaterfall::drawGraticule(wxGraphicsContext* ctx) // Horizontal gridlines ctx->SetPen(m_penDotDash); - for(time=0; time<=WATERFALL_SECS_Y; time+=WATERFALL_SECS_STEP) { - y = m_rGrid.GetHeight() - (WATERFALL_SECS_Y - time)*time_s_to_py; - y += PLOT_BORDER + YBOTTOM_TEXT_OFFSET; - + for(y = PLOT_BORDER + YBOTTOM_TEXT_OFFSET, time=0; + y < m_rGrid.GetHeight(); + time += WATERFALL_SECS_STEP, y += Y_PER_SECOND * WATERFALL_SECS_STEP) + { if (m_graticule) ctx->StrokeLine(PLOT_BORDER + XLEFT_OFFSET, y, (m_rGrid.GetWidth() + PLOT_BORDER + XLEFT_OFFSET), y); @@ -315,13 +317,11 @@ void PlotWaterfall::drawGraticule(wxGraphicsContext* ctx) //------------------------------------------------------------------------- void PlotWaterfall::plotPixelData() { - float spec_index_per_px; float intensity_per_dB; float px_per_sec; int index; int dy; int px; - int py; int intensity; /* @@ -336,7 +336,7 @@ void PlotWaterfall::plotPixelData() */ // determine dy, the height of one "block" - px_per_sec = (float)m_imgHeight / WATERFALL_SECS_Y; + px_per_sec = Y_PER_SECOND; dy = m_dT * px_per_sec; // update min and max amplitude estimates @@ -356,44 +356,41 @@ void PlotWaterfall::plotPixelData() m_max_mag = BETA*m_max_mag + (1 - BETA)*max_mag; m_min_mag = max_mag - 20.0; intensity_per_dB = (float)256 /(m_max_mag - m_min_mag); - spec_index_per_px = ((float)(MAX_F_HZ)/(float)m_modem_stats_max_f_hz)*(float)MODEM_STATS_NSPEC / (float)m_imgWidth; // Draw last line of blocks using latest amplitude data ------------------ - unsigned char dyImageData[3 * dy * m_imgWidth]; - for(py = dy - 1; py >= 0; py--) + int baseRowWidthPixels = ((float)MODEM_STATS_NSPEC / (float)m_modem_stats_max_f_hz) * MAX_F_HZ; + unsigned char dyImageData[3 * baseRowWidthPixels]; + for(px = 0; px < baseRowWidthPixels; px++) { - for(px = 0; px < m_imgWidth; px++) - { - index = px * spec_index_per_px; - assert(index < MODEM_STATS_NSPEC); + index = px; + assert(index < MODEM_STATS_NSPEC); - intensity = intensity_per_dB * (g_avmag[index] - m_min_mag); - if(intensity > 255) intensity = 255; - if (intensity < 0) intensity = 0; + intensity = intensity_per_dB * (g_avmag[index] - m_min_mag); + if(intensity > 255) intensity = 255; + if (intensity < 0) intensity = 0; - int pixelPos = (py * m_imgWidth * 3) + (px * 3); + int pixelPos = (px * 3); - switch (m_colour) { - case 0: - dyImageData[pixelPos] = m_heatmap_lut[intensity] & 0xff; - dyImageData[pixelPos + 1] = (m_heatmap_lut[intensity] >> 8) & 0xff; - dyImageData[pixelPos + 2] = (m_heatmap_lut[intensity] >> 16) & 0xff; - break; - case 1: - dyImageData[pixelPos] = intensity; - dyImageData[pixelPos + 1] = intensity; - dyImageData[pixelPos + 2] = intensity; - break; - case 2: - dyImageData[pixelPos] = intensity; - dyImageData[pixelPos + 1] = intensity; - if (intensity < 127) - dyImageData[pixelPos + 2] = intensity*2; - else - dyImageData[pixelPos + 2] = 255; - - break; - } + switch (m_colour) { + case 0: + dyImageData[pixelPos] = m_heatmap_lut[intensity] & 0xff; + dyImageData[pixelPos + 1] = (m_heatmap_lut[intensity] >> 8) & 0xff; + dyImageData[pixelPos + 2] = (m_heatmap_lut[intensity] >> 16) & 0xff; + break; + case 1: + dyImageData[pixelPos] = intensity; + dyImageData[pixelPos + 1] = intensity; + dyImageData[pixelPos + 2] = intensity; + break; + case 2: + dyImageData[pixelPos] = intensity; + dyImageData[pixelPos + 1] = intensity; + if (intensity < 127) + dyImageData[pixelPos + 2] = intensity*2; + else + dyImageData[pixelPos + 2] = 255; + + break; } } @@ -403,7 +400,7 @@ void PlotWaterfall::plotPixelData() if (dy > 0) { - wxImage* tmpImage = new wxImage(m_imgWidth, dy, (unsigned char*)&dyImageData, true); + wxImage* tmpImage = new wxImage(baseRowWidthPixels, 1, (unsigned char*)&dyImageData, true); wxBitmap* tmpBmp = new wxBitmap(*tmpImage); { wxMemoryDC fullBmpSourceDC(*m_fullBmp); @@ -411,7 +408,7 @@ void PlotWaterfall::plotPixelData() wxMemoryDC tmpBmpSourceDC(*tmpBmp); fullBmpDestDC.Blit(0, dy, m_imgWidth, m_imgHeight - dy, &fullBmpSourceDC, 0, 0); - fullBmpDestDC.Blit(0, 0, m_imgWidth, dy, &tmpBmpSourceDC, 0, 0); + fullBmpDestDC.StretchBlit(0, 0, m_imgWidth, dy, &tmpBmpSourceDC, 0, 0, baseRowWidthPixels, 1); } delete tmpBmp; delete tmpImage; diff --git a/src/topFrame.cpp b/src/topFrame.cpp index 864917d0e..8bc77f4e7 100644 --- a/src/topFrame.cpp +++ b/src/topFrame.cpp @@ -279,7 +279,6 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const long nb_style = wxAUI_NB_BOTTOM | wxAUI_NB_TAB_SPLIT | wxAUI_NB_TAB_MOVE | wxAUI_NB_SCROLL_BUTTONS; m_auiNbookCtrl = new TabFreeAuiNotebook(m_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, nb_style); // This line sets the fontsize for the tabs on the notebook control - m_auiNbookCtrl->SetFont(wxFont(8, 70, 90, 90, false, wxEmptyString)); m_auiNbookCtrl->SetMinSize(wxSize(375,375)); upperSizer->Add(m_auiNbookCtrl, 1, wxALIGN_TOP|wxEXPAND, 1);