Fixed issues with new module system

This commit is contained in:
Ryzerth
2020-09-20 00:19:39 +02:00
parent d6b9e1d86a
commit 9de585190f
38 changed files with 460 additions and 358 deletions

122
core/src/gui/bandplan.cpp Normal file
View File

@ -0,0 +1,122 @@
#include <gui/bandplan.h>
namespace bandplan {
std::map<std::string, BandPlan_t> bandplans;
std::vector<std::string> bandplanNames;
std::string bandplanNameTxt;
std::map<std::string, BandPlanColor_t> colorTable;
void generateTxt() {
bandplanNameTxt = "";
for (int i = 0; i < bandplanNames.size(); i++) {
bandplanNameTxt += bandplanNames[i];
bandplanNameTxt += '\0';
}
}
void to_json(json& j, const Band_t& b) {
j = json{
{"name", b.name},
{"type", b.type},
{"start", b.start},
{"end", b.end},
};
}
void from_json(const json& j, Band_t& b) {
j.at("name").get_to(b.name);
j.at("type").get_to(b.type);
j.at("start").get_to(b.start);
j.at("end").get_to(b.end);
}
void to_json(json& j, const BandPlan_t& b) {
j = json{
{"name", b.name},
{"country_name", b.countryName},
{"country_code", b.countryCode},
{"author_name", b.authorName},
{"author_url", b.authorURL},
{"bands", b.bands}
};
}
void from_json(const json& j, BandPlan_t& b) {
j.at("name").get_to(b.name);
j.at("country_name").get_to(b.countryName);
j.at("country_code").get_to(b.countryCode);
j.at("author_name").get_to(b.authorName);
j.at("author_url").get_to(b.authorURL);
j.at("bands").get_to(b.bands);
}
void to_json(json& j, const BandPlanColor_t& ct) {
spdlog::error("ImGui color to JSON not implemented!!!");
}
void from_json(const json& j, BandPlanColor_t& ct) {
std::string col = j.get<std::string>();
if (col[0] != '#' || !std::all_of(col.begin() + 1, col.end(), ::isxdigit)) {
return;
}
uint8_t r, g, b, a;
r = std::stoi(col.substr(1, 2), NULL, 16);
g = std::stoi(col.substr(3, 2), NULL, 16);
b = std::stoi(col.substr(5, 2), NULL, 16);
a = std::stoi(col.substr(7, 2), NULL, 16);
ct.colorValue = IM_COL32(r, g, b, a);
ct.transColorValue = IM_COL32(r, g, b, 100);
}
void loadBandPlan(std::string path) {
std::ifstream file(path.c_str());
json data;
data << file;
file.close();
BandPlan_t plan = data.get<BandPlan_t>();
if (bandplans.find(plan.name) != bandplans.end()) {
spdlog::error("Duplicate band plan name ({0}), not loading.", plan.name);
return;
}
bandplans[plan.name] = plan;
bandplanNames.push_back(plan.name);
generateTxt();
}
void loadFromDir(std::string path) {
if (!std::filesystem::exists(path)) {
spdlog::error("Band Plan directory does not exist");
return;
}
if (!std::filesystem::is_directory(path)) {
spdlog::error("Band Plan directory isn't a directory...");
return;
}
bandplans.clear();
for (const auto & file : std::filesystem::directory_iterator(path)) {
std::string path = file.path().generic_string();
if (file.path().extension().generic_string() != ".json") {
continue;
}
loadBandPlan(path);
}
}
void loadColorTable(std::string path) {
if (!std::filesystem::exists(path)) {
spdlog::error("Band Plan Color Table file does not exist");
return;
}
if (!std::filesystem::is_regular_file(path)) {
spdlog::error("Band Plan Color Table file isn't a file...");
return;
}
std::ifstream file(path.c_str());
json data;
data << file;
file.close();
colorTable = data.get<std::map<std::string, BandPlanColor_t>>();
}
};

51
core/src/gui/bandplan.h Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include <json.hpp>
#include <fstream>
#include <spdlog/spdlog.h>
#include <filesystem>
#include <sstream>
#include <iomanip>
#include <imgui/imgui.h>
using nlohmann::json;
namespace bandplan {
struct Band_t {
std::string name;
std::string type;
float start;
float end;
};
void to_json(json& j, const Band_t& b);
void from_json(const json& j, Band_t& b);
struct BandPlan_t {
std::string name;
std::string countryName;
std::string countryCode;
std::string authorName;
std::string authorURL;
std::vector<Band_t> bands;
};
void to_json(json& j, const BandPlan_t& b);
void from_json(const json& j, BandPlan_t& b);
struct BandPlanColor_t {
uint32_t colorValue;
uint32_t transColorValue;
};
void to_json(json& j, const BandPlanColor_t& ct);
void from_json(const json& j, BandPlanColor_t& ct);
void loadBandPlan(std::string path);
void loadFromDir(std::string path);
void loadColorTable(std::string path);
extern std::map<std::string, BandPlan_t> bandplans;
extern std::vector<std::string> bandplanNames;
extern std::string bandplanNameTxt;
extern std::map<std::string, BandPlanColor_t> colorTable;
};

View File

@ -0,0 +1,164 @@
#include <gui/frequency_select.h>
bool isInArea(ImVec2 val, ImVec2 min, ImVec2 max) {
return val.x >= min.x && val.x < max.x && val.y >= min.y && val.y < max.y;
}
FrequencySelect::FrequencySelect() {
}
void FrequencySelect::init() {
font = ImGui::GetIO().Fonts->AddFontFromFileTTF((config::getRootDirectory() + "/res/fonts/Roboto-Medium.ttf").c_str(), 42.0f);
for (int i = 0; i < 12; i++) {
digits[i] = 0;
}
}
void FrequencySelect::onPosChange() {
int digitHeight = ImGui::CalcTextSize("0").y;
int commaOffset = 0;
for (int i = 0; i < 12; i++) {
digitTopMins[i] = ImVec2(widgetPos.x + (i * 22) + commaOffset, widgetPos.y);
digitBottomMins[i] = ImVec2(widgetPos.x + (i * 22) + commaOffset, widgetPos.y + (digitHeight / 2));
digitTopMaxs[i] = ImVec2(widgetPos.x + (i * 22) + commaOffset + 22, widgetPos.y + (digitHeight / 2));
digitBottomMaxs[i] = ImVec2(widgetPos.x + (i * 22) + commaOffset + 22, widgetPos.y + digitHeight);
if ((i + 1) % 3 == 0 && i < 11) {
commaOffset += 12;
}
}
}
void FrequencySelect::onResize() {
}
void FrequencySelect::incrementDigit(int i) {
if (i < 0) {
return;
}
if (digits[i] < 9) {
digits[i]++;
}
else {
digits[i] = 0;
incrementDigit(i - 1);
}
frequencyChanged = true;
}
void FrequencySelect::decrementDigit(int i) {
if (i < 0) {
return;
}
if (digits[i] > 0) {
digits[i]--;
}
else {
digits[i] = 9;
decrementDigit(i - 1);
}
frequencyChanged = true;
}
void FrequencySelect::draw() {
window = ImGui::GetCurrentWindow();
widgetPos = ImGui::GetWindowContentRegionMin();
widgetEndPos = ImGui::GetWindowContentRegionMax();
ImVec2 cursorPos = ImGui::GetCursorPos();
widgetPos.x += window->Pos.x + cursorPos.x;
widgetPos.y += window->Pos.y - 3;
widgetEndPos.x += window->Pos.x + cursorPos.x;
widgetEndPos.y += window->Pos.y - 3;
widgetSize = ImVec2(widgetEndPos.x - widgetPos.x, widgetEndPos.y - widgetPos.y);
ImGui::PushFont(font);
if (widgetPos.x != lastWidgetPos.x || widgetPos.y != lastWidgetPos.y) {
lastWidgetPos = widgetPos;
onPosChange();
}
if (widgetSize.x != lastWidgetSize.x || widgetSize.y != lastWidgetSize.y) {
lastWidgetSize = widgetSize;
onResize();
}
ImU32 disabledColor = ImGui::GetColorU32(ImGuiCol_Text, 0.3f);
ImU32 textColor = ImGui::GetColorU32(ImGuiCol_Text);
int commaOffset = 0;
bool zeros = true;
for (int i = 0; i < 12; i++) {
if (digits[i] != 0) {
zeros = false;
}
sprintf(buf, "%d", digits[i]);
window->DrawList->AddText(ImVec2(widgetPos.x + (i * 22) + commaOffset, widgetPos.y),
zeros ? disabledColor : textColor, buf);
if ((i + 1) % 3 == 0 && i < 11) {
commaOffset += 12;
window->DrawList->AddText(ImVec2(widgetPos.x + (i * 22) + commaOffset + 10, widgetPos.y),
zeros ? disabledColor : textColor, ".");
}
}
ImVec2 mousePos = ImGui::GetMousePos();
bool leftClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool rightClick = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
int mw = ImGui::GetIO().MouseWheel;
bool onDigit = false;
for (int i = 0; i < 12; i++) {
onDigit = false;
if (isInArea(mousePos, digitTopMins[i], digitTopMaxs[i])) {
window->DrawList->AddRectFilled(digitTopMins[i], digitTopMaxs[i], IM_COL32(255, 0, 0, 75));
if (leftClick) {
incrementDigit(i);
}
onDigit = true;
}
if (isInArea(mousePos, digitBottomMins[i], digitBottomMaxs[i])) {
window->DrawList->AddRectFilled(digitBottomMins[i], digitBottomMaxs[i], IM_COL32(0, 0, 255, 75));
if (leftClick) {
decrementDigit(i);
}
onDigit = true;
}
if (onDigit) {
if (rightClick) {
for (int j = i; j < 12; j++) {
digits[j] = 0;
}
frequencyChanged = true;
}
if (mw != 0) {
int count = abs(mw);
for (int j = 0; j < count; j++) {
mw > 0 ? incrementDigit(i) : decrementDigit(i);
}
}
}
}
uint64_t freq = 0;
for (int i = 0; i < 12; i++) {
freq += digits[i] * pow(10, 11 - i);
}
frequency = freq;
ImGui::PopFont();
ImGui::NewLine();
}
void FrequencySelect::setFrequency(uint64_t freq) {
int i = 11;
for (uint64_t f = freq; i >= 0; i--) {
digits[i] = f % 10;
f -= digits[i];
f /= 10;
}
frequency = freq;
}

