From 7a0f523eaf9463aa1efa09ee9fa1e6fe6498adb2 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Wed, 19 Aug 2015 23:22:46 -0400 Subject: [PATCH 1/6] Test of draggable scope area + fixes - Will be able to drag back/forth to cycle scope/spectrum/plot - Fix for two crashes --- external/cubicvr2/math/mat4.h | 33 +++++++++-- src/AppFrame.cpp | 5 +- src/panel/ScopePanel.cpp | 8 ++- src/ui/GLPanel.cpp | 8 +++ src/ui/GLPanel.h | 1 + src/util/ThreadQueue.h | 2 + src/visual/ScopeCanvas.cpp | 108 ++++++++++++++++++++++++++++++++-- src/visual/ScopeCanvas.h | 14 ++++- 8 files changed, 161 insertions(+), 18 deletions(-) diff --git a/external/cubicvr2/math/mat4.h b/external/cubicvr2/math/mat4.h index 0059cd8..b847d26 100644 --- a/external/cubicvr2/math/mat4.h +++ b/external/cubicvr2/math/mat4.h @@ -74,13 +74,24 @@ namespace CubicVR { return mOut; } - - static mat4 perspective(__float fovy, __float aspect, __float znear, __float zfar) { + static mat4 frustum(__float left, __float right, __float bottom, __float top, __float zNear, __float zFar) { + __float A = (right + left) / (right - left); + __float B = (top + bottom) / (top - bottom); + __float C = - (zFar + zNear) / (zFar - zNear); + __float D = - (-2.0 * zFar * zNear) / (zFar - zNear); + + + return mat4((2.0 * zNear) / (right - left), 0, A, 0, + 0, (2.0 * zNear) / (top - bottom), B, 0, + 0, 0, C, D, + 0, 0, -1, 0); + }; + static mat4 perspective(__float fovy, __float aspect, __float zNear, __float zFar) { __float yFac = tan(fovy * (float)M_PI / 360.0f); __float xFac = yFac * aspect; - return mat4( - 1.0f / xFac, 0, 0, 0, 0, 1.0f / yFac, 0, 0, 0, 0, -(zfar + znear) / (zfar - znear), -1, 0, 0, -(2.0f * zfar * znear) / (zfar - znear), 0); + return mat4::frustum(-xFac, xFac, -yFac, yFac, zNear, zFar); + }; static mat4 ortho(__float left,__float right,__float bottom,__float top,__float znear,__float zfar) { return mat4(2.0f / (right - left), 0, 0, 0, 0, 2.0f / (top - bottom), 0, 0, 0, 0, -2.0f / (zfar - znear), 0, -(left + right) / (right - left), -(top + bottom) / (top - bottom), -(zfar + znear) / (zfar - znear), 1); @@ -300,8 +311,18 @@ namespace CubicVR { return mat4::translate(-eyex,-eyey,-eyez) * mat4( side[0], up[0], -forward[0], 0, side[1], up[1], -forward[1], 0, side[2], up[2], -forward[2], 0, 0, 0, 0, 1); }; + + static vec3 unProject(mat4 pMatrix, mat4 mvMatrix, float width, float height, float winx, float winy, float winz) { + vec4 p(((winx / width) * 2.0) - 1.0, -(((winy / height) * 2.0) - 1.0), 1.0, 1.0); + + vec4 invp = mat4::vec4_multiply(mat4::vec4_multiply(p, mat4::inverse(pMatrix)), mat4::inverse(mvMatrix)); + + vec3 result(invp[0] / invp[3], invp[1] / invp[3], invp[2] / invp[3]); + + return result; + }; }; - -} + } + #endif /* defined(__CubicVR2__mat4__) */ diff --git a/src/AppFrame.cpp b/src/AppFrame.cpp index e843109..f85d2b2 100644 --- a/src/AppFrame.cpp +++ b/src/AppFrame.cpp @@ -153,11 +153,12 @@ AppFrame::AppFrame() : waterfallCanvas->setup(2048, 512); waterfallDataThread = new FFTVisualDataThread(); - t_FFTData = new std::thread(&FFTVisualDataThread::threadMain, waterfallDataThread); waterfallDataThread->setInputQueue("IQDataInput", wxGetApp().getWaterfallVisualQueue()); waterfallDataThread->setOutputQueue("FFTDataOutput", waterfallCanvas->getVisualDataQueue()); - + + t_FFTData = new std::thread(&FFTVisualDataThread::threadMain, waterfallDataThread); + waterfallSpeedMeter = new MeterCanvas(this, attribList); waterfallSpeedMeter->setHelpTip("Waterfall speed, click or drag to adjust (max 1024 lines per second)"); waterfallSpeedMeter->setMax(sqrt(1024)); diff --git a/src/panel/ScopePanel.cpp b/src/panel/ScopePanel.cpp index c50fa08..f72a14c 100644 --- a/src/panel/ScopePanel.cpp +++ b/src/panel/ScopePanel.cpp @@ -2,6 +2,7 @@ #include "ColorTheme.h" ScopePanel::ScopePanel() : GLPanel(), scopeMode(SCOPE_MODE_Y) { + setFill(GLPanelFillType::GLPANEL_FILL_NONE); bgPanel.setFill(GLPanelFillType::GLPANEL_FILL_GRAD_BAR_Y); bgPanelStereo[0].setFill(GLPanelFillType::GLPANEL_FILL_GRAD_BAR_Y); bgPanelStereo[0].setPosition(0, 0.5); @@ -21,14 +22,13 @@ void ScopePanel::setPoints(std::vector &points) { } void ScopePanel::drawPanelContents() { - - glLineWidth(1.0); if (scopeMode == SCOPE_MODE_Y) { bgPanel.setFillColor(ThemeMgr::mgr.currentTheme->scopeBackground, ThemeMgr::mgr.currentTheme->scopeBackground * 2.0); bgPanel.calcTransform(transform); bgPanel.draw(); - + glLineWidth(1.0); + glEnable(GL_LINE_SMOOTH); glLoadMatrixf(transform); glColor3f(ThemeMgr::mgr.currentTheme->scopeLine.r * 0.35, ThemeMgr::mgr.currentTheme->scopeLine.g * 0.35, ThemeMgr::mgr.currentTheme->scopeLine.b * 0.35); @@ -45,8 +45,10 @@ void ScopePanel::drawPanelContents() { bgPanelStereo[1].calcTransform(transform); bgPanelStereo[1].draw(); + glLineWidth(1.0); glLoadMatrixf(transform); glColor3f(ThemeMgr::mgr.currentTheme->scopeLine.r, ThemeMgr::mgr.currentTheme->scopeLine.g, ThemeMgr::mgr.currentTheme->scopeLine.b); + glEnable(GL_LINE_SMOOTH); glBegin (GL_LINES); glVertex2f(-1.0, 0.0); glVertex2f(1.0, 0.0); diff --git a/src/ui/GLPanel.cpp b/src/ui/GLPanel.cpp index 00fa2be..7d06c81 100644 --- a/src/ui/GLPanel.cpp +++ b/src/ui/GLPanel.cpp @@ -8,6 +8,9 @@ using namespace CubicVR; GLPanel::GLPanel() : fillType(GLPANEL_FILL_SOLID), contentsVisible(true), transform(mat4::identity()) { pos[0] = 0.0f; pos[1] = 0.0f; + rot[0] = 0.0f; + rot[1] = 0.0f; + rot[2] = 0.0f; size[0] = 1.0f; size[1] = 1.0f; fill[0] = RGBA4f(0.5,0.5,0.5); @@ -241,6 +244,11 @@ void GLPanel::drawPanelContents() { void GLPanel::calcTransform(mat4 transform_in) { // compute local transform localTransform = mat4::translate(pos[0], pos[1], 0) * mat4::scale(size[0], size[1], 1); + + if (rot[0] || rot[1] || rot[2]) { + localTransform *= mat4::rotate(rot[0], rot[1], rot[2]); + } + // compute global transform transform = transform_in * localTransform; diff --git a/src/ui/GLPanel.h b/src/ui/GLPanel.h index b87bac2..0f3e3b6 100644 --- a/src/ui/GLPanel.h +++ b/src/ui/GLPanel.h @@ -36,6 +36,7 @@ public: typedef enum GLPanelFillType { GLPANEL_FILL_NONE, GLPANEL_FILL_SOLID, GLPANEL_FILL_GRAD_X, GLPANEL_FILL_GRAD_Y, GLPANEL_FILL_GRAD_BAR_X, GLPANEL_FILL_GRAD_BAR_Y } GLPanelFillType; typedef enum GLPanelCoordinateSystem { GLPANEL_Y_DOWN_ZERO_ONE, GLPANEL_Y_UP_ZERO_ONE, GLPANEL_Y_UP, GLPANEL_Y_DOWN } GLPanelCoordinateSystem; float pos[2]; + float rot[3]; float size[2]; float view[2]; GLPanelFillType fillType; diff --git a/src/util/ThreadQueue.h b/src/util/ThreadQueue.h index 8ba5e0a..a2d693e 100644 --- a/src/util/ThreadQueue.h +++ b/src/util/ThreadQueue.h @@ -49,6 +49,8 @@ public: * \param[in] item An item. */ void set_max_num_items(unsigned int max_num_items) { + std::lock_guard < std::mutex > lock(m_mutex); + m_max_num_items = max_num_items; } diff --git a/src/visual/ScopeCanvas.cpp b/src/visual/ScopeCanvas.cpp index a4cd4be..220997f 100644 --- a/src/visual/ScopeCanvas.cpp +++ b/src/visual/ScopeCanvas.cpp @@ -14,18 +14,28 @@ #include "CubicSDRDefs.h" #include "AppFrame.h" #include +#include wxBEGIN_EVENT_TABLE(ScopeCanvas, wxGLCanvas) EVT_PAINT(ScopeCanvas::OnPaint) EVT_IDLE(ScopeCanvas::OnIdle) +EVT_MOTION(ScopeCanvas::OnMouseMoved) +EVT_LEFT_DOWN(ScopeCanvas::OnMouseDown) +EVT_LEFT_UP(ScopeCanvas::OnMouseReleased) +EVT_RIGHT_DOWN(ScopeCanvas::OnMouseRightDown) +EVT_RIGHT_UP(ScopeCanvas::OnMouseRightReleased) +EVT_LEAVE_WINDOW(ScopeCanvas::OnMouseLeftWindow) +EVT_ENTER_WINDOW(ScopeCanvas::OnMouseEnterWindow) wxEND_EVENT_TABLE() -ScopeCanvas::ScopeCanvas(wxWindow *parent, int *attribList) : - wxGLCanvas(parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, - wxFULL_REPAINT_ON_RESIZE), stereo(false), ppmMode(false) { +ScopeCanvas::ScopeCanvas(wxWindow *parent, int *attribList) : InteractiveCanvas(parent, attribList), stereo(false), ppmMode(false), ctr(0), ctrTarget(0), dragAccel(0) { glContext = new ScopeContext(this, &wxGetApp().GetContext(this)); inputData.set_max_num_items(1); + bgPanel.setFill(GLPanel::GLPANEL_FILL_GRAD_Y); + bgPanel.setSize(1.0, 0.5); + bgPanel.setPosition(0.0, -0.5); + panelSpacing = 0.2; } ScopeCanvas::~ScopeCanvas() { @@ -71,21 +81,74 @@ void ScopeCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { initGLExtensions(); glViewport(0, 0, ClientSize.x, ClientSize.y); - + glContext->DrawBegin(); + + bgPanel.setFillColor(ThemeMgr::mgr.currentTheme->scopeBackground * 3.0, RGBA4f(0,0,0,0)); + bgPanel.calcTransform(CubicVR::mat4::identity()); + bgPanel.draw(); + scopePanel.setMode(stereo?ScopePanel::SCOPE_MODE_2Y:ScopePanel::SCOPE_MODE_Y); - scopePanel.calcTransform(CubicVR::mat4::identity()); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glLoadMatrixf(CubicVR::mat4::perspective(45.0, 1.0, 1.0, 1000.0)); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + CubicVR::mat4 modelView = CubicVR::mat4::lookat(0, 0, -1.2, 0, 0, 0, 0, -1, 0); + + float panelWidth = 1.0; + float panelInterval = (panelWidth * 2.0 + panelSpacing); + + if (!mouseTracker.mouseDown()) { + if (!dragAccel) { + ctrTarget = round(ctr / panelInterval); + if (ctrTarget < -1.0) { + ctrTarget = -1.0; + } else if (ctrTarget > 0.0) { + ctrTarget = 0.0; + } + ctrTarget *= panelInterval; + if (ctr != ctrTarget) { + ctr += (ctrTarget-ctr)*0.2; + } + } else { + dragAccel -= dragAccel * 0.01; + if (abs(dragAccel) < 0.1 || ctr < ctrTarget-panelInterval/2.0 || ctr > ctrTarget+panelInterval/2.0 ) { + dragAccel = 0; + } else { + ctr += dragAccel; + } + } + } + + scopePanel.setPosition(ctr, 0); + float roty = atan2(scopePanel.pos[0],1.2); + scopePanel.rot[1] = -(roty * (180.0 / M_PI)); + scopePanel.calcTransform(modelView); scopePanel.draw(); + + scopePanel.setPosition(panelInterval+ctr, 0); + roty = atan2(scopePanel.pos[0],1.2); + scopePanel.rot[1] = -(roty * (180.0 / M_PI)); + scopePanel.calcTransform(modelView); + scopePanel.draw(); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); glContext->DrawTunerTitles(ppmMode); if (!deviceName.empty()) { glContext->DrawDeviceName(deviceName); } glContext->DrawEnd(); - SwapBuffers(); } + void ScopeCanvas::OnIdle(wxIdleEvent &event) { Refresh(); event.RequestMore(); @@ -94,3 +157,36 @@ void ScopeCanvas::OnIdle(wxIdleEvent &event) { ScopeRenderDataQueue *ScopeCanvas::getInputQueue() { return &inputData; } + +void ScopeCanvas::OnMouseMoved(wxMouseEvent& event) { + InteractiveCanvas::OnMouseMoved(event); + if (mouseTracker.mouseDown()) { + dragAccel = 4.0*mouseTracker.getDeltaMouseX(); + ctr += dragAccel; + } +} + +void ScopeCanvas::OnMouseWheelMoved(wxMouseEvent& event) { + +} + +void ScopeCanvas::OnMouseDown(wxMouseEvent& event) { + InteractiveCanvas::OnMouseDown(event); + +} + +void ScopeCanvas::OnMouseReleased(wxMouseEvent& event) { + InteractiveCanvas::OnMouseReleased(event); + +} + +void ScopeCanvas::OnMouseEnterWindow(wxMouseEvent& event) { + InteractiveCanvas::OnMouseEnterWindow(event); + +} + +void ScopeCanvas::OnMouseLeftWindow(wxMouseEvent& event) { + InteractiveCanvas::OnMouseLeftWindow(event); + +} + diff --git a/src/visual/ScopeCanvas.h b/src/visual/ScopeCanvas.h index 6cdff03..d5bfba5 100644 --- a/src/visual/ScopeCanvas.h +++ b/src/visual/ScopeCanvas.h @@ -10,8 +10,9 @@ #include "ScopeVisualProcessor.h" #include "ScopePanel.h" #include "fftw3.h" +#include "InteractiveCanvas.h" -class ScopeCanvas: public wxGLCanvas { +class ScopeCanvas: public InteractiveCanvas { public: ScopeCanvas(wxWindow *parent, int *attribList = NULL); ~ScopeCanvas(); @@ -26,13 +27,24 @@ public: private: void OnPaint(wxPaintEvent& event); void OnIdle(wxIdleEvent &event); + void OnMouseMoved(wxMouseEvent& event); + void OnMouseWheelMoved(wxMouseEvent& event); + void OnMouseDown(wxMouseEvent& event); + void OnMouseReleased(wxMouseEvent& event); + void OnMouseEnterWindow(wxMouseEvent& event); + void OnMouseLeftWindow(wxMouseEvent& event); ScopeRenderDataQueue inputData; ScopePanel scopePanel; + GLPanel bgPanel; ScopeContext *glContext; std::string deviceName; bool stereo; bool ppmMode; + float panelSpacing; + float ctr; + float ctrTarget; + float dragAccel; // event table wxDECLARE_EVENT_TABLE(); }; From 13140ec28c9635874e3ffdab970d4c80ebe6cfcc Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Sun, 23 Aug 2015 17:51:20 -0400 Subject: [PATCH 2/6] SpectrumVisualProcessor thread fixes, spectrum label optimize --- src/panel/SpectrumPanel.cpp | 28 ++++++++++++++----------- src/process/SpectrumVisualProcessor.cpp | 27 +++++++++++++++++------- src/process/SpectrumVisualProcessor.h | 5 +++-- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/panel/SpectrumPanel.cpp b/src/panel/SpectrumPanel.cpp index c615490..2b6f65e 100644 --- a/src/panel/SpectrumPanel.cpp +++ b/src/panel/SpectrumPanel.cpp @@ -132,27 +132,31 @@ void SpectrumPanel::drawPanelContents() { long long leftFreq = (double) freq - ((double) bandwidth / 2.0); long long rightFreq = leftFreq + (double) bandwidth; - - long long firstMhz = (leftFreq / 1000000) * 1000000; - long double mhzStart = ((long double) (firstMhz - leftFreq) / (long double) (rightFreq - leftFreq)) * 2.0; + + long long hzStep = 1000000; long double mhzStep = (100000.0 / (long double) (rightFreq - leftFreq)) * 2.0; - double mhzVisualStep = 0.1f; - + double mhzVisualStep = 0.1; + + std::stringstream label; + label.precision(1); + if (mhzStep * 0.5 * viewWidth < 40) { mhzStep = (250000.0 / (long double) (rightFreq - leftFreq)) * 2.0; - mhzVisualStep = 0.25f; + mhzVisualStep = 0.25; + label.precision(2); } - if (mhzStep * 0.5 * viewWidth > 400) { + if (mhzStep * 0.5 * viewWidth > 350) { mhzStep = (10000.0 / (long double) (rightFreq - leftFreq)) * 2.0; - mhzVisualStep = 0.01f; + mhzVisualStep = 0.01; + label.precision(2); } - - long double currentMhz = trunc(floor(firstMhz / 1000000.0)); - std::stringstream label; - label.precision(2); + long long firstMhz = (leftFreq / hzStep) * hzStep; + long double mhzStart = ((long double) (firstMhz - leftFreq) / (long double) (rightFreq - leftFreq)) * 2.0; + long double currentMhz = trunc(floor(firstMhz / (long double)1000000.0)); + double hPos = 1.0 - (16.0 / viewHeight); double lMhzPos = 1.0 - (5.0 / viewHeight); diff --git a/src/process/SpectrumVisualProcessor.cpp b/src/process/SpectrumVisualProcessor.cpp index 9322462..8dc890d 100644 --- a/src/process/SpectrumVisualProcessor.cpp +++ b/src/process/SpectrumVisualProcessor.cpp @@ -14,7 +14,7 @@ SpectrumVisualProcessor::SpectrumVisualProcessor() : lastInputBandwidth(0), last fft_ceil_ma = fft_ceil_maa = 100.0; fft_floor_ma = fft_floor_maa = 0.0; - desiredInputSize = 0; + desiredInputSize.store(0); fft_average_rate = 0.65; } @@ -27,19 +27,25 @@ bool SpectrumVisualProcessor::isView() { } void SpectrumVisualProcessor::setView(bool bView) { + busy_run.lock(); is_view.store(bView); + busy_run.unlock(); } void SpectrumVisualProcessor::setFFTAverageRate(float fftAverageRate) { - this->fft_average_rate = fftAverageRate; + busy_run.lock(); + this->fft_average_rate.store(fftAverageRate); + busy_run.unlock(); } float SpectrumVisualProcessor::getFFTAverageRate() { - return this->fft_average_rate; + return this->fft_average_rate.load(); } void SpectrumVisualProcessor::setCenterFrequency(long long centerFreq_in) { + busy_run.lock(); centerFreq.store(centerFreq_in); + busy_run.unlock(); } long long SpectrumVisualProcessor::getCenterFrequency() { @@ -47,7 +53,9 @@ long long SpectrumVisualProcessor::getCenterFrequency() { } void SpectrumVisualProcessor::setBandwidth(long bandwidth_in) { + busy_run.lock(); bandwidth.store(bandwidth_in); + busy_run.unlock(); } long SpectrumVisualProcessor::getBandwidth() { @@ -55,12 +63,14 @@ long SpectrumVisualProcessor::getBandwidth() { } int SpectrumVisualProcessor::getDesiredInputSize() { - return desiredInputSize; + return desiredInputSize.load(); } void SpectrumVisualProcessor::setup(int fftSize_in) { + busy_run.lock(); + fftSize = fftSize_in; - desiredInputSize = fftSize; + desiredInputSize.store(fftSize); if (fftwInput) { free(fftwInput); @@ -82,7 +92,7 @@ void SpectrumVisualProcessor::setup(int fftSize_in) { fftwf_destroy_plan(fftw_plan); } fftw_plan = fftwf_plan_dft_1d(fftSize, fftwInput, fftwOutput, FFTW_FORWARD, FFTW_ESTIMATE); - + busy_run.unlock(); } void SpectrumVisualProcessor::process() { @@ -102,6 +112,7 @@ void SpectrumVisualProcessor::process() { } iqData->busy_rw.lock(); + busy_run.lock(); std::vector *data = &iqData->data; @@ -118,6 +129,7 @@ void SpectrumVisualProcessor::process() { if (!iqData->frequency || !iqData->sampleRate) { iqData->decRefCount(); iqData->busy_rw.unlock(); + busy_run.unlock(); return; } @@ -125,7 +137,7 @@ void SpectrumVisualProcessor::process() { int desired_input_size = fftSize / resamplerRatio; - this->desiredInputSize = desired_input_size; + this->desiredInputSize.store(desired_input_size); if (iqData->data.size() < desired_input_size) { // std::cout << "fft underflow, desired: " << desired_input_size << " actual:" << input->data.size() << std::endl; @@ -304,5 +316,6 @@ void SpectrumVisualProcessor::process() { iqData->decRefCount(); iqData->busy_rw.unlock(); + busy_run.unlock(); } diff --git a/src/process/SpectrumVisualProcessor.h b/src/process/SpectrumVisualProcessor.h index 8e1e694..149c170 100644 --- a/src/process/SpectrumVisualProcessor.h +++ b/src/process/SpectrumVisualProcessor.h @@ -53,7 +53,7 @@ private: double fft_ceil_ma, fft_ceil_maa; double fft_floor_ma, fft_floor_maa; - float fft_average_rate; + std::atomic fft_average_rate; std::vector fft_result; std::vector fft_result_ma; @@ -66,7 +66,8 @@ private: std::vector shiftBuffer; std::vector resampleBuffer; - int desiredInputSize; + std::atomic_int desiredInputSize; + std::mutex busy_run; }; From c30cce9114b87fbda0423fec73dc94f0d272658a Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Mon, 24 Aug 2015 01:31:37 -0400 Subject: [PATCH 3/6] Add functional Spectrum view to demodulator visuals - Might need to do some renaming from Scope->AVDisplay or something for ScopeCanvas to avoid confusion. --- src/AppFrame.cpp | 7 + src/audio/AudioThread.h | 1 + src/demod/DemodulatorThread.cpp | 12 +- src/demod/DemodulatorThread.h | 2 +- src/panel/SpectrumPanel.cpp | 50 +++++-- src/panel/SpectrumPanel.h | 6 +- src/process/ScopeVisualProcessor.cpp | 204 +++++++++++++++++++++++---- src/process/ScopeVisualProcessor.h | 30 ++++ src/util/ThreadQueue.h | 19 ++- src/visual/ScopeCanvas.cpp | 136 ++++++++++++++---- src/visual/ScopeCanvas.h | 13 ++ 11 files changed, 405 insertions(+), 75 deletions(-) diff --git a/src/AppFrame.cpp b/src/AppFrame.cpp index f85d2b2..0a20f77 100644 --- a/src/AppFrame.cpp +++ b/src/AppFrame.cpp @@ -90,7 +90,9 @@ AppFrame::AppFrame() : demodTray->AddSpacer(1); scopeCanvas = new ScopeCanvas(this, attribList); + scopeCanvas->setHelpTip("Audio Visuals, drag left/right to toggle Scope or Spectrum."); demodScopeTray->Add(scopeCanvas, 8, wxEXPAND | wxALL, 0); + wxGetApp().getScopeProcessor()->setup(2048); wxGetApp().getScopeProcessor()->attachOutput(scopeCanvas->getInputQueue()); demodScopeTray->AddSpacer(1); @@ -790,6 +792,11 @@ void AppFrame::OnIdle(wxIdleEvent& event) { scopeCanvas->setPPMMode(demodTuner->isAltDown()); + scopeCanvas->setShowDb(spectrumCanvas->getShowDb()); + wxGetApp().getScopeProcessor()->setScopeEnabled(scopeCanvas->scopeVisible()); + wxGetApp().getScopeProcessor()->setSpectrumEnabled(scopeCanvas->spectrumVisible()); + wxGetApp().getAudioVisualQueue()->set_max_num_items((scopeCanvas->scopeVisible()?1:0) + (scopeCanvas->spectrumVisible()?1:0)); + wxGetApp().getScopeProcessor()->run(); wxGetApp().getSpectrumDistributor()->run(); diff --git a/src/audio/AudioThread.h b/src/audio/AudioThread.h index d5dcb61..01ebda5 100644 --- a/src/audio/AudioThread.h +++ b/src/audio/AudioThread.h @@ -14,6 +14,7 @@ class AudioThreadInput: public ReferenceCounter { public: long long frequency; + int inputRate; int sampleRate; int channels; float peak; diff --git a/src/demod/DemodulatorThread.cpp b/src/demod/DemodulatorThread.cpp index 485fd54..2ed31b0 100644 --- a/src/demod/DemodulatorThread.cpp +++ b/src/demod/DemodulatorThread.cpp @@ -310,6 +310,7 @@ void DemodulatorThread::run() { ati = outputBuffers.getBuffer(); ati->sampleRate = audioSampleRate; + ati->inputRate = inp->sampleRate; ati->setRefCount(1); if (demodulatorType == DEMOD_TYPE_RAW) { @@ -359,7 +360,9 @@ void DemodulatorThread::run() { if (ati && audioVisOutputQueue != NULL && audioVisOutputQueue->empty()) { ati_vis->busy_update.lock(); - + ati_vis->sampleRate = inp->sampleRate; + ati_vis->inputRate = inp->sampleRate; + int num_vis = DEMOD_VIS_SIZE; if (demodulatorType == DEMOD_TYPE_RAW || (stereo && inp->sampleRate >= 100000)) { ati_vis->channels = 2; @@ -377,6 +380,8 @@ void DemodulatorThread::run() { } } else { for (int i = 0; i < stereoSize / 2; i++) { + ati_vis->inputRate = audioSampleRate; + ati_vis->sampleRate = 36000; ati_vis->data[i] = ati->data[i * 2]; ati_vis->data[i + stereoSize / 2] = ati->data[i * 2 + 1]; } @@ -384,7 +389,7 @@ void DemodulatorThread::run() { } else { ati_vis->channels = 1; if (numAudioWritten > bufSize) { - + ati_vis->inputRate = audioSampleRate; if (num_vis > numAudioWritten) { num_vis = numAudioWritten; } @@ -399,9 +404,8 @@ void DemodulatorThread::run() { // std::cout << "Signal: " << agc_crcf_get_signal_level(agc) << " -- " << agc_crcf_get_rssi(agc) << "dB " << std::endl; } - audioVisOutputQueue->push(ati_vis); - ati_vis->busy_update.unlock(); + audioVisOutputQueue->push(ati_vis); } if (ati != NULL) { diff --git a/src/demod/DemodulatorThread.h b/src/demod/DemodulatorThread.h index 7f7e417..91d6b0c 100644 --- a/src/demod/DemodulatorThread.h +++ b/src/demod/DemodulatorThread.h @@ -8,7 +8,7 @@ typedef ThreadQueue DemodulatorThreadOutputQueue; -#define DEMOD_VIS_SIZE 1024 +#define DEMOD_VIS_SIZE 2048 class DemodulatorThread : public IOThread { public: diff --git a/src/panel/SpectrumPanel.cpp b/src/panel/SpectrumPanel.cpp index 2b6f65e..142e1a4 100644 --- a/src/panel/SpectrumPanel.cpp +++ b/src/panel/SpectrumPanel.cpp @@ -5,7 +5,7 @@ #include #include "ColorTheme.h" -SpectrumPanel::SpectrumPanel() : floorValue(0), ceilValue(1), showDb(false) { +SpectrumPanel::SpectrumPanel() : floorValue(0), ceilValue(1), showDb(false), fftSize(2048) { setFill(GLPANEL_FILL_GRAD_Y); setFillColor(ThemeMgr::mgr.currentTheme->fftBackground * 2.0, ThemeMgr::mgr.currentTheme->fftBackground); @@ -51,6 +51,14 @@ long long SpectrumPanel::getBandwidth() { return bandwidth; } +void SpectrumPanel::setFFTSize(int fftSize_in) { + this->fftSize = fftSize_in; +} + +int SpectrumPanel::getFFTSize() { + return fftSize; +} + void SpectrumPanel::setShowDb(bool showDb) { this->showDb = showDb; if (showDb) { @@ -133,7 +141,7 @@ void SpectrumPanel::drawPanelContents() { long long leftFreq = (double) freq - ((double) bandwidth / 2.0); long long rightFreq = leftFreq + (double) bandwidth; - long long hzStep = 1000000; + long long hzStep = 100000; long double mhzStep = (100000.0 / (long double) (rightFreq - leftFreq)) * 2.0; double mhzVisualStep = 0.1; @@ -144,10 +152,37 @@ void SpectrumPanel::drawPanelContents() { if (mhzStep * 0.5 * viewWidth < 40) { mhzStep = (250000.0 / (long double) (rightFreq - leftFreq)) * 2.0; mhzVisualStep = 0.25; - label.precision(2); - } - if (mhzStep * 0.5 * viewWidth > 350) { + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (500000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 0.5; + } + + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (1000000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 1.0; + } + + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (2500000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 2.5; + } + + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (5000000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 5.0; + } + + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (10000000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 10.0; + } + + if (mhzStep * 0.5 * viewWidth < 40) { + mhzStep = (50000000.0 / (long double) (rightFreq - leftFreq)) * 2.0; + mhzVisualStep = 50.0; + } + } else if (mhzStep * 0.5 * viewWidth > 350) { mhzStep = (10000.0 / (long double) (rightFreq - leftFreq)) * 2.0; mhzVisualStep = 0.01; label.precision(2); @@ -193,21 +228,20 @@ void SpectrumPanel::drawPanelContents() { glLineWidth(1.0); - if (showDb) { float dbPanelWidth = (1.0/viewWidth)*75.0; float dbPanelHeight = (1.0/viewHeight)*14.0; std::stringstream ssLabel; - ssLabel << std::fixed << std::setprecision(1) << (20.0 * log10(2.0*(getCeilValue())/2048.0)) << "dB"; + ssLabel << std::fixed << std::setprecision(1) << (20.0 * log10(2.0*(getCeilValue())/(double)fftSize)) << "dB"; dbPanelCeil.setText(ssLabel.str(), GLFont::GLFONT_ALIGN_RIGHT); dbPanelCeil.setSize(dbPanelWidth, dbPanelHeight); dbPanelCeil.setPosition(-1.0 + dbPanelWidth, 1.0 - dbPanelHeight); ssLabel.str(""); - ssLabel << (20.0 * log10(2.0*(getFloorValue())/2048.0)) << "dB"; + ssLabel << (20.0 * log10(2.0*(getFloorValue())/(double)fftSize)) << "dB"; dbPanelFloor.setText(ssLabel.str(), GLFont::GLFONT_ALIGN_RIGHT); dbPanelFloor.setSize(dbPanelWidth, dbPanelHeight); diff --git a/src/panel/SpectrumPanel.h b/src/panel/SpectrumPanel.h index 6ffa3f8..3ccd747 100644 --- a/src/panel/SpectrumPanel.h +++ b/src/panel/SpectrumPanel.h @@ -19,7 +19,10 @@ public: void setBandwidth(long long bandwidth); long long getBandwidth(); - + + void setFFTSize(int fftSize_in); + int getFFTSize(); + void setShowDb(bool showDb); bool getShowDb(); @@ -28,6 +31,7 @@ protected: private: float floorValue, ceilValue; + int fftSize; long long freq; long long bandwidth; std::vector points; diff --git a/src/process/ScopeVisualProcessor.cpp b/src/process/ScopeVisualProcessor.cpp index 15669da..1cc4d24 100644 --- a/src/process/ScopeVisualProcessor.cpp +++ b/src/process/ScopeVisualProcessor.cpp @@ -1,4 +1,54 @@ #include "ScopeVisualProcessor.h" +#include +#include + +ScopeVisualProcessor::ScopeVisualProcessor(): fftInData(NULL), fftwOutput(NULL), fftw_plan(NULL), maxScopeSamples(1024) { + scopeEnabled.store(true); + spectrumEnabled.store(true); + fft_average_rate = 0.65; +} + +ScopeVisualProcessor::~ScopeVisualProcessor() { + if (fftInData) { + free(fftInData); + } + if (fftwOutput) { + free(fftwOutput); + } + if (fftw_plan) { + fftwf_destroy_plan(fftw_plan); + } +} + + +void ScopeVisualProcessor::setup(int fftSize_in) { + fftSize = fftSize_in; + desiredInputSize = fftSize; + + if (fftInData) { + free(fftInData); + } + fftInData = (float*) fftwf_malloc(sizeof(float) * fftSize); + if (fftwOutput) { + free(fftwOutput); + } + fftwOutput = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * fftSize); + if (fftw_plan) { + fftwf_destroy_plan(fftw_plan); + } + fftw_plan = fftwf_plan_dft_r2c_1d(fftSize, fftInData, fftwOutput, FFTW_ESTIMATE); + //(fftSize, fftInData, fftwOutput, 0); + //(fftSize, fftwInput, fftwOutput, FFTW_R2HC, FFTW_ESTIMATE); + +} + +void ScopeVisualProcessor::setScopeEnabled(bool scopeEnable) { + scopeEnabled.store(scopeEnable); +} + +void ScopeVisualProcessor::setSpectrumEnabled(bool spectrumEnable) { + spectrumEnabled.store(spectrumEnable); +} void ScopeVisualProcessor::process() { if (!isOutputEmpty()) { @@ -11,42 +61,144 @@ void ScopeVisualProcessor::process() { if (!audioInputData) { return; } - int iMax = audioInputData->data.size(); + int i, iMax = audioInputData->data.size(); if (!iMax) { audioInputData->decRefCount(); return; } audioInputData->busy_update.lock(); - ScopeRenderData *renderData = outputBuffers.getBuffer(); - renderData->channels = audioInputData->channels; - if (renderData->waveform_points.size() != iMax * 2) { - renderData->waveform_points.resize(iMax * 2); - } + ScopeRenderData *renderData = NULL; + + if (scopeEnabled) { + iMax = audioInputData->data.size(); + if (iMax > maxScopeSamples) { + iMax = maxScopeSamples; + } - float peak = 1.0f; - - for (int i = 0; i < iMax; i++) { - float p = fabs(audioInputData->data[i]); - if (p > peak) { - peak = p; + renderData = outputBuffers.getBuffer(); + renderData->channels = audioInputData->channels; + renderData->inputRate = audioInputData->inputRate; + renderData->sampleRate = audioInputData->sampleRate; + + if (renderData->waveform_points.size() != iMax * 2) { + renderData->waveform_points.resize(iMax * 2); } - } - - if (audioInputData->channels == 2) { - for (int i = 0; i < iMax; i++) { - renderData->waveform_points[i * 2] = (((double) (i % (iMax/2)) / (double) iMax) * 2.0 - 0.5) * 2.0; - renderData->waveform_points[i * 2 + 1] = audioInputData->data[i] / peak; - } - } else { - for (int i = 0; i < iMax; i++) { - renderData->waveform_points[i * 2] = (((double) i / (double) iMax) - 0.5) * 2.0; - renderData->waveform_points[i * 2 + 1] = audioInputData->data[i] / peak; - } - } - distribute(renderData); + float peak = 1.0f; + + for (i = 0; i < iMax; i++) { + float p = fabs(audioInputData->data[i]); + if (p > peak) { + peak = p; + } + } + + if (audioInputData->channels == 2) { + iMax = audioInputData->data.size(); + if (renderData->waveform_points.size() != iMax * 2) { + renderData->waveform_points.resize(iMax * 2); + } + for (i = 0; i < iMax; i++) { + renderData->waveform_points[i * 2] = (((double) (i % (iMax/2)) / (double) iMax) * 2.0 - 0.5) * 2.0; + renderData->waveform_points[i * 2 + 1] = audioInputData->data[i] / peak; + } + } else { + for (i = 0; i < iMax; i++) { + renderData->waveform_points[i * 2] = (((double) i / (double) iMax) - 0.5) * 2.0; + renderData->waveform_points[i * 2 + 1] = audioInputData->data[i] / peak; + } + } + + renderData->spectrum = false; + + distribute(renderData); + } + + if (spectrumEnabled) { + renderData = outputBuffers.getBuffer(); + iMax = audioInputData->data.size(); + + if (audioInputData->channels==1) { + for (i = 0; i < fftSize; i++) { + if (i < iMax) { + fftInData[i] = audioInputData->data[i]; + } else { + fftInData[i] = 0; + } + } + } else if (audioInputData->channels==2) { + iMax = iMax/2; + for (i = 0; i < fftSize; i++) { + if (i < iMax) { + fftInData[i] = audioInputData->data[i] + audioInputData->data[iMax+i]; + } else { + fftInData[i] = 0; + } + } + } + + + fftwf_execute(fftw_plan); + + float fft_ceil = 0, fft_floor = 1; + + if (fft_result.size() < (fftSize/2)) { + fft_result.resize((fftSize/2)); + fft_result_ma.resize((fftSize/2)); + fft_result_maa.resize((fftSize/2)); + } + + for (i = 0; i < (fftSize/2); i++) { + float a = fftwOutput[i][0]; + float b = fftwOutput[i][1]; + fft_result[i] = sqrt( a * a + b * b); + } + + for (i = 0; i < (fftSize/2); i++) { + fft_result_maa[i] += (fft_result_ma[i] - fft_result_maa[i]) * fft_average_rate; + fft_result_ma[i] += (fft_result[i] - fft_result_ma[i]) * fft_average_rate; + + if (fft_result_maa[i] > fft_ceil) { + fft_ceil = fft_result_maa[i]; + } + if (fft_result_maa[i] < fft_floor) { + fft_floor = fft_result_maa[i]; + } + } + + fft_ceil_ma = fft_ceil_ma + (fft_ceil - fft_ceil_ma) * 0.05; + fft_ceil_maa = fft_ceil_maa + (fft_ceil_ma - fft_ceil_maa) * 0.05; + + fft_floor_ma = fft_floor_ma + (fft_floor - fft_floor_ma) * 0.05; + fft_floor_maa = fft_floor_maa + (fft_floor_ma - fft_floor_maa) * 0.05; + + int outSize = fftSize/2; + + if (audioInputData->sampleRate != audioInputData->inputRate) { + outSize = (int)floor((float)outSize * ((float)audioInputData->sampleRate/(float)audioInputData->inputRate)); + } + + if (renderData->waveform_points.size() != outSize*2) { + renderData->waveform_points.resize(outSize*2); + } + + for (i = 0; i < outSize; i++) { + float v = (log10(fft_result_maa[i]+0.25 - (fft_floor_maa-0.75)) / log10((fft_ceil_maa+0.25) - (fft_floor_maa-0.75))); + renderData->waveform_points[i * 2] = ((double) i / (double) (outSize)); + renderData->waveform_points[i * 2 + 1] = v; + } + + renderData->fft_floor = fft_floor_maa; + renderData->fft_ceil = fft_ceil_maa; + renderData->fft_size = fftSize/2; + renderData->inputRate = audioInputData->inputRate; + renderData->sampleRate = audioInputData->sampleRate; + renderData->spectrum = true; + distribute(renderData); + } + audioInputData->busy_update.unlock(); } } diff --git a/src/process/ScopeVisualProcessor.h b/src/process/ScopeVisualProcessor.h index 08ce3bb..98a2398 100644 --- a/src/process/ScopeVisualProcessor.h +++ b/src/process/ScopeVisualProcessor.h @@ -2,17 +2,47 @@ #include "VisualProcessor.h" #include "AudioThread.h" +#include "fftw3.h" class ScopeRenderData: public ReferenceCounter { public: std::vector waveform_points; + int inputRate; + int sampleRate; int channels; + bool spectrum; + int fft_size; + double fft_floor, fft_ceil; }; typedef ThreadQueue ScopeRenderDataQueue; class ScopeVisualProcessor : public VisualProcessor { +public: + ScopeVisualProcessor(); + ~ScopeVisualProcessor(); + void setup(int fftSize_in); + void setScopeEnabled(bool scopeEnable); + void setSpectrumEnabled(bool spectrumEnable); protected: void process(); ReBuffer outputBuffers; + + std::atomic_bool scopeEnabled; + std::atomic_bool spectrumEnabled; + + float *fftInData; + fftwf_complex *fftwOutput; + fftwf_plan fftw_plan; + int fftSize; + int desiredInputSize; + int maxScopeSamples; + + double fft_ceil_ma, fft_ceil_maa; + double fft_floor_ma, fft_floor_maa; + float fft_average_rate; + + std::vector fft_result; + std::vector fft_result_ma; + std::vector fft_result_maa; }; diff --git a/src/util/ThreadQueue.h b/src/util/ThreadQueue.h index a2d693e..2d88b59 100644 --- a/src/util/ThreadQueue.h +++ b/src/util/ThreadQueue.h @@ -30,13 +30,17 @@ class ThreadQueue : public ThreadQueueBase { public: /*! Create safe queue. */ - ThreadQueue() = default; + ThreadQueue() { + m_max_num_items.store(0); + }; ThreadQueue(ThreadQueue&& sq) { m_queue = std::move(sq.m_queue); + m_max_num_items.store(0); } ThreadQueue(const ThreadQueue& sq) { std::lock_guard < std::mutex > lock(sq.m_mutex); m_queue = sq.m_queue; + m_max_num_items.store(0); } /*! Destroy safe queue. */ @@ -50,8 +54,9 @@ public: */ void set_max_num_items(unsigned int max_num_items) { std::lock_guard < std::mutex > lock(m_mutex); - - m_max_num_items = max_num_items; + if (m_max_num_items.load() != max_num_items) { + m_max_num_items.store(max_num_items); + } } /** @@ -62,7 +67,7 @@ public: bool push(const value_type& item) { std::lock_guard < std::mutex > lock(m_mutex); - if (m_max_num_items > 0 && m_queue.size() > m_max_num_items) + if (m_max_num_items.load() > 0 && m_queue.size() > m_max_num_items.load()) return false; m_queue.push(item); @@ -78,7 +83,7 @@ public: bool push(const value_type&& item) { std::lock_guard < std::mutex > lock(m_mutex); - if (m_max_num_items > 0 && m_queue.size() > m_max_num_items) + if (m_max_num_items.load() > 0 && m_queue.size() > m_max_num_items.load()) return false; m_queue.push(item); @@ -219,7 +224,7 @@ public: */ bool full() const { std::lock_guard < std::mutex > lock(m_mutex); - return (m_max_num_items != 0) && (m_queue.size() >= m_max_num_items); + return (m_max_num_items.load() != 0) && (m_queue.size() >= m_max_num_items.load()); } /** @@ -280,7 +285,7 @@ private: std::queue m_queue; mutable std::mutex m_mutex; std::condition_variable m_condition; - unsigned int m_max_num_items = 0; + std::atomic_uint m_max_num_items; }; /*! Swaps the contents of two ThreadQueue objects. */ diff --git a/src/visual/ScopeCanvas.cpp b/src/visual/ScopeCanvas.cpp index 220997f..9dfaced 100644 --- a/src/visual/ScopeCanvas.cpp +++ b/src/visual/ScopeCanvas.cpp @@ -28,20 +28,51 @@ EVT_LEAVE_WINDOW(ScopeCanvas::OnMouseLeftWindow) EVT_ENTER_WINDOW(ScopeCanvas::OnMouseEnterWindow) wxEND_EVENT_TABLE() -ScopeCanvas::ScopeCanvas(wxWindow *parent, int *attribList) : InteractiveCanvas(parent, attribList), stereo(false), ppmMode(false), ctr(0), ctrTarget(0), dragAccel(0) { +ScopeCanvas::ScopeCanvas(wxWindow *parent, int *attribList) : InteractiveCanvas(parent, attribList), stereo(false), ppmMode(false), ctr(0), ctrTarget(0), dragAccel(0), helpTip("") { glContext = new ScopeContext(this, &wxGetApp().GetContext(this)); - inputData.set_max_num_items(1); + inputData.set_max_num_items(2); bgPanel.setFill(GLPanel::GLPANEL_FILL_GRAD_Y); bgPanel.setSize(1.0, 0.5); bgPanel.setPosition(0.0, -0.5); - panelSpacing = 0.2; + panelSpacing = 0.4; + + parentPanel.addChild(&scopePanel); + parentPanel.addChild(&spectrumPanel); + parentPanel.setFill(GLPanel::GLPANEL_FILL_NONE); + scopePanel.setSize(1.0,-1.0); + spectrumPanel.setSize(1.0,-1.0); + spectrumPanel.setShowDb(true); } ScopeCanvas::~ScopeCanvas() { } +bool ScopeCanvas::scopeVisible() { + float panelInterval = (2.0 + panelSpacing); + + ctrTarget = abs(round(ctr / panelInterval)); + + if (ctrTarget == 0 || dragAccel || (ctr != ctrTarget)) { + return true; + } + + return false; +} + +bool ScopeCanvas::spectrumVisible() { + float panelInterval = (2.0 + panelSpacing); + + ctrTarget = abs(round(ctr / panelInterval)); + + if (ctrTarget == 1 || dragAccel || (ctr != ctrTarget)) { + return true; + } + + return false; +} + void ScopeCanvas::setStereo(bool state) { stereo = state; } @@ -59,20 +90,40 @@ bool ScopeCanvas::getPPMMode() { return ppmMode; } +void ScopeCanvas::setShowDb(bool showDb) { + this->showDb = showDb; +} + +bool ScopeCanvas::getShowDb() { + return showDb; +} + void ScopeCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { wxPaintDC dc(this); const wxSize ClientSize = GetClientSize(); - if (!inputData.empty()) { + while (!inputData.empty()) { ScopeRenderData *avData; inputData.pop(avData); - if (avData) { + if (!avData->spectrum) { if (avData->waveform_points.size()) { scopePanel.setPoints(avData->waveform_points); setStereo(avData->channels == 2); } + avData->decRefCount(); + } else { + if (avData->waveform_points.size()) { + spectrumPanel.setPoints(avData->waveform_points); + spectrumPanel.setFloorValue(avData->fft_floor); + spectrumPanel.setCeilValue(avData->fft_ceil); + spectrumPanel.setBandwidth((avData->sampleRate/2)*1000); + spectrumPanel.setFreq((avData->sampleRate/4)*1000); + spectrumPanel.setFFTSize(avData->fft_size); + spectrumPanel.setShowDb(showDb); + } + avData->decRefCount(); } } @@ -96,53 +147,74 @@ void ScopeCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - CubicVR::mat4 modelView = CubicVR::mat4::lookat(0, 0, -1.2, 0, 0, 0, 0, -1, 0); + CubicVR::mat4 modelView = CubicVR::mat4::lookat(0, 0, -1.205, 0, 0, 0, 0, -1, 0); float panelWidth = 1.0; float panelInterval = (panelWidth * 2.0 + panelSpacing); if (!mouseTracker.mouseDown()) { + ctrTarget = round(ctr / panelInterval); + if (ctrTarget < -1.0) { + ctrTarget = -1.0; + } else if (ctrTarget > 0.0) { + ctrTarget = 0.0; + } + ctrTarget *= panelInterval; if (!dragAccel) { - ctrTarget = round(ctr / panelInterval); - if (ctrTarget < -1.0) { - ctrTarget = -1.0; - } else if (ctrTarget > 0.0) { - ctrTarget = 0.0; - } - ctrTarget *= panelInterval; if (ctr != ctrTarget) { ctr += (ctrTarget-ctr)*0.2; } + if (abs(ctr - ctrTarget) < 0.001) { + ctr=ctrTarget; + } } else { - dragAccel -= dragAccel * 0.01; - if (abs(dragAccel) < 0.1 || ctr < ctrTarget-panelInterval/2.0 || ctr > ctrTarget+panelInterval/2.0 ) { + dragAccel -= dragAccel * 0.1; + if ((abs(dragAccel) < 0.2) || (ctr < (ctrTarget-panelInterval/2.0)) || (ctr > (ctrTarget+panelInterval/2.0)) ) { dragAccel = 0; } else { ctr += dragAccel; } } } + + float roty = 0; scopePanel.setPosition(ctr, 0); - float roty = atan2(scopePanel.pos[0],1.2); - scopePanel.rot[1] = -(roty * (180.0 / M_PI)); - scopePanel.calcTransform(modelView); - scopePanel.draw(); + if (scopeVisible()) { + scopePanel.contentsVisible = true; + roty = atan2(scopePanel.pos[0],1.2); + scopePanel.rot[1] = -(roty * (180.0 / M_PI)); + } else { + scopePanel.contentsVisible = false; + } + + spectrumPanel.setPosition(panelInterval+ctr, 0); + if (spectrumVisible()) { + spectrumPanel.setFillColor(ThemeMgr::mgr.currentTheme->scopeBackground * 2.0, RGBA4f(0,0,0,0)); + spectrumPanel.contentsVisible = true; + roty = atan2(spectrumPanel.pos[0],1.2); + spectrumPanel.rot[1] = -(roty * (180.0 / M_PI)); + } else { + spectrumPanel.contentsVisible = false; + } - scopePanel.setPosition(panelInterval+ctr, 0); - roty = atan2(scopePanel.pos[0],1.2); - scopePanel.rot[1] = -(roty * (180.0 / M_PI)); - scopePanel.calcTransform(modelView); - scopePanel.draw(); + parentPanel.calcTransform(modelView); + parentPanel.draw(); + + if (spectrumVisible()) { + spectrumPanel.drawChildren(); + } + + glLoadMatrixf(scopePanel.transform); + if (!deviceName.empty()) { + glContext->DrawDeviceName(deviceName); + } glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glContext->DrawTunerTitles(ppmMode); - if (!deviceName.empty()) { - glContext->DrawDeviceName(deviceName); - } glContext->DrawEnd(); SwapBuffers(); @@ -182,7 +254,10 @@ void ScopeCanvas::OnMouseReleased(wxMouseEvent& event) { void ScopeCanvas::OnMouseEnterWindow(wxMouseEvent& event) { InteractiveCanvas::OnMouseEnterWindow(event); - + if (!helpTip.empty()) { + setStatusText(helpTip); + } + SetCursor(wxCURSOR_SIZEWE); } void ScopeCanvas::OnMouseLeftWindow(wxMouseEvent& event) { @@ -190,3 +265,8 @@ void ScopeCanvas::OnMouseLeftWindow(wxMouseEvent& event) { } + +void ScopeCanvas::setHelpTip(std::string tip) { + helpTip = tip; +} + diff --git a/src/visual/ScopeCanvas.h b/src/visual/ScopeCanvas.h index d5bfba5..d868b2e 100644 --- a/src/visual/ScopeCanvas.h +++ b/src/visual/ScopeCanvas.h @@ -9,6 +9,7 @@ #include "ScopeContext.h" #include "ScopeVisualProcessor.h" #include "ScopePanel.h" +#include "SpectrumPanel.h" #include "fftw3.h" #include "InteractiveCanvas.h" @@ -22,6 +23,14 @@ public: void setPPMMode(bool ppmMode); bool getPPMMode(); + void setShowDb(bool showDb); + bool getShowDb(); + + bool scopeVisible(); + bool spectrumVisible(); + + void setHelpTip(std::string tip); + ScopeRenderDataQueue *getInputQueue(); private: @@ -36,15 +45,19 @@ private: ScopeRenderDataQueue inputData; ScopePanel scopePanel; + GLPanel parentPanel; + SpectrumPanel spectrumPanel; GLPanel bgPanel; ScopeContext *glContext; std::string deviceName; bool stereo; bool ppmMode; + bool showDb; float panelSpacing; float ctr; float ctrTarget; float dragAccel; + std::string helpTip; // event table wxDECLARE_EVENT_TABLE(); }; From fe46fb191f0540f836c23d26320cfe82f3723324 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Mon, 24 Aug 2015 07:25:04 -0400 Subject: [PATCH 4/6] Fix demodulator worker thread crash on terminate --- src/demod/DemodulatorPreThread.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/demod/DemodulatorPreThread.cpp b/src/demod/DemodulatorPreThread.cpp index c93df6f..5c3820f 100644 --- a/src/demod/DemodulatorPreThread.cpp +++ b/src/demod/DemodulatorPreThread.cpp @@ -72,9 +72,6 @@ void DemodulatorPreThread::initialize() { } DemodulatorPreThread::~DemodulatorPreThread() { - delete workerThread; - delete workerQueue; - delete workerResults; } void DemodulatorPreThread::run() { @@ -260,7 +257,7 @@ void DemodulatorPreThread::run() { inp->decRefCount(); - if (!workerResults->empty()) { + if (!terminated && !workerResults->empty()) { while (!workerResults->empty()) { DemodulatorWorkerThreadResult result; workerResults->pop(result); @@ -323,7 +320,12 @@ void DemodulatorPreThread::terminate() { terminated = true; DemodulatorThreadIQData *inp = new DemodulatorThreadIQData; // push dummy to nudge queue iqInputQueue->push(inp); + DemodulatorWorkerThreadCommand command(DemodulatorWorkerThreadCommand::DEMOD_WORKER_THREAD_CMD_NULL); + workerQueue->push(command); workerThread->terminate(); - t_Worker->detach(); + t_Worker->join(); delete t_Worker; + delete workerThread; + delete workerResults; + delete workerQueue; } From 233e07164a3a92ddeee639208d841e57f5909828 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Mon, 24 Aug 2015 08:58:08 -0400 Subject: [PATCH 5/6] spectrum label calc error --- src/panel/SpectrumPanel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panel/SpectrumPanel.cpp b/src/panel/SpectrumPanel.cpp index 142e1a4..034d3c5 100644 --- a/src/panel/SpectrumPanel.cpp +++ b/src/panel/SpectrumPanel.cpp @@ -141,7 +141,7 @@ void SpectrumPanel::drawPanelContents() { long long leftFreq = (double) freq - ((double) bandwidth / 2.0); long long rightFreq = leftFreq + (double) bandwidth; - long long hzStep = 100000; + long long hzStep = 1000000; long double mhzStep = (100000.0 / (long double) (rightFreq - leftFreq)) * 2.0; double mhzVisualStep = 0.1; From 261afbce8e5e8aca8968021006d9da00b94eaaa3 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Tue, 25 Aug 2015 21:35:38 -0400 Subject: [PATCH 6/6] Windows fix-up --- external/fftw-3.3.4/64/libfftw3f-3.lib | Bin 246562 -> 0 bytes src/util/ThreadQueue.h | 1 + 2 files changed, 1 insertion(+) delete mode 100644 external/fftw-3.3.4/64/libfftw3f-3.lib diff --git a/external/fftw-3.3.4/64/libfftw3f-3.lib b/external/fftw-3.3.4/64/libfftw3f-3.lib deleted file mode 100644 index 4f2f89db33ecc005567ad0ff351211606e46a81c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246562 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 #include #include +#include class ThreadQueueBase {