diff --git a/CMakeLists.txt b/CMakeLists.txt index 72ad941..d2b37bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,13 @@ SET (cubicsdr_sources src/SDRThreadQueue.cpp src/SDRThreadTask.cpp src/Demodulator.cpp + src/Gradient.cpp + src/visual/ScopeCanvas.cpp + src/visual/ScopeContext.cpp + src/visual/SpectrumCanvas.cpp + src/visual/SpectrumContext.cpp + src/visual/WaterfallCanvas.cpp + src/visual/WaterfallContext.cpp ) SET (cubicsdr_headers @@ -103,7 +110,18 @@ SET (cubicsdr_headers src/SDRThreadQueue.h src/SDRThreadTask.h src/Demodulator.h + src/Gradient.h + src/visual/ScopeCanvas.h + src/visual/ScopeContext.h + src/visual/SpectrumCanvas.h + src/visual/SpectrumContext.h + src/visual/WaterfallCanvas.h + src/visual/WaterfallContext.h ) + +include_directories ( ${PROJECT_SOURCE_DIR}/src/visual + ${PROJECT_SOURCE_DIR}/src ) + #configure_files(${PROJECT_SOURCE_DIR}/shaders ${PROJECT_BINARY_DIR}/shaders COPYONLY) #configure_files(${PROJECT_SOURCE_DIR}/png ${PROJECT_BINARY_DIR}/png COPYONLY) diff --git a/src/AppFrame.cpp b/src/AppFrame.cpp index eb1046f..f9665e0 100644 --- a/src/AppFrame.cpp +++ b/src/AppFrame.cpp @@ -23,7 +23,18 @@ wxEND_EVENT_TABLE() AppFrame::AppFrame() : wxFrame(NULL, wxID_ANY, wxT("CubicSDR")), frequency(DEFAULT_FREQ) { - canvas = new TestGLCanvas(this, NULL); + wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL); + + scopeCanvas = new ScopeCanvas(this, NULL); + vbox->Add(scopeCanvas, 1, wxEXPAND | wxALL, 0); + vbox->AddSpacer(2); + spectrumCanvas = new SpectrumCanvas(this, NULL); + vbox->Add(spectrumCanvas, 1, wxEXPAND | wxALL, 0); + vbox->AddSpacer(2); + waterfallCanvas = new WaterfallCanvas(this, NULL); + vbox->Add(waterfallCanvas, 4, wxEXPAND | wxALL, 0); + + this->SetSizer(vbox); // SetIcon(wxICON(sample)); @@ -66,13 +77,6 @@ AppFrame::AppFrame() : } AppFrame::~AppFrame() { - delete t_SDR; -// delete t_IQBuffer; - delete m_pQueue; -} - -void AppFrame::OnClose(wxCommandEvent& WXUNUSED(event)) { - { wxCriticalSectionLocker enter(m_pThreadCS); if (t_SDR) { @@ -84,16 +88,24 @@ void AppFrame::OnClose(wxCommandEvent& WXUNUSED(event)) { } } - { - wxCriticalSectionLocker enter(m_pThreadCS); - if (t_IQBuffer) { - wxMessageOutputDebug().Printf("CubicSDR: deleting thread"); - if (t_IQBuffer->Delete() != wxTHREAD_NO_ERROR) { - wxLogError - ("Can't delete the thread!"); - } - } - } +// { +// wxCriticalSectionLocker enter(m_pThreadCS); +// if (t_IQBuffer) { +// wxMessageOutputDebug().Printf("CubicSDR: deleting thread"); +// if (t_IQBuffer->Delete() != wxTHREAD_NO_ERROR) { +// wxLogError +// ("Can't delete the thread!"); +// } +// } +// } + + delete t_SDR; +// delete t_IQBuffer; + delete m_pQueue; +} + +void AppFrame::OnClose(wxCommandEvent& WXUNUSED(event)) { + // true is to force the frame to close Close(true); } @@ -105,9 +117,10 @@ void AppFrame::OnNewWindow(wxCommandEvent& WXUNUSED(event)) { void AppFrame::OnEventInput(wxThreadEvent& event) { std::vector *new_buffer = event.GetPayload *>(); -// std::cout << "Got IQ buffer, length: " << new_buffer->size() << std::endl; - - canvas->setData(new_buffer); + test_demod.writeBuffer(new_buffer); + scopeCanvas->setWaveformPoints(test_demod.waveform_points); + spectrumCanvas->setData(new_buffer); + waterfallCanvas->setData(new_buffer); delete new_buffer; } diff --git a/src/AppFrame.h b/src/AppFrame.h index 50cfe8b..5fb80bc 100644 --- a/src/AppFrame.h +++ b/src/AppFrame.h @@ -3,6 +3,10 @@ #include "wx/frame.h" #include "PrimaryGLContext.h" #include "SDRThread.h" +#include "ScopeCanvas.h" +#include "SpectrumCanvas.h" +#include "WaterfallCanvas.h" +#include "Demodulator.h" // Define a new frame type class AppFrame: public wxFrame { @@ -19,12 +23,17 @@ private: void OnNewWindow(wxCommandEvent& event); void OnIdle(wxIdleEvent& event); - TestGLCanvas *canvas; + ScopeCanvas *scopeCanvas; + SpectrumCanvas *spectrumCanvas; + WaterfallCanvas *waterfallCanvas; SDRThread *t_SDR; IQBufferThread *t_IQBuffer; wxCriticalSection m_pThreadCS; SDRThreadQueue* m_pQueue; unsigned int frequency; + Demodulator test_demod; + +// event table wxDECLARE_EVENT_TABLE(); }; diff --git a/src/CubicSDR.cpp b/src/CubicSDR.cpp index 1f93b67..54dba5f 100644 --- a/src/CubicSDR.cpp +++ b/src/CubicSDR.cpp @@ -42,7 +42,7 @@ int CubicSDR::OnExit() { PrimaryGLContext& CubicSDR::GetContext(wxGLCanvas *canvas) { PrimaryGLContext *glContext; if (!m_glContext) { - m_glContext = new PrimaryGLContext(canvas); + m_glContext = new PrimaryGLContext(canvas, NULL); } glContext = m_glContext; diff --git a/src/CubicSDRDefs.h b/src/CubicSDRDefs.h index ed1593c..d772066 100644 --- a/src/CubicSDRDefs.h +++ b/src/CubicSDRDefs.h @@ -1,8 +1,8 @@ #pragma once #define BUF_SIZE (16 * 32 * 256) -#define SRATE 2500000 -#define FFT_SIZE 8192 +#define SRATE 2000000 +#define FFT_SIZE 2048 -#define DEFAULT_FREQ 107500000 +#define DEFAULT_FREQ 98900000 diff --git a/src/Demodulator.cpp b/src/Demodulator.cpp index 792a2f0..c2af8f3 100644 --- a/src/Demodulator.cpp +++ b/src/Demodulator.cpp @@ -41,10 +41,12 @@ static int patestCallback(const void *inputBuffer, void *outputBuffer, unsigned Demodulator::Demodulator() { - bandwidth = 800000; + bandwidth = 200000; resample_ratio = (float) (bandwidth) / (float) SRATE; - audio_frequency = 44100; - audio_resample_ratio = (float) (audio_frequency) / (float) bandwidth; + wbfm_frequency = 100000; + wbfm_resample_ratio = (float) (wbfm_frequency) / (float) bandwidth; + audio_frequency = 48000; + audio_resample_ratio = (float) (audio_frequency) / (float) wbfm_frequency; PaError err; err = Pa_Initialize(); @@ -83,7 +85,7 @@ Demodulator::Demodulator() { stream = NULL; - err = Pa_OpenStream(&stream, NULL, &outputParameters, 44100, 256, paClipOff, &patestCallback, this); + err = Pa_OpenStream(&stream, NULL, &outputParameters, audio_frequency, 1024, paClipOff, &patestCallback, this); err = Pa_StartStream(stream); if (err != paNoError) { @@ -91,7 +93,7 @@ Demodulator::Demodulator() { std::cout << "\tPortAudio error: " << Pa_GetErrorText(err) << std::endl; } - float fc = 0.5f * (bandwidth / SRATE); // filter cutoff frequency + float fc = 0.5f * ((float)bandwidth / (float)SRATE) * 0.75; // filter cutoff frequency float ft = 0.05f; // filter transition float As = 60.0f; // stop-band attenuation [dB] float mu = 0.0f; // fractional timing offset @@ -103,14 +105,22 @@ Demodulator::Demodulator() { fir_filter = firfilt_crcf_create(h, h_len); + h_len = estimate_req_filter_len(ft, As); + liquid_firdes_kaiser(h_len, 32000.0/(float)wbfm_frequency, As, mu, h); + + fir_audio_filter = firfilt_crcf_create(h, h_len); + // create multi-stage arbitrary resampler object resampler = msresamp_crcf_create(resample_ratio, As); msresamp_crcf_print(resampler); + wbfm_resampler = msresamp_crcf_create(wbfm_resample_ratio, As); + msresamp_crcf_print(wbfm_resampler); + audio_resampler = msresamp_crcf_create(audio_resample_ratio, As); msresamp_crcf_print(audio_resampler); - float kf = 0.1f; // modulation factor + float kf = 0.75; // modulation factor fdem = freqdem_create(kf); freqdem_print(fdem); @@ -166,17 +176,32 @@ void Demodulator::writeBuffer(std::vector *data) { } } - int audio_out_size = ceil((float) (num_written) * audio_resample_ratio); + + int wbfm_out_size = ceil((float) (num_written) * wbfm_resample_ratio); + liquid_float_complex resampled_wbfm_output[wbfm_out_size]; + + unsigned int num_wbfm_written; + msresamp_crcf_execute(wbfm_resampler, resampled_output, num_written, resampled_wbfm_output, &num_wbfm_written); + + + for (int i = 0; i < num_wbfm_written; i++) { + firfilt_crcf_push(fir_audio_filter, resampled_wbfm_output[i]); + firfilt_crcf_execute(fir_audio_filter, &resampled_wbfm_output[i]); + } + + int audio_out_size = ceil((float) (num_wbfm_written) * audio_resample_ratio); liquid_float_complex resampled_audio_output[audio_out_size]; - unsigned int num_audio_written; // number of values written to buffer - msresamp_crcf_execute(audio_resampler, resampled_output, num_written, resampled_audio_output, &num_audio_written); + unsigned int num_audio_written; + msresamp_crcf_execute(audio_resampler, resampled_wbfm_output, num_wbfm_written, resampled_audio_output, &num_audio_written); std::vector *newBuffer = new std::vector; newBuffer->resize(num_audio_written * 2); for (int i = 0; i < num_audio_written; i++) { - (*newBuffer)[i * 2] = resampled_audio_output[i].real; - (*newBuffer)[i * 2 + 1] = resampled_audio_output[i].real; + liquid_float_complex y = resampled_audio_output[i]; + + (*newBuffer)[i * 2] = y.real; + (*newBuffer)[i * 2 + 1] = y.real; } audio_queue.push(newBuffer); diff --git a/src/Demodulator.h b/src/Demodulator.h index 6d085c4..cbf483e 100644 --- a/src/Demodulator.h +++ b/src/Demodulator.h @@ -31,14 +31,22 @@ public: private: firfilt_crcf fir_filter; + firfilt_crcf fir_audio_filter; + + unsigned int bandwidth; msresamp_crcf resampler; - msresamp_crcf audio_resampler; float resample_ratio; - unsigned int bandwidth; - unsigned int audio_frequency; + + msresamp_crcf wbfm_resampler; + float wbfm_resample_ratio; + unsigned int wbfm_frequency; + + msresamp_crcf audio_resampler; float audio_resample_ratio; + unsigned int audio_frequency; + PaStreamParameters outputParameters; PaStream *stream; freqdem fdem; diff --git a/src/Gradient.cpp b/src/Gradient.cpp new file mode 100644 index 0000000..0dba7ca --- /dev/null +++ b/src/Gradient.cpp @@ -0,0 +1,75 @@ +#include "Gradient.h" + +Gradient::Gradient() { + +} + +void Gradient::addColor(GradientColor c) { + colors.push_back(c); +} + +std::vector &Gradient::getRed() { + return r_val; +} + +std::vector &Gradient::getGreen() { + return g_val; +} + +std::vector &Gradient::getBlue() { + return b_val; +} + +void Gradient::generate(unsigned int len) { + int chunk_size = len / (colors.size() - 1); + + int p = 0; + r_val.resize(len); + g_val.resize(len); + b_val.resize(len); + + for (unsigned int j = 0, jMax = colors.size() - 1; j < jMax; j++) { + if (chunk_size * (jMax + 1) < len && j == jMax - 1) { + chunk_size += len - chunk_size * (jMax + 1); + } + + for (unsigned int i = 0; i < chunk_size; i++) { + float idx = (float) (i) / (float) chunk_size; + + float r1 = colors[j].r; + float g1 = colors[j].g; + float b1 = colors[j].b; + + float r2 = colors[j + 1].r; + float g2 = colors[j + 1].g; + float b2 = colors[j + 1].b; + + float r = r1 + (r2 - r1) * idx; + float g = g1 + (g2 - g1) * idx; + float b = b1 + (b2 - b1) * idx; + + if (r < 0.0) + r = 0.0; + if (r > 1.0) + r = 1.0; + if (g < 0.0) + g = 0.0; + if (g > 1.0) + g = 1.0; + if (b < 0.0) + b = 0.0; + if (b > 1.0) + b = 1.0; + + r_val[p] = r; + g_val[p] = g; + b_val[p] = b; + + p++; + } + } +} + +Gradient::~Gradient() { + +} diff --git a/src/Gradient.h b/src/Gradient.h new file mode 100644 index 0000000..10080cd --- /dev/null +++ b/src/Gradient.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +class GradientColor { +public: + float r, g, b; + float w; + + GradientColor(float r_in, float g_in, float b_in) : + r(r_in), g(g_in), b(b_in), w(1) { + } +}; + +class Gradient { +public: + Gradient(); + + void addColor(GradientColor c); + + std::vector &getRed();; + std::vector &getGreen(); + std::vector &getBlue(); + + void generate(unsigned int len); + + ~Gradient(); +private: + std::vector colors; + std::vector r_val; + std::vector g_val; + std::vector b_val; +}; diff --git a/src/PrimaryGLContext.cpp b/src/PrimaryGLContext.cpp index 636a852..6cde9a7 100644 --- a/src/PrimaryGLContext.cpp +++ b/src/PrimaryGLContext.cpp @@ -15,7 +15,7 @@ #include "AppFrame.h" #include -wxString glGetwxString(GLenum name) { +wxString PrimaryGLContext::glGetwxString(GLenum name) { const GLubyte *v = glGetString(name); if (v == 0) { // The error is not important. It is GL_INVALID_ENUM. @@ -28,7 +28,7 @@ wxString glGetwxString(GLenum name) { return wxString((const char*) v); } -static void CheckGLError() { +void PrimaryGLContext::CheckGLError() { GLenum errLast = GL_NO_ERROR; for (;;) { @@ -49,201 +49,10 @@ static void CheckGLError() { } } -PrimaryGLContext::PrimaryGLContext(wxGLCanvas *canvas) : - wxGLContext(canvas) { +PrimaryGLContext::PrimaryGLContext(wxGLCanvas *canvas, wxGLContext *sharedContext) : + wxGLContext(canvas, sharedContext) { SetCurrent(*canvas); - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); - - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - CheckGLError(); } -void PrimaryGLContext::Plot(std::vector &points, std::vector &points2) { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - -// glEnable(GL_LINE_SMOOTH); - - if (points.size()) { - glPushMatrix(); - glTranslatef(-1.0f, -0.9f, 0.0f); - glScalef(2.0f, 1.0f, 1.0f); - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, &points[0]); - glDrawArrays(GL_LINE_STRIP, 0, points.size() / 2); - glDisableClientState(GL_VERTEX_ARRAY); - glPopMatrix(); - } - - if (points2.size()) { - glPushMatrix(); - glTranslatef(-1.0f, 0.5f, 0.0f); - glScalef(2.0f, 1.0f, 1.0f); - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, &points2[0]); - glDrawArrays(GL_LINE_STRIP, 0, points2.size() / 2); - glDisableClientState(GL_VERTEX_ARRAY); - glPopMatrix(); - } - - glFlush(); - - CheckGLError(); -} - -wxBEGIN_EVENT_TABLE(TestGLCanvas, wxGLCanvas) EVT_PAINT(TestGLCanvas::OnPaint) -EVT_KEY_DOWN(TestGLCanvas::OnKeyDown) -EVT_IDLE(TestGLCanvas::OnIdle) -wxEND_EVENT_TABLE() - - - -TestGLCanvas::TestGLCanvas(wxWindow *parent, int *attribList) : - wxGLCanvas(parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, - wxFULL_REPAINT_ON_RESIZE), parent(parent) { - - - int in_block_size = BUF_SIZE / 2; - int out_block_size = FFT_SIZE; - - in = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * in_block_size); - out[0] = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * out_block_size); - out[1] = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * out_block_size); - plan[0] = fftw_plan_dft_1d(out_block_size, in, out[0], FFTW_FORWARD, FFTW_MEASURE); - plan[1] = fftw_plan_dft_1d(out_block_size, out[0], out[1], FFTW_BACKWARD, FFTW_MEASURE); - - fft_ceil_ma = fft_ceil_maa = 1.0; - - -} - -TestGLCanvas::~TestGLCanvas() { - -} - -void TestGLCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { - wxPaintDC dc(this); - const wxSize ClientSize = GetClientSize(); - - PrimaryGLContext& canvas = wxGetApp().GetContext(this); - glViewport(0, 0, ClientSize.x, ClientSize.y); - - std::vector null_pts; - - canvas.Plot(spectrum_points, test_demod.waveform_points); - - SwapBuffers(); -} - -void TestGLCanvas::OnKeyDown(wxKeyEvent& event) { - float angle = 5.0; - - unsigned int freq; - switch (event.GetKeyCode()) { - case WXK_RIGHT: - freq = ((AppFrame*) parent)->getFrequency(); - freq += 100000; - ((AppFrame*) parent)->setFrequency(freq); - break; - case WXK_LEFT: - freq = ((AppFrame*) parent)->getFrequency(); - freq -= 100000; - ((AppFrame*) parent)->setFrequency(freq); - break; - case WXK_DOWN: - break; - case WXK_UP: - break; - case WXK_SPACE: - break; - default: - event.Skip(); - return; - } -} - -void multiply2(float ar, float aj, float br, float bj, float *cr, float *cj) { - *cr = ar * br - aj * bj; - *cj = aj * br + ar * bj; -} -float polar_discriminant2(float ar, float aj, float br, float bj) { - float cr, cj; - double angle; - multiply2(ar, aj, br, -bj, &cr, &cj); - angle = atan2(cj, cr); - return (angle / M_PI); -} - -void TestGLCanvas::setData(std::vector *data) { - - if (data && data->size()) { - if (spectrum_points.size() < FFT_SIZE * 2) { - spectrum_points.resize(FFT_SIZE * 2); - } - - for (int i = 0; i < BUF_SIZE / 2; i++) { - in[i][0] = (float) (*data)[i * 2] / 127.0f; - in[i][1] = (float) (*data)[i * 2 + 1] / 127.0f; - } - - fftw_execute(plan[0]); - - double fft_ceil = 0; - - if (fft_result.size() < FFT_SIZE) { - fft_result.resize(FFT_SIZE); - fft_result_ma.resize(FFT_SIZE); - fft_result_maa.resize(FFT_SIZE); - } - - for (int j = 0; j < 2; j++) { - for (int i = 0, iMax = FFT_SIZE / 2; i < iMax; i++) { - double a = out[0][i][0]; - double b = out[0][i][1]; - double c = sqrt(a * a + b * b); - - double x = out[0][FFT_SIZE / 2 + i][0]; - double y = out[0][FFT_SIZE / 2 + i][1]; - double z = sqrt(x * x + y * y); - - fft_result[i] = (z); - fft_result[FFT_SIZE / 2 + i] = (c); - } - } - - float time_slice = (float) SRATE / (float) (BUF_SIZE / 2); - - for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { - fft_result_maa[i] += (fft_result_ma[i] - fft_result_maa[i]) * 0.65; - fft_result_ma[i] += (fft_result[i] - fft_result_ma[i]) * 0.65; - - if (fft_result_maa[i] > fft_ceil) { - fft_ceil = 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 - fft_ceil_maa) * 0.05; - - // fftw_execute(plan[1]); - - for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { - spectrum_points[i * 2 + 1] = log10(fft_result_maa[i]) / log10(fft_ceil_maa); -// spectrum_points[i * 2 + 1] = (fft_result_maa[i]) / (fft_ceil_maa); - spectrum_points[i * 2] = ((double) i / (double) iMax); - } - - test_demod.writeBuffer(data); - } -} - -void TestGLCanvas::OnIdle(wxIdleEvent &event) { - Refresh(false); -} - diff --git a/src/PrimaryGLContext.h b/src/PrimaryGLContext.h index 9d25844..5a92605 100644 --- a/src/PrimaryGLContext.h +++ b/src/PrimaryGLContext.h @@ -7,43 +7,13 @@ #include #include "CubicSDRDefs.h" -#include "fftw3.h" -#include "Demodulator.h" class PrimaryGLContext: public wxGLContext { public: - PrimaryGLContext(wxGLCanvas *canvas); + PrimaryGLContext(wxGLCanvas *canvas, wxGLContext *sharedContext); - void Plot(std::vector &points, std::vector &points2); + static wxString glGetwxString(GLenum name); + static void CheckGLError(); private: }; - -class TestGLCanvas: public wxGLCanvas { -public: - TestGLCanvas(wxWindow *parent, int *attribList = NULL); - ~TestGLCanvas(); - - void setData(std::vector *data); - -private: - void OnPaint(wxPaintEvent& event); - void OnKeyDown(wxKeyEvent& event); - - void OnIdle(wxIdleEvent &event); - - wxWindow *parent; - std::vector spectrum_points; - - fftw_complex *in, *out[2]; - fftw_plan plan[2]; - - float fft_ceil_ma, fft_ceil_maa; - - std::vector fft_result; - std::vector fft_result_ma; - std::vector fft_result_maa; - - Demodulator test_demod; -wxDECLARE_EVENT_TABLE(); -}; diff --git a/src/visual/ScopeCanvas.cpp b/src/visual/ScopeCanvas.cpp new file mode 100644 index 0000000..f6ab56e --- /dev/null +++ b/src/visual/ScopeCanvas.cpp @@ -0,0 +1,79 @@ +#include "ScopeCanvas.h" + +#include "wx/wxprec.h" + +#ifndef WX_PRECOMP +#include "wx/wx.h" +#endif + +#if !wxUSE_GLCANVAS +#error "OpenGL required: set wxUSE_GLCANVAS to 1 and rebuild the library" +#endif + +#include "CubicSDR.h" +#include "CubicSDRDefs.h" +#include "AppFrame.h" +#include + +wxBEGIN_EVENT_TABLE(ScopeCanvas, wxGLCanvas) EVT_PAINT(ScopeCanvas::OnPaint) +EVT_KEY_DOWN(ScopeCanvas::OnKeyDown) +EVT_IDLE(ScopeCanvas::OnIdle) +wxEND_EVENT_TABLE() + +ScopeCanvas::ScopeCanvas(wxWindow *parent, int *attribList) : + wxGLCanvas(parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, + wxFULL_REPAINT_ON_RESIZE), parent(parent) { + + glContext = new ScopeContext(this, &wxGetApp().GetContext(this)); +} + +ScopeCanvas::~ScopeCanvas() { + +} + +void ScopeCanvas::setWaveformPoints(std::vector &waveform_points_in) { + waveform_points = waveform_points_in; +} + +void ScopeCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { + wxPaintDC dc(this); + const wxSize ClientSize = GetClientSize(); + + glContext->SetCurrent(*this); + glViewport(0, 0, ClientSize.x, ClientSize.y); + + glContext->Plot(waveform_points); + + SwapBuffers(); +} + +void ScopeCanvas::OnKeyDown(wxKeyEvent& event) { + float angle = 5.0; + + unsigned int freq; + switch (event.GetKeyCode()) { + case WXK_RIGHT: + freq = ((AppFrame*) parent)->getFrequency(); + freq += 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_LEFT: + freq = ((AppFrame*) parent)->getFrequency(); + freq -= 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_DOWN: + break; + case WXK_UP: + break; + case WXK_SPACE: + break; + default: + event.Skip(); + return; + } +} + +void ScopeCanvas::OnIdle(wxIdleEvent &event) { + Refresh(false); +} diff --git a/src/visual/ScopeCanvas.h b/src/visual/ScopeCanvas.h new file mode 100644 index 0000000..03faf9e --- /dev/null +++ b/src/visual/ScopeCanvas.h @@ -0,0 +1,32 @@ +#pragma once + +#include "wx/glcanvas.h" +#include "wx/timer.h" + +#include +#include + +#include "ScopeContext.h" + +#include "fftw3.h" + +class ScopeCanvas: public wxGLCanvas { +public: + ScopeCanvas(wxWindow *parent, int *attribList = NULL); + ~ScopeCanvas(); + + void setWaveformPoints(std::vector &waveform_points_in); +private: + void OnPaint(wxPaintEvent& event); + void OnKeyDown(wxKeyEvent& event); + + void OnIdle(wxIdleEvent &event); + + wxWindow *parent; + std::vector waveform_points; + + ScopeContext *glContext; +// event table +wxDECLARE_EVENT_TABLE(); +}; + diff --git a/src/visual/ScopeContext.cpp b/src/visual/ScopeContext.cpp new file mode 100644 index 0000000..26368e2 --- /dev/null +++ b/src/visual/ScopeContext.cpp @@ -0,0 +1,39 @@ +#include "ScopeContext.h" + +#include "ScopeCanvas.h" + +ScopeContext::ScopeContext(ScopeCanvas *canvas, wxGLContext *sharedContext) : + PrimaryGLContext(canvas, sharedContext) { + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); +} + +void ScopeContext::Plot(std::vector &points) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glDisable(GL_TEXTURE_2D); + + glColor3f(1.0, 1.0, 1.0); + + if (points.size()) { + glPushMatrix(); + glTranslatef(-1.0f, 0.0f, 0.0f); + glScalef(2.0f, 2.0f, 1.0f); + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(2, GL_FLOAT, 0, &points[0]); + glDrawArrays(GL_LINE_STRIP, 0, points.size() / 2); + glDisableClientState(GL_VERTEX_ARRAY); + glPopMatrix(); + } + + + glFlush(); + + CheckGLError(); +} diff --git a/src/visual/ScopeContext.h b/src/visual/ScopeContext.h new file mode 100644 index 0000000..41cad0d --- /dev/null +++ b/src/visual/ScopeContext.h @@ -0,0 +1,17 @@ +#pragma once + +#include "PrimaryGLContext.h" +#include "Gradient.h" + +#define NUM_WATERFALL_LINES 512 + +class ScopeCanvas; + +class ScopeContext: public PrimaryGLContext { +public: + ScopeContext(ScopeCanvas *canvas, wxGLContext *sharedContext); + + void Plot(std::vector &points); + +private: +}; diff --git a/src/visual/SpectrumCanvas.cpp b/src/visual/SpectrumCanvas.cpp new file mode 100644 index 0000000..17ca1fb --- /dev/null +++ b/src/visual/SpectrumCanvas.cpp @@ -0,0 +1,156 @@ +#include "SpectrumCanvas.h" + +#include "wx/wxprec.h" + +#ifndef WX_PRECOMP +#include "wx/wx.h" +#endif + +#if !wxUSE_GLCANVAS +#error "OpenGL required: set wxUSE_GLCANVAS to 1 and rebuild the library" +#endif + +#include "CubicSDR.h" +#include "CubicSDRDefs.h" +#include "AppFrame.h" +#include + +wxBEGIN_EVENT_TABLE(SpectrumCanvas, wxGLCanvas) EVT_PAINT(SpectrumCanvas::OnPaint) +EVT_KEY_DOWN(SpectrumCanvas::OnKeyDown) +EVT_IDLE(SpectrumCanvas::OnIdle) +wxEND_EVENT_TABLE() + +SpectrumCanvas::SpectrumCanvas(wxWindow *parent, int *attribList) : + wxGLCanvas(parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, + wxFULL_REPAINT_ON_RESIZE), parent(parent) { + + int in_block_size = BUF_SIZE / 2; + int out_block_size = FFT_SIZE; + + in = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * in_block_size); + out = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * out_block_size); + plan = fftw_plan_dft_1d(out_block_size, in, out, FFTW_FORWARD, FFTW_MEASURE); + + fft_ceil_ma = fft_ceil_maa = 100.0; + fft_floor_ma = fft_floor_maa = 0.0; + + glContext = new SpectrumContext(this, &wxGetApp().GetContext(this)); +} + +SpectrumCanvas::~SpectrumCanvas() { + +} + +void SpectrumCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { + wxPaintDC dc(this); + const wxSize ClientSize = GetClientSize(); + + glContext->SetCurrent(*this); + glViewport(0, 0, ClientSize.x, ClientSize.y); + + glContext->Draw(spectrum_points); + + SwapBuffers(); +} + +void SpectrumCanvas::OnKeyDown(wxKeyEvent& event) { + float angle = 5.0; + + unsigned int freq; + switch (event.GetKeyCode()) { + case WXK_RIGHT: + freq = ((AppFrame*) parent)->getFrequency(); + freq += 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_LEFT: + freq = ((AppFrame*) parent)->getFrequency(); + freq -= 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_DOWN: + break; + case WXK_UP: + break; + case WXK_SPACE: + break; + default: + event.Skip(); + return; + } +} + +void SpectrumCanvas::setData(std::vector *data) { + + if (data && data->size()) { + if (spectrum_points.size() < FFT_SIZE * 2) { + spectrum_points.resize(FFT_SIZE * 2); + } + + for (int i = 0; i < BUF_SIZE / 2; i++) { + in[i][0] = (float) (*data)[i * 2] / 127.0f; + in[i][1] = (float) (*data)[i * 2 + 1] / 127.0f; + } + + fftw_execute(plan); + + double fft_ceil = 0, fft_floor = 1; + + if (fft_result.size() < FFT_SIZE) { + fft_result.resize(FFT_SIZE); + fft_result_ma.resize(FFT_SIZE); + fft_result_maa.resize(FFT_SIZE); + } + + for (int j = 0; j < 2; j++) { + for (int i = 0, iMax = FFT_SIZE / 2; i < iMax; i++) { + double a = out[i][0]; + double b = out[i][1]; + double c = sqrt(a * a + b * b); + + double x = out[FFT_SIZE / 2 + i][0]; + double y = out[FFT_SIZE / 2 + i][1]; + double z = sqrt(x * x + y * y); + + fft_result[i] = (z); + fft_result[FFT_SIZE / 2 + i] = (c); + } + } + + float time_slice = (float) SRATE / (float) (BUF_SIZE / 2); + + for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { + fft_result_maa[i] += (fft_result_ma[i] - fft_result_maa[i]) * 0.65; + fft_result_ma[i] += (fft_result[i] - fft_result_ma[i]) * 0.65; + + 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 += 1; + fft_floor -= 1; + + fft_ceil_ma = fft_ceil_ma + (fft_ceil - fft_ceil_ma) * 0.01; + fft_ceil_maa = fft_ceil_maa + (fft_ceil_ma - fft_ceil_maa) * 0.01; + + fft_floor_ma = fft_floor_ma + (fft_floor - fft_floor_ma) * 0.01; + fft_floor_maa = fft_floor_maa + (fft_floor_ma - fft_floor_maa) * 0.01; + + // fftw_execute(plan[1]); + + for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { + float v = (log10(fft_result_maa[i] - fft_floor_maa) / log10(fft_ceil_maa - fft_floor_maa)); + spectrum_points[i * 2] = ((float) i / (float) iMax); + spectrum_points[i * 2 + 1] = v; + } + + } +} + +void SpectrumCanvas::OnIdle(wxIdleEvent &event) { + Refresh(false); +} diff --git a/src/visual/SpectrumCanvas.h b/src/visual/SpectrumCanvas.h new file mode 100644 index 0000000..649e8ed --- /dev/null +++ b/src/visual/SpectrumCanvas.h @@ -0,0 +1,43 @@ +#pragma once + +#include "wx/glcanvas.h" +#include "wx/timer.h" + +#include +#include + +#include "SpectrumContext.h" + +#include "fftw3.h" +#include "Demodulator.h" + +class SpectrumCanvas: public wxGLCanvas { +public: + SpectrumCanvas(wxWindow *parent, int *attribList = NULL); + ~SpectrumCanvas(); + + void setData(std::vector *data); +private: + void OnPaint(wxPaintEvent& event); + void OnKeyDown(wxKeyEvent& event); + + void OnIdle(wxIdleEvent &event); + + wxWindow *parent; + std::vector spectrum_points; + + fftw_complex *in, *out; + fftw_plan plan; + + float fft_ceil_ma, fft_ceil_maa; + float fft_floor_ma, fft_floor_maa; + + std::vector fft_result; + std::vector fft_result_ma; + std::vector fft_result_maa; + + SpectrumContext *glContext; +// event table +wxDECLARE_EVENT_TABLE(); +}; + diff --git a/src/visual/SpectrumContext.cpp b/src/visual/SpectrumContext.cpp new file mode 100644 index 0000000..d90d06c --- /dev/null +++ b/src/visual/SpectrumContext.cpp @@ -0,0 +1,36 @@ +#include "SpectrumContext.h" + +#include "SpectrumCanvas.h" + +SpectrumContext::SpectrumContext(SpectrumCanvas *canvas, wxGLContext *sharedContext) : + PrimaryGLContext(canvas, sharedContext) { + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + +} + +void SpectrumContext::Draw(std::vector &points) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glDisable(GL_TEXTURE_2D); + + glColor3f(1.0, 1.0, 1.0); + + if (points.size()) { + glPushMatrix(); + glTranslatef(-1.0f, -0.9f, 0.0f); + glScalef(2.0f, 1.8f, 1.0f); + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(2, GL_FLOAT, 0, &points[0]); + glDrawArrays(GL_LINE_STRIP, 0, points.size() / 2); + glDisableClientState(GL_VERTEX_ARRAY); + glPopMatrix(); + } + CheckGLError(); +} diff --git a/src/visual/SpectrumContext.h b/src/visual/SpectrumContext.h new file mode 100644 index 0000000..be5a62f --- /dev/null +++ b/src/visual/SpectrumContext.h @@ -0,0 +1,17 @@ +#pragma once + +#include "PrimaryGLContext.h" +#include "Gradient.h" + +#define NUM_WATERFALL_LINES 512 + +class SpectrumCanvas; + +class SpectrumContext: public PrimaryGLContext { +public: + SpectrumContext(SpectrumCanvas *canvas, wxGLContext *sharedContext); + + void Draw(std::vector &points); + +private: +}; diff --git a/src/visual/WaterfallCanvas.cpp b/src/visual/WaterfallCanvas.cpp new file mode 100644 index 0000000..6cb3bd4 --- /dev/null +++ b/src/visual/WaterfallCanvas.cpp @@ -0,0 +1,154 @@ +#include "WaterfallCanvas.h" + +#include "wx/wxprec.h" + +#ifndef WX_PRECOMP +#include "wx/wx.h" +#endif + +#if !wxUSE_GLCANVAS +#error "OpenGL required: set wxUSE_GLCANVAS to 1 and rebuild the library" +#endif + +#include "CubicSDR.h" +#include "CubicSDRDefs.h" +#include "AppFrame.h" +#include + +wxBEGIN_EVENT_TABLE(WaterfallCanvas, wxGLCanvas) EVT_PAINT(WaterfallCanvas::OnPaint) +EVT_KEY_DOWN(WaterfallCanvas::OnKeyDown) +EVT_IDLE(WaterfallCanvas::OnIdle) +wxEND_EVENT_TABLE() + +WaterfallCanvas::WaterfallCanvas(wxWindow *parent, int *attribList) : + wxGLCanvas(parent, wxID_ANY, attribList, wxDefaultPosition, wxDefaultSize, + wxFULL_REPAINT_ON_RESIZE), parent(parent) { + + int in_block_size = BUF_SIZE / 2; + int out_block_size = FFT_SIZE; + + in = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * in_block_size); + out = (fftw_complex*) fftw_malloc(sizeof(fftw_complex) * out_block_size); + plan = fftw_plan_dft_1d(out_block_size, in, out, FFTW_FORWARD, FFTW_MEASURE); + + fft_ceil_ma = fft_ceil_maa = 100.0; + fft_floor_ma = fft_floor_maa = 0.0; + + glContext = new WaterfallContext(this, &wxGetApp().GetContext(this)); +} + +WaterfallCanvas::~WaterfallCanvas() { + +} + +void WaterfallCanvas::OnPaint(wxPaintEvent& WXUNUSED(event)) { + wxPaintDC dc(this); + const wxSize ClientSize = GetClientSize(); + + glContext->SetCurrent(*this); + glViewport(0, 0, ClientSize.x, ClientSize.y); + + glContext->Draw(spectrum_points); + + SwapBuffers(); +} + +void WaterfallCanvas::OnKeyDown(wxKeyEvent& event) { + float angle = 5.0; + + unsigned int freq; + switch (event.GetKeyCode()) { + case WXK_RIGHT: + freq = ((AppFrame*) parent)->getFrequency(); + freq += 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_LEFT: + freq = ((AppFrame*) parent)->getFrequency(); + freq -= 100000; + ((AppFrame*) parent)->setFrequency(freq); + break; + case WXK_DOWN: + break; + case WXK_UP: + break; + case WXK_SPACE: + break; + default: + event.Skip(); + return; + } +} + +void WaterfallCanvas::setData(std::vector *data) { + + if (data && data->size()) { + if (spectrum_points.size() < FFT_SIZE * 2) { + spectrum_points.resize(FFT_SIZE * 2); + } + + for (int i = 0; i < BUF_SIZE / 2; i++) { + in[i][0] = (float) (*data)[i * 2] / 127.0f; + in[i][1] = (float) (*data)[i * 2 + 1] / 127.0f; + } + + fftw_execute(plan); + + double fft_ceil = 0, fft_floor = 1; + + if (fft_result.size() < FFT_SIZE) { + fft_result.resize(FFT_SIZE); + fft_result_ma.resize(FFT_SIZE); + fft_result_maa.resize(FFT_SIZE); + } + + for (int j = 0; j < 2; j++) { + for (int i = 0, iMax = FFT_SIZE / 2; i < iMax; i++) { + double a = out[i][0]; + double b = out[i][1]; + double c = sqrt(a * a + b * b); + + double x = out[FFT_SIZE / 2 + i][0]; + double y = out[FFT_SIZE / 2 + i][1]; + double z = sqrt(x * x + y * y); + + fft_result[i] = (z); + fft_result[FFT_SIZE / 2 + i] = (c); + } + } + + float time_slice = (float) SRATE / (float) (BUF_SIZE / 2); + + for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { + fft_result_maa[i] += (fft_result_ma[i] - fft_result_maa[i]) * 0.65; + fft_result_ma[i] += (fft_result[i] - fft_result_ma[i]) * 0.65; + + 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 += 1; + fft_floor -= 1; + + fft_ceil_ma = fft_ceil_ma + (fft_ceil - fft_ceil_ma) * 0.01; + fft_ceil_maa = fft_ceil_maa + (fft_ceil_ma - fft_ceil_maa) * 0.01; + + fft_floor_ma = fft_floor_ma + (fft_floor - fft_floor_ma) * 0.01; + fft_floor_maa = fft_floor_maa + (fft_floor_ma - fft_floor_maa) * 0.01; + + for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { + float v = (log10(fft_result_maa[i] - fft_floor_maa) / log10(fft_ceil_maa - fft_floor_maa)); + spectrum_points[i * 2] = ((float) i / (float) iMax); + spectrum_points[i * 2 + 1] = v; + } + + } +} + +void WaterfallCanvas::OnIdle(wxIdleEvent &event) { + Refresh(false); +} diff --git a/src/visual/WaterfallCanvas.h b/src/visual/WaterfallCanvas.h new file mode 100644 index 0000000..3d36593 --- /dev/null +++ b/src/visual/WaterfallCanvas.h @@ -0,0 +1,42 @@ +#pragma once + +#include "wx/glcanvas.h" +#include "wx/timer.h" + +#include +#include + +#include "WaterfallContext.h" + +#include "fftw3.h" + +class WaterfallCanvas: public wxGLCanvas { +public: + WaterfallCanvas(wxWindow *parent, int *attribList = NULL); + ~WaterfallCanvas(); + + void setData(std::vector *data); +private: + void OnPaint(wxPaintEvent& event); + void OnKeyDown(wxKeyEvent& event); + + void OnIdle(wxIdleEvent &event); + + wxWindow *parent; + std::vector spectrum_points; + + fftw_complex *in, *out; + fftw_plan plan; + + float fft_ceil_ma, fft_ceil_maa; + float fft_floor_ma, fft_floor_maa; + + std::vector fft_result; + std::vector fft_result_ma; + std::vector fft_result_maa; + + WaterfallContext *glContext; +// event table +wxDECLARE_EVENT_TABLE(); +}; + diff --git a/src/visual/WaterfallContext.cpp b/src/visual/WaterfallContext.cpp new file mode 100644 index 0000000..dddb87c --- /dev/null +++ b/src/visual/WaterfallContext.cpp @@ -0,0 +1,84 @@ +#include "WaterfallContext.h" +#include "WaterfallCanvas.h" + +WaterfallContext::WaterfallContext(WaterfallCanvas *canvas, wxGLContext *sharedContext) : + PrimaryGLContext(canvas, sharedContext) { + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + + glGenTextures(1, &waterfall); + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, waterfall); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); + + grad.addColor(GradientColor(0, 0, 0)); + grad.addColor(GradientColor(0, 0, 1.0)); + grad.addColor(GradientColor(0, 1.0, 0)); + grad.addColor(GradientColor(1.0, 1.0, 0)); + grad.addColor(GradientColor(1.0, 0.2, 0.0)); + + grad.generate(256); + + glPixelTransferi(GL_MAP_COLOR, GL_TRUE); + glPixelMapfv(GL_PIXEL_MAP_I_TO_R, 256, &(grad.getRed())[0]); + glPixelMapfv(GL_PIXEL_MAP_I_TO_G, 256, &(grad.getGreen())[0]); + glPixelMapfv(GL_PIXEL_MAP_I_TO_B, 256, &(grad.getBlue())[0]); +} + +void WaterfallContext::Draw(std::vector &points) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + if (points.size()) { + memmove(waterfall_tex + FFT_SIZE, waterfall_tex, (NUM_WATERFALL_LINES - 1) * FFT_SIZE); + + for (int i = 0, iMax = FFT_SIZE; i < iMax; i++) { + float v = points[i * 2 + 1]; + + float wv = v; + if (wv < 0.0) + wv = 0.0; + if (wv > 1.0) + wv = 1.0; + waterfall_tex[i] = (unsigned char) floor(wv * 255.0); + } + } + + glBindTexture(GL_TEXTURE_2D, waterfall); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, FFT_SIZE, NUM_WATERFALL_LINES, 0, GL_COLOR_INDEX, GL_UNSIGNED_BYTE, (GLvoid *) waterfall_tex); + + glDisable(GL_TEXTURE_2D); + + glColor3f(1.0, 1.0, 1.0); + + glEnable(GL_TEXTURE_2D); + // glEnable(GL_COLOR_TABLE); + glBindTexture(GL_TEXTURE_2D, waterfall); + glBegin(GL_QUADS); + glTexCoord2f(0.0, 1.0); + glVertex3f(-1.0, -1.0, 0.0); + glTexCoord2f(1.0, 1.0); + glVertex3f(1.0, -1.0, 0.0); + glTexCoord2f(1.0, 0.0); + glVertex3f(1.0, 1.0, 0.0); + glTexCoord2f(0.0, 0.0); + glVertex3f(-1.0, 1.0, 0.0); + glEnd(); + + glFlush(); + + CheckGLError(); +} diff --git a/src/visual/WaterfallContext.h b/src/visual/WaterfallContext.h new file mode 100644 index 0000000..a488f6d --- /dev/null +++ b/src/visual/WaterfallContext.h @@ -0,0 +1,20 @@ +#pragma once + +#include "PrimaryGLContext.h" +#include "Gradient.h" + +#define NUM_WATERFALL_LINES 512 + +class WaterfallCanvas; + +class WaterfallContext: public PrimaryGLContext { +public: + WaterfallContext(WaterfallCanvas *canvas, wxGLContext *sharedContext); + + void Draw(std::vector &points); + +private: + Gradient grad; + GLuint waterfall; + unsigned char waterfall_tex[FFT_SIZE * NUM_WATERFALL_LINES]; +};