View File

@ -0,0 +1,40 @@
#pragma once
#include <imgui.h>
#include <imgui_internal.h>
#include <stdint.h>
#include <config.h>
class FrequencySelect {
public:
FrequencySelect();
void init();
void draw();
void setFrequency(uint64_t freq);
uint64_t frequency;
bool frequencyChanged = false;
private:
void onPosChange();
void onResize();
void incrementDigit(int i);
void decrementDigit(int i);
ImVec2 widgetPos;
ImVec2 widgetEndPos;
ImVec2 widgetSize;
ImVec2 lastWidgetPos;
ImVec2 lastWidgetSize;
ImGuiWindow* window;
ImFont* font;
int digits[12];
ImVec2 digitBottomMins[12];
ImVec2 digitTopMins[12];
ImVec2 digitBottomMaxs[12];
ImVec2 digitTopMaxs[12];
char buf[100];
};

6
core/src/gui/gui.cpp Normal file
View File

@ -0,0 +1,6 @@
#include <gui/gui.h>
namespace gui {
ImGui::WaterFall waterfall;
FrequencySelect freqSelect;
};

9
core/src/gui/gui.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <gui/waterfall.h>
#include <gui/frequency_select.h>
#include <module.h>
namespace gui {
SDRPP_EXPORT ImGui::WaterFall waterfall;
SDRPP_EXPORT FrequencySelect freqSelect;
};

32
core/src/gui/icons.cpp Normal file
View File

@ -0,0 +1,32 @@
#include <gui/icons.h>
#define STB_IMAGE_IMPLEMENTATION
#include <imgui/stb_image.h>
namespace icons {
ImTextureID LOGO;
ImTextureID PLAY;
ImTextureID STOP;
ImTextureID MENU;
GLuint loadTexture(std::string path) {
int w,h,n;
stbi_uc* data = stbi_load(path.c_str(), &w, &h, &n, NULL);
GLuint texId;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (uint8_t*)data);
stbi_image_free(data);
return texId;
}
void load() {
LOGO = (ImTextureID)loadTexture(config::getRootDirectory() + "/res/icons/sdrpp.png");
PLAY = (ImTextureID)loadTexture(config::getRootDirectory() + "/res/icons/play.png");
STOP = (ImTextureID)loadTexture(config::getRootDirectory() + "/res/icons/stop.png");
MENU = (ImTextureID)loadTexture(config::getRootDirectory() + "/res/icons/menu.png");
}
}

15
core/src/gui/icons.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include <imgui/imgui.h>
#include <stdint.h>
#include <GL/glew.h>
#include <config.h>
namespace icons {
extern ImTextureID LOGO;
extern ImTextureID PLAY;
extern ImTextureID STOP;
extern ImTextureID MENU;
GLuint loadTexture(char* path);
void load();
}

View File

