diff --git a/CMakeLists.txt b/CMakeLists.txt index 11daabd7..58be071e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ if (MSVC) link_directories(sdrpp "C:/Program Files/PothosSDR/lib/") include_directories(sdrpp "C:/Program Files/PothosSDR/include/volk/") include_directories(sdrpp "C:/Program Files/PothosSDR/include/") + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) else() set(CMAKE_CXX_FLAGS "-O3 -std=c++17 -fpermissive -fsanitize=address -g") # set(CMAKE_CXX_FLAGS "-O3 -std=c++17 -fpermissive") @@ -37,6 +38,7 @@ if (MSVC) endif (MSVC) add_executable(sdrpp ${SRC} ${IMGUI}) +# add_library(sdrpp ${SRC} ${IMGUI}) if (MSVC) # Glew diff --git a/modules/radio/src/main.cpp b/modules/radio/src/main.cpp index ee97cbd7..29caf395 100644 --- a/modules/radio/src/main.cpp +++ b/modules/radio/src/main.cpp @@ -11,8 +11,6 @@ struct RadioContext_t { std::string name; int demod = 1; SigPath sigPath; - // watcher volume; - // watcher audioDevice; }; MOD_EXPORT void* _INIT_(mod::API_t* _API, ImGuiContext* imctx, std::string _name) { @@ -21,24 +19,12 @@ MOD_EXPORT void* _INIT_(mod::API_t* _API, ImGuiContext* imctx, std::string _name ctx->name = _name; ctx->sigPath.init(_name, 200000, 1000, API->registerVFO(_name, mod::API_t::REF_CENTER, 0, 200000, 200000, 1000)); ctx->sigPath.start(); - // ctx->volume.val = 1.0f; - // ctx->volume.markAsChanged(); - // API->bindVolumeVariable(&ctx->volume.val); - // ctx->audioDevice.val = ctx->sigPath.audio.getDeviceId(); - // ctx->audioDevice.changed(); // clear change ImGui::SetCurrentContext(imctx); return ctx; } MOD_EXPORT void _NEW_FRAME_(RadioContext_t* ctx) { - // if (ctx->volume.changed()) { - // ctx->sigPath.setVolume(ctx->volume.val); - // } - // if (ctx->audioDevice.changed()) { - // ctx->sigPath.audio.stop(); - // ctx->sigPath.audio.setDevice(ctx->audioDevice.val); - // ctx->sigPath.audio.start(); - // } + } MOD_EXPORT void _DRAW_MENU_(RadioContext_t* ctx) { @@ -48,28 +34,28 @@ MOD_EXPORT void _DRAW_MENU_(RadioContext_t* ctx) { if (ImGui::RadioButton(CONCAT("NFM##_", ctx->name), ctx->demod == 0) && ctx->demod != 0) { ctx->sigPath.setDemodulator(SigPath::DEMOD_NFM); ctx->demod = 0; - API->setVFOBandwidth(ctx->name, 12500); API->setVFOReference(ctx->name, mod::API_t::REF_CENTER); } if (ImGui::RadioButton(CONCAT("WFM##_", ctx->name), ctx->demod == 1) && ctx->demod != 1) { ctx->sigPath.setDemodulator(SigPath::DEMOD_FM); ctx->demod = 1; - API->setVFOBandwidth(ctx->name, 200000); API->setVFOReference(ctx->name, mod::API_t::REF_CENTER); } ImGui::NextColumn(); if (ImGui::RadioButton(CONCAT("AM##_", ctx->name), ctx->demod == 2) && ctx->demod != 2) { ctx->sigPath.setDemodulator(SigPath::DEMOD_AM); ctx->demod = 2; - API->setVFOBandwidth(ctx->name, 12500); API->setVFOReference(ctx->name, mod::API_t::REF_CENTER); } - if (ImGui::RadioButton(CONCAT("DSB##_", ctx->name), ctx->demod == 3) && ctx->demod != 3) { ctx->demod = 3; }; + if (ImGui::RadioButton(CONCAT("DSB##_", ctx->name), ctx->demod == 3) && ctx->demod != 3) { + ctx->sigPath.setDemodulator(SigPath::DEMOD_DSB); + ctx->demod = 3; + API->setVFOReference(ctx->name, mod::API_t::REF_CENTER); + } ImGui::NextColumn(); if (ImGui::RadioButton(CONCAT("USB##_", ctx->name), ctx->demod == 4) && ctx->demod != 4) { ctx->sigPath.setDemodulator(SigPath::DEMOD_USB); ctx->demod = 4; - API->setVFOBandwidth(ctx->name, 3000); API->setVFOReference(ctx->name, mod::API_t::REF_LOWER); } if (ImGui::RadioButton(CONCAT("CW##_", ctx->name), ctx->demod == 5) && ctx->demod != 5) { ctx->demod = 5; }; @@ -77,7 +63,6 @@ MOD_EXPORT void _DRAW_MENU_(RadioContext_t* ctx) { if (ImGui::RadioButton(CONCAT("LSB##_", ctx->name), ctx->demod == 6) && ctx->demod != 6) { ctx->sigPath.setDemodulator(SigPath::DEMOD_LSB); ctx->demod = 6; - API->setVFOBandwidth(ctx->name, 3000); API->setVFOReference(ctx->name, mod::API_t::REF_UPPER); } if (ImGui::RadioButton(CONCAT("RAW##_", ctx->name), ctx->demod == 7) && ctx->demod != 7) { ctx->demod = 7; }; @@ -85,21 +70,11 @@ MOD_EXPORT void _DRAW_MENU_(RadioContext_t* ctx) { ImGui::EndGroup(); - // ImGui::PushItemWidth(ImGui::GetWindowSize().x); - // ImGui::Combo(CONCAT("##_audio_dev_", ctx->name), &ctx->audioDevice.val, ctx->sigPath.audio.devTxtList.c_str()); - // ImGui::PopItemWidth(); + ImGui::Checkbox(CONCAT("Deemphasis##_", ctx->name), &ctx->sigPath.deemp.bypass); } MOD_EXPORT void _HANDLE_EVENT_(RadioContext_t* ctx, int eventId) { - // INSTEAD OF EVENTS, REGISTER HANDLER WHEN CREATING VFO - if (eventId == mod::EVENT_STREAM_PARAM_CHANGED) { - ctx->sigPath.updateBlockSize(); - } - else if (eventId == mod::EVENT_SELECTED_VFO_CHANGED) { - // if (API->getSelectedVFOName() == ctx->name) { - // API->bindVolumeVariable(&ctx->volume.val); - // } - } + } MOD_EXPORT void _STOP_(RadioContext_t* ctx) { diff --git a/modules/radio/src/path.cpp b/modules/radio/src/path.cpp index 7efa4fdc..2dbb99b2 100644 --- a/modules/radio/src/path.cpp +++ b/modules/radio/src/path.cpp @@ -6,9 +6,16 @@ SigPath::SigPath() { int SigPath::sampleRateChangeHandler(void* ctx, float sampleRate) { SigPath* _this = (SigPath*)ctx; + _this->outputSampleRate = sampleRate; _this->audioResamp.stop(); - _this->audioResamp.setOutputSampleRate(sampleRate, sampleRate / 2.0f, sampleRate / 2.0f); + _this->deemp.stop(); + float bw = std::min(_this->bandwidth, sampleRate / 2.0f); + spdlog::warn("New bandwidth: {0}", bw); + _this->audioResamp.setOutputSampleRate(sampleRate, bw, bw); + _this->deemp.setBlockSize(_this->audioResamp.getOutputBlockSize()); + _this->deemp.setSamplerate(sampleRate); _this->audioResamp.start(); + _this->deemp.start(); return _this->audioResamp.getOutputBlockSize(); } @@ -18,6 +25,7 @@ void SigPath::init(std::string vfoName, uint64_t sampleRate, int blockSize, dsp: this->vfoName = vfoName; _demod = DEMOD_FM; + bandwidth = 200000; // TODO: Set default VFO options @@ -26,8 +34,11 @@ void SigPath::init(std::string vfoName, uint64_t sampleRate, int blockSize, dsp: ssbDemod.init(input, 6000, 3000, 22); audioResamp.init(&demod.output, 200000, 48000, 800); - API->registerMonoStream(&audioResamp.output, vfoName, vfoName, sampleRateChangeHandler, this); + deemp.init(&audioResamp.output, 800, 50e-6, 48000); + outputSampleRate = API->registerMonoStream(&deemp.output, vfoName, vfoName, sampleRateChangeHandler, this); API->setBlockSize(vfoName, audioResamp.getOutputBlockSize()); + + setDemodulator(_demod); } void SigPath::setSampleRate(float sampleRate) { @@ -43,6 +54,7 @@ void SigPath::setDemodulator(int demId) { } audioResamp.stop(); + deemp.stop(); // Stop current demodulator if (_demod == DEMOD_FM) { @@ -65,47 +77,76 @@ void SigPath::setDemodulator(int demId) { // Set input of the audio resampler if (demId == DEMOD_FM) { API->setVFOSampleRate(vfoName, 200000, 200000); + bandwidth = 15000; demod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); demod.setSampleRate(200000); demod.setDeviation(100000); audioResamp.setInput(&demod.output); - audioResamp.setInputSampleRate(200000, API->getVFOOutputBlockSize(vfoName), 15000, 15000); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(200000, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = false; demod.start(); } if (demId == DEMOD_NFM) { - API->setVFOSampleRate(vfoName, 12500, 12500); + API->setVFOSampleRate(vfoName, 16000, 16000); + bandwidth = 8000; demod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); - demod.setSampleRate(12500); - demod.setDeviation(6250); + demod.setSampleRate(16000); + demod.setDeviation(8000); audioResamp.setInput(&demod.output); - audioResamp.setInputSampleRate(12500, API->getVFOOutputBlockSize(vfoName)); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(16000, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = true; demod.start(); } else if (demId == DEMOD_AM) { API->setVFOSampleRate(vfoName, 12500, 12500); + bandwidth = 6250; amDemod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); audioResamp.setInput(&amDemod.output); - audioResamp.setInputSampleRate(12500, API->getVFOOutputBlockSize(vfoName)); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(12500, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = true; amDemod.start(); } else if (demId == DEMOD_USB) { API->setVFOSampleRate(vfoName, 6000, 3000); + bandwidth = 3000; ssbDemod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); ssbDemod.setMode(dsp::SSBDemod::MODE_USB); audioResamp.setInput(&ssbDemod.output); - audioResamp.setInputSampleRate(6000, API->getVFOOutputBlockSize(vfoName)); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(6000, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = true; ssbDemod.start(); } else if (demId == DEMOD_LSB) { API->setVFOSampleRate(vfoName, 6000, 3000); + bandwidth = 3000; ssbDemod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); ssbDemod.setMode(dsp::SSBDemod::MODE_LSB); audioResamp.setInput(&ssbDemod.output); - audioResamp.setInputSampleRate(6000, API->getVFOOutputBlockSize(vfoName)); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(6000, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = true; + ssbDemod.start(); + } + else if (demId == DEMOD_DSB) { + API->setVFOSampleRate(vfoName, 6000, 6000); + bandwidth = 3000; + ssbDemod.setBlockSize(API->getVFOOutputBlockSize(vfoName)); + ssbDemod.setMode(dsp::SSBDemod::MODE_DSB); + audioResamp.setInput(&ssbDemod.output); + float audioBw = std::min(bandwidth, outputSampleRate / 2.0f); + audioResamp.setInputSampleRate(6000, API->getVFOOutputBlockSize(vfoName), audioBw, audioBw); + deemp.bypass = true; ssbDemod.start(); } + deemp.setBlockSize(audioResamp.getOutputBlockSize()); + audioResamp.start(); + deemp.start(); } void SigPath::updateBlockSize() { @@ -115,5 +156,6 @@ void SigPath::updateBlockSize() { void SigPath::start() { demod.start(); audioResamp.start(); + deemp.start(); API->startStream(vfoName); } \ No newline at end of file diff --git a/modules/radio/src/path.h b/modules/radio/src/path.h index 164c436e..f28f32e2 100644 --- a/modules/radio/src/path.h +++ b/modules/radio/src/path.h @@ -18,7 +18,7 @@ public: void start(); void setSampleRate(float sampleRate); - void setVFOFrequency(long frequency); + void setVFOFrequency(uint64_t frequency); void updateBlockSize(); @@ -30,9 +30,12 @@ public: DEMOD_AM, DEMOD_USB, DEMOD_LSB, + DEMOD_DSB, _DEMOD_COUNT }; + dsp::FMDeemphasis deemp; + private: static int sampleRateChangeHandler(void* ctx, float sampleRate); @@ -49,6 +52,8 @@ private: std::string vfoName; float sampleRate; + float bandwidth; + float outputSampleRate; int blockSize; int _demod; }; \ No newline at end of file diff --git a/modules/recorder/src/main.cpp b/modules/recorder/src/main.cpp index 7c4f1757..950620ea 100644 --- a/modules/recorder/src/main.cpp +++ b/modules/recorder/src/main.cpp @@ -21,6 +21,8 @@ struct RecorderContext_t { std::string lastNameList; std::string selectedStreamName; int selectedStreamId; + uint64_t samplesWritten; + float sampleRate; }; void _writeWorker(RecorderContext_t* ctx) { @@ -34,6 +36,7 @@ void _writeWorker(RecorderContext_t* ctx) { sampleBuf[(i * 2) + 0] = floatBuf[i].l * 0x7FFF; sampleBuf[(i * 2) + 1] = floatBuf[i].r * 0x7FFF; } + ctx->samplesWritten += 1024; ctx->writer->writeSamples(sampleBuf, 2048 * sizeof(int16_t)); } delete[] floatBuf; @@ -60,6 +63,9 @@ MOD_EXPORT void* _INIT_(mod::API_t* _API, ImGuiContext* imctx, std::string _name API = _API; RecorderContext_t* ctx = new RecorderContext_t; ctx->recording = false; + ctx->selectedStreamName = ""; + ctx->selectedStreamId = 0; + ctx->lastNameList = ""; ImGui::SetCurrentContext(imctx); return ctx; } @@ -78,14 +84,30 @@ MOD_EXPORT void _DRAW_MENU_(RecorderContext_t* ctx) { nameList += '\0'; } + if (nameList == "") { + ImGui::Text("No audio stream available"); + return; + } + if (ctx->lastNameList != nameList) { ctx->lastNameList = nameList; - + auto _nameIt = std::find(streamNames.begin(), streamNames.end(), ctx->selectedStreamName); + if (_nameIt == streamNames.end()) { + // TODO: verify if there even is a stream + ctx->selectedStreamId = 0; + ctx->selectedStreamName = streamNames[ctx->selectedStreamId]; + } + else { + ctx->selectedStreamId = std::distance(streamNames.begin(), _nameIt); + ctx->selectedStreamName = streamNames[ctx->selectedStreamId]; + } } ImGui::PushItemWidth(menuColumnWidth); if (!ctx->recording) { - ImGui::Combo(CONCAT("##_strea_select_", ctx->name), &ctx->selectedStreamId, nameList.c_str()); + if (ImGui::Combo(CONCAT("##_strea_select_", ctx->name), &ctx->selectedStreamId, nameList.c_str())) { + ctx->selectedStreamName = nameList[ctx->selectedStreamId]; + } } else { ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); @@ -99,8 +121,10 @@ MOD_EXPORT void _DRAW_MENU_(RecorderContext_t* ctx) { if (!ctx->recording) { if (ImGui::Button("Record", ImVec2(menuColumnWidth, 0))) { + ctx->samplesWritten = 0; + ctx->sampleRate = 48000; ctx->writer = new WavWriter("recordings/" + genFileName("audio_"), 16, 2, 48000); - ctx->stream = API->bindToStreamStereo("Radio", streamRemovedHandler, sampleRateChanged, ctx); + ctx->stream = API->bindToStreamStereo(ctx->selectedStreamName, streamRemovedHandler, sampleRateChanged, ctx); ctx->workerThread = std::thread(_writeWorker, ctx); ctx->recording = true; ctx->startTime = time(0); @@ -112,12 +136,13 @@ MOD_EXPORT void _DRAW_MENU_(RecorderContext_t* ctx) { ctx->stream->stopReader(); ctx->workerThread.join(); ctx->stream->clearReadStop(); - API->unbindFromStreamStereo("Radio", ctx->stream); + API->unbindFromStreamStereo(ctx->selectedStreamName, ctx->stream); ctx->writer->close(); delete ctx->writer; ctx->recording = false; } - time_t diff = time(0) - ctx->startTime; + uint64_t seconds = ctx->samplesWritten / (uint64_t)ctx->sampleRate; + time_t diff = seconds; tm *dtm = gmtime(&diff); ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Recording %02d:%02d:%02d", dtm->tm_hour, dtm->tm_min, dtm->tm_sec); } diff --git a/src/dsp/demodulator.h b/src/dsp/demodulator.h index 059faedf..7d5f4353 100644 --- a/src/dsp/demodulator.h +++ b/src/dsp/demodulator.h @@ -260,6 +260,9 @@ namespace dsp { else if (mode == MODE_LSB) { lo.setFrequency(-_bandWidth / 2.0f); } + else if (mode == MODE_LSB) { + lo.setFrequency(0); + } } stream output; @@ -267,6 +270,7 @@ namespace dsp { enum { MODE_USB, MODE_LSB, + MODE_DSB, _MODE_COUNT }; @@ -309,4 +313,102 @@ namespace dsp { int _mode; bool running = false; }; + + + + + + // class CWDemod { + // public: + // CWDemod() { + + // } + + // void init(stream* input, float sampleRate, float bandWidth, int blockSize) { + // _blockSize = blockSize; + // _bandWidth = bandWidth; + // _mode = MODE_USB; + // output.init(blockSize * 2); + // lo.init(bandWidth / 2.0f, sampleRate, blockSize); + // mixer.init(input, &lo.output, blockSize); + // lo.start(); + // } + + // void start() { + // mixer.start(); + // _workerThread = std::thread(_worker, this); + // running = true; + // } + + // void stop() { + // mixer.stop(); + // mixer.output.stopReader(); + // output.stopWriter(); + // _workerThread.join(); + // mixer.output.clearReadStop(); + // output.clearWriteStop(); + // running = false; + // } + + // void setBlockSize(int blockSize) { + // if (running) { + // return; + // } + // _blockSize = blockSize; + // } + + // void setMode(int mode) { + // if (mode < 0 && mode >= _MODE_COUNT) { + // return; + // } + // _mode = mode; + // if (mode == MODE_USB) { + // lo.setFrequency(_bandWidth / 2.0f); + // } + // else if (mode == MODE_LSB) { + // lo.setFrequency(-_bandWidth / 2.0f); + // } + // } + + // stream output; + + // private: + // static void _worker(CWDemod* _this) { + // complex_t* inBuf = new complex_t[_this->_blockSize]; + // float* outBuf = new float[_this->_blockSize]; + + // float min, max, factor; + + // while (true) { + // if (_this->mixer.output.read(inBuf, _this->_blockSize) < 0) { break; }; + // min = INFINITY; + // max = -INFINITY; + // for (int i = 0; i < _this->_blockSize; i++) { + // outBuf[i] = inBuf[i].q; + // if (inBuf[i].q < min) { + // min = inBuf[i].q; + // } + // if (inBuf[i].q > max) { + // max = inBuf[i].q; + // } + // } + // factor = (max - min) / 2; + // for (int i = 0; i < _this->_blockSize; i++) { + // outBuf[i] /= factor; + // } + // if (_this->output.write(outBuf, _this->_blockSize) < 0) { break; }; + // } + + // delete[] inBuf; + // delete[] outBuf; + // } + + // std::thread _workerThread; + // SineSource lo; + // Multiplier mixer; + // int _blockSize; + // float _bandWidth; + // int _mode; + // bool running = false; + // }; }; \ No newline at end of file diff --git a/src/dsp/filter.h b/src/dsp/filter.h index 250404e2..eab22535 100644 --- a/src/dsp/filter.h +++ b/src/dsp/filter.h @@ -4,11 +4,12 @@ #include #include #include +#include #define GET_FROM_RIGHT_BUF(buffer, delayLine, delayLineSz, n) (((n) < 0) ? delayLine[(delayLineSz) + (n)] : buffer[(n)]) namespace dsp { - inline void BlackmanWindow(std::vector& taps, float sampleRate, float cutoff, float transWidth) { + inline void BlackmanWindow(std::vector& taps, float sampleRate, float cutoff, float transWidth, int addedTaps = 0) { taps.clear(); float fc = cutoff / sampleRate; @@ -16,7 +17,7 @@ namespace dsp { fc = 1.0f; } - int _M = 4.0f / (transWidth / sampleRate); + int _M = (4.0f / (transWidth / sampleRate)) + (float)addedTaps; if (_M < 4) { _M = 4; } @@ -377,4 +378,113 @@ namespace dsp { float* _taps; bool running; }; + + class FMDeemphasis { + public: + FMDeemphasis() { + + } + + FMDeemphasis(stream* input, int bufferSize, float tau, float sampleRate) : output(bufferSize * 2) { + _in = input; + _bufferSize = bufferSize; + bypass = false; + _tau = tau; + _sampleRate = sampleRate; + } + + void init(stream* input, int bufferSize, float tau, float sampleRate) { + output.init(bufferSize * 2); + _in = input; + _bufferSize = bufferSize; + bypass = false; + _tau = tau; + _sampleRate = sampleRate; + } + + void start() { + if (running) { + return; + } + _workerThread = std::thread(_worker, this); + running = true; + } + + void stop() { + if (!running) { + return; + } + _in->stopReader(); + output.stopWriter(); + _workerThread.join(); + _in->clearReadStop(); + output.clearWriteStop(); + running = false; + } + + void setBlockSize(int blockSize) { + if (running) { + return; + } + _bufferSize = blockSize; + output.setMaxLatency(blockSize * 2); + } + + void setSamplerate(float sampleRate) { + if (running) { + return; + } + _sampleRate = sampleRate; + } + + void setTau(float tau) { + if (running) { + return; + } + _tau = tau; + } + + stream output; + bool bypass; + + private: + static void _worker(FMDeemphasis* _this) { + float* inBuf = new float[_this->_bufferSize]; + float* outBuf = new float[_this->_bufferSize]; + int count = _this->_bufferSize; + float lastOut = 0.0f; + float dt = 1.0f / _this->_sampleRate; + float alpha = dt / (_this->_tau + dt); + + spdlog::warn("Deemp filter started: {0}, {1}", _this->_tau * 1000000.0, _this->_sampleRate); + + while (true) { + if (_this->_in->read(inBuf, count) < 0) { break; }; + if (_this->bypass) { + if (_this->output.write(inBuf, count) < 0) { break; }; + continue; + } + + if (isnan(lastOut)) { + lastOut = 0.0f; + } + outBuf[0] = (alpha * inBuf[0]) + ((1-alpha) * lastOut); + for (int i = 1; i < count; i++) { + outBuf[i] = (alpha * inBuf[i]) + ((1 - alpha) * outBuf[i - 1]); + } + lastOut = outBuf[count - 1]; + + if (_this->output.write(outBuf, count) < 0) { break; }; + } + delete[] inBuf; + delete[] outBuf; + } + + stream* _in; + int _bufferSize; + std::thread _workerThread; + bool running = false; + float _sampleRate; + float _tau; + }; }; \ No newline at end of file diff --git a/src/dsp/resampling.h b/src/dsp/resampling.h index 49978709..f5ce64bc 100644 --- a/src/dsp/resampling.h +++ b/src/dsp/resampling.h @@ -564,4 +564,212 @@ namespace dsp { int _blockSize; bool running = false; }; + + + + + + + + + + + + + + + + + class FloatPolyphaseFIRResampler { + public: + FloatPolyphaseFIRResampler() { + + } + + void init(stream* in, float inputSampleRate, float outputSampleRate, int blockSize, float passBand = -1.0f, float transWidth = -1.0f) { + _input = in; + _outputSampleRate = outputSampleRate; + _inputSampleRate = inputSampleRate; + int _gcd = std::gcd((int)inputSampleRate, (int)outputSampleRate); + _interp = outputSampleRate / _gcd; + _decim = inputSampleRate / _gcd; + _blockSize = blockSize; + outputBlockSize = (blockSize * _interp) / _decim; + output.init(outputBlockSize * 2); + + float cutoff = std::min(_outputSampleRate / 2.0f, _inputSampleRate / 2.0f); + if (passBand > 0.0f && transWidth > 0.0f) { + dsp::BlackmanWindow(_taps, _outputSampleRate, passBand, transWidth, _interp - 1); + } + else { + dsp::BlackmanWindow(_taps, _outputSampleRate, cutoff, cutoff, _interp - 1); + } + } + + void start() { + if (running) { + return; + } + _workerThread = std::thread(_worker, this); + running = true; + } + + void stop() { + if (!running) { + return; + } + _input->stopReader(); + output.stopWriter(); + _workerThread.join(); + _input->clearReadStop(); + output.clearWriteStop(); + running = false; + } + + void setInputSampleRate(float inputSampleRate, int blockSize = -1, float passBand = -1.0f, float transWidth = -1.0f) { + stop(); + _inputSampleRate = inputSampleRate; + int _gcd = std::gcd((int)inputSampleRate, (int)_outputSampleRate); + _interp = _outputSampleRate / _gcd; + _decim = inputSampleRate / _gcd; + + float cutoff = std::min(_outputSampleRate / 2.0f, _inputSampleRate / 2.0f); + if (passBand > 0.0f && transWidth > 0.0f) { + dsp::BlackmanWindow(_taps, _outputSampleRate, passBand, transWidth, _interp - 1); + } + else { + dsp::BlackmanWindow(_taps,_outputSampleRate, cutoff, cutoff, _interp - 1); + } + + if (blockSize > 0) { + _blockSize = blockSize; + } + outputBlockSize = (blockSize * _interp) / _decim; + output.setMaxLatency(outputBlockSize * 2); + start(); + } + + void setOutputSampleRate(float outputSampleRate, float passBand = -1.0f, float transWidth = -1.0f) { + stop(); + _outputSampleRate = outputSampleRate; + int _gcd = std::gcd((int)_inputSampleRate, (int)outputSampleRate); + _interp = outputSampleRate / _gcd; + _decim = _inputSampleRate / _gcd; + outputBlockSize = (_blockSize * _interp) / _decim; + output.setMaxLatency(outputBlockSize * 2); + + float cutoff = std::min(_outputSampleRate / 2.0f, _inputSampleRate / 2.0f); + if (passBand > 0.0f && transWidth > 0.0f) { + dsp::BlackmanWindow(_taps, _outputSampleRate, passBand, transWidth, _interp - 1); + } + else { + dsp::BlackmanWindow(_taps, _outputSampleRate, cutoff, cutoff, _interp - 1); + } + + start(); + } + + void setFilterParams(float passBand, float transWidth) { + stop(); + dsp::BlackmanWindow(_taps, _outputSampleRate, passBand, transWidth, _interp - 1); + start(); + } + + void setBlockSize(int blockSize) { + stop(); + _blockSize = blockSize; + outputBlockSize = (_blockSize * _interp) / _decim; + output.setMaxLatency(outputBlockSize * 2); + start(); + } + + void setInput(stream* input) { + if (running) { + return; + } + _input = input; + } + + int getOutputBlockSize() { + return outputBlockSize; + } + + stream output; + + private: + static void _worker(FloatPolyphaseFIRResampler* _this) { + float* inBuf = new float[_this->_blockSize]; + float* outBuf = new float[_this->outputBlockSize]; + + int inCount = _this->_blockSize; + int outCount = _this->outputBlockSize; + + int interp = _this->_interp; + int decim = _this->_decim; + float correction = interp;//(float)sqrt((float)interp); + + int tapCount = _this->_taps.size(); + float* taps = new float[tapCount]; + for (int i = 0; i < tapCount; i++) { + taps[i] = _this->_taps[i] * correction; + } + + float* delayBuf = new float[tapCount]; + + float* delayStart = &inBuf[std::max(inCount - tapCount, 0)]; + int delaySize = tapCount * sizeof(float); + float* delayBufEnd = &delayBuf[std::max(tapCount - inCount, 0)]; + int moveSize = std::min(inCount, tapCount - inCount) * sizeof(float); + int inSize = inCount * sizeof(float); + + int afterInterp = inCount * interp; + int outIndex = 0; + + tapCount -= interp - 1; + + while (true) { + if (_this->_input->read(inBuf, inCount) < 0) { break; }; + + + for (int i = 0; i < outCount; i++) { + outBuf[i] = 0; + int filterId = (i * decim) % interp; + int inputId = (i * decim) / interp; + for (int j = 0; j < tapCount; j++) { + outBuf[i] += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, inputId - j) * taps[j + filterId]; + } + } + + + + + + + if (tapCount > inCount) { + memmove(delayBuf, delayBufEnd, moveSize); + memcpy(delayBufEnd, delayStart, inSize); + } + else { + memcpy(delayBuf, delayStart, delaySize); + } + + if (_this->output.write(outBuf, _this->outputBlockSize) < 0) { break; }; + } + delete[] inBuf; + delete[] outBuf; + delete[] delayBuf; + } + + std::thread _workerThread; + + stream* _input; + std::vector _taps; + int _interp; + int _decim; + int outputBlockSize; + float _outputSampleRate; + float _inputSampleRate; + int _blockSize; + bool running = false; + }; }; \ No newline at end of file diff --git a/src/frequency_select.cpp b/src/frequency_select.cpp index 424ef32b..2115a62c 100644 --- a/src/frequency_select.cpp +++ b/src/frequency_select.cpp @@ -141,7 +141,7 @@ void FrequencySelect::draw() { } } - long freq = 0; + uint64_t freq = 0; for (int i = 0; i < 12; i++) { freq += digits[i] * pow(10, 11 - i); } @@ -151,9 +151,9 @@ void FrequencySelect::draw() { ImGui::NewLine(); } -void FrequencySelect::setFrequency(long freq) { +void FrequencySelect::setFrequency(uint64_t freq) { int i = 11; - for (long f = freq; i >= 0; i--) { + for (uint64_t f = freq; i >= 0; i--) { digits[i] = f % 10; f -= digits[i]; f /= 10; diff --git a/src/frequency_select.h b/src/frequency_select.h index b96bbd31..ebbe9bae 100644 --- a/src/frequency_select.h +++ b/src/frequency_select.h @@ -1,15 +1,16 @@ #pragma once #include #include +#include class FrequencySelect { public: FrequencySelect(); void init(); void draw(); - void setFrequency(long freq); + void setFrequency(uint64_t freq); - long frequency; + uint64_t frequency; bool frequencyChanged = false; private: diff --git a/src/main.cpp b/src/main.cpp index 1ef2450f..8d62635f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,10 +21,21 @@ #include #endif +bool maximized = false; + static void glfw_error_callback(int error, const char* description) { spdlog::error("Glfw Error {0}: {1}", error, description); } +static void maximized_callback(GLFWwindow* window, int n) { + if (n == GLFW_TRUE) { + maximized = true; + } + else { + maximized = false; + } +} + int main() { #ifdef _WIN32 //FreeConsole(); @@ -32,6 +43,11 @@ int main() { spdlog::info("SDR++ v" VERSION_STR); + // Load config + spdlog::info("Loading config"); + config::load("config.json"); + config::startAutoSave(); + // Setup window glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) { @@ -43,14 +59,23 @@ int main() { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac + int winWidth = config::config["windowSize"]["w"]; + int winHeight = config::config["windowSize"]["h"]; + maximized = config::config["maximized"]; // Create window with graphics context - GLFWwindow* window = glfwCreateWindow(1280, 720, "SDR++ v" VERSION_STR " (Built at " __TIME__ ", " __DATE__ ")", NULL, NULL); + GLFWwindow* window = glfwCreateWindow(winWidth, winHeight, "SDR++ v" VERSION_STR " (Built at " __TIME__ ", " __DATE__ ")", NULL, NULL); if (window == NULL) return 1; glfwMakeContextCurrent(window); glfwSwapInterval(1); // Enable vsync + if (maximized) { + glfwMaximizeWindow(window); + } + + glfwSetWindowMaximizeCallback(window, maximized_callback); + // Load app icon GLFWimage icons[10]; icons[0].pixels = stbi_load("res/icons/sdrpp.png", &icons[0].width, &icons[0].height, 0, 4); @@ -94,11 +119,6 @@ int main() { ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 150"); - // Load config - spdlog::info("Loading config"); - config::load("config.json"); - config::startAutoSave(); - style::setDarkStyle(); spdlog::info("Loading icons"); @@ -114,6 +134,8 @@ int main() { spdlog::info("Ready."); + bool _maximized = maximized; + // Main loop while (!glfwWindowShouldClose(window)) { glfwPollEvents(); @@ -123,15 +145,31 @@ int main() { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); - - + if (_maximized != maximized) { + _maximized = maximized; + config::config["maximized"]= _maximized; + config::configModified = true; + if (!maximized) { + glfwSetWindowSize(window, config::config["windowSize"]["w"], config::config["windowSize"]["h"]); + } + } - int wwidth, wheight; - glfwGetWindowSize(window, &wwidth, &wheight); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImVec2(wwidth, wheight)); - - drawWindow(); + int _winWidth, _winHeight; + glfwGetWindowSize(window, &_winWidth, &_winHeight); + + if ((_winWidth != winWidth || _winHeight != winHeight) && !maximized && _winWidth > 0 && _winHeight > 0) { + winWidth = _winWidth; + winHeight = _winHeight; + config::config["windowSize"]["w"] = winWidth; + config::config["windowSize"]["h"] = winHeight; + config::configModified = true; + } + + if (winWidth > 0 && winHeight > 0) { + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(_winWidth, _winHeight)); + drawWindow(); + } // Rendering ImGui::Render(); diff --git a/src/main_window.cpp b/src/main_window.cpp index 4ae7d31e..3dacf205 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -41,7 +41,7 @@ dsp::NullSink sink; int devId = 0; int srId = 0; watcher bandplanId(0, true); -watcher freq(90500000L); +watcher freq(90500000Ui64); int demod = 1; watcher vfoFreq(92000000.0f); float dummyVolume = 1.0f; @@ -57,6 +57,11 @@ watcher bandPlanEnabled(true, false); bool showCredits = false; std::string audioStreamName = ""; std::string sourceName = ""; +int menuWidth = 300; +bool grabbingMenu = false; +int newWidth = 300; +bool showWaterfall = true; +int fftHeight = 300; void saveCurrentSource() { int i = 0; @@ -188,25 +193,23 @@ void windowInit() { } if (!settingsFound) { sampleRate = soapy.getSampleRate(); + sigPath.setSampleRate(sampleRate); } // Search for the first source in the list to have a config // If no pre-defined source, selected default device } - // Load last band plan configuration - - // TODO: Save/load config window size/fullscreen // Also add a loading screen - // And a module add/remove/change order menu - // get rid of watchers and use if() instead + // Adjustable "snap to grid" for each VFO + // Finish the recorder module // Add squelsh // Bandwidth ajustment // DSB / CW and RAW modes; - // Write a recorder - // Adjustable "snap to grid" for each VFO // Bring VFO to a visible place when changing sample rate if it's smaller - // Possibility to resize waterfall and menu // Have a proper root directory + + // And a module add/remove/change order menu + // get rid of watchers and use if() instead // Switch to double for all frequecies and bandwidth // Update UI settings @@ -265,6 +268,17 @@ void windowInit() { if (audioStreamName != "") { volume = &audio::streams[audioStreamName]->volume; } + + menuWidth = config::config["menuWidth"]; + newWidth = menuWidth; + + showWaterfall = config::config["showWaterfall"]; + if (!showWaterfall) { + wtf.hideWaterfall(); + } + + fftHeight = config::config["fftHeight"]; + wtf.setFFTHeight(fftHeight); } void setVFO(float freq) { @@ -409,6 +423,13 @@ void drawWindow() { wtf.bandplan = bandPlanEnabled.val ? &bandplan::bandplans[bandplan::bandplanNames[bandplanId.val]] : NULL; } + int _fftHeight = wtf.getFFTHeight(); + if (fftHeight != _fftHeight) { + fftHeight = _fftHeight; + config::config["fftHeight"] = fftHeight; + config::configModified = true; + } + ImVec2 vMin = ImGui::GetWindowContentRegionMin(); ImVec2 vMax = ImGui::GetWindowContentRegionMax(); @@ -466,10 +487,36 @@ void drawWindow() { showCredits = false; } - ImGui::Columns(3, "WindowColumns", false); + // Handle menu resize + float curY = ImGui::GetCursorPosY(); ImVec2 winSize = ImGui::GetWindowSize(); - ImGui::SetColumnWidth(0, 300); - ImGui::SetColumnWidth(1, winSize.x - 300 - 60); + ImVec2 mousePos = ImGui::GetMousePos(); + bool click = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left); + if (grabbingMenu) { + newWidth = mousePos.x; + newWidth = std::clamp(newWidth, 250, winSize.x - 250); + ImGui::GetForegroundDrawList()->AddLine(ImVec2(newWidth, curY), ImVec2(newWidth, winSize.y - 10), ImGui::GetColorU32(ImGuiCol_SeparatorActive)); + } + if (mousePos.x >= newWidth - 2 && mousePos.x <= newWidth + 2 && mousePos.y > curY) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (click) { + grabbingMenu = true; + } + } + else { + ImGui::SetMouseCursor(ImGuiMouseCursor_Arrow); + } + if(!down && grabbingMenu) { + grabbingMenu = false; + menuWidth = newWidth; + config::config["menuWidth"] = menuWidth; + config::configModified = true; + } + + ImGui::Columns(3, "WindowColumns", false); + ImGui::SetColumnWidth(0, menuWidth); + ImGui::SetColumnWidth(1, winSize.x - menuWidth - 60); ImGui::SetColumnWidth(2, 60); // Left Column @@ -668,7 +715,10 @@ void drawWindow() { ImGui::Spacing(); } - if (ImGui::CollapsingHeader("Display")) { + if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Checkbox("Show waterfall", &showWaterfall)) { + showWaterfall ? wtf.showWaterfall() : wtf.hideWaterfall(); + } ImGui::Spacing(); } @@ -693,6 +743,7 @@ void drawWindow() { ImGui::EndChild(); + ImGui::NextColumn(); ImGui::BeginChild("WaterfallControls"); @@ -735,6 +786,7 @@ void drawWindow() { wtf.setWaterfallMin(fftMin); wtf.setWaterfallMax(fftMax); + ImGui::End(); if (showCredits) { diff --git a/src/vfo_manager.cpp b/src/vfo_manager.cpp index 498eb9fe..fc23a34a 100644 --- a/src/vfo_manager.cpp +++ b/src/vfo_manager.cpp @@ -58,7 +58,9 @@ namespace vfoman { if (vfos.find(name) == vfos.end()) { return; } - vfos[name].dspVFO->setOutputSampleRate(sampleRate, bandwidth); + VFO_t vfo = vfos[name]; + vfo.dspVFO->setOutputSampleRate(sampleRate, bandwidth); + vfo.wtfVFO->setBandwidth(bandwidth); } void setReference(std::string name, int ref){ diff --git a/src/waterfall.cpp b/src/waterfall.cpp index 4cf9e376..3057cf31 100644 --- a/src/waterfall.cpp +++ b/src/waterfall.cpp @@ -83,7 +83,9 @@ namespace ImGui { fftMax = 0.0f; waterfallMin = -70.0f; waterfallMax = 0.0f; - fftHeight = 250; + FFTAreaHeight = 300; + newFFTAreaHeight = FFTAreaHeight; + fftHeight = FFTAreaHeight - 50; dataWidth = 600; lastWidgetPos.x = 0; lastWidgetPos.y = 0; @@ -113,8 +115,8 @@ namespace ImGui { // Vertical scale for (float line = startLine; line > fftMin; line -= vRange) { float yPos = widgetPos.y + fftHeight + 10 - ((line - fftMin) * scaleFactor); - window->DrawList->AddLine(ImVec2(widgetPos.x + 50, roundf(yPos)), - ImVec2(widgetPos.x + dataWidth + 50, roundf(yPos)), + window->DrawList->AddLine(ImVec2(roundf(widgetPos.x + 50), roundf(yPos)), + ImVec2(roundf(widgetPos.x + dataWidth + 50), roundf(yPos)), IM_COL32(50, 50, 50, 255), 1.0f); sprintf(buf, "%d", (int)line); ImVec2 txtSz = ImGui::CalcTextSize(buf); @@ -263,6 +265,9 @@ namespace ImGui { } void WaterFall::updateWaterfallFb() { + if (!waterfallVisible) { + return; + } float offsetRatio = viewOffset / (wholeBandwidth / 2.0f); int drawDataSize; int drawDataStart; @@ -357,14 +362,31 @@ namespace ImGui { } void WaterFall::onResize() { + // return if widget is too small + if (widgetSize.x < 100 || widgetSize.y < 100) { + return; + } + + if (waterfallVisible) { + FFTAreaHeight = std::min(FFTAreaHeight, widgetSize.y - 50); + fftHeight = FFTAreaHeight - 50; + waterfallHeight = widgetSize.y - fftHeight - 52; + } + else { + fftHeight = widgetSize.y - 50; + } dataWidth = widgetSize.x - 60.0f; - waterfallHeight = widgetSize.y - fftHeight - 52; delete[] latestFFT; - delete[] waterfallFb; + + if (waterfallVisible) { + delete[] waterfallFb; + } latestFFT = new float[dataWidth]; - waterfallFb = new uint32_t[dataWidth * waterfallHeight]; - memset(waterfallFb, 0, dataWidth * waterfallHeight * sizeof(uint32_t)); + if (waterfallVisible) { + waterfallFb = new uint32_t[dataWidth * waterfallHeight]; + memset(waterfallFb, 0, dataWidth * waterfallHeight * sizeof(uint32_t)); + } for (int i = 0; i < dataWidth; i++) { latestFFT[i] = -1000.0f; // Hide everything } @@ -389,15 +411,8 @@ namespace ImGui { buf_mtx.lock(); window = GetCurrentWindow(); - // Fix for weird ImGui bug - ImVec2 tmpWidgetEndPos = ImGui::GetWindowContentRegionMax(); - if (tmpWidgetEndPos.x < 100 || tmpWidgetEndPos.y < fftHeight + 100) { - buf_mtx.unlock(); - return; - } - widgetPos = ImGui::GetWindowContentRegionMin(); - widgetEndPos = tmpWidgetEndPos; + widgetEndPos = ImGui::GetWindowContentRegionMax(); widgetPos.x += window->Pos.x; widgetPos.y += window->Pos.y; widgetEndPos.x += window->Pos.x - 4; // Padding @@ -422,12 +437,44 @@ namespace ImGui { processInputs(); drawFFT(); - drawWaterfall(); + if (waterfallVisible) { + drawWaterfall(); + } drawVFOs(); if (bandplan != NULL) { drawBandPlan(); } + if (!waterfallVisible) { + buf_mtx.unlock(); + return; + } + + // Handle fft resize + ImVec2 winSize = ImGui::GetWindowSize(); + ImVec2 mousePos = ImGui::GetMousePos(); + mousePos.x -= widgetPos.x; + mousePos.y -= widgetPos.y; + bool click = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left); + if (draggingFW) { + newFFTAreaHeight = mousePos.y; + newFFTAreaHeight = std::clamp(newFFTAreaHeight, 150, widgetSize.y - 50); + ImGui::GetForegroundDrawList()->AddLine(ImVec2(widgetPos.x, newFFTAreaHeight + widgetPos.y), ImVec2(widgetEndPos.x, newFFTAreaHeight + widgetPos.y), + ImGui::GetColorU32(ImGuiCol_SeparatorActive)); + } + if (mousePos.y >= newFFTAreaHeight - 2 && mousePos.y <= newFFTAreaHeight + 2 && mousePos.x > 0 && mousePos.x < widgetSize.x) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + if (click) { + draggingFW = true; + } + } + if(!down && draggingFW) { + draggingFW = false; + FFTAreaHeight = newFFTAreaHeight; + onResize(); + } + buf_mtx.unlock(); } @@ -443,15 +490,18 @@ namespace ImGui { rawFFTs.resize(waterfallHeight); } - memmove(&waterfallFb[dataWidth], waterfallFb, dataWidth * (waterfallHeight - 1) * sizeof(uint32_t)); - float pixel; - float dataRange = waterfallMax - waterfallMin; - for (int j = 0; j < dataWidth; j++) { - pixel = (std::clamp(latestFFT[j], waterfallMin, waterfallMax) - waterfallMin) / dataRange; - int id = (int)(pixel * (WATERFALL_RESOLUTION - 1)); - waterfallFb[j] = waterfallPallet[id]; + if (waterfallVisible) { + memmove(&waterfallFb[dataWidth], waterfallFb, dataWidth * (waterfallHeight - 1) * sizeof(uint32_t)); + float pixel; + float dataRange = waterfallMax - waterfallMin; + for (int j = 0; j < dataWidth; j++) { + pixel = (std::clamp(latestFFT[j], waterfallMin, waterfallMax) - waterfallMin) / dataRange; + int id = (int)(pixel * (WATERFALL_RESOLUTION - 1)); + waterfallFb[j] = waterfallPallet[id]; + } + waterfallUpdate = true; } - waterfallUpdate = true; + buf_mtx.unlock(); } @@ -710,5 +760,24 @@ namespace ImGui { window->DrawList->AddLine(lineMin, lineMax, selected ? IM_COL32(255, 0, 0, 255) : IM_COL32(255, 255, 0, 255)); } }; + + void WaterFall::showWaterfall() { + waterfallVisible = true; + onResize(); + } + + void WaterFall::hideWaterfall() { + waterfallVisible = false; + onResize(); + } + + void WaterFall::setFFTHeight(int height) { + FFTAreaHeight = height; + onResize(); + } + + int WaterFall::getFFTHeight() { + return FFTAreaHeight; + } }; diff --git a/src/waterfall.h b/src/waterfall.h index defee9f4..ef7e0051 100644 --- a/src/waterfall.h +++ b/src/waterfall.h @@ -87,6 +87,12 @@ namespace ImGui { void selectFirstVFO(); + void showWaterfall(); + void hideWaterfall(); + + void setFFTHeight(int height); + int getFFTHeight(); + bool centerFreqMoved = false; bool vfoFreqChanged = false; bool bandplanEnabled = false; @@ -175,5 +181,10 @@ namespace ImGui { uint32_t* waterfallFb; + bool draggingFW = false; + int FFTAreaHeight; + int newFFTAreaHeight; + + bool waterfallVisible = true; }; }; \ No newline at end of file