@ -0,0 +1,869 @@
#include <gui/main_window.h>
#include <gui/gui.h>
std::thread worker;
std::mutex fft_mtx;
fftwf_complex *fft_in, *fft_out;
fftwf_plan p;
float* tempData;
float* uiGains;
char buf[1024];
ImFont* bigFont;
int fftSize = 8192 * 8;
io::SoapyWrapper soapy;
std::vector<float> _data;
std::vector<float> fftTaps;
void fftHandler(dsp::complex_t* samples) {
fftwf_execute(p);
int half = fftSize / 2;
for (int i = 0; i < half; i++) {
_data.push_back(log10(std::abs(std::complex<float>(fft_out[half + i][0], fft_out[half + i][1])) / (float)fftSize) * 10.0f);
}
for (int i = 0; i < half; i++) {
_data.push_back(log10(std::abs(std::complex<float>(fft_out[i][0], fft_out[i][1])) / (float)fftSize) * 10.0f);
}
for (int i = 5; i < fftSize; i++) {
_data[i] = (_data[i - 4] + _data[i - 3] + _data[i - 2] + _data[i - 1] + _data[i]) / 5.0f;
}
gui::waterfall.pushFFT(_data, fftSize);
_data.clear();
}
dsp::NullSink sink;
int devId = 0;
int srId = 0;
watcher<int> bandplanId(0, true);
watcher<uint64_t> freq((uint64_t)90500000);
int demod = 1;
watcher<float> vfoFreq(92000000.0f);
float dummyVolume = 1.0f;
float* volume = &dummyVolume;
float fftMin = -70.0f;
float fftMax = 0.0f;
watcher<float> offset(0.0f, true);
watcher<float> bw(8000000.0f, true);
int sampleRate = 8000000;
bool playing = false;
watcher<bool> dcbias(false, false);
watcher<bool> 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;
bool showMenu = true;
void saveCurrentSource() {
int i = 0;
for (std::string gainName : soapy.gainList) {
config::config["sourceSettings"][sourceName]["gains"][gainName] = uiGains[i];
i++;
}
config::config["sourceSettings"][sourceName]["sampleRate"] = soapy.sampleRates[srId];
}
void loadSourceConfig(std::string name) {
json sourceSettings = config::config["sourceSettings"][name];
sampleRate = sourceSettings["sampleRate"];
auto _srIt = std::find(soapy.sampleRates.begin(), soapy.sampleRates.end(), sampleRate);
// If the sample rate isn't valid, set to minimum
if (_srIt == soapy.sampleRates.end()) {
srId = 0;
sampleRate = soapy.sampleRates[0];
}
else {
srId = std::distance(soapy.sampleRates.begin(), _srIt);
}
sigpath::signalPath.setSampleRate(sampleRate);
soapy.setSampleRate(sampleRate);
// Set gains
delete[] uiGains;
uiGains = new float[soapy.gainList.size()];
int i = 0;
for (std::string gainName : soapy.gainList) {
// If gain doesn't exist in config, set it to the minimum value
if (!sourceSettings["gains"].contains(gainName)) {
uiGains[i] = soapy.gainRanges[i].minimum();
soapy.setGain(i, uiGains[i]);
continue;
}
uiGains[i] = sourceSettings["gains"][gainName];
soapy.setGain(i, uiGains[i]);
i++;
}
// Update GUI
gui::waterfall.setBandwidth(sampleRate);
gui::waterfall.setViewBandwidth(sampleRate);
bw.val = sampleRate;
}
void loadAudioConfig(std::string name) {
json audioSettings = config::config["audio"][name];
std::string devName = audioSettings["device"];
auto _devIt = std::find(audio::streams[name]->audio->deviceNames.begin(), audio::streams[name]->audio->deviceNames.end(), devName);
// If audio device doesn't exist anymore
if (_devIt == audio::streams[name]->audio->deviceNames.end()) {
audio::streams[name]->audio->setToDefault();
int deviceId = audio::streams[name]->audio->getDeviceId();
audio::setAudioDevice(name, deviceId, audio::streams[name]->audio->devices[deviceId].sampleRates[0]);
audio::streams[name]->sampleRateId = 0;
audio::streams[name]->volume = audioSettings["volume"];
audio::streams[name]->audio->setVolume(audio::streams[name]->volume);
return;
}
int deviceId = std::distance(audio::streams[name]->audio->deviceNames.begin(), _devIt);
float sr = audioSettings["sampleRate"];
auto _srIt = std::find(audio::streams[name]->audio->devices[deviceId].sampleRates.begin(), audio::streams[name]->audio->devices[deviceId].sampleRates.end(), sr);
// If sample rate doesn't exist anymore
if (_srIt == audio::streams[name]->audio->devices[deviceId].sampleRates.end()) {
audio::streams[name]->sampleRateId = 0;
audio::setAudioDevice(name, deviceId, audio::streams[name]->audio->devices[deviceId].sampleRates[0]);
audio::streams[name]->volume = audioSettings["volume"];
audio::streams[name]->audio->setVolume(audio::streams[name]->volume);
return;
}
int samplerateId = std::distance(audio::streams[name]->audio->devices[deviceId].sampleRates.begin(), _srIt);
audio::streams[name]->sampleRateId = samplerateId;
audio::setAudioDevice(name, deviceId, audio::streams[name]->audio->devices[deviceId].sampleRates[samplerateId]);
audio::streams[name]->deviceId = deviceId;
audio::streams[name]->volume = audioSettings["volume"];
audio::streams[name]->audio->setVolume(audio::streams[name]->volume);
}
void saveAudioConfig(std::string name) {
config::config["audio"][name]["device"] = audio::streams[name]->audio->deviceNames[audio::streams[name]->deviceId];
config::config["audio"][name]["volume"] = audio::streams[name]->volume;
config::config["audio"][name]["sampleRate"] = audio::streams[name]->sampleRate;
}
void windowInit() {
spdlog::info("Initializing SoapySDR");
soapy.init();
gui::freqSelect.init();
fft_in = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * fftSize);
fft_out = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * fftSize);
p = fftwf_plan_dft_1d(fftSize, fft_in, fft_out, FFTW_FORWARD, FFTW_ESTIMATE);
sigpath::signalPath.init(sampleRate, 20, fftSize, &soapy.output, (dsp::complex_t*)fft_in, fftHandler);
sigpath::signalPath.start();
uiGains = new float[soapy.gainList.size()];
spdlog::info("Loading modules");
mod::initAPI(&gui::waterfall);
mod::loadFromList(config::getRootDirectory() + "/module_list.json");
bigFont = ImGui::GetIO().Fonts->AddFontFromFileTTF((config::getRootDirectory() + "/res/fonts/Roboto-Medium.ttf").c_str(), 128.0f);
// Load last source configuration
uint64_t frequency = config::config["frequency"];
sourceName = config::config["source"];
auto _sourceIt = std::find(soapy.devNameList.begin(), soapy.devNameList.end(), sourceName);
if (_sourceIt != soapy.devNameList.end() && config::config["sourceSettings"].contains(sourceName)) {
json sourceSettings = config::config["sourceSettings"][sourceName];
devId = std::distance(soapy.devNameList.begin(), _sourceIt);
soapy.setDevice(soapy.devList[devId]);
loadSourceConfig(sourceName);
}
else {
int i = 0;
bool settingsFound = false;
for (std::string devName : soapy.devNameList) {
if (config::config["sourceSettings"].contains(devName)) {
sourceName = devName;
settingsFound = true;
devId = i;
soapy.setDevice(soapy.devList[i]);
loadSourceConfig(sourceName);
break;
}
i++;
}
if (!settingsFound) {
if (soapy.devNameList.size() > 0) {
sourceName = soapy.devNameList[0];
}
sampleRate = soapy.getSampleRate();
sigpath::signalPath.setSampleRate(sampleRate);
}
// Search for the first source in the list to have a config
// If no pre-defined source, selected default device
}
// Also add a loading screen
// Adjustable "snap to grid" for each VFO
// Finish the recorder module
// Add squelsh
// Bandwidth ajustment
// CW and RAW modes;
// Bring VFO to a visible place when changing sample rate if it's smaller
// Add save config for modules
// Do VFO in two steps: First sample rate conversion, then filtering
// 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
fftMin = config::config["min"];
fftMax = config::config["max"];
gui::waterfall.setFFTMin(fftMin);
gui::waterfall.setWaterfallMin(fftMin);
gui::waterfall.setFFTMax(fftMax);
gui::waterfall.setWaterfallMax(fftMax);
bandPlanEnabled.val = config::config["bandPlanEnabled"];
bandPlanEnabled.markAsChanged();
std::string bandPlanName = config::config["bandPlan"];
auto _bandplanIt = bandplan::bandplans.find(bandPlanName);
if (_bandplanIt != bandplan::bandplans.end()) {
bandplanId.val = std::distance(bandplan::bandplans.begin(), bandplan::bandplans.find(bandPlanName));
if (bandPlanEnabled.val) {
gui::waterfall.bandplan = &bandplan::bandplans[bandPlanName];
}
else {
gui::waterfall.bandplan = NULL;
}
}
else {
bandplanId.val = 0;
}
bandplanId.markAsChanged();
gui::freqSelect.setFrequency(frequency);
gui::freqSelect.frequencyChanged = false;
soapy.setFrequency(frequency);
gui::waterfall.setCenterFrequency(frequency);
gui::waterfall.setBandwidth(sampleRate);
gui::waterfall.setViewBandwidth(sampleRate);
bw.val = sampleRate;
gui::waterfall.vfoFreqChanged = false;
gui::waterfall.centerFreqMoved = false;
gui::waterfall.selectFirstVFO();
for (auto [name, stream] : audio::streams) {
if (config::config["audio"].contains(name)) {
bool running = audio::streams[name]->running;
audio::stopStream(name);
loadAudioConfig(name);
if (running) {
audio::startStream(name);
}
}
}
audioStreamName = audio::getNameFromVFO(gui::waterfall.selectedVFO);
if (audioStreamName != "") {
volume = &audio::streams[audioStreamName]->volume;
}
menuWidth = config::config["menuWidth"];
newWidth = menuWidth;
showWaterfall = config::config["showWaterfall"];
if (!showWaterfall) {
gui::waterfall.hideWaterfall();
}
fftHeight = config::config["fftHeight"];
gui::waterfall.setFFTHeight(fftHeight);
}
void setVFO(float freq) {
ImGui::WaterfallVFO* vfo = gui::waterfall.vfos[gui::waterfall.selectedVFO];
float currentOff = vfo->centerOffset;
float currentTune = gui::waterfall.getCenterFrequency() + vfo->generalOffset;
float delta = freq - currentTune;
float newVFO = currentOff + delta;
float vfoBW = vfo->bandwidth;
float vfoBottom = newVFO - (vfoBW / 2.0f);
float vfoTop = newVFO + (vfoBW / 2.0f);
float view = gui::waterfall.getViewOffset();
float viewBW = gui::waterfall.getViewBandwidth();
float viewBottom = view - (viewBW / 2.0f);
float viewTop = view + (viewBW / 2.0f);
float wholeFreq = gui::waterfall.getCenterFrequency();
float BW = gui::waterfall.getBandwidth();
float bottom = -(BW / 2.0f);
float top = (BW / 2.0f);
// VFO still fints in the view
if (vfoBottom > viewBottom && vfoTop < viewTop) {
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFO);
return;
}
// VFO too low for current SDR tuning
if (vfoBottom < bottom) {
gui::waterfall.setViewOffset((BW / 2.0f) - (viewBW / 2.0f));
float newVFOOffset = (BW / 2.0f) - (vfoBW / 2.0f) - (viewBW / 10.0f);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFOOffset);
gui::waterfall.setCenterFrequency(freq - newVFOOffset);
soapy.setFrequency(freq - newVFOOffset);
return;
}
// VFO too high for current SDR tuning
if (vfoTop > top) {
gui::waterfall.setViewOffset((viewBW / 2.0f) - (BW / 2.0f));
float newVFOOffset = (vfoBW / 2.0f) - (BW / 2.0f) + (viewBW / 10.0f);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFOOffset);
gui::waterfall.setCenterFrequency(freq - newVFOOffset);
soapy.setFrequency(freq - newVFOOffset);
return;
}
// VFO is still without the SDR's bandwidth
if (delta < 0) {
float newViewOff = vfoTop - (viewBW / 2.0f) + (viewBW / 10.0f);
float newViewBottom = newViewOff - (viewBW / 2.0f);
float newViewTop = newViewOff + (viewBW / 2.0f);
if (newViewBottom > bottom) {
gui::waterfall.setViewOffset(newViewOff);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFO);
return;
}
gui::waterfall.setViewOffset((BW / 2.0f) - (viewBW / 2.0f));
float newVFOOffset = (BW / 2.0f) - (vfoBW / 2.0f) - (viewBW / 10.0f);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFOOffset);
gui::waterfall.setCenterFrequency(freq - newVFOOffset);
soapy.setFrequency(freq - newVFOOffset);
}
else {
float newViewOff = vfoBottom + (viewBW / 2.0f) - (viewBW / 10.0f);
float newViewBottom = newViewOff - (viewBW / 2.0f);
float newViewTop = newViewOff + (viewBW / 2.0f);
if (newViewTop < top) {
gui::waterfall.setViewOffset(newViewOff);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFO);
return;
}
gui::waterfall.setViewOffset((viewBW / 2.0f) - (BW / 2.0f));
float newVFOOffset = (vfoBW / 2.0f) - (BW / 2.0f) + (viewBW / 10.0f);
sigpath::vfoManager.setCenterOffset(gui::waterfall.selectedVFO, newVFOOffset);
gui::waterfall.setCenterFrequency(freq - newVFOOffset);
soapy.setFrequency(freq - newVFOOffset);
}
}
void drawWindow() {
ImGui::Begin("Main", NULL, WINDOW_FLAGS);
ImGui::WaterfallVFO* vfo = gui::waterfall.vfos[gui::waterfall.selectedVFO];
if (vfo->centerOffsetChanged) {
gui::freqSelect.setFrequency(gui::waterfall.getCenterFrequency() + vfo->generalOffset);
gui::freqSelect.frequencyChanged = false;
config::config["frequency"] = gui::freqSelect.frequency;
config::configModified = true;
}
sigpath::vfoManager.updateFromWaterfall(&gui::waterfall);
if (gui::waterfall.selectedVFOChanged) {
gui::waterfall.selectedVFOChanged = false;
gui::freqSelect.setFrequency(vfo->generalOffset + gui::waterfall.getCenterFrequency());
gui::freqSelect.frequencyChanged = false;
mod::broadcastEvent(mod::EVENT_SELECTED_VFO_CHANGED);
audioStreamName = audio::getNameFromVFO(gui::waterfall.selectedVFO);
if (audioStreamName != "") {
volume = &audio::streams[audioStreamName]->volume;
}
config::config["frequency"] = gui::freqSelect.frequency;
config::configModified = true;
}
if (gui::freqSelect.frequencyChanged) {
gui::freqSelect.frequencyChanged = false;
setVFO(gui::freqSelect.frequency);
vfo->centerOffsetChanged = false;
vfo->lowerOffsetChanged = false;
vfo->upperOffsetChanged = false;
config::config["frequency"] = gui::freqSelect.frequency;
config::configModified = true;
}
if (gui::waterfall.centerFreqMoved) {
gui::waterfall.centerFreqMoved = false;
soapy.setFrequency(gui::waterfall.getCenterFrequency());
gui::freqSelect.setFrequency(gui::waterfall.getCenterFrequency() + vfo->generalOffset);
config::config["frequency"] = gui::freqSelect.frequency;
config::configModified = true;
}
if (dcbias.changed()) {
sigpath::signalPath.setDCBiasCorrection(dcbias.val);
}
if (bandplanId.changed() && bandPlanEnabled.val) {
gui::waterfall.bandplan = &bandplan::bandplans[bandplan::bandplanNames[bandplanId.val]];
}
if (bandPlanEnabled.changed()) {
gui::waterfall.bandplan = bandPlanEnabled.val ? &bandplan::bandplans[bandplan::bandplanNames[bandplanId.val]] : NULL;
}
int _fftHeight = gui::waterfall.getFFTHeight();
if (fftHeight != _fftHeight) {
fftHeight = _fftHeight;
config::config["fftHeight"] = fftHeight;
config::configModified = true;
}
ImVec2 vMin = ImGui::GetWindowContentRegionMin();
ImVec2 vMax = ImGui::GetWindowContentRegionMax();
int width = vMax.x - vMin.x;
int height = vMax.y - vMin.y;
int modCount = mod::moduleNames.size();
mod::Module_t mod;
for (int i = 0; i < modCount; i++) {
mod = mod::modules[mod::moduleNames[i]];
mod._NEW_FRAME_(mod.ctx);
}
// To Bar
if (ImGui::ImageButton(icons::MENU, ImVec2(40, 40), ImVec2(0, 0), ImVec2(1, 1), 0)) {
showMenu = !showMenu;
}
ImGui::SameLine();
if (playing) {
if (ImGui::ImageButton(icons::STOP, ImVec2(40, 40), ImVec2(0, 0), ImVec2(1, 1), 0)) {
soapy.stop();
playing = false;
}
}
else {
if (ImGui::ImageButton(icons::PLAY, ImVec2(40, 40), ImVec2(0, 0), ImVec2(1, 1), 0) && soapy.devList.size() > 0) {
soapy.start();
soapy.setFrequency(gui::waterfall.getCenterFrequency());
playing = true;
}
}
ImGui::SameLine();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8);
ImGui::SetNextItemWidth(200);
if (ImGui::SliderFloat("##_2_", volume, 0.0f, 1.0f, "")) {
if (audioStreamName != "") {
if (!config::config["audio"].contains(audioStreamName)) {
saveAudioConfig(audioStreamName);
}
audio::streams[audioStreamName]->audio->setVolume(*volume);
config::config["audio"][audioStreamName]["volume"] = *volume;
config::configModified = true;
}
}
ImGui::SameLine();
gui::freqSelect.draw();
ImGui::SameLine();
// Logo button
ImGui::SetCursorPosX(ImGui::GetWindowSize().x - 48);
ImGui::SetCursorPosY(10);
if (ImGui::ImageButton(icons::LOGO, ImVec2(32, 32), ImVec2(0, 0), ImVec2(1, 1), 0)) {
showCredits = true;
}
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
showCredits = false;
}
if (ImGui::IsKeyPressed(GLFW_KEY_ESCAPE)) {
showCredits = false;
}
// Handle menu resize
float curY = ImGui::GetCursorPosY();
ImVec2 winSize = ImGui::GetWindowSize();
ImVec2 mousePos = ImGui::GetMousePos();
bool click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
if (grabbingMenu) {
newWidth = mousePos.x;
newWidth = std::clamp<float>(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;
}
// Left Column
if (showMenu) {
ImGui::Columns(3, "WindowColumns", false);
ImGui::SetColumnWidth(0, menuWidth);
ImGui::SetColumnWidth(1, winSize.x - menuWidth - 60);
ImGui::SetColumnWidth(2, 60);
ImGui::BeginChild("Left Column");
float menuColumnWidth = ImGui::GetContentRegionAvailWidth();
if (ImGui::CollapsingHeader("Source", ImGuiTreeNodeFlags_DefaultOpen)) {
if (playing) { style::beginDisabled(); };
ImGui::PushItemWidth(menuColumnWidth);
if (ImGui::Combo("##_0_", &devId, soapy.txtDevList.c_str())) {
spdlog::info("Changed input device: {0}", devId);
sourceName = soapy.devNameList[devId];
soapy.setDevice(soapy.devList[devId]);
if (config::config["sourceSettings"].contains(sourceName)) {
loadSourceConfig(sourceName);
}
else {
srId = 0;
sampleRate = soapy.getSampleRate();
bw.val = sampleRate;
gui::waterfall.setBandwidth(sampleRate);
gui::waterfall.setViewBandwidth(sampleRate);
sigpath::signalPath.setSampleRate(sampleRate);
if (soapy.gainList.size() >= 0) {
delete[] uiGains;
uiGains = new float[soapy.gainList.size()];
for (int i = 0; i < soapy.gainList.size(); i++) {
uiGains[i] = soapy.currentGains[i];
}
}
}
setVFO(gui::freqSelect.frequency);
config::config["source"] = sourceName;
config::configModified = true;
}
ImGui::PopItemWidth();
if (ImGui::Combo("##_1_", &srId, soapy.txtSampleRateList.c_str())) {
spdlog::info("Changed sample rate: {0}", srId);
sampleRate = soapy.sampleRates[srId];
soapy.setSampleRate(sampleRate);
gui::waterfall.setBandwidth(sampleRate);
gui::waterfall.setViewBandwidth(sampleRate);
sigpath::signalPath.setSampleRate(sampleRate);
bw.val = sampleRate;
if (!config::config["sourceSettings"].contains(sourceName)) {
saveCurrentSource();
}
config::config["sourceSettings"][sourceName]["sampleRate"] = sampleRate;
config::configModified = true;
}
ImGui::SameLine();
bool noDevice = (soapy.devList.size() == 0);
if (ImGui::Button("Refresh", ImVec2(menuColumnWidth - ImGui::GetCursorPosX(), 0.0f))) {
soapy.refresh();
if (noDevice && soapy.devList.size() > 0) {
sourceName = soapy.devNameList[0];
soapy.setDevice(soapy.devList[0]);
if (config::config["sourceSettings"][sourceName]) {
loadSourceConfig(sourceName);
}
}
}
if (playing) { style::endDisabled(); };
float maxTextLength = 0;
float txtLen = 0;
char buf[100];
// Calculate the spacing
for (int i = 0; i < soapy.gainList.size(); i++) {
sprintf(buf, "%s gain", soapy.gainList[i].c_str());
txtLen = ImGui::CalcTextSize(buf).x;
if (txtLen > maxTextLength) {
maxTextLength = txtLen;
}
}
for (int i = 0; i < soapy.gainList.size(); i++) {
ImGui::Text("%s gain", soapy.gainList[i].c_str());
ImGui::SameLine();
sprintf(buf, "##_gain_slide_%d_", i);
ImGui::SetCursorPosX(maxTextLength + 5);
ImGui::PushItemWidth(menuColumnWidth - (maxTextLength + 5));
if (ImGui::SliderFloat(buf, &uiGains[i], soapy.gainRanges[i].minimum(), soapy.gainRanges[i].maximum())) {
soapy.setGain(i, uiGains[i]);
if (!config::config["sourceSettings"].contains(sourceName)) {
saveCurrentSource();
}
config::config["sourceSettings"][sourceName]["gains"][soapy.gainList[i]] = uiGains[i];
config::configModified = true;
}
ImGui::PopItemWidth();
}
ImGui::Spacing();
}
for (int i = 0; i < modCount; i++) {
if (ImGui::CollapsingHeader(mod::moduleNames[i].c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
mod = mod::modules[mod::moduleNames[i]];
mod._DRAW_MENU_(mod.ctx);
ImGui::Spacing();
}
}
if (ImGui::CollapsingHeader("Audio", ImGuiTreeNodeFlags_DefaultOpen)) {
int count = 0;
int maxCount = audio::streams.size();
for (auto const& [name, stream] : audio::streams) {
int deviceId;
float vol = 1.0f;
deviceId = stream->audio->getDeviceId();
ImGui::SetCursorPosX((menuColumnWidth / 2.0f) - (ImGui::CalcTextSize(name.c_str()).x / 2.0f));
ImGui::Text(name.c_str());
ImGui::PushItemWidth(menuColumnWidth);
bool running = stream->running;
if (ImGui::Combo(("##_audio_dev_0_"+ name).c_str(), &stream->deviceId, stream->audio->devTxtList.c_str())) {
audio::stopStream(name);
audio::setAudioDevice(name, stream->deviceId, stream->audio->devices[deviceId].sampleRates[0]);
if (running) {
audio::startStream(name);
}
stream->sampleRateId = 0;
// Create config if it doesn't exist
if (!config::config["audio"].contains(name)) {
saveAudioConfig(name);
}
config::config["audio"][name]["device"] = stream->audio->deviceNames[stream->deviceId];
config::config["audio"][name]["sampleRate"] = stream->audio->devices[stream->deviceId].sampleRates[0];
config::configModified = true;
}
if (ImGui::Combo(("##_audio_sr_0_" + name).c_str(), &stream->sampleRateId, stream->audio->devices[deviceId].txtSampleRates.c_str())) {
audio::stopStream(name);
audio::setSampleRate(name, stream->audio->devices[deviceId].sampleRates[stream->sampleRateId]);
if (running) {
audio::startStream(name);
}
// Create config if it doesn't exist
if (!config::config["audio"].contains(name)) {
saveAudioConfig(name);
}
config::config["audio"][name]["sampleRate"] = stream->audio->devices[deviceId].sampleRates[stream->sampleRateId];
config::configModified = true;
}
if (ImGui::SliderFloat(("##_audio_vol_0_" + name).c_str(), &stream->volume, 0.0f, 1.0f, "")) {
stream->audio->setVolume(stream->volume);
// Create config if it doesn't exist
if (!config::config["audio"].contains(name)) {
saveAudioConfig(name);
}
config::config["audio"][name]["volume"] = stream->volume;
config::configModified = true;
}
ImGui::PopItemWidth();
count++;
if (count < maxCount) {
ImGui::Spacing();
ImGui::Separator();
}
ImGui::Spacing();
}
ImGui::Spacing();
}
if (ImGui::CollapsingHeader("Band Plan", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushItemWidth(menuColumnWidth);
if (ImGui::Combo("##_4_", &bandplanId.val, bandplan::bandplanNameTxt.c_str())) {
config::config["bandPlan"] = bandplan::bandplanNames[bandplanId.val];
config::configModified = true;
}
ImGui::PopItemWidth();
if (ImGui::Checkbox("Enabled", &bandPlanEnabled.val)) {
config::config["bandPlanEnabled"] = bandPlanEnabled.val;
config::configModified = true;
}
bandplan::BandPlan_t plan = bandplan::bandplans[bandplan::bandplanNames[bandplanId.val]];
ImGui::Text("Country: %s (%s)", plan.countryName.c_str(), plan.countryCode.c_str());
ImGui::Text("Author: %s", plan.authorName.c_str());
ImGui::Spacing();
}
if (ImGui::CollapsingHeader("Display", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Show waterfall", &showWaterfall)) {
showWaterfall ? gui::waterfall.showWaterfall() : gui::waterfall.hideWaterfall();
}
ImGui::Spacing();
}
if(ImGui::CollapsingHeader("Debug")) {
ImGui::Text("Frame time: %.3f ms/frame", 1000.0f / ImGui::GetIO().Framerate);
ImGui::Text("Framerate: %.1f FPS", ImGui::GetIO().Framerate);
ImGui::Text("Center Frequency: %.0f Hz", gui::waterfall.getCenterFrequency());
ImGui::Text("Source name: %s", sourceName.c_str());
ImGui::Spacing();
}
ImGui::EndChild();
}
else {
// When hiding the menu bar
ImGui::Columns(3, "WindowColumns", false);
ImGui::SetColumnWidth(0, 8);
ImGui::SetColumnWidth(1, winSize.x - 8 - 60);
ImGui::SetColumnWidth(2, 60);
}
// Right Column
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::NextColumn();
ImGui::PopStyleVar();
ImGui::BeginChild("Waterfall");
gui::waterfall.draw();
ImGui::EndChild();
ImGui::NextColumn();
ImGui::BeginChild("WaterfallControls");
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - (ImGui::CalcTextSize("Zoom").x / 2.0f));
ImGui::Text("Zoom");
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - 10);
ImGui::VSliderFloat("##_7_", ImVec2(20.0f, 150.0f), &bw.val, sampleRate, 1000.0f, "");
ImGui::NewLine();
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - (ImGui::CalcTextSize("Max").x / 2.0f));
ImGui::Text("Max");
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - 10);
if (ImGui::VSliderFloat("##_8_", ImVec2(20.0f, 150.0f), &fftMax, 0.0f, -100.0f, "")) {
fftMax = std::max<float>(fftMax, fftMin + 10);
config::config["max"] = fftMax;
config::configModified = true;
}
ImGui::NewLine();
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - (ImGui::CalcTextSize("Min").x / 2.0f));
ImGui::Text("Min");
ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - 10);
if (ImGui::VSliderFloat("##_9_", ImVec2(20.0f, 150.0f), &fftMin, 0.0f, -100.0f, "")) {
fftMin = std::min<float>(fftMax - 10, fftMin);
config::config["min"] = fftMin;
config::configModified = true;
}
ImGui::EndChild();
if (bw.changed()) {
gui::waterfall.setViewBandwidth(bw.val);
gui::waterfall.setViewOffset(vfo->centerOffset);
}
gui::waterfall.setFFTMin(fftMin);
gui::waterfall.setFFTMax(fftMax);
gui::waterfall.setWaterfallMin(fftMin);
gui::waterfall.setWaterfallMax(fftMax);
ImGui::End();
if (showCredits) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20.0f, 20.0f));
ImGui::OpenPopup("Credits");
ImGui::BeginPopupModal("Credits", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove);
ImGui::PushFont(bigFont);
ImGui::Text("SDR++ ");
ImGui::PopFont();
ImGui::SameLine();
ImGui::Image(icons::LOGO, ImVec2(128, 128));
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Text("This software is brought to you by\n\n");
ImGui::Columns(3, "CreditColumns", true);
// Contributors
ImGui::Text("Contributors");
ImGui::BulletText("Ryzerth (Creator)");
ImGui::BulletText("aosync");
ImGui::BulletText("Benjamin Kyd");
ImGui::BulletText("Tobias Mädel");
ImGui::BulletText("Raov");
ImGui::BulletText("Howard0su");
// Libraries
ImGui::NextColumn();
ImGui::Text("Libraries");
ImGui::BulletText("SoapySDR (PothosWare)");
ImGui::BulletText("Dear ImGui (ocornut)");
ImGui::BulletText("spdlog (gabime)");
ImGui::BulletText("json (nlohmann)");
ImGui::BulletText("portaudio (PA Comm.)");
// Patrons
ImGui::NextColumn();
ImGui::Text("Patrons");
ImGui::BulletText("SignalsEverywhere");
ImGui::EndPopup();
ImGui::PopStyleVar(1);
}
}
void bindVolumeVariable(float* vol) {
volume = vol;
}
void unbindVolumeVariable() {
volume = &dummyVolume;
}

View File

@ -0,0 +1,36 @@
#pragma once
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <imgui_plot.h>
#include <dsp/resampling.h>
#include <dsp/demodulator.h>
#include <dsp/filter.h>
#include <thread>
#include <complex>
#include <dsp/source.h>
#include <dsp/math.h>
#include <gui/waterfall.h>
#include <gui/frequency_select.h>
#include <fftw3.h>
#include <signal_path/dsp.h>
#include <io/soapy.h>
#include <gui/icons.h>
#include <gui/bandplan.h>
#include <watcher.h>
#include <module.h>
#include <signal_path/vfo_manager.h>
#include <signal_path/audio.h>
#include <gui/style.h>
#include <config.h>
#include <signal_path/signal_path.h>
#define WINDOW_FLAGS ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground
void windowInit();
void drawWindow();
void bindVolumeVariable(float* vol);
void unbindVolumeVariable();

24
core/src/gui/menu.cpp Normal file
View File

@ -0,0 +1,24 @@
#include <gui/menu.h>
Menu::Menu() {
}
void Menu::registerEntry(std::string name, void (*drawHandler)(void* ctx), void* ctx) {
MenuItem_t item;
item.drawHandler = drawHandler;
item.ctx = ctx;
items[name] = item;
}
void Menu::removeEntry(std::string name) {
items.erase(name);
}
void Menu::draw() {
MenuItem_t item;
for (std::string name : order) {
item = items[name];
item.drawHandler(item.ctx);
}
}

23
core/src/gui/menu.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
#include <map>
class Menu {
public:
Menu();
struct MenuItem_t {
void (*drawHandler)(void* ctx);
void* ctx;
};
void registerEntry(std::string name, void (*drawHandler)(void* ctx), void* ctx = NULL);
void removeEntry(std::string name);
void draw();
std::vector<std::string> order;
private:
std::map<std::string, MenuItem_t> items;
};

93
core/src/gui/style.cpp Normal file
View File

@ -0,0 +1,93 @@
#include <gui/style.h>
namespace style {
void setDefaultStyle() {
ImGui::GetStyle().WindowRounding = 0.0f;
ImGui::GetStyle().ChildRounding = 0.0f;
ImGui::GetStyle().FrameRounding = 0.0f;
ImGui::GetStyle().GrabRounding = 0.0f;
ImGui::GetStyle().PopupRounding = 0.0f;
ImGui::GetStyle().ScrollbarRounding = 0.0f;
ImGui::GetIO().Fonts->AddFontFromFileTTF((config::getRootDirectory() + "/res/fonts/Roboto-Medium.ttf").c_str(), 16.0f);
ImGui::StyleColorsDark();
//ImGui::StyleColorsLight();
}
void testtt() {
ImGui::StyleColorsLight();
}
void setDarkStyle() {
ImGui::GetStyle().WindowRounding = 0.0f;
ImGui::GetStyle().ChildRounding = 0.0f;
ImGui::GetStyle().FrameRounding = 0.0f;
ImGui::GetStyle().GrabRounding = 0.0f;
ImGui::GetStyle().PopupRounding = 0.0f;
ImGui::GetStyle().ScrollbarRounding = 0.0f;
ImGui::GetIO().Fonts->AddFontFromFileTTF((config::getRootDirectory() + "/res/fonts/Roboto-Medium.ttf").c_str(), 16.0f);
ImGui::StyleColorsDark();
auto& style = ImGui::GetStyle();
ImVec4* colors = style.Colors;
colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f);
colors[ImGuiCol_ChildBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.00f);
colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f);
colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f);
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.21f, 0.22f, 0.54f);
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.21f, 0.22f, 0.54f);
colors[ImGuiCol_FrameBgActive] = ImVec4(0.20f, 0.21f, 0.22f, 0.54f);
colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.29f, 0.29f, 0.29f, 1.00f);
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f);
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f);
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f);
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f);
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f);
colors[ImGuiCol_CheckMark] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f);
colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f);
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.44f, 0.44f, 0.44f, 0.40f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.44f, 0.44f, 0.44f, 0.45f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.44f, 0.44f, 0.44f, 0.40f);
colors[ImGuiCol_Header] = ImVec4(0.63f, 0.63f, 0.70f, 0.31f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.63f, 0.63f, 0.70f, 0.40f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.63f, 0.63f, 0.70f, 0.31f);
colors[ImGuiCol_Separator] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f);
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.72f, 0.72f, 0.72f, 0.78f);
colors[ImGuiCol_SeparatorActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f);
colors[ImGuiCol_ResizeGrip] = ImVec4(0.91f, 0.91f, 0.91f, 0.25f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.81f, 0.81f, 0.81f, 0.67f);
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.46f, 0.46f, 0.46f, 0.95f);
colors[ImGuiCol_PlotLines] = ImVec4(0.4f, 0.9f, 1.0f, 1.00f);
colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f);
colors[ImGuiCol_PlotHistogram] = ImVec4(0.73f, 0.60f, 0.15f, 1.00f);
colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(0.87f, 0.87f, 0.87f, 0.35f);
colors[ImGuiCol_ModalWindowDarkening] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f);
colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
colors[ImGuiCol_NavHighlight] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
}
void beginDisabled() {
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.44f, 0.44f, 0.44f, 0.15f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.20f, 0.21f, 0.22f, 0.30f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 0.65f));
}
void endDisabled() {
ImGui::PopItemFlag();
ImGui::PopStyleColor(3);
}
}

12
core/src/gui/style.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include <imgui.h>
#include <imgui_internal.h>
#include <config.h>
namespace style {
void setDefaultStyle();
void setDarkStyle();
void beginDisabled();
void endDisabled();
void testtt();
}

785
core/src/gui/waterfall.cpp Normal file
View File

@ -0,0 +1,785 @@
#include <gui/waterfall.h>
float COLOR_MAP[][3] = {
{0x00, 0x00, 0x20},
{0x00, 0x00, 0x30},
{0x00, 0x00, 0x50},
{0x00, 0x00, 0x91},
{0x1E, 0x90, 0xFF},
{0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0x00},
{0xFE, 0x6D, 0x16},
{0xFF, 0x00, 0x00},
{0xC6, 0x00, 0x00},
{0x9F, 0x00, 0x00},
{0x75, 0x00, 0x00},
{0x4A, 0x00, 0x00}
};
void doZoom(int offset, int width, int outWidth, std::vector<float> data, float* out) {
// NOTE: REMOVE THAT SHIT, IT'S JUST A HACKY FIX
if (offset < 0) {
offset = 0;
}
if (width > 65535) {
width = 65535;
}
float factor = (float)width / (float)outWidth;
for (int i = 0; i < outWidth; i++) {
out[i] = data[offset + ((float)i * factor)];
}
}
float freq_ranges[] = {
1.0f, 2.0f, 2.5f, 5.0f,
10.0f, 20.0f, 25.0f, 50.0f,
100.0f, 200.0f, 250.0f, 500.0f,
1000.0f, 2000.0f, 2500.0f, 5000.0f,
10000.0f, 20000.0f, 25000.0f, 50000.0f,
100000.0f, 200000.0f, 250000.0f, 500000.0f,
1000000.0f, 2000000.0f, 2500000.0f, 5000000.0f,
10000000.0f, 20000000.0f, 25000000.0f, 50000000.0f
};
float findBestRange(float bandwidth, int maxSteps) {
for (int i = 0; i < 32; i++) {
if (bandwidth / freq_ranges[i] < (float)maxSteps) {
return freq_ranges[i];
}
}
return 50000000.0f;
}
void printAndScale(float freq, char* buf) {
if (freq < 1000) {
sprintf(buf, "%.3f", freq);
}
else if (freq < 1000000) {
sprintf(buf, "%.3fK", freq / 1000.0f);
}
else if (freq < 1000000000) {
sprintf(buf, "%.3fM", freq / 1000000.0f);
}
else if (freq < 1000000000000) {
sprintf(buf, "%.3fG", freq / 1000000000.0f);
}
for (int i = strlen(buf) - 2; i >= 0; i--) {
if (buf[i] != '0') {
if (buf[i] == '.') {
i--;
}
char scale = buf[strlen(buf) - 1];
buf[i + 1] = scale;
buf[i + 2] = 0;
return;
}
}
}
namespace ImGui {
WaterFall::WaterFall() {
fftMin = -70.0f;
fftMax = 0.0f;
waterfallMin = -70.0f;
waterfallMax = 0.0f;
FFTAreaHeight = 300;
newFFTAreaHeight = FFTAreaHeight;
fftHeight = FFTAreaHeight - 50;
dataWidth = 600;
lastWidgetPos.x = 0;
lastWidgetPos.y = 0;
lastWidgetSize.x = 0;
lastWidgetSize.y = 0;
latestFFT = new float[1];
waterfallFb = new uint32_t[1];
viewBandwidth = 1.0f;
wholeBandwidth = 1.0f;
glGenTextures(1, &textureId);
updatePallette(COLOR_MAP, 13);
}
void WaterFall::drawFFT() {
// Calculate scaling factor
float startLine = floorf(fftMax / vRange) * vRange;
float vertRange = fftMax - fftMin;
float scaleFactor = fftHeight / vertRange;
char buf[100];
ImU32 trace = ImGui::GetColorU32(ImGuiCol_PlotLines);
ImU32 shadow = ImGui::GetColorU32(ImGuiCol_PlotLines, 0.2);
// Vertical scale
for (float line = startLine; line > fftMin; line -= vRange) {
float yPos = widgetPos.y + fftHeight + 10 - ((line - fftMin) * scaleFactor);
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);
window->DrawList->AddText(ImVec2(widgetPos.x + 40 - txtSz.x, roundf(yPos - (txtSz.y / 2))), IM_COL32( 255, 255, 255, 255 ), buf);
}
// Horizontal scale
float startFreq = ceilf(lowerFreq / range) * range;
float horizScale = (float)dataWidth / viewBandwidth;
for (float freq = startFreq; freq < upperFreq; freq += range) {
float xPos = widgetPos.x + 50 + ((freq - lowerFreq) * horizScale);
window->DrawList->AddLine(ImVec2(roundf(xPos), widgetPos.y + 10),
ImVec2(roundf(xPos), widgetPos.y + fftHeight + 10),
IM_COL32(50, 50, 50, 255), 1.0f);
window->DrawList->AddLine(ImVec2(roundf(xPos), widgetPos.y + fftHeight + 10),
ImVec2(roundf(xPos), widgetPos.y + fftHeight + 17),
IM_COL32(255, 255, 255, 255), 1.0f);
printAndScale(freq, buf);
ImVec2 txtSz = ImGui::CalcTextSize(buf);
window->DrawList->AddText(ImVec2(roundf(xPos - (txtSz.x / 2.0f)), widgetPos.y + fftHeight + 10 + txtSz.y), IM_COL32( 255, 255, 255, 255 ), buf);
}
// Data
for (int i = 1; i < dataWidth; i++) {
float aPos = widgetPos.y + fftHeight + 10 - ((latestFFT[i - 1] - fftMin) * scaleFactor);
float bPos = widgetPos.y + fftHeight + 10 - ((latestFFT[i] - fftMin) * scaleFactor);
if (aPos < fftMin && bPos < fftMin) {
continue;
}
aPos = std::clamp<float>(aPos, widgetPos.y + 10, widgetPos.y + fftHeight + 10);
bPos = std::clamp<float>(bPos, widgetPos.y + 10, widgetPos.y + fftHeight + 10);
window->DrawList->AddLine(ImVec2(widgetPos.x + 49 + i, roundf(aPos)),
ImVec2(widgetPos.x + 50 + i, roundf(bPos)), trace, 1.0f);
window->DrawList->AddLine(ImVec2(widgetPos.x + 50 + i, roundf(bPos)),
ImVec2(widgetPos.x + 50 + i, widgetPos.y + fftHeight + 10), shadow, 1.0f);
}
// X Axis
window->DrawList->AddLine(ImVec2(widgetPos.x + 50, widgetPos.y + fftHeight + 10),
ImVec2(widgetPos.x + dataWidth + 50, widgetPos.y + fftHeight + 10),
IM_COL32(255, 255, 255, 255), 1.0f);
// Y Axis
window->DrawList->AddLine(ImVec2(widgetPos.x + 50, widgetPos.y + 9),
ImVec2(widgetPos.x + 50, widgetPos.y + fftHeight + 9),
IM_COL32(255, 255, 255, 255), 1.0f);
}
void WaterFall::drawWaterfall() {
if (waterfallUpdate) {
waterfallUpdate = false;
updateWaterfallTexture();
}
window->DrawList->AddImage((void*)(intptr_t)textureId, ImVec2(widgetPos.x + 50, widgetPos.y + fftHeight + 51),
ImVec2(widgetPos.x + 50 + dataWidth, widgetPos.y + fftHeight + 51 + waterfallHeight));
}
void WaterFall::drawVFOs() {
for (auto const& [name, vfo] : vfos) {
if (vfo->redrawRequired) {
vfo->redrawRequired = false;
vfo->updateDrawingVars(viewBandwidth, dataWidth, viewOffset, widgetPos, fftHeight);
}
vfo->draw(window, name == selectedVFO);
}
}
void WaterFall::selectFirstVFO() {
for (auto const& [name, vfo] : vfos) {
selectedVFO = name;
return;
}
}
void WaterFall::processInputs() {
WaterfallVFO* vfo = vfos[selectedVFO];
ImVec2 mousePos = ImGui::GetMousePos();
ImVec2 drag = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left);
ImVec2 dragOrigin(mousePos.x - drag.x, mousePos.y - drag.y);
bool mouseHovered, mouseHeld;
bool mouseClicked = ImGui::ButtonBehavior(ImRect(fftAreaMin, fftAreaMax), ImGuiID("WaterfallID"), &mouseHovered, &mouseHeld,
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_PressedOnClick);
bool draging = ImGui::IsMouseDragging(ImGuiMouseButton_Left) && ImGui::IsWindowFocused();
bool mouseInFreq = IS_IN_AREA(dragOrigin, freqAreaMin, freqAreaMax);
bool mouseInFFT = IS_IN_AREA(dragOrigin, fftAreaMin, fftAreaMax);
// If mouse was clicked on a VFO, select VFO and return
// If mouse was clicked but not on a VFO, move selected VFO to position
if (mouseClicked) {
for (auto const& [name, _vfo] : vfos) {
if (name == selectedVFO) {
continue;
}
if (IS_IN_AREA(mousePos, _vfo->rectMin, _vfo->rectMax)) {
selectedVFO = name;
selectedVFOChanged = true;
return;
}
}
int refCenter = mousePos.x - (widgetPos.x + 50);
if (refCenter >= 0 && refCenter < dataWidth && mousePos.y > widgetPos.y && mousePos.y < (widgetPos.y + widgetSize.y)) {
vfo->setOffset(((((float)refCenter / ((float)dataWidth / 2.0f)) - 1.0f) * (viewBandwidth / 2.0f)) + viewOffset);
}
}
// Draging VFO
if (draging && mouseInFFT) {
int refCenter = mousePos.x - (widgetPos.x + 50);
if (refCenter >= 0 && refCenter < dataWidth && mousePos.y > widgetPos.y && mousePos.y < (widgetPos.y + widgetSize.y)) {
vfo->setOffset(((((float)refCenter / ((float)dataWidth / 2.0f)) - 1.0f) * (viewBandwidth / 2.0f)) + viewOffset);
}
}
// Dragon frequency scale
if (draging && mouseInFreq) {
float deltax = drag.x - lastDrag;
lastDrag = drag.x;
float viewDelta = deltax * (viewBandwidth / (float)dataWidth);
viewOffset -= viewDelta;
if (viewOffset + (viewBandwidth / 2.0f) > wholeBandwidth / 2.0f) {
float freqOffset = (viewOffset + (viewBandwidth / 2.0f)) - (wholeBandwidth / 2.0f);
viewOffset = (wholeBandwidth / 2.0f) - (viewBandwidth / 2.0f);
centerFreq += freqOffset;
centerFreqMoved = true;
}
if (viewOffset - (viewBandwidth / 2.0f) < -(wholeBandwidth / 2.0f)) {
float freqOffset = (viewOffset - (viewBandwidth / 2.0f)) + (wholeBandwidth / 2.0f);
viewOffset = (viewBandwidth / 2.0f) - (wholeBandwidth / 2.0f);
centerFreq += freqOffset;
centerFreqMoved = true;
}
lowerFreq = (centerFreq + viewOffset) - (viewBandwidth / 2.0f);
upperFreq = (centerFreq + viewOffset) + (viewBandwidth / 2.0f);
updateWaterfallFb();
}
else {
lastDrag = 0;
}
}
void WaterFall::updateWaterfallFb() {
if (!waterfallVisible) {
return;
}
float offsetRatio = viewOffset / (wholeBandwidth / 2.0f);
int drawDataSize;
int drawDataStart;
int count = std::min<int>(waterfallHeight, rawFFTs.size());
float* tempData = new float[dataWidth];
float pixel;
float dataRange = waterfallMax - waterfallMin;
int size;
for (int i = 0; i < count; i++) {
size = rawFFTs[i].size();
drawDataSize = (viewBandwidth / wholeBandwidth) * size;
drawDataStart = (((float)size / 2.0f) * (offsetRatio + 1)) - (drawDataSize / 2);
doZoom(drawDataStart, drawDataSize, dataWidth, rawFFTs[i], tempData);
for (int j = 0; j < dataWidth; j++) {
pixel = (std::clamp<float>(tempData[j], waterfallMin, waterfallMax) - waterfallMin) / dataRange;
waterfallFb[(i * dataWidth) + j] = waterfallPallet[(int)(pixel * (WATERFALL_RESOLUTION - 1))];
}
}
delete[] tempData;
waterfallUpdate = true;
}
void WaterFall::drawBandPlan() {
int count = bandplan->bands.size();
float horizScale = (float)dataWidth / viewBandwidth;
float start, end, center, aPos, bPos, cPos, width;
ImVec2 txtSz;
bool startVis, endVis;
uint32_t color, colorTrans;
for (int i = 0; i < count; i++) {
start = bandplan->bands[i].start;
end = bandplan->bands[i].end;
if (start < lowerFreq && end < lowerFreq) {
continue;
}
if (start > upperFreq && end > upperFreq) {
continue;
}
startVis = (start > lowerFreq);
endVis = (end < upperFreq);
start = std::clamp<float>(start, lowerFreq, upperFreq);
end = std::clamp<float>(end, lowerFreq, upperFreq);
center = (start + end) / 2.0f;
aPos = widgetPos.x + 50 + ((start - lowerFreq) * horizScale);
bPos = widgetPos.x + 50 + ((end - lowerFreq) * horizScale);
cPos = widgetPos.x + 50 + ((center - lowerFreq) * horizScale);
width = bPos - aPos;
txtSz = ImGui::CalcTextSize(bandplan->bands[i].name.c_str());
if (bandplan::colorTable.find(bandplan->bands[i].type.c_str()) != bandplan::colorTable.end()) {
color = bandplan::colorTable[bandplan->bands[i].type].colorValue;
colorTrans = bandplan::colorTable[bandplan->bands[i].type].transColorValue;
}
else {
color = IM_COL32(255, 255, 255, 255);
colorTrans = IM_COL32(255, 255, 255, 100);
}
if (aPos <= widgetPos.x + 50) {
aPos = widgetPos.x + 51;
}
if (bPos <= widgetPos.x + 50) {
bPos = widgetPos.x + 51;
}
if (width >= 1.0f) {
window->DrawList->AddRectFilled(ImVec2(roundf(aPos), widgetPos.y + fftHeight - 25),
ImVec2(roundf(bPos), widgetPos.y + fftHeight + 10), colorTrans);
if (startVis) {
window->DrawList->AddLine(ImVec2(roundf(aPos), widgetPos.y + fftHeight - 26),
ImVec2(roundf(aPos), widgetPos.y + fftHeight + 9), color);
}
if (endVis) {
window->DrawList->AddLine(ImVec2(roundf(bPos), widgetPos.y + fftHeight - 26),
ImVec2(roundf(bPos), widgetPos.y + fftHeight + 9), color);
}
}
if (txtSz.x <= width) {
window->DrawList->AddText(ImVec2(cPos - (txtSz.x / 2.0f), widgetPos.y + fftHeight - 17),
IM_COL32(255, 255, 255, 255), bandplan->bands[i].name.c_str());
}
}
}
void WaterFall::updateWaterfallTexture() {
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, dataWidth, waterfallHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, (uint8_t*)waterfallFb);
}
void WaterFall::onPositionChange() {
// Nothing to see here...
}
void WaterFall::onResize() {
// return if widget is too small
if (widgetSize.x < 100 || widgetSize.y < 100) {
return;
}
if (waterfallVisible) {
FFTAreaHeight = std::min<int>(FFTAreaHeight, widgetSize.y - 50);
fftHeight = FFTAreaHeight - 50;
waterfallHeight = widgetSize.y - fftHeight - 52;
}
else {
fftHeight = widgetSize.y - 50;
}
dataWidth = widgetSize.x - 60.0f;
delete[] latestFFT;
if (waterfallVisible) {
delete[] waterfallFb;
}
latestFFT = new float[dataWidth];
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
}
fftAreaMin = ImVec2(widgetPos.x + 50, widgetPos.y + 9);
fftAreaMax = ImVec2(widgetPos.x + dataWidth + 50, widgetPos.y + fftHeight + 10);
freqAreaMin = ImVec2(widgetPos.x + 50, widgetPos.y + fftHeight + 11);
freqAreaMax = ImVec2(widgetPos.x + dataWidth + 50, widgetPos.y + fftHeight + 50);
maxHSteps = dataWidth / (ImGui::CalcTextSize("000.000").x + 10);
maxVSteps = fftHeight / (ImGui::CalcTextSize("000.000").y);
range = findBestRange(viewBandwidth, maxHSteps);
vRange = findBestRange(fftMax - fftMin, maxVSteps);
vRange = 10.0f;
updateWaterfallFb();
updateAllVFOs();
}
void WaterFall::draw() {
buf_mtx.lock();
window = GetCurrentWindow();
widgetPos = ImGui::GetWindowContentRegionMin();
widgetEndPos = ImGui::GetWindowContentRegionMax();
widgetPos.x += window->Pos.x;
widgetPos.y += window->Pos.y;
widgetEndPos.x += window->Pos.x - 4; // Padding
widgetEndPos.y += window->Pos.y;
widgetSize = ImVec2(widgetEndPos.x - widgetPos.x, widgetEndPos.y - widgetPos.y);
if (widgetPos.x != lastWidgetPos.x || widgetPos.y != lastWidgetPos.y) {
lastWidgetPos = widgetPos;
onPositionChange();
}
if (widgetSize.x != lastWidgetSize.x || widgetSize.y != lastWidgetSize.y) {
lastWidgetSize = widgetSize;
onResize();
}
window->DrawList->AddRectFilled(widgetPos, widgetEndPos, IM_COL32( 0, 0, 0, 255 ));
window->DrawList->AddRect(widgetPos, widgetEndPos, IM_COL32( 50, 50, 50, 255 ));
window->DrawList->AddLine(ImVec2(widgetPos.x, widgetPos.y + fftHeight + 50), ImVec2(widgetPos.x + widgetSize.x, widgetPos.y + fftHeight + 50), IM_COL32(50, 50, 50, 255), 1.0f);
processInputs();
drawFFT();
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<float>(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();
}
void WaterFall::pushFFT(std::vector<float> data, int n) {
buf_mtx.lock();
float offsetRatio = viewOffset / (wholeBandwidth / 2.0f);
int drawDataSize = (viewBandwidth / wholeBandwidth) * data.size();
int drawDataStart = (((float)data.size() / 2.0f) * (offsetRatio + 1)) - (drawDataSize / 2);
doZoom(drawDataStart, drawDataSize, dataWidth, data, latestFFT);
rawFFTs.insert(rawFFTs.begin(), data);
if (rawFFTs.size() > waterfallHeight + 300) {
rawFFTs.resize(waterfallHeight);
}
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<float>(latestFFT[j], waterfallMin, waterfallMax) - waterfallMin) / dataRange;
int id = (int)(pixel * (WATERFALL_RESOLUTION - 1));
waterfallFb[j] = waterfallPallet[id];
}
waterfallUpdate = true;
}
buf_mtx.unlock();
}
void WaterFall::updatePallette(float colors[][3], int colorCount) {
for (int i = 0; i < WATERFALL_RESOLUTION; i++) {
int lowerId = floorf(((float)i / (float)WATERFALL_RESOLUTION) * colorCount);
int upperId = ceilf(((float)i / (float)WATERFALL_RESOLUTION) * colorCount);
lowerId = std::clamp<int>(lowerId, 0, colorCount - 1);
upperId = std::clamp<int>(upperId, 0, colorCount - 1);
float ratio = (((float)i / (float)WATERFALL_RESOLUTION) * colorCount) - lowerId;
float r = (colors[lowerId][0] * (1.0f - ratio)) + (colors[upperId][0] * (ratio));
float g = (colors[lowerId][1] * (1.0f - ratio)) + (colors[upperId][1] * (ratio));
float b = (colors[lowerId][2] * (1.0f - ratio)) + (colors[upperId][2] * (ratio));
waterfallPallet[i] = ((uint32_t)255 << 24) | ((uint32_t)b << 16) | ((uint32_t)g << 8) | (uint32_t)r;
}
}
void WaterFall::autoRange() {
float min = INFINITY;
float max = -INFINITY;
for (int i = 0; i < dataWidth; i++) {
if (latestFFT[i] < min) {
min = latestFFT[i];
}
if (latestFFT[i] > max) {
max = latestFFT[i];
}
}
fftMin = min - 5;
fftMax = max + 5;
}
void WaterFall::setCenterFrequency(float freq) {
centerFreq = freq;
lowerFreq = (centerFreq + viewOffset) - (viewBandwidth / 2.0f);
upperFreq = (centerFreq + viewOffset) + (viewBandwidth / 2.0f);
updateAllVFOs();
}
float WaterFall::getCenterFrequency() {
return centerFreq;
}
void WaterFall::setBandwidth(float bandWidth) {
float currentRatio = viewBandwidth / wholeBandwidth;
wholeBandwidth = bandWidth;
setViewBandwidth(bandWidth * currentRatio);
updateAllVFOs();
}
float WaterFall::getBandwidth() {
return wholeBandwidth;
}
void WaterFall::setViewBandwidth(float bandWidth) {
if (bandWidth == viewBandwidth) {
return;
}
if (abs(viewOffset) + (bandWidth / 2.0f) > wholeBandwidth / 2.0f) {
if (viewOffset < 0) {
viewOffset = (bandWidth / 2.0f) - (wholeBandwidth / 2.0f);
}
else {
viewOffset = (wholeBandwidth / 2.0f) - (bandWidth / 2.0f);
}
}
viewBandwidth = bandWidth;
lowerFreq = (centerFreq + viewOffset) - (viewBandwidth / 2.0f);
upperFreq = (centerFreq + viewOffset) + (viewBandwidth / 2.0f);
range = findBestRange(bandWidth, maxHSteps);
updateWaterfallFb();
updateAllVFOs();
}
float WaterFall::getViewBandwidth() {
return viewBandwidth;
}
void WaterFall::setViewOffset(float offset) {
if (offset == viewOffset) {
return;
}
if (offset - (viewBandwidth / 2.0f) < -(wholeBandwidth / 2.0f)) {
offset = (viewBandwidth / 2.0f) - (wholeBandwidth / 2.0f);
}
if (offset + (viewBandwidth / 2.0f) > (wholeBandwidth / 2.0f)) {
offset = (wholeBandwidth / 2.0f) - (viewBandwidth / 2.0f);
}
viewOffset = offset;
lowerFreq = (centerFreq + viewOffset) - (viewBandwidth / 2.0f);
upperFreq = (centerFreq + viewOffset) + (viewBandwidth / 2.0f);
updateWaterfallFb();
updateAllVFOs();
}
float WaterFall::getViewOffset() {
return viewOffset;
}
void WaterFall::setFFTMin(float min) {
fftMin = min;
vRange = findBestRange(fftMax - fftMin, maxVSteps);
}
float WaterFall::getFFTMin() {
return fftMin;
}
void WaterFall::setFFTMax(float max) {
fftMax = max;
vRange = findBestRange(fftMax - fftMin, maxVSteps);
}
float WaterFall::getFFTMax() {
return fftMax;
}
void WaterFall::setWaterfallMin(float min) {
if (min == waterfallMin) {
return;
}
waterfallMin = min;
updateWaterfallFb();
}
float WaterFall::getWaterfallMin() {
return waterfallMin;
}
void WaterFall::setWaterfallMax(float max) {
if (max == waterfallMax) {
return;
}
waterfallMax = max;
updateWaterfallFb();
}
float WaterFall::getWaterfallMax() {
return waterfallMax;
}
void WaterFall::updateAllVFOs() {
for (auto const& [name, vfo] : vfos) {
vfo->updateDrawingVars(viewBandwidth, dataWidth, viewOffset, widgetPos, fftHeight);
}
}
void WaterfallVFO::setOffset(float offset) {
printf("WaterfallVFO::SetOffset: %p\n", this);
generalOffset = offset;
if (reference == REF_CENTER) {
centerOffset = offset;
lowerOffset = offset - (bandwidth / 2.0f);
upperOffset = offset + (bandwidth / 2.0f);
}
else if (reference == REF_LOWER) {
lowerOffset = offset;
centerOffset = offset + (bandwidth / 2.0f);
upperOffset = offset + bandwidth;
}
else if (reference == REF_UPPER) {
upperOffset = offset;
centerOffset = offset - (bandwidth / 2.0f);
lowerOffset = offset - bandwidth;
}
centerOffsetChanged = true;
upperOffsetChanged = true;
lowerOffsetChanged = true;
redrawRequired = true;
}
void WaterfallVFO::setCenterOffset(float offset) {
if (reference == REF_CENTER) {
generalOffset = offset;
}
else if (reference == REF_LOWER) {
generalOffset = offset - (bandwidth / 2.0f);
}
else if (reference == REF_UPPER) {
generalOffset = offset + (bandwidth / 2.0f);
}
centerOffset = offset;
lowerOffset = offset - (bandwidth / 2.0f);
upperOffset = offset + (bandwidth / 2.0f);
centerOffsetChanged = true;
upperOffsetChanged = true;
lowerOffsetChanged = true;
redrawRequired = true;
}
void WaterfallVFO::setBandwidth(float bw) {
if (bandwidth == bw || bw < 0) {
return;
}
bandwidth = bw;
if (reference == REF_CENTER) {
lowerOffset = centerOffset - (bandwidth / 2.0f);
upperOffset = centerOffset + (bandwidth / 2.0f);
}
else if (reference == REF_LOWER) {
centerOffset = lowerOffset + (bandwidth / 2.0f);
upperOffset = lowerOffset + bandwidth;
centerOffsetChanged = true;
}
else if (reference == REF_UPPER) {
centerOffset = upperOffset - (bandwidth / 2.0f);
lowerOffset = upperOffset - bandwidth;
centerOffsetChanged = true;
}
redrawRequired = true;
}
void WaterfallVFO::setReference(int ref) {
if (reference == ref || ref < 0 || ref >= _REF_COUNT) {
return;
}
reference = ref;
setOffset(generalOffset);
}
void WaterfallVFO::updateDrawingVars(float viewBandwidth, float dataWidth, float viewOffset, ImVec2 widgetPos, int fftHeight) {
float width = (bandwidth / viewBandwidth) * (float)dataWidth;
int center = roundf((((centerOffset - viewOffset) / (viewBandwidth / 2.0f)) + 1.0f) * ((float)dataWidth / 2.0f));
int left = roundf((((lowerOffset - viewOffset) / (viewBandwidth / 2.0f)) + 1.0f) * ((float)dataWidth / 2.0f));
int right = roundf((((upperOffset - viewOffset) / (viewBandwidth / 2.0f)) + 1.0f) * ((float)dataWidth / 2.0f));
if (left >= 0 && left < dataWidth && reference == REF_LOWER) {
lineMin = ImVec2(widgetPos.x + 50 + left, widgetPos.y + 9);
lineMax = ImVec2(widgetPos.x + 50 + left, widgetPos.y + fftHeight + 9);
lineVisible = true;
}
else if (center >= 0 && center < dataWidth && reference == REF_CENTER) {
lineMin = ImVec2(widgetPos.x + 50 + center, widgetPos.y + 9);
lineMax = ImVec2(widgetPos.x + 50 + center, widgetPos.y + fftHeight + 9);
lineVisible = true;
}
else if (right >= 0 && right < dataWidth && reference == REF_UPPER) {
lineMin = ImVec2(widgetPos.x + 50 + right, widgetPos.y + 9);
lineMax = ImVec2(widgetPos.x + 50 + right, widgetPos.y + fftHeight + 9);
lineVisible = true;
}
else {
lineVisible = false;
}
left = std::clamp<int>(left, 0, dataWidth - 1);
right = std::clamp<int>(right, 0, dataWidth - 1);
rectMin = ImVec2(widgetPos.x + 50 + left, widgetPos.y + 10);
rectMax = ImVec2(widgetPos.x + 51 + right, widgetPos.y + fftHeight + 10);
}
void WaterfallVFO::draw(ImGuiWindow* window, bool selected) {
window->DrawList->AddRectFilled(rectMin, rectMax, IM_COL32(255, 255, 255, 50));
if (lineVisible) {
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;
newFFTAreaHeight = height;
onResize();
}
int WaterFall::getFFTHeight() {
return FFTAreaHeight;
}
};

191
core/src/gui/waterfall.h Normal file
View File

@ -0,0 +1,191 @@
#pragma once
#include <imgui.h>
#include <imgui_internal.h>
#include <vector>
#include <mutex>
#include <GL/glew.h>
#include <imutils.h>
#include <gui/bandplan.h>
#include <algorithm>
#define WATERFALL_RESOLUTION 1000000
namespace ImGui {
class WaterfallVFO {
public:
void setOffset(float offset);
void setCenterOffset(float offset);
void setBandwidth(float bw);
void setReference(int ref);
void updateDrawingVars(float viewBandwidth, float dataWidth, float viewOffset, ImVec2 widgetPos, int fftHeight);
void draw(ImGuiWindow* window, bool selected);
enum {
REF_LOWER,
REF_CENTER,
REF_UPPER,
_REF_COUNT
};
float generalOffset;
float centerOffset;
float lowerOffset;
float upperOffset;
float bandwidth;
float snapInterval;
int reference = REF_CENTER;
ImVec2 rectMin;
ImVec2 rectMax;
ImVec2 lineMin;
ImVec2 lineMax;
bool centerOffsetChanged = false;
bool lowerOffsetChanged = false;
bool upperOffsetChanged = false;
bool redrawRequired = true;
bool lineVisible = true;
};
class WaterFall {
public:
WaterFall();
void draw();
void pushFFT(std::vector<float> data, int n);
void updatePallette(float colors[][3], int colorCount);
void setCenterFrequency(float freq);
float getCenterFrequency();
void setBandwidth(float bandWidth);
float getBandwidth();
void setViewBandwidth(float bandWidth);
float getViewBandwidth();
void setViewOffset(float offset);
float getViewOffset();
void setFFTMin(float min);
float getFFTMin();
void setFFTMax(float max);
float getFFTMax();
void setWaterfallMin(float min);
float getWaterfallMin();
void setWaterfallMax(float max);
float getWaterfallMax();
void setZoom(float zoomLevel);
void setOffset(float zoomOffset);
void autoRange();
void selectFirstVFO();
void showWaterfall();
void hideWaterfall();
void setFFTHeight(int height);
int getFFTHeight();
bool centerFreqMoved = false;
bool vfoFreqChanged = false;
bool bandplanEnabled = false;
bandplan::BandPlan_t* bandplan = NULL;
std::map<std::string, WaterfallVFO*> vfos;
std::string selectedVFO;
bool selectedVFOChanged = false;
enum {
REF_LOWER,
REF_CENTER,
REF_UPPER,
_REF_COUNT
};
private:
void drawWaterfall();
void drawFFT();
void drawVFOs();
void drawBandPlan();
void processInputs();
void onPositionChange();
void onResize();
void updateWaterfallFb();
void updateWaterfallTexture();
void updateAllVFOs();
bool waterfallUpdate = false;
uint32_t waterfallPallet[WATERFALL_RESOLUTION];
ImVec2 widgetPos;
ImVec2 widgetEndPos;
ImVec2 widgetSize;
ImVec2 lastWidgetPos;
ImVec2 lastWidgetSize;
ImVec2 fftAreaMin;
ImVec2 fftAreaMax;
ImVec2 freqAreaMin;
ImVec2 freqAreaMax;
ImVec2 waterfallAreaMin;
ImVec2 waterfallAreaMax;
ImGuiWindow* window;
GLuint textureId;
std::mutex buf_mtx;
float vRange;
int maxVSteps;
int maxHSteps;
int dataWidth; // Width of the FFT and waterfall
int fftHeight; // Height of the fft graph
int waterfallHeight; // Height of the waterfall
float viewBandwidth;
float viewOffset;
float lowerFreq;
float upperFreq;
float range;
float lastDrag;
int vfoRef = REF_CENTER;
// Absolute values
float centerFreq;
float wholeBandwidth;
// Ranges
float fftMin;
float fftMax;
float waterfallMin;
float waterfallMax;
std::vector<std::vector<float>> rawFFTs;
float* latestFFT;
uint32_t* waterfallFb;
bool draggingFW = false;
int FFTAreaHeight;
int newFFTAreaHeight;
bool waterfallVisible = true;
};
};