From eadaf3ce6be9687a691cc78f583e657addc8d140 Mon Sep 17 00:00:00 2001 From: Ryzerth Date: Sun, 16 Aug 2020 03:39:05 +0200 Subject: [PATCH] a LOT of new stuff --- CMakeLists.txt | 6 + config.json | 5 + modules/radio/src/main.cpp | 42 +- modules/radio/src/path.cpp | 24 +- modules/radio/src/path.h | 7 +- readme.md | 2 +- res/icons/logo.png | Bin 39771 -> 0 bytes res/icons/play.png | Bin 0 -> 2676 bytes res/icons/sdrpp.ico | Bin 0 -> 33891 bytes res/icons/sdrpp.png | Bin 0 -> 21113 bytes res/icons/stop.png | Bin 0 -> 1733 bytes src/audio.cpp | 259 +++- src/audio.h | 61 +- src/config.cpp | 53 + src/config.h | 20 + src/dsp/resampling.h | 54 +- src/dsp/routing.h | 151 +- src/dsp/types.h | 5 + src/icons.cpp | 4 +- src/imgui/imgui_widgets.cpp | 1 + src/imgui/stb_image_resize.h | 2631 ++++++++++++++++++++++++++++++++++ src/io/audio.h | 233 ++- src/io/soapy.h | 15 +- src/main.cpp | 49 +- src/main_window.cpp | 461 +++++- src/main_window.h | 3 + src/module.cpp | 17 + src/module.h | 12 + src/signal_path.h | 2 +- src/style.h | 9 + src/styles.cpp | 28 + src/styles.h | 16 - src/waterfall.cpp | 20 +- win32/resources.rc | 1 + 34 files changed, 3988 insertions(+), 203 deletions(-) create mode 100644 config.json delete mode 100644 res/icons/logo.png create mode 100644 res/icons/play.png create mode 100644 res/icons/sdrpp.ico create mode 100644 res/icons/sdrpp.png create mode 100644 res/icons/stop.png create mode 100644 src/config.cpp create mode 100644 src/config.h create mode 100644 src/imgui/stb_image_resize.h create mode 100644 src/style.h create mode 100644 src/styles.cpp delete mode 100644 src/styles.h create mode 100644 win32/resources.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index ff30aec..635dbbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,12 @@ include_directories(sdrpp "src/") include_directories(sdrpp "src/imgui") file(GLOB SRC "src/*.cpp") file(GLOB IMGUI "src/imgui/*.cpp") + +# If on windows, set the executable icon +if (MSVC) + set(SRC ${SRC} "win32/resources.rc") +endif (MSVC) + add_executable(sdrpp ${SRC} ${IMGUI}) if (MSVC) diff --git a/config.json b/config.json new file mode 100644 index 0000000..cdec843 --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "frequency": 90500000, + "source": "", + "sourceSettings": {} +} \ No newline at end of file diff --git a/modules/radio/src/main.cpp b/modules/radio/src/main.cpp index 847938c..ee97cbd 100644 --- a/modules/radio/src/main.cpp +++ b/modules/radio/src/main.cpp @@ -11,8 +11,8 @@ struct RadioContext_t { std::string name; int demod = 1; SigPath sigPath; - watcher volume; - watcher audioDevice; + // watcher volume; + // watcher audioDevice; }; MOD_EXPORT void* _INIT_(mod::API_t* _API, ImGuiContext* imctx, std::string _name) { @@ -21,24 +21,24 @@ 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 + // 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(); - } + // 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) { @@ -85,9 +85,9 @@ 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::PushItemWidth(ImGui::GetWindowSize().x); + // ImGui::Combo(CONCAT("##_audio_dev_", ctx->name), &ctx->audioDevice.val, ctx->sigPath.audio.devTxtList.c_str()); + // ImGui::PopItemWidth(); } MOD_EXPORT void _HANDLE_EVENT_(RadioContext_t* ctx, int eventId) { @@ -96,9 +96,9 @@ MOD_EXPORT void _HANDLE_EVENT_(RadioContext_t* ctx, int eventId) { ctx->sigPath.updateBlockSize(); } else if (eventId == mod::EVENT_SELECTED_VFO_CHANGED) { - if (API->getSelectedVFOName() == ctx->name) { - API->bindVolumeVariable(&ctx->volume.val); - } + // if (API->getSelectedVFOName() == ctx->name) { + // API->bindVolumeVariable(&ctx->volume.val); + // } } } diff --git a/modules/radio/src/path.cpp b/modules/radio/src/path.cpp index 5fffa4e..7efa4fd 100644 --- a/modules/radio/src/path.cpp +++ b/modules/radio/src/path.cpp @@ -4,6 +4,14 @@ SigPath::SigPath() { } +int SigPath::sampleRateChangeHandler(void* ctx, float sampleRate) { + SigPath* _this = (SigPath*)ctx; + _this->audioResamp.stop(); + _this->audioResamp.setOutputSampleRate(sampleRate, sampleRate / 2.0f, sampleRate / 2.0f); + _this->audioResamp.start(); + return _this->audioResamp.getOutputBlockSize(); +} + void SigPath::init(std::string vfoName, uint64_t sampleRate, int blockSize, dsp::stream* input) { this->sampleRate = sampleRate; this->blockSize = blockSize; @@ -18,7 +26,8 @@ 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); - audio.init(&audioResamp.output, 64); + API->registerMonoStream(&audioResamp.output, vfoName, vfoName, sampleRateChangeHandler, this); + API->setBlockSize(vfoName, audioResamp.getOutputBlockSize()); } void SigPath::setSampleRate(float sampleRate) { @@ -28,10 +37,6 @@ void SigPath::setSampleRate(float sampleRate) { setDemodulator(_demod); } -void SigPath::setVolume(float volume) { - audio.setVolume(volume); -} - void SigPath::setDemodulator(int demId) { if (demId < 0 || demId >= _DEMOD_COUNT) { return; @@ -64,7 +69,7 @@ void SigPath::setDemodulator(int demId) { demod.setSampleRate(200000); demod.setDeviation(100000); audioResamp.setInput(&demod.output); - audioResamp.setInputSampleRate(200000, API->getVFOOutputBlockSize(vfoName)); + audioResamp.setInputSampleRate(200000, API->getVFOOutputBlockSize(vfoName), 15000, 15000); demod.start(); } if (demId == DEMOD_NFM) { @@ -110,10 +115,5 @@ void SigPath::updateBlockSize() { void SigPath::start() { demod.start(); audioResamp.start(); - audio.start(); -} - -void SigPath::DEBUG_TEST() { - audio.stop(); - audio.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 0d83762..164c436 100644 --- a/modules/radio/src/path.h +++ b/modules/radio/src/path.h @@ -19,16 +19,11 @@ public: void setSampleRate(float sampleRate); void setVFOFrequency(long frequency); - void setVolume(float volume); void updateBlockSize(); void setDemodulator(int demod); - void DEBUG_TEST(); - - io::AudioSink audio; - enum { DEMOD_FM, DEMOD_NFM, @@ -39,6 +34,8 @@ public: }; private: + static int sampleRateChangeHandler(void* ctx, float sampleRate); + dsp::stream input; // Demodulators diff --git a/readme.md b/readme.md index d4f5cef..f89f024 100644 --- a/readme.md +++ b/readme.md @@ -71,7 +71,7 @@ I will soon publish a contributing.md listing the code style to use. # Credits ## Libaries used * [SoapySDR (PothosWare)](https://github.com/pothosware/SoapySDR) -* [ImGui (ocornut)](https://github.com/ocornut/imgui) +* [Dear ImGui (ocornut)](https://github.com/ocornut/imgui) * [spdlog (gabime)](https://github.com/gabime/spdlog) * [json (nlohmann)](https://github.com/nlohmann/json) * [portaudio (PortAudio community)](http://www.portaudio.com/) diff --git a/res/icons/logo.png b/res/icons/logo.png deleted file mode 100644 index 6b4ec18aa0abeab6f93dc5aaea7da53e32f53a4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39771 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 XtI<(`?WCPCfJ0ecU9Mc_{ipu{K{O=K diff --git a/res/icons/play.png b/res/icons/play.png new file mode 100644 index 0000000000000000000000000000000000000000..93684dd27ba610dd5c696e7c813c087b4760bbd1 GIT binary patch literal 2676 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 D`>Ge= literal 0 HcmV?d00001 diff --git a/res/icons/sdrpp.png b/res/icons/sdrpp.png new file mode 100644 index 0000000000000000000000000000000000000000..03cf61838dddb018ecb2e3cb51ece291b80101a1 GIT binary patch literal 21113 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 literal 0 HcmV?d00001 diff --git a/res/icons/stop.png b/res/icons/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..366651bfe3b40d24bf528215c918febd65552bb9 GIT binary patch literal 1733 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe+UdOTemLn`LHy|z*Ckb}Sh zhk1o8f2Vf^UuIBzb}VB%Q|qp~K(!+V{7CR+|4@6paL+R7hV1BU+vU<0*Ue-PWXfG; zJdpbKGUJ^0cCri%f95eVG$=4LFgOAOvWbU*fkl#mfy0P_K_G>JLE!`ggTsVT1*73G unjS{;1tsOff?TVH%Wp0-j*!y#!})uKFE~_KzJ+W9`PkFd&t;ucLK6U2*S@v@ literal 0 HcmV?d00001 diff --git a/src/audio.cpp b/src/audio.cpp index f743247..f7727ab 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -1 +1,258 @@ -#include \ No newline at end of file +#include + +namespace audio { + std::map streams; + + float registerMonoStream(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx) { + AudioStream_t* astr = new AudioStream_t; + astr->type = STREAM_TYPE_MONO; + astr->ctx = ctx; + astr->audio = new io::AudioSink; + astr->audio->init(1); + astr->deviceId = astr->audio->getDeviceId(); + float sampleRate = astr->audio->devices[astr->deviceId].sampleRates[0]; + int blockSize = sampleRate / 200; // default block size + astr->monoAudioStream = new dsp::stream(blockSize * 2); + astr->audio->setBlockSize(blockSize); + astr->audio->setStreamType(io::AudioSink::MONO); + astr->audio->setMonoInput(astr->monoAudioStream); + astr->audio->setSampleRate(sampleRate); + astr->blockSize = blockSize; + astr->sampleRate = sampleRate; + astr->monoStream = stream; + astr->sampleRateChangeHandler = sampleRateChangeHandler; + astr->monoDynSplit = new dsp::DynamicSplitter(stream, blockSize); + astr->monoDynSplit->bind(astr->monoAudioStream); + astr->running = false; + astr->volume = 1.0f; + astr->sampleRateId = 0; + astr->vfoName = vfoName; + streams[name] = astr; + return sampleRate; + } + + float registerStereoStream(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx) { + AudioStream_t* astr = new AudioStream_t; + astr->type = STREAM_TYPE_STEREO; + astr->ctx = ctx; + astr->audio = new io::AudioSink; + astr->audio->init(1); + float sampleRate = astr->audio->devices[astr->audio->getDeviceId()].sampleRates[0]; + int blockSize = sampleRate / 200; // default block size + astr->stereoAudioStream = new dsp::stream(blockSize * 2); + astr->audio->setBlockSize(blockSize); + astr->audio->setStreamType(io::AudioSink::STEREO); + astr->audio->setStereoInput(astr->stereoAudioStream); + astr->audio->setSampleRate(sampleRate); + astr->blockSize = blockSize; + astr->sampleRate = sampleRate; + astr->stereoStream = stream; + astr->sampleRateChangeHandler = sampleRateChangeHandler; + astr->stereoDynSplit = new dsp::DynamicSplitter(stream, blockSize); + astr->stereoDynSplit->bind(astr->stereoAudioStream); + astr->running = false; + streams[name] = astr; + astr->vfoName = vfoName; + return sampleRate; + } + + void startStream(std::string name) { + AudioStream_t* astr = streams[name]; + if (astr->running) { + return; + } + if (astr->type == STREAM_TYPE_MONO) { + astr->monoDynSplit->start(); + } + else { + astr->stereoDynSplit->start(); + } + astr->audio->start(); + astr->running = true; + } + + void stopStream(std::string name) { + AudioStream_t* astr = streams[name]; + if (!astr->running) { + return; + } + if (astr->type == STREAM_TYPE_MONO) { + astr->monoDynSplit->stop(); + } + else { + astr->stereoDynSplit->stop(); + } + astr->audio->stop(); + astr->running = false; + } + + void removeStream(std::string name) { + AudioStream_t* astr = streams[name]; + stopStream(name); + for (int i = 0; i < astr->boundStreams.size(); i++) { + astr->boundStreams[i].streamRemovedHandler(astr->ctx); + } + delete astr->monoDynSplit; + } + + dsp::stream* bindToStreamMono(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx) { + AudioStream_t* astr = streams[name]; + BoundStream_t bstr; + bstr.type = STREAM_TYPE_MONO; + bstr.ctx = ctx; + bstr.streamRemovedHandler = streamRemovedHandler; + bstr.sampleRateChangeHandler = sampleRateChangeHandler; + if (astr->type == STREAM_TYPE_MONO) { + bstr.monoStream = new dsp::stream(astr->blockSize * 2); + astr->monoDynSplit->bind(bstr.monoStream); + return bstr.monoStream; + } + bstr.stereoStream = new dsp::stream(astr->blockSize * 2); + bstr.s2m = new dsp::StereoToMono(bstr.stereoStream, astr->blockSize * 2); + bstr.monoStream = &bstr.s2m->output; + astr->stereoDynSplit->bind(bstr.stereoStream); + bstr.s2m->start(); + return bstr.monoStream; + } + + dsp::stream* bindToStreamStereo(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx) { + AudioStream_t* astr = streams[name]; + BoundStream_t bstr; + bstr.type = STREAM_TYPE_STEREO; + bstr.ctx = ctx; + bstr.streamRemovedHandler = streamRemovedHandler; + bstr.sampleRateChangeHandler = sampleRateChangeHandler; + if (astr->type == STREAM_TYPE_STEREO) { + bstr.stereoStream = new dsp::stream(astr->blockSize * 2); + astr->stereoDynSplit->bind(bstr.stereoStream); + return bstr.stereoStream; + } + bstr.monoStream = new dsp::stream(astr->blockSize * 2); + bstr.m2s = new dsp::MonoToStereo(bstr.monoStream, astr->blockSize * 2); + bstr.stereoStream = &bstr.m2s->output; + astr->monoDynSplit->bind(bstr.monoStream); + bstr.m2s->start(); + } + + void setBlockSize(std::string name, int blockSize) { + AudioStream_t* astr = streams[name]; + if (astr->running) { + return; + } + if (astr->type == STREAM_TYPE_MONO) { + astr->monoDynSplit->setBlockSize(blockSize); + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + bstr.monoStream->setMaxLatency(blockSize * 2); + if (bstr.type == STREAM_TYPE_STEREO) { + bstr.m2s->stop(); + bstr.m2s->setBlockSize(blockSize); + bstr.m2s->start(); + } + } + astr->blockSize = blockSize; + return; + } + astr->monoDynSplit->setBlockSize(blockSize); + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + bstr.stereoStream->setMaxLatency(blockSize * 2); + if (bstr.type == STREAM_TYPE_MONO) { + bstr.s2m->stop(); + bstr.s2m->setBlockSize(blockSize); + bstr.s2m->start(); + } + } + astr->blockSize = blockSize; + } + + void unbindFromStreamMono(std::string name, dsp::stream* stream) { + AudioStream_t* astr = streams[name]; + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + if (bstr.monoStream != stream) { + continue; + } + if (astr->type == STREAM_TYPE_STEREO) { + bstr.s2m->stop(); + delete bstr.s2m; + } + delete stream; + return; + } + } + + void unbindFromStreamStereo(std::string name, dsp::stream* stream) { + AudioStream_t* astr = streams[name]; + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + if (bstr.stereoStream != stream) { + continue; + } + if (astr->type == STREAM_TYPE_MONO) { + bstr.s2m->stop(); + delete bstr.m2s; + } + delete stream; + return; + } + } + + std::string getNameFromVFO(std::string vfoName) { + for (auto const& [name, stream] : streams) { + if (stream->vfoName == vfoName) { + return name; + } + } + return ""; + } + + void setSampleRate(std::string name, float sampleRate) { + AudioStream_t* astr = streams[name]; + if (astr->running) { + return; + } + int blockSize = astr->sampleRateChangeHandler(astr->ctx, sampleRate); + astr->audio->setSampleRate(sampleRate); + astr->audio->setBlockSize(blockSize); + if (astr->type == STREAM_TYPE_MONO) { + astr->monoDynSplit->setBlockSize(blockSize); + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + if (bstr.type == STREAM_TYPE_STEREO) { + bstr.m2s->stop(); + bstr.m2s->setBlockSize(blockSize); + bstr.sampleRateChangeHandler(bstr.ctx, sampleRate, blockSize); + bstr.m2s->start(); + continue; + } + bstr.sampleRateChangeHandler(bstr.ctx, sampleRate, blockSize); + } + } + else { + astr->stereoDynSplit->setBlockSize(blockSize); + for (int i = 0; i < astr->boundStreams.size(); i++) { + BoundStream_t bstr = astr->boundStreams[i]; + if (bstr.type == STREAM_TYPE_MONO) { + bstr.s2m->stop(); + bstr.s2m->setBlockSize(blockSize); + bstr.sampleRateChangeHandler(bstr.ctx, sampleRate, blockSize); + bstr.s2m->start(); + continue; + } + bstr.sampleRateChangeHandler(bstr.ctx, sampleRate, blockSize); + } + } + } + + void setAudioDevice(std::string name, int deviceId, float sampleRate) { + AudioStream_t* astr = streams[name]; + if (astr->running) { + return; + } + astr->deviceId = deviceId; + astr->audio->setDevice(deviceId); + setSampleRate(name, sampleRate); + } +}; + diff --git a/src/audio.h b/src/audio.h index 7f06369..c9de6c3 100644 --- a/src/audio.h +++ b/src/audio.h @@ -1,6 +1,63 @@ #pragma once #include +#include +#include +#include +#include namespace audio { - void registerStream(dsp::stream* stream, std::string name, std::string vfoName); -}; \ No newline at end of file + enum { + STREAM_TYPE_MONO, + STREAM_TYPE_STEREO, + _STREAM_TYPE_COUNT + }; + + struct BoundStream_t { + dsp::stream* monoStream; + dsp::stream* stereoStream; + dsp::StereoToMono* s2m; + dsp::MonoToStereo* m2s; + void (*streamRemovedHandler)(void* ctx); + void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize); + void* ctx; + int type; + }; + + struct AudioStream_t { + io::AudioSink* audio; + dsp::stream* monoAudioStream; + dsp::stream* stereoAudioStream; + std::vector boundStreams; + dsp::stream* monoStream; + dsp::DynamicSplitter* monoDynSplit; + dsp::stream* stereoStream; + dsp::DynamicSplitter* stereoDynSplit; + int (*sampleRateChangeHandler)(void* ctx, float sampleRate); + float sampleRate; + int blockSize; + int type; + bool running = false; + float volume; + int sampleRateId; + int deviceId; + void* ctx; + std::string vfoName; + }; + + extern std::map streams; + + float registerMonoStream(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx); + float registerStereoStream(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx); + void startStream(std::string name); + void stopStream(std::string name); + void removeStream(std::string name); + dsp::stream* bindToStreamMono(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx); + dsp::stream* bindToStreamStereo(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx); + void setBlockSize(std::string name, int blockSize); + void unbindFromStreamMono(std::string name, dsp::stream* stream); + void unbindFromStreamStereo(std::string name, dsp::stream* stream); + std::string getNameFromVFO(std::string vfoName); + void setSampleRate(std::string name, float sampleRate); + void setAudioDevice(std::string name, int deviceId, float sampleRate); +}; + diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..1766fe3 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,53 @@ +#include + +namespace config { + bool configModified = false; + json config; + bool autoSaveRunning = false; + std::string _path; + std::thread _workerThread; + + void _autoSaveWorker() { + while (autoSaveRunning) { + if (configModified) { + configModified = false; + std::ofstream file(_path.c_str()); + file << std::setw(4) << config; + file.close(); + spdlog::info("Config saved"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + } + + void load(std::string path) { + if (!std::filesystem::exists(path)) { + spdlog::error("Config file does not exist"); + return; + } + if (!std::filesystem::is_regular_file(path)) { + spdlog::error("Config file isn't a file..."); + return; + } + _path = path; + std::ifstream file(path.c_str()); + config << file; + file.close(); + } + + void startAutoSave() { + if (autoSaveRunning) { + return; + } + autoSaveRunning = true; + _workerThread = std::thread(_autoSaveWorker); + } + + void stopAutoSave() { + if (!autoSaveRunning) { + return; + } + autoSaveRunning = false; + _workerThread.join(); + } +}; \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..4dd3292 --- /dev/null +++ b/src/config.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +using nlohmann::json; + +namespace config { + void load(std::string path); + void startAutoSave(); + void stopAutoSave(); + + extern bool configModified; + extern json config; +}; \ No newline at end of file diff --git a/src/dsp/resampling.h b/src/dsp/resampling.h index e284e58..4997870 100644 --- a/src/dsp/resampling.h +++ b/src/dsp/resampling.h @@ -312,9 +312,17 @@ namespace dsp { int inCount = _this->_blockSize; int outCount = _this->outputBlockSize; - - float* taps = _this->_taps.data(); + + 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; + } + complex_t* delayBuf = new complex_t[tapCount]; complex_t* delayStart = &inBuf[std::max(inCount - tapCount, 0)]; @@ -322,11 +330,6 @@ namespace dsp { complex_t* delayBufEnd = &delayBuf[std::max(tapCount - inCount, 0)]; int moveSize = std::min(inCount, tapCount - inCount) * sizeof(complex_t); int inSize = inCount * sizeof(complex_t); - - int interp = _this->_interp; - int decim = _this->_decim; - - float correction = (float)sqrt((float)interp); int afterInterp = inCount * interp; int outIndex = 0; @@ -335,12 +338,9 @@ namespace dsp { for (int i = 0; outIndex < outCount; i += decim) { outBuf[outIndex].i = 0; outBuf[outIndex].q = 0; - for (int j = 0; j < tapCount; j++) { - if ((i - j) % interp != 0) { - continue; - } - outBuf[outIndex].i += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp).i * taps[j] * correction; - outBuf[outIndex].q += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp).q * taps[j] * correction; + for (int j = i % interp; j < tapCount; j += interp) { + outBuf[outIndex].i += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp).i * taps[j]; + outBuf[outIndex].q += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp).q * taps[j]; } outIndex++; } @@ -358,6 +358,7 @@ namespace dsp { delete[] inBuf; delete[] outBuf; delete[] delayBuf; + delete[] taps; } std::thread _workerThread; @@ -500,9 +501,17 @@ namespace dsp { int inCount = _this->_blockSize; int outCount = _this->outputBlockSize; + + int interp = _this->_interp; + int decim = _this->_decim; + float correction = interp;//(float)sqrt((float)interp); - float* taps = _this->_taps.data(); 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)]; @@ -510,26 +519,23 @@ namespace dsp { float* delayBufEnd = &delayBuf[std::max(tapCount - inCount, 0)]; int moveSize = std::min(inCount, tapCount - inCount) * sizeof(float); int inSize = inCount * sizeof(float); - - int interp = _this->_interp; - int decim = _this->_decim; - - float correction = (float)sqrt((float)interp); int afterInterp = inCount * interp; int outIndex = 0; while (true) { if (_this->_input->read(inBuf, inCount) < 0) { break; }; + + for (int i = 0; outIndex < outCount; i += decim) { outBuf[outIndex] = 0; - for (int j = 0; j < tapCount; j++) { - if ((i - j) % interp != 0) { - continue; - } - outBuf[outIndex] += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp) * taps[j] * correction; + for (int j = (i % interp); j < tapCount; j += interp) { + outBuf[outIndex] += GET_FROM_RIGHT_BUF(inBuf, delayBuf, tapCount, (i - j) / interp) * taps[j]; } outIndex++; } + + + outIndex = 0; if (tapCount > inCount) { memmove(delayBuf, delayBufEnd, moveSize); diff --git a/src/dsp/routing.h b/src/dsp/routing.h index 7555d00..b8be191 100644 --- a/src/dsp/routing.h +++ b/src/dsp/routing.h @@ -76,18 +76,19 @@ namespace dsp { bool running = false; }; + template class DynamicSplitter { public: DynamicSplitter() { } - DynamicSplitter(stream* input, int bufferSize) { + DynamicSplitter(stream* input, int bufferSize) { _in = input; _bufferSize = bufferSize; } - void init(stream* input, int bufferSize) { + void init(stream* input, int bufferSize) { _in = input; _bufferSize = bufferSize; } @@ -128,14 +129,14 @@ namespace dsp { } } - void bind(stream* stream) { + void bind(stream* stream) { if (running) { return; } outputs.push_back(stream); } - void unbind(stream* stream) { + void unbind(stream* stream) { if (running) { return; } @@ -150,7 +151,7 @@ namespace dsp { private: static void _worker(DynamicSplitter* _this) { - complex_t* buf = new complex_t[_this->_bufferSize]; + T* buf = new T[_this->_bufferSize]; int outputCount = _this->outputs.size(); while (true) { if (_this->_in->read(buf, _this->_bufferSize) < 0) { break; }; @@ -161,10 +162,146 @@ namespace dsp { delete[] buf; } - stream* _in; + stream* _in; + int _bufferSize; + std::thread _workerThread; + bool running = false; + std::vector*> outputs; + }; + + + class MonoToStereo { + public: + MonoToStereo() { + + } + + MonoToStereo(stream* input, int bufferSize) { + _in = input; + _bufferSize = bufferSize; + } + + void init(stream* input, int bufferSize) { + _in = input; + _bufferSize = bufferSize; + } + + 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); + } + + stream output; + + private: + static void _worker(MonoToStereo* _this) { + float* inBuf = new float[_this->_bufferSize]; + StereoFloat_t* outBuf = new StereoFloat_t[_this->_bufferSize]; + while (true) { + if (_this->_in->read(inBuf, _this->_bufferSize) < 0) { break; }; + for (int i = 0; i < _this->_bufferSize; i++) { + outBuf[i].l = inBuf[i]; + outBuf[i].r = inBuf[i]; + } + if (_this->output.write(outBuf, _this->_bufferSize) < 0) { break; }; + } + delete[] inBuf; + delete[] outBuf; + } + + stream* _in; + int _bufferSize; + std::thread _workerThread; + bool running = false; + }; + + class StereoToMono { + public: + StereoToMono() { + + } + + StereoToMono(stream* input, int bufferSize) { + _in = input; + _bufferSize = bufferSize; + } + + void init(stream* input, int bufferSize) { + _in = input; + _bufferSize = bufferSize; + } + + 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); + } + + stream output; + + private: + static void _worker(StereoToMono* _this) { + StereoFloat_t* inBuf = new StereoFloat_t[_this->_bufferSize]; + float* outBuf = new float[_this->_bufferSize]; + while (true) { + if (_this->_in->read(inBuf, _this->_bufferSize) < 0) { break; }; + for (int i = 0; i < _this->_bufferSize; i++) { + outBuf[i] = (inBuf[i].l + inBuf[i].r) / 2.0f; + } + if (_this->output.write(outBuf, _this->_bufferSize) < 0) { break; }; + } + delete[] inBuf; + delete[] outBuf; + } + + stream* _in; int _bufferSize; std::thread _workerThread; bool running = false; - std::vector*> outputs; }; }; \ No newline at end of file diff --git a/src/dsp/types.h b/src/dsp/types.h index bc7347f..09b4430 100644 --- a/src/dsp/types.h +++ b/src/dsp/types.h @@ -5,4 +5,9 @@ namespace dsp { float q; float i; }; + + struct StereoFloat_t { + float l; + float r; + }; }; \ No newline at end of file diff --git a/src/icons.cpp b/src/icons.cpp index 3a77362..8d60fd6 100644 --- a/src/icons.cpp +++ b/src/icons.cpp @@ -25,7 +25,9 @@ namespace icons { } void load() { - LOGO = (ImTextureID)loadTexture("res/icons/logo.png"); + LOGO = (ImTextureID)loadTexture("res/icons/sdrpp.png"); + PLAY = (ImTextureID)loadTexture("res/icons/play.png"); + STOP = (ImTextureID)loadTexture("res/icons/stop.png"); PLAY_RAW = (ImTextureID)loadTexture("res/icons/play_raw.png"); STOP_RAW = (ImTextureID)loadTexture("res/icons/stop_raw.png"); } diff --git a/src/imgui/imgui_widgets.cpp b/src/imgui/imgui_widgets.cpp index 7d5bc5c..6961d80 100644 --- a/src/imgui/imgui_widgets.cpp +++ b/src/imgui/imgui_widgets.cpp @@ -2640,6 +2640,7 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat // Slider behavior ImRect grab_bb; + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, power, ImGuiSliderFlags_None, &grab_bb); if (value_changed) MarkItemEdited(id); diff --git a/src/imgui/stb_image_resize.h b/src/imgui/stb_image_resize.h new file mode 100644 index 0000000..42a8efb --- /dev/null +++ b/src/imgui/stb_image_resize.h @@ -0,0 +1,2631 @@ +/* stb_image_resize - v0.96 - public domain image resizing + by Jorge L Rodriguez (@VinoBS) - 2014 + http://github.com/nothings/stb + + Written with emphasis on usability, portability, and efficiency. (No + SIMD or threads, so it be easily outperformed by libs that use those.) + Only scaling and translation is supported, no rotations or shears. + Easy API downsamples w/Mitchell filter, upsamples w/cubic interpolation. + + COMPILING & LINKING + In one C/C++ file that #includes this file, do this: + #define STB_IMAGE_RESIZE_IMPLEMENTATION + before the #include. That will create the implementation in that file. + + QUICKSTART + stbir_resize_uint8( input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, num_channels) + stbir_resize_float(...) + stbir_resize_uint8_srgb( input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, + num_channels , alpha_chan , 0) + stbir_resize_uint8_srgb_edgemode( + input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, + num_channels , alpha_chan , 0, STBIR_EDGE_CLAMP) + // WRAP/REFLECT/ZERO + + FULL API + See the "header file" section of the source for API documentation. + + ADDITIONAL DOCUMENTATION + + SRGB & FLOATING POINT REPRESENTATION + The sRGB functions presume IEEE floating point. If you do not have + IEEE floating point, define STBIR_NON_IEEE_FLOAT. This will use + a slower implementation. + + MEMORY ALLOCATION + The resize functions here perform a single memory allocation using + malloc. To control the memory allocation, before the #include that + triggers the implementation, do: + + #define STBIR_MALLOC(size,context) ... + #define STBIR_FREE(ptr,context) ... + + Each resize function makes exactly one call to malloc/free, so to use + temp memory, store the temp memory in the context and return that. + + ASSERT + Define STBIR_ASSERT(boolval) to override assert() and not use assert.h + + OPTIMIZATION + Define STBIR_SATURATE_INT to compute clamp values in-range using + integer operations instead of float operations. This may be faster + on some platforms. + + DEFAULT FILTERS + For functions which don't provide explicit control over what filters + to use, you can change the compile-time defaults with + + #define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_something + #define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_something + + See stbir_filter in the header-file section for the list of filters. + + NEW FILTERS + A number of 1D filter kernels are used. For a list of + supported filters see the stbir_filter enum. To add a new filter, + write a filter function and add it to stbir__filter_info_table. + + PROGRESS + For interactive use with slow resize operations, you can install + a progress-report callback: + + #define STBIR_PROGRESS_REPORT(val) some_func(val) + + The parameter val is a float which goes from 0 to 1 as progress is made. + + For example: + + static void my_progress_report(float progress); + #define STBIR_PROGRESS_REPORT(val) my_progress_report(val) + + #define STB_IMAGE_RESIZE_IMPLEMENTATION + #include "stb_image_resize.h" + + static void my_progress_report(float progress) + { + printf("Progress: %f%%\n", progress*100); + } + + MAX CHANNELS + If your image has more than 64 channels, define STBIR_MAX_CHANNELS + to the max you'll have. + + ALPHA CHANNEL + Most of the resizing functions provide the ability to control how + the alpha channel of an image is processed. The important things + to know about this: + + 1. The best mathematically-behaved version of alpha to use is + called "premultiplied alpha", in which the other color channels + have had the alpha value multiplied in. If you use premultiplied + alpha, linear filtering (such as image resampling done by this + library, or performed in texture units on GPUs) does the "right + thing". While premultiplied alpha is standard in the movie CGI + industry, it is still uncommon in the videogame/real-time world. + + If you linearly filter non-premultiplied alpha, strange effects + occur. (For example, the 50/50 average of 99% transparent bright green + and 1% transparent black produces 50% transparent dark green when + non-premultiplied, whereas premultiplied it produces 50% + transparent near-black. The former introduces green energy + that doesn't exist in the source image.) + + 2. Artists should not edit premultiplied-alpha images; artists + want non-premultiplied alpha images. Thus, art tools generally output + non-premultiplied alpha images. + + 3. You will get best results in most cases by converting images + to premultiplied alpha before processing them mathematically. + + 4. If you pass the flag STBIR_FLAG_ALPHA_PREMULTIPLIED, the + resizer does not do anything special for the alpha channel; + it is resampled identically to other channels. This produces + the correct results for premultiplied-alpha images, but produces + less-than-ideal results for non-premultiplied-alpha images. + + 5. If you do not pass the flag STBIR_FLAG_ALPHA_PREMULTIPLIED, + then the resizer weights the contribution of input pixels + based on their alpha values, or, equivalently, it multiplies + the alpha value into the color channels, resamples, then divides + by the resultant alpha value. Input pixels which have alpha=0 do + not contribute at all to output pixels unless _all_ of the input + pixels affecting that output pixel have alpha=0, in which case + the result for that pixel is the same as it would be without + STBIR_FLAG_ALPHA_PREMULTIPLIED. However, this is only true for + input images in integer formats. For input images in float format, + input pixels with alpha=0 have no effect, and output pixels + which have alpha=0 will be 0 in all channels. (For float images, + you can manually achieve the same result by adding a tiny epsilon + value to the alpha channel of every image, and then subtracting + or clamping it at the end.) + + 6. You can suppress the behavior described in #5 and make + all-0-alpha pixels have 0 in all channels by #defining + STBIR_NO_ALPHA_EPSILON. + + 7. You can separately control whether the alpha channel is + interpreted as linear or affected by the colorspace. By default + it is linear; you almost never want to apply the colorspace. + (For example, graphics hardware does not apply sRGB conversion + to the alpha channel.) + + CONTRIBUTORS + Jorge L Rodriguez: Implementation + Sean Barrett: API design, optimizations + Aras Pranckevicius: bugfix + Nathan Reed: warning fixes + + REVISIONS + 0.97 (2020-02-02) fixed warning + 0.96 (2019-03-04) fixed warnings + 0.95 (2017-07-23) fixed warnings + 0.94 (2017-03-18) fixed warnings + 0.93 (2017-03-03) fixed bug with certain combinations of heights + 0.92 (2017-01-02) fix integer overflow on large (>2GB) images + 0.91 (2016-04-02) fix warnings; fix handling of subpixel regions + 0.90 (2014-09-17) first released version + + LICENSE + See end of file for license information. + + TODO + Don't decode all of the image data when only processing a partial tile + Don't use full-width decode buffers when only processing a partial tile + When processing wide images, break processing into tiles so data fits in L1 cache + Installable filters? + Resize that respects alpha test coverage + (Reference code: FloatImage::alphaTestCoverage and FloatImage::scaleAlphaToCoverage: + https://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvimage/FloatImage.cpp ) +*/ + +#ifndef STBIR_INCLUDE_STB_IMAGE_RESIZE_H +#define STBIR_INCLUDE_STB_IMAGE_RESIZE_H + +#ifdef _MSC_VER +typedef unsigned char stbir_uint8; +typedef unsigned short stbir_uint16; +typedef unsigned int stbir_uint32; +#else +#include +typedef uint8_t stbir_uint8; +typedef uint16_t stbir_uint16; +typedef uint32_t stbir_uint32; +#endif + +#ifndef STBIRDEF +#ifdef STB_IMAGE_RESIZE_STATIC +#define STBIRDEF static +#else +#ifdef __cplusplus +#define STBIRDEF extern "C" +#else +#define STBIRDEF extern +#endif +#endif +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// Easy-to-use API: +// +// * "input pixels" points to an array of image data with 'num_channels' channels (e.g. RGB=3, RGBA=4) +// * input_w is input image width (x-axis), input_h is input image height (y-axis) +// * stride is the offset between successive rows of image data in memory, in bytes. you can +// specify 0 to mean packed continuously in memory +// * alpha channel is treated identically to other channels. +// * colorspace is linear or sRGB as specified by function name +// * returned result is 1 for success or 0 in case of an error. +// #define STBIR_ASSERT() to trigger an assert on parameter validation errors. +// * Memory required grows approximately linearly with input and output size, but with +// discontinuities at input_w == output_w and input_h == output_h. +// * These functions use a "default" resampling filter defined at compile time. To change the filter, +// you can change the compile-time defaults by #defining STBIR_DEFAULT_FILTER_UPSAMPLE +// and STBIR_DEFAULT_FILTER_DOWNSAMPLE, or you can use the medium-complexity API. + +STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels); + +STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels); + + +// The following functions interpret image data as gamma-corrected sRGB. +// Specify STBIR_ALPHA_CHANNEL_NONE if you have no alpha channel, +// or otherwise provide the index of the alpha channel. Flags value +// of 0 will probably do the right thing if you're not sure what +// the flags mean. + +#define STBIR_ALPHA_CHANNEL_NONE -1 + +// Set this flag if your texture has premultiplied alpha. Otherwise, stbir will +// use alpha-weighted resampling (effectively premultiplying, resampling, +// then unpremultiplying). +#define STBIR_FLAG_ALPHA_PREMULTIPLIED (1 << 0) +// The specified alpha channel should be handled as gamma-corrected value even +// when doing sRGB operations. +#define STBIR_FLAG_ALPHA_USES_COLORSPACE (1 << 1) + +STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags); + + +typedef enum +{ + STBIR_EDGE_CLAMP = 1, + STBIR_EDGE_REFLECT = 2, + STBIR_EDGE_WRAP = 3, + STBIR_EDGE_ZERO = 4, +} stbir_edge; + +// This function adds the ability to specify how requests to sample off the edge of the image are handled. +STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode); + +////////////////////////////////////////////////////////////////////////////// +// +// Medium-complexity API +// +// This extends the easy-to-use API as follows: +// +// * Alpha-channel can be processed separately +// * If alpha_channel is not STBIR_ALPHA_CHANNEL_NONE +// * Alpha channel will not be gamma corrected (unless flags&STBIR_FLAG_GAMMA_CORRECT) +// * Filters will be weighted by alpha channel (unless flags&STBIR_FLAG_ALPHA_PREMULTIPLIED) +// * Filter can be selected explicitly +// * uint16 image type +// * sRGB colorspace available for all types +// * context parameter for passing to STBIR_MALLOC + +typedef enum +{ + STBIR_FILTER_DEFAULT = 0, // use same filter type that easy-to-use API chooses + STBIR_FILTER_BOX = 1, // A trapezoid w/1-pixel wide ramps, same result as box for integer scale ratios + STBIR_FILTER_TRIANGLE = 2, // On upsampling, produces same results as bilinear texture filtering + STBIR_FILTER_CUBICBSPLINE = 3, // The cubic b-spline (aka Mitchell-Netrevalli with B=1,C=0), gaussian-esque + STBIR_FILTER_CATMULLROM = 4, // An interpolating cubic spline + STBIR_FILTER_MITCHELL = 5, // Mitchell-Netrevalli filter with B=1/3, C=1/3 +} stbir_filter; + +typedef enum +{ + STBIR_COLORSPACE_LINEAR, + STBIR_COLORSPACE_SRGB, + + STBIR_MAX_COLORSPACES, +} stbir_colorspace; + +// The following functions are all identical except for the type of the image data + +STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + +STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + stbir_uint16 *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + +STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + + + +////////////////////////////////////////////////////////////////////////////// +// +// Full-complexity API +// +// This extends the medium API as follows: +// +// * uint32 image type +// * not typesafe +// * separate filter types for each axis +// * separate edge modes for each axis +// * can specify scale explicitly for subpixel correctness +// * can specify image source tile using texture coordinates + +typedef enum +{ + STBIR_TYPE_UINT8 , + STBIR_TYPE_UINT16, + STBIR_TYPE_UINT32, + STBIR_TYPE_FLOAT , + + STBIR_MAX_TYPES +} stbir_datatype; + +STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context); + +STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float x_scale, float y_scale, + float x_offset, float y_offset); + +STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float s0, float t0, float s1, float t1); +// (s0, t0) & (s1, t1) are the top-left and bottom right corner (uv addressing style: [0, 1]x[0, 1]) of a region of the input image to use. + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBIR_INCLUDE_STB_IMAGE_RESIZE_H + + + + + +#ifdef STB_IMAGE_RESIZE_IMPLEMENTATION + +#ifndef STBIR_ASSERT +#include +#define STBIR_ASSERT(x) assert(x) +#endif + +// For memset +#include + +#include + +#ifndef STBIR_MALLOC +#include +// use comma operator to evaluate c, to avoid "unused parameter" warnings +#define STBIR_MALLOC(size,c) ((void)(c), malloc(size)) +#define STBIR_FREE(ptr,c) ((void)(c), free(ptr)) +#endif + +#ifndef _MSC_VER +#ifdef __cplusplus +#define stbir__inline inline +#else +#define stbir__inline +#endif +#else +#define stbir__inline __forceinline +#endif + + +// should produce compiler error if size is wrong +typedef unsigned char stbir__validate_uint32[sizeof(stbir_uint32) == 4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBIR__NOTUSED(v) (void)(v) +#else +#define STBIR__NOTUSED(v) (void)sizeof(v) +#endif + +#define STBIR__ARRAY_SIZE(a) (sizeof((a))/sizeof((a)[0])) + +#ifndef STBIR_DEFAULT_FILTER_UPSAMPLE +#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM +#endif + +#ifndef STBIR_DEFAULT_FILTER_DOWNSAMPLE +#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL +#endif + +#ifndef STBIR_PROGRESS_REPORT +#define STBIR_PROGRESS_REPORT(float_0_to_1) +#endif + +#ifndef STBIR_MAX_CHANNELS +#define STBIR_MAX_CHANNELS 64 +#endif + +#if STBIR_MAX_CHANNELS > 65536 +#error "Too many channels; STBIR_MAX_CHANNELS must be no more than 65536." +// because we store the indices in 16-bit variables +#endif + +// This value is added to alpha just before premultiplication to avoid +// zeroing out color values. It is equivalent to 2^-80. If you don't want +// that behavior (it may interfere if you have floating point images with +// very small alpha values) then you can define STBIR_NO_ALPHA_EPSILON to +// disable it. +#ifndef STBIR_ALPHA_EPSILON +#define STBIR_ALPHA_EPSILON ((float)1 / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20)) +#endif + + + +#ifdef _MSC_VER +#define STBIR__UNUSED_PARAM(v) (void)(v) +#else +#define STBIR__UNUSED_PARAM(v) (void)sizeof(v) +#endif + +// must match stbir_datatype +static unsigned char stbir__type_size[] = { + 1, // STBIR_TYPE_UINT8 + 2, // STBIR_TYPE_UINT16 + 4, // STBIR_TYPE_UINT32 + 4, // STBIR_TYPE_FLOAT +}; + +// Kernel function centered at 0 +typedef float (stbir__kernel_fn)(float x, float scale); +typedef float (stbir__support_fn)(float scale); + +typedef struct +{ + stbir__kernel_fn* kernel; + stbir__support_fn* support; +} stbir__filter_info; + +// When upsampling, the contributors are which source pixels contribute. +// When downsampling, the contributors are which destination pixels are contributed to. +typedef struct +{ + int n0; // First contributing pixel + int n1; // Last contributing pixel +} stbir__contributors; + +typedef struct +{ + const void* input_data; + int input_w; + int input_h; + int input_stride_bytes; + + void* output_data; + int output_w; + int output_h; + int output_stride_bytes; + + float s0, t0, s1, t1; + + float horizontal_shift; // Units: output pixels + float vertical_shift; // Units: output pixels + float horizontal_scale; + float vertical_scale; + + int channels; + int alpha_channel; + stbir_uint32 flags; + stbir_datatype type; + stbir_filter horizontal_filter; + stbir_filter vertical_filter; + stbir_edge edge_horizontal; + stbir_edge edge_vertical; + stbir_colorspace colorspace; + + stbir__contributors* horizontal_contributors; + float* horizontal_coefficients; + + stbir__contributors* vertical_contributors; + float* vertical_coefficients; + + int decode_buffer_pixels; + float* decode_buffer; + + float* horizontal_buffer; + + // cache these because ceil/floor are inexplicably showing up in profile + int horizontal_coefficient_width; + int vertical_coefficient_width; + int horizontal_filter_pixel_width; + int vertical_filter_pixel_width; + int horizontal_filter_pixel_margin; + int vertical_filter_pixel_margin; + int horizontal_num_contributors; + int vertical_num_contributors; + + int ring_buffer_length_bytes; // The length of an individual entry in the ring buffer. The total number of ring buffers is stbir__get_filter_pixel_width(filter) + int ring_buffer_num_entries; // Total number of entries in the ring buffer. + int ring_buffer_first_scanline; + int ring_buffer_last_scanline; + int ring_buffer_begin_index; // first_scanline is at this index in the ring buffer + float* ring_buffer; + + float* encode_buffer; // A temporary buffer to store floats so we don't lose precision while we do multiply-adds. + + int horizontal_contributors_size; + int horizontal_coefficients_size; + int vertical_contributors_size; + int vertical_coefficients_size; + int decode_buffer_size; + int horizontal_buffer_size; + int ring_buffer_size; + int encode_buffer_size; +} stbir__info; + + +static const float stbir__max_uint8_as_float = 255.0f; +static const float stbir__max_uint16_as_float = 65535.0f; +static const double stbir__max_uint32_as_float = 4294967295.0; + + +static stbir__inline int stbir__min(int a, int b) +{ + return a < b ? a : b; +} + +static stbir__inline float stbir__saturate(float x) +{ + if (x < 0) + return 0; + + if (x > 1) + return 1; + + return x; +} + +#ifdef STBIR_SATURATE_INT +static stbir__inline stbir_uint8 stbir__saturate8(int x) +{ + if ((unsigned int) x <= 255) + return x; + + if (x < 0) + return 0; + + return 255; +} + +static stbir__inline stbir_uint16 stbir__saturate16(int x) +{ + if ((unsigned int) x <= 65535) + return x; + + if (x < 0) + return 0; + + return 65535; +} +#endif + +static float stbir__srgb_uchar_to_linear_float[256] = { + 0.000000f, 0.000304f, 0.000607f, 0.000911f, 0.001214f, 0.001518f, 0.001821f, 0.002125f, 0.002428f, 0.002732f, 0.003035f, + 0.003347f, 0.003677f, 0.004025f, 0.004391f, 0.004777f, 0.005182f, 0.005605f, 0.006049f, 0.006512f, 0.006995f, 0.007499f, + 0.008023f, 0.008568f, 0.009134f, 0.009721f, 0.010330f, 0.010960f, 0.011612f, 0.012286f, 0.012983f, 0.013702f, 0.014444f, + 0.015209f, 0.015996f, 0.016807f, 0.017642f, 0.018500f, 0.019382f, 0.020289f, 0.021219f, 0.022174f, 0.023153f, 0.024158f, + 0.025187f, 0.026241f, 0.027321f, 0.028426f, 0.029557f, 0.030713f, 0.031896f, 0.033105f, 0.034340f, 0.035601f, 0.036889f, + 0.038204f, 0.039546f, 0.040915f, 0.042311f, 0.043735f, 0.045186f, 0.046665f, 0.048172f, 0.049707f, 0.051269f, 0.052861f, + 0.054480f, 0.056128f, 0.057805f, 0.059511f, 0.061246f, 0.063010f, 0.064803f, 0.066626f, 0.068478f, 0.070360f, 0.072272f, + 0.074214f, 0.076185f, 0.078187f, 0.080220f, 0.082283f, 0.084376f, 0.086500f, 0.088656f, 0.090842f, 0.093059f, 0.095307f, + 0.097587f, 0.099899f, 0.102242f, 0.104616f, 0.107023f, 0.109462f, 0.111932f, 0.114435f, 0.116971f, 0.119538f, 0.122139f, + 0.124772f, 0.127438f, 0.130136f, 0.132868f, 0.135633f, 0.138432f, 0.141263f, 0.144128f, 0.147027f, 0.149960f, 0.152926f, + 0.155926f, 0.158961f, 0.162029f, 0.165132f, 0.168269f, 0.171441f, 0.174647f, 0.177888f, 0.181164f, 0.184475f, 0.187821f, + 0.191202f, 0.194618f, 0.198069f, 0.201556f, 0.205079f, 0.208637f, 0.212231f, 0.215861f, 0.219526f, 0.223228f, 0.226966f, + 0.230740f, 0.234551f, 0.238398f, 0.242281f, 0.246201f, 0.250158f, 0.254152f, 0.258183f, 0.262251f, 0.266356f, 0.270498f, + 0.274677f, 0.278894f, 0.283149f, 0.287441f, 0.291771f, 0.296138f, 0.300544f, 0.304987f, 0.309469f, 0.313989f, 0.318547f, + 0.323143f, 0.327778f, 0.332452f, 0.337164f, 0.341914f, 0.346704f, 0.351533f, 0.356400f, 0.361307f, 0.366253f, 0.371238f, + 0.376262f, 0.381326f, 0.386430f, 0.391573f, 0.396755f, 0.401978f, 0.407240f, 0.412543f, 0.417885f, 0.423268f, 0.428691f, + 0.434154f, 0.439657f, 0.445201f, 0.450786f, 0.456411f, 0.462077f, 0.467784f, 0.473532f, 0.479320f, 0.485150f, 0.491021f, + 0.496933f, 0.502887f, 0.508881f, 0.514918f, 0.520996f, 0.527115f, 0.533276f, 0.539480f, 0.545725f, 0.552011f, 0.558340f, + 0.564712f, 0.571125f, 0.577581f, 0.584078f, 0.590619f, 0.597202f, 0.603827f, 0.610496f, 0.617207f, 0.623960f, 0.630757f, + 0.637597f, 0.644480f, 0.651406f, 0.658375f, 0.665387f, 0.672443f, 0.679543f, 0.686685f, 0.693872f, 0.701102f, 0.708376f, + 0.715694f, 0.723055f, 0.730461f, 0.737911f, 0.745404f, 0.752942f, 0.760525f, 0.768151f, 0.775822f, 0.783538f, 0.791298f, + 0.799103f, 0.806952f, 0.814847f, 0.822786f, 0.830770f, 0.838799f, 0.846873f, 0.854993f, 0.863157f, 0.871367f, 0.879622f, + 0.887923f, 0.896269f, 0.904661f, 0.913099f, 0.921582f, 0.930111f, 0.938686f, 0.947307f, 0.955974f, 0.964686f, 0.973445f, + 0.982251f, 0.991102f, 1.0f +}; + +static float stbir__srgb_to_linear(float f) +{ + if (f <= 0.04045f) + return f / 12.92f; + else + return (float)pow((f + 0.055f) / 1.055f, 2.4f); +} + +static float stbir__linear_to_srgb(float f) +{ + if (f <= 0.0031308f) + return f * 12.92f; + else + return 1.055f * (float)pow(f, 1 / 2.4f) - 0.055f; +} + +#ifndef STBIR_NON_IEEE_FLOAT +// From https://gist.github.com/rygorous/2203834 + +typedef union +{ + stbir_uint32 u; + float f; +} stbir__FP32; + +static const stbir_uint32 fp32_to_srgb8_tab4[104] = { + 0x0073000d, 0x007a000d, 0x0080000d, 0x0087000d, 0x008d000d, 0x0094000d, 0x009a000d, 0x00a1000d, + 0x00a7001a, 0x00b4001a, 0x00c1001a, 0x00ce001a, 0x00da001a, 0x00e7001a, 0x00f4001a, 0x0101001a, + 0x010e0033, 0x01280033, 0x01410033, 0x015b0033, 0x01750033, 0x018f0033, 0x01a80033, 0x01c20033, + 0x01dc0067, 0x020f0067, 0x02430067, 0x02760067, 0x02aa0067, 0x02dd0067, 0x03110067, 0x03440067, + 0x037800ce, 0x03df00ce, 0x044600ce, 0x04ad00ce, 0x051400ce, 0x057b00c5, 0x05dd00bc, 0x063b00b5, + 0x06970158, 0x07420142, 0x07e30130, 0x087b0120, 0x090b0112, 0x09940106, 0x0a1700fc, 0x0a9500f2, + 0x0b0f01cb, 0x0bf401ae, 0x0ccb0195, 0x0d950180, 0x0e56016e, 0x0f0d015e, 0x0fbc0150, 0x10630143, + 0x11070264, 0x1238023e, 0x1357021d, 0x14660201, 0x156601e9, 0x165a01d3, 0x174401c0, 0x182401af, + 0x18fe0331, 0x1a9602fe, 0x1c1502d2, 0x1d7e02ad, 0x1ed4028d, 0x201a0270, 0x21520256, 0x227d0240, + 0x239f0443, 0x25c003fe, 0x27bf03c4, 0x29a10392, 0x2b6a0367, 0x2d1d0341, 0x2ebe031f, 0x304d0300, + 0x31d105b0, 0x34a80555, 0x37520507, 0x39d504c5, 0x3c37048b, 0x3e7c0458, 0x40a8042a, 0x42bd0401, + 0x44c20798, 0x488e071e, 0x4c1c06b6, 0x4f76065d, 0x52a50610, 0x55ac05cc, 0x5892058f, 0x5b590559, + 0x5e0c0a23, 0x631c0980, 0x67db08f6, 0x6c55087f, 0x70940818, 0x74a007bd, 0x787d076c, 0x7c330723, +}; + +static stbir_uint8 stbir__linear_to_srgb_uchar(float in) +{ + static const stbir__FP32 almostone = { 0x3f7fffff }; // 1-eps + static const stbir__FP32 minval = { (127-13) << 23 }; + stbir_uint32 tab,bias,scale,t; + stbir__FP32 f; + + // Clamp to [2^(-13), 1-eps]; these two values map to 0 and 1, respectively. + // The tests are carefully written so that NaNs map to 0, same as in the reference + // implementation. + if (!(in > minval.f)) // written this way to catch NaNs + in = minval.f; + if (in > almostone.f) + in = almostone.f; + + // Do the table lookup and unpack bias, scale + f.f = in; + tab = fp32_to_srgb8_tab4[(f.u - minval.u) >> 20]; + bias = (tab >> 16) << 9; + scale = tab & 0xffff; + + // Grab next-highest mantissa bits and perform linear interpolation + t = (f.u >> 12) & 0xff; + return (unsigned char) ((bias + scale*t) >> 16); +} + +#else +// sRGB transition values, scaled by 1<<28 +static int stbir__srgb_offset_to_linear_scaled[256] = +{ + 0, 40738, 122216, 203693, 285170, 366648, 448125, 529603, + 611080, 692557, 774035, 855852, 942009, 1033024, 1128971, 1229926, + 1335959, 1447142, 1563542, 1685229, 1812268, 1944725, 2082664, 2226148, + 2375238, 2529996, 2690481, 2856753, 3028870, 3206888, 3390865, 3580856, + 3776916, 3979100, 4187460, 4402049, 4622919, 4850123, 5083710, 5323731, + 5570236, 5823273, 6082892, 6349140, 6622065, 6901714, 7188133, 7481369, + 7781466, 8088471, 8402427, 8723380, 9051372, 9386448, 9728650, 10078021, + 10434603, 10798439, 11169569, 11548036, 11933879, 12327139, 12727857, 13136073, + 13551826, 13975156, 14406100, 14844697, 15290987, 15745007, 16206795, 16676389, + 17153826, 17639142, 18132374, 18633560, 19142734, 19659934, 20185196, 20718552, + 21260042, 21809696, 22367554, 22933648, 23508010, 24090680, 24681686, 25281066, + 25888850, 26505076, 27129772, 27762974, 28404716, 29055026, 29713942, 30381490, + 31057708, 31742624, 32436272, 33138682, 33849884, 34569912, 35298800, 36036568, + 36783260, 37538896, 38303512, 39077136, 39859796, 40651528, 41452360, 42262316, + 43081432, 43909732, 44747252, 45594016, 46450052, 47315392, 48190064, 49074096, + 49967516, 50870356, 51782636, 52704392, 53635648, 54576432, 55526772, 56486700, + 57456236, 58435408, 59424248, 60422780, 61431036, 62449032, 63476804, 64514376, + 65561776, 66619028, 67686160, 68763192, 69850160, 70947088, 72053992, 73170912, + 74297864, 75434880, 76581976, 77739184, 78906536, 80084040, 81271736, 82469648, + 83677792, 84896192, 86124888, 87363888, 88613232, 89872928, 91143016, 92423512, + 93714432, 95015816, 96327688, 97650056, 98982952, 100326408, 101680440, 103045072, + 104420320, 105806224, 107202800, 108610064, 110028048, 111456776, 112896264, 114346544, + 115807632, 117279552, 118762328, 120255976, 121760536, 123276016, 124802440, 126339832, + 127888216, 129447616, 131018048, 132599544, 134192112, 135795792, 137410592, 139036528, + 140673648, 142321952, 143981456, 145652208, 147334208, 149027488, 150732064, 152447968, + 154175200, 155913792, 157663776, 159425168, 161197984, 162982240, 164777968, 166585184, + 168403904, 170234160, 172075968, 173929344, 175794320, 177670896, 179559120, 181458992, + 183370528, 185293776, 187228736, 189175424, 191133888, 193104112, 195086128, 197079968, + 199085648, 201103184, 203132592, 205173888, 207227120, 209292272, 211369392, 213458480, + 215559568, 217672656, 219797792, 221934976, 224084240, 226245600, 228419056, 230604656, + 232802400, 235012320, 237234432, 239468736, 241715280, 243974080, 246245120, 248528464, + 250824112, 253132064, 255452368, 257785040, 260130080, 262487520, 264857376, 267239664, +}; + +static stbir_uint8 stbir__linear_to_srgb_uchar(float f) +{ + int x = (int) (f * (1 << 28)); // has headroom so you don't need to clamp + int v = 0; + int i; + + // Refine the guess with a short binary search. + i = v + 128; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 64; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 32; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 16; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 8; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 4; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 2; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 1; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + + return (stbir_uint8) v; +} +#endif + +static float stbir__filter_trapezoid(float x, float scale) +{ + float halfscale = scale / 2; + float t = 0.5f + halfscale; + STBIR_ASSERT(scale <= 1); + + x = (float)fabs(x); + + if (x >= t) + return 0; + else + { + float r = 0.5f - halfscale; + if (x <= r) + return 1; + else + return (t - x) / scale; + } +} + +static float stbir__support_trapezoid(float scale) +{ + STBIR_ASSERT(scale <= 1); + return 0.5f + scale / 2; +} + +static float stbir__filter_triangle(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x <= 1.0f) + return 1 - x; + else + return 0; +} + +static float stbir__filter_cubic(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return (4 + x*x*(3*x - 6))/6; + else if (x < 2.0f) + return (8 + x*(-12 + x*(6 - x)))/6; + + return (0.0f); +} + +static float stbir__filter_catmullrom(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return 1 - x*x*(2.5f - 1.5f*x); + else if (x < 2.0f) + return 2 - x*(4 + x*(0.5f*x - 2.5f)); + + return (0.0f); +} + +static float stbir__filter_mitchell(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return (16 + x*x*(21 * x - 36))/18; + else if (x < 2.0f) + return (32 + x*(-60 + x*(36 - 7*x)))/18; + + return (0.0f); +} + +static float stbir__support_zero(float s) +{ + STBIR__UNUSED_PARAM(s); + return 0; +} + +static float stbir__support_one(float s) +{ + STBIR__UNUSED_PARAM(s); + return 1; +} + +static float stbir__support_two(float s) +{ + STBIR__UNUSED_PARAM(s); + return 2; +} + +static stbir__filter_info stbir__filter_info_table[] = { + { NULL, stbir__support_zero }, + { stbir__filter_trapezoid, stbir__support_trapezoid }, + { stbir__filter_triangle, stbir__support_one }, + { stbir__filter_cubic, stbir__support_two }, + { stbir__filter_catmullrom, stbir__support_two }, + { stbir__filter_mitchell, stbir__support_two }, +}; + +stbir__inline static int stbir__use_upsampling(float ratio) +{ + return ratio > 1; +} + +stbir__inline static int stbir__use_width_upsampling(stbir__info* stbir_info) +{ + return stbir__use_upsampling(stbir_info->horizontal_scale); +} + +stbir__inline static int stbir__use_height_upsampling(stbir__info* stbir_info) +{ + return stbir__use_upsampling(stbir_info->vertical_scale); +} + +// This is the maximum number of input samples that can affect an output sample +// with the given filter +static int stbir__get_filter_pixel_width(stbir_filter filter, float scale) +{ + STBIR_ASSERT(filter != 0); + STBIR_ASSERT(filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + + if (stbir__use_upsampling(scale)) + return (int)ceil(stbir__filter_info_table[filter].support(1/scale) * 2); + else + return (int)ceil(stbir__filter_info_table[filter].support(scale) * 2 / scale); +} + +// This is how much to expand buffers to account for filters seeking outside +// the image boundaries. +static int stbir__get_filter_pixel_margin(stbir_filter filter, float scale) +{ + return stbir__get_filter_pixel_width(filter, scale) / 2; +} + +static int stbir__get_coefficient_width(stbir_filter filter, float scale) +{ + if (stbir__use_upsampling(scale)) + return (int)ceil(stbir__filter_info_table[filter].support(1 / scale) * 2); + else + return (int)ceil(stbir__filter_info_table[filter].support(scale) * 2); +} + +static int stbir__get_contributors(float scale, stbir_filter filter, int input_size, int output_size) +{ + if (stbir__use_upsampling(scale)) + return output_size; + else + return (input_size + stbir__get_filter_pixel_margin(filter, scale) * 2); +} + +static int stbir__get_total_horizontal_coefficients(stbir__info* info) +{ + return info->horizontal_num_contributors + * stbir__get_coefficient_width (info->horizontal_filter, info->horizontal_scale); +} + +static int stbir__get_total_vertical_coefficients(stbir__info* info) +{ + return info->vertical_num_contributors + * stbir__get_coefficient_width (info->vertical_filter, info->vertical_scale); +} + +static stbir__contributors* stbir__get_contributor(stbir__contributors* contributors, int n) +{ + return &contributors[n]; +} + +// For perf reasons this code is duplicated in stbir__resample_horizontal_upsample/downsample, +// if you change it here change it there too. +static float* stbir__get_coefficient(float* coefficients, stbir_filter filter, float scale, int n, int c) +{ + int width = stbir__get_coefficient_width(filter, scale); + return &coefficients[width*n + c]; +} + +static int stbir__edge_wrap_slow(stbir_edge edge, int n, int max) +{ + switch (edge) + { + case STBIR_EDGE_ZERO: + return 0; // we'll decode the wrong pixel here, and then overwrite with 0s later + + case STBIR_EDGE_CLAMP: + if (n < 0) + return 0; + + if (n >= max) + return max - 1; + + return n; // NOTREACHED + + case STBIR_EDGE_REFLECT: + { + if (n < 0) + { + if (n < max) + return -n; + else + return max - 1; + } + + if (n >= max) + { + int max2 = max * 2; + if (n >= max2) + return 0; + else + return max2 - n - 1; + } + + return n; // NOTREACHED + } + + case STBIR_EDGE_WRAP: + if (n >= 0) + return (n % max); + else + { + int m = (-n) % max; + + if (m != 0) + m = max - m; + + return (m); + } + // NOTREACHED + + default: + STBIR_ASSERT(!"Unimplemented edge type"); + return 0; + } +} + +stbir__inline static int stbir__edge_wrap(stbir_edge edge, int n, int max) +{ + // avoid per-pixel switch + if (n >= 0 && n < max) + return n; + return stbir__edge_wrap_slow(edge, n, max); +} + +// What input pixels contribute to this output pixel? +static void stbir__calculate_sample_range_upsample(int n, float out_filter_radius, float scale_ratio, float out_shift, int* in_first_pixel, int* in_last_pixel, float* in_center_of_out) +{ + float out_pixel_center = (float)n + 0.5f; + float out_pixel_influence_lowerbound = out_pixel_center - out_filter_radius; + float out_pixel_influence_upperbound = out_pixel_center + out_filter_radius; + + float in_pixel_influence_lowerbound = (out_pixel_influence_lowerbound + out_shift) / scale_ratio; + float in_pixel_influence_upperbound = (out_pixel_influence_upperbound + out_shift) / scale_ratio; + + *in_center_of_out = (out_pixel_center + out_shift) / scale_ratio; + *in_first_pixel = (int)(floor(in_pixel_influence_lowerbound + 0.5)); + *in_last_pixel = (int)(floor(in_pixel_influence_upperbound - 0.5)); +} + +// What output pixels does this input pixel contribute to? +static void stbir__calculate_sample_range_downsample(int n, float in_pixels_radius, float scale_ratio, float out_shift, int* out_first_pixel, int* out_last_pixel, float* out_center_of_in) +{ + float in_pixel_center = (float)n + 0.5f; + float in_pixel_influence_lowerbound = in_pixel_center - in_pixels_radius; + float in_pixel_influence_upperbound = in_pixel_center + in_pixels_radius; + + float out_pixel_influence_lowerbound = in_pixel_influence_lowerbound * scale_ratio - out_shift; + float out_pixel_influence_upperbound = in_pixel_influence_upperbound * scale_ratio - out_shift; + + *out_center_of_in = in_pixel_center * scale_ratio - out_shift; + *out_first_pixel = (int)(floor(out_pixel_influence_lowerbound + 0.5)); + *out_last_pixel = (int)(floor(out_pixel_influence_upperbound - 0.5)); +} + +static void stbir__calculate_coefficients_upsample(stbir_filter filter, float scale, int in_first_pixel, int in_last_pixel, float in_center_of_out, stbir__contributors* contributor, float* coefficient_group) +{ + int i; + float total_filter = 0; + float filter_scale; + + STBIR_ASSERT(in_last_pixel - in_first_pixel <= (int)ceil(stbir__filter_info_table[filter].support(1/scale) * 2)); // Taken directly from stbir__get_coefficient_width() which we can't call because we don't know if we're horizontal or vertical. + + contributor->n0 = in_first_pixel; + contributor->n1 = in_last_pixel; + + STBIR_ASSERT(contributor->n1 >= contributor->n0); + + for (i = 0; i <= in_last_pixel - in_first_pixel; i++) + { + float in_pixel_center = (float)(i + in_first_pixel) + 0.5f; + coefficient_group[i] = stbir__filter_info_table[filter].kernel(in_center_of_out - in_pixel_center, 1 / scale); + + // If the coefficient is zero, skip it. (Don't do the <0 check here, we want the influence of those outside pixels.) + if (i == 0 && !coefficient_group[i]) + { + contributor->n0 = ++in_first_pixel; + i--; + continue; + } + + total_filter += coefficient_group[i]; + } + + STBIR_ASSERT(stbir__filter_info_table[filter].kernel((float)(in_last_pixel + 1) + 0.5f - in_center_of_out, 1/scale) == 0); + + STBIR_ASSERT(total_filter > 0.9); + STBIR_ASSERT(total_filter < 1.1f); // Make sure it's not way off. + + // Make sure the sum of all coefficients is 1. + filter_scale = 1 / total_filter; + + for (i = 0; i <= in_last_pixel - in_first_pixel; i++) + coefficient_group[i] *= filter_scale; + + for (i = in_last_pixel - in_first_pixel; i >= 0; i--) + { + if (coefficient_group[i]) + break; + + // This line has no weight. We can skip it. + contributor->n1 = contributor->n0 + i - 1; + } +} + +static void stbir__calculate_coefficients_downsample(stbir_filter filter, float scale_ratio, int out_first_pixel, int out_last_pixel, float out_center_of_in, stbir__contributors* contributor, float* coefficient_group) +{ + int i; + + STBIR_ASSERT(out_last_pixel - out_first_pixel <= (int)ceil(stbir__filter_info_table[filter].support(scale_ratio) * 2)); // Taken directly from stbir__get_coefficient_width() which we can't call because we don't know if we're horizontal or vertical. + + contributor->n0 = out_first_pixel; + contributor->n1 = out_last_pixel; + + STBIR_ASSERT(contributor->n1 >= contributor->n0); + + for (i = 0; i <= out_last_pixel - out_first_pixel; i++) + { + float out_pixel_center = (float)(i + out_first_pixel) + 0.5f; + float x = out_pixel_center - out_center_of_in; + coefficient_group[i] = stbir__filter_info_table[filter].kernel(x, scale_ratio) * scale_ratio; + } + + STBIR_ASSERT(stbir__filter_info_table[filter].kernel((float)(out_last_pixel + 1) + 0.5f - out_center_of_in, scale_ratio) == 0); + + for (i = out_last_pixel - out_first_pixel; i >= 0; i--) + { + if (coefficient_group[i]) + break; + + // This line has no weight. We can skip it. + contributor->n1 = contributor->n0 + i - 1; + } +} + +static void stbir__normalize_downsample_coefficients(stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, int input_size, int output_size) +{ + int num_contributors = stbir__get_contributors(scale_ratio, filter, input_size, output_size); + int num_coefficients = stbir__get_coefficient_width(filter, scale_ratio); + int i, j; + int skip; + + for (i = 0; i < output_size; i++) + { + float scale; + float total = 0; + + for (j = 0; j < num_contributors; j++) + { + if (i >= contributors[j].n0 && i <= contributors[j].n1) + { + float coefficient = *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i - contributors[j].n0); + total += coefficient; + } + else if (i < contributors[j].n0) + break; + } + + STBIR_ASSERT(total > 0.9f); + STBIR_ASSERT(total < 1.1f); + + scale = 1 / total; + + for (j = 0; j < num_contributors; j++) + { + if (i >= contributors[j].n0 && i <= contributors[j].n1) + *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i - contributors[j].n0) *= scale; + else if (i < contributors[j].n0) + break; + } + } + + // Optimize: Skip zero coefficients and contributions outside of image bounds. + // Do this after normalizing because normalization depends on the n0/n1 values. + for (j = 0; j < num_contributors; j++) + { + int range, max, width; + + skip = 0; + while (*stbir__get_coefficient(coefficients, filter, scale_ratio, j, skip) == 0) + skip++; + + contributors[j].n0 += skip; + + while (contributors[j].n0 < 0) + { + contributors[j].n0++; + skip++; + } + + range = contributors[j].n1 - contributors[j].n0 + 1; + max = stbir__min(num_coefficients, range); + + width = stbir__get_coefficient_width(filter, scale_ratio); + for (i = 0; i < max; i++) + { + if (i + skip >= width) + break; + + *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i) = *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i + skip); + } + + continue; + } + + // Using min to avoid writing into invalid pixels. + for (i = 0; i < num_contributors; i++) + contributors[i].n1 = stbir__min(contributors[i].n1, output_size - 1); +} + +// Each scan line uses the same kernel values so we should calculate the kernel +// values once and then we can use them for every scan line. +static void stbir__calculate_filters(stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, float shift, int input_size, int output_size) +{ + int n; + int total_contributors = stbir__get_contributors(scale_ratio, filter, input_size, output_size); + + if (stbir__use_upsampling(scale_ratio)) + { + float out_pixels_radius = stbir__filter_info_table[filter].support(1 / scale_ratio) * scale_ratio; + + // Looping through out pixels + for (n = 0; n < total_contributors; n++) + { + float in_center_of_out; // Center of the current out pixel in the in pixel space + int in_first_pixel, in_last_pixel; + + stbir__calculate_sample_range_upsample(n, out_pixels_radius, scale_ratio, shift, &in_first_pixel, &in_last_pixel, &in_center_of_out); + + stbir__calculate_coefficients_upsample(filter, scale_ratio, in_first_pixel, in_last_pixel, in_center_of_out, stbir__get_contributor(contributors, n), stbir__get_coefficient(coefficients, filter, scale_ratio, n, 0)); + } + } + else + { + float in_pixels_radius = stbir__filter_info_table[filter].support(scale_ratio) / scale_ratio; + + // Looping through in pixels + for (n = 0; n < total_contributors; n++) + { + float out_center_of_in; // Center of the current out pixel in the in pixel space + int out_first_pixel, out_last_pixel; + int n_adjusted = n - stbir__get_filter_pixel_margin(filter, scale_ratio); + + stbir__calculate_sample_range_downsample(n_adjusted, in_pixels_radius, scale_ratio, shift, &out_first_pixel, &out_last_pixel, &out_center_of_in); + + stbir__calculate_coefficients_downsample(filter, scale_ratio, out_first_pixel, out_last_pixel, out_center_of_in, stbir__get_contributor(contributors, n), stbir__get_coefficient(coefficients, filter, scale_ratio, n, 0)); + } + + stbir__normalize_downsample_coefficients(contributors, coefficients, filter, scale_ratio, input_size, output_size); + } +} + +static float* stbir__get_decode_buffer(stbir__info* stbir_info) +{ + // The 0 index of the decode buffer starts after the margin. This makes + // it okay to use negative indexes on the decode buffer. + return &stbir_info->decode_buffer[stbir_info->horizontal_filter_pixel_margin * stbir_info->channels]; +} + +#define STBIR__DECODE(type, colorspace) ((int)(type) * (STBIR_MAX_COLORSPACES) + (int)(colorspace)) + +static void stbir__decode_scanline(stbir__info* stbir_info, int n) +{ + int c; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int input_w = stbir_info->input_w; + size_t input_stride_bytes = stbir_info->input_stride_bytes; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir_edge edge_horizontal = stbir_info->edge_horizontal; + stbir_edge edge_vertical = stbir_info->edge_vertical; + size_t in_buffer_row_offset = stbir__edge_wrap(edge_vertical, n, stbir_info->input_h) * input_stride_bytes; + const void* input_data = (char *) stbir_info->input_data + in_buffer_row_offset; + int max_x = input_w + stbir_info->horizontal_filter_pixel_margin; + int decode = STBIR__DECODE(type, colorspace); + + int x = -stbir_info->horizontal_filter_pixel_margin; + + // special handling for STBIR_EDGE_ZERO because it needs to return an item that doesn't appear in the input, + // and we want to avoid paying overhead on every pixel if not STBIR_EDGE_ZERO + if (edge_vertical == STBIR_EDGE_ZERO && (n < 0 || n >= stbir_info->input_h)) + { + for (; x < max_x; x++) + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + return; + } + + switch (decode) + { + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((float)((const unsigned char*)input_data)[input_pixel_index + c]) / stbir__max_uint8_as_float; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_uchar_to_linear_float[((const unsigned char*)input_data)[input_pixel_index + c]]; + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((float)((const unsigned char*)input_data)[input_pixel_index + alpha_channel]) / stbir__max_uint8_as_float; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((float)((const unsigned short*)input_data)[input_pixel_index + c]) / stbir__max_uint16_as_float; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear(((float)((const unsigned short*)input_data)[input_pixel_index + c]) / stbir__max_uint16_as_float); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((float)((const unsigned short*)input_data)[input_pixel_index + alpha_channel]) / stbir__max_uint16_as_float; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = (float)(((double)((const unsigned int*)input_data)[input_pixel_index + c]) / stbir__max_uint32_as_float); + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear((float)(((double)((const unsigned int*)input_data)[input_pixel_index + c]) / stbir__max_uint32_as_float)); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = (float)(((double)((const unsigned int*)input_data)[input_pixel_index + alpha_channel]) / stbir__max_uint32_as_float); + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((const float*)input_data)[input_pixel_index + c]; + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear(((const float*)input_data)[input_pixel_index + c]); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((const float*)input_data)[input_pixel_index + alpha_channel]; + } + + break; + + default: + STBIR_ASSERT(!"Unknown type/colorspace/channels combination."); + break; + } + + if (!(stbir_info->flags & STBIR_FLAG_ALPHA_PREMULTIPLIED)) + { + for (x = -stbir_info->horizontal_filter_pixel_margin; x < max_x; x++) + { + int decode_pixel_index = x * channels; + + // If the alpha value is 0 it will clobber the color values. Make sure it's not. + float alpha = decode_buffer[decode_pixel_index + alpha_channel]; +#ifndef STBIR_NO_ALPHA_EPSILON + if (stbir_info->type != STBIR_TYPE_FLOAT) { + alpha += STBIR_ALPHA_EPSILON; + decode_buffer[decode_pixel_index + alpha_channel] = alpha; + } +#endif + for (c = 0; c < channels; c++) + { + if (c == alpha_channel) + continue; + + decode_buffer[decode_pixel_index + c] *= alpha; + } + } + } + + if (edge_horizontal == STBIR_EDGE_ZERO) + { + for (x = -stbir_info->horizontal_filter_pixel_margin; x < 0; x++) + { + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + } + for (x = input_w; x < max_x; x++) + { + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + } + } +} + +static float* stbir__get_ring_buffer_entry(float* ring_buffer, int index, int ring_buffer_length) +{ + return &ring_buffer[index * ring_buffer_length]; +} + +static float* stbir__add_empty_ring_buffer_entry(stbir__info* stbir_info, int n) +{ + int ring_buffer_index; + float* ring_buffer; + + stbir_info->ring_buffer_last_scanline = n; + + if (stbir_info->ring_buffer_begin_index < 0) + { + ring_buffer_index = stbir_info->ring_buffer_begin_index = 0; + stbir_info->ring_buffer_first_scanline = n; + } + else + { + ring_buffer_index = (stbir_info->ring_buffer_begin_index + (stbir_info->ring_buffer_last_scanline - stbir_info->ring_buffer_first_scanline)) % stbir_info->ring_buffer_num_entries; + STBIR_ASSERT(ring_buffer_index != stbir_info->ring_buffer_begin_index); + } + + ring_buffer = stbir__get_ring_buffer_entry(stbir_info->ring_buffer, ring_buffer_index, stbir_info->ring_buffer_length_bytes / sizeof(float)); + memset(ring_buffer, 0, stbir_info->ring_buffer_length_bytes); + + return ring_buffer; +} + + +static void stbir__resample_horizontal_upsample(stbir__info* stbir_info, float* output_buffer) +{ + int x, k; + int output_w = stbir_info->output_w; + int channels = stbir_info->channels; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir__contributors* horizontal_contributors = stbir_info->horizontal_contributors; + float* horizontal_coefficients = stbir_info->horizontal_coefficients; + int coefficient_width = stbir_info->horizontal_coefficient_width; + + for (x = 0; x < output_w; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int out_pixel_index = x * channels; + int coefficient_group = coefficient_width * x; + int coefficient_counter = 0; + + STBIR_ASSERT(n1 >= n0); + STBIR_ASSERT(n0 >= -stbir_info->horizontal_filter_pixel_margin); + STBIR_ASSERT(n1 >= -stbir_info->horizontal_filter_pixel_margin); + STBIR_ASSERT(n0 < stbir_info->input_w + stbir_info->horizontal_filter_pixel_margin); + STBIR_ASSERT(n1 < stbir_info->input_w + stbir_info->horizontal_filter_pixel_margin); + + switch (channels) { + case 1: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 1; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + } + break; + case 2: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 2; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + } + break; + case 3: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 3; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + } + break; + case 4: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 4; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + output_buffer[out_pixel_index + 3] += decode_buffer[in_pixel_index + 3] * coefficient; + } + break; + default: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * channels; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + int c; + STBIR_ASSERT(coefficient != 0); + for (c = 0; c < channels; c++) + output_buffer[out_pixel_index + c] += decode_buffer[in_pixel_index + c] * coefficient; + } + break; + } + } +} + +static void stbir__resample_horizontal_downsample(stbir__info* stbir_info, float* output_buffer) +{ + int x, k; + int input_w = stbir_info->input_w; + int channels = stbir_info->channels; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir__contributors* horizontal_contributors = stbir_info->horizontal_contributors; + float* horizontal_coefficients = stbir_info->horizontal_coefficients; + int coefficient_width = stbir_info->horizontal_coefficient_width; + int filter_pixel_margin = stbir_info->horizontal_filter_pixel_margin; + int max_x = input_w + filter_pixel_margin * 2; + + STBIR_ASSERT(!stbir__use_width_upsampling(stbir_info)); + + switch (channels) { + case 1: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 1; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 1; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + } + } + break; + + case 2: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 2; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 2; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + } + } + break; + + case 3: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 3; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 3; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + } + } + break; + + case 4: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 4; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 4; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + output_buffer[out_pixel_index + 3] += decode_buffer[in_pixel_index + 3] * coefficient; + } + } + break; + + default: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * channels; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int c; + int out_pixel_index = k * channels; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR_ASSERT(coefficient != 0); + for (c = 0; c < channels; c++) + output_buffer[out_pixel_index + c] += decode_buffer[in_pixel_index + c] * coefficient; + } + } + break; + } +} + +static void stbir__decode_and_resample_upsample(stbir__info* stbir_info, int n) +{ + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline(stbir_info, n); + + // Now resample it into the ring buffer. + if (stbir__use_width_upsampling(stbir_info)) + stbir__resample_horizontal_upsample(stbir_info, stbir__add_empty_ring_buffer_entry(stbir_info, n)); + else + stbir__resample_horizontal_downsample(stbir_info, stbir__add_empty_ring_buffer_entry(stbir_info, n)); + + // Now it's sitting in the ring buffer ready to be used as source for the vertical sampling. +} + +static void stbir__decode_and_resample_downsample(stbir__info* stbir_info, int n) +{ + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline(stbir_info, n); + + memset(stbir_info->horizontal_buffer, 0, stbir_info->output_w * stbir_info->channels * sizeof(float)); + + // Now resample it into the horizontal buffer. + if (stbir__use_width_upsampling(stbir_info)) + stbir__resample_horizontal_upsample(stbir_info, stbir_info->horizontal_buffer); + else + stbir__resample_horizontal_downsample(stbir_info, stbir_info->horizontal_buffer); + + // Now it's sitting in the horizontal buffer ready to be distributed into the ring buffers. +} + +// Get the specified scan line from the ring buffer. +static float* stbir__get_ring_buffer_scanline(int get_scanline, float* ring_buffer, int begin_index, int first_scanline, int ring_buffer_num_entries, int ring_buffer_length) +{ + int ring_buffer_index = (begin_index + (get_scanline - first_scanline)) % ring_buffer_num_entries; + return stbir__get_ring_buffer_entry(ring_buffer, ring_buffer_index, ring_buffer_length); +} + + +static void stbir__encode_scanline(stbir__info* stbir_info, int num_pixels, void *output_buffer, float *encode_buffer, int channels, int alpha_channel, int decode) +{ + int x; + int n; + int num_nonalpha; + stbir_uint16 nonalpha[STBIR_MAX_CHANNELS]; + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_PREMULTIPLIED)) + { + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + float alpha = encode_buffer[pixel_index + alpha_channel]; + float reciprocal_alpha = alpha ? 1.0f / alpha : 0; + + // unrolling this produced a 1% slowdown upscaling a large RGBA linear-space image on my machine - stb + for (n = 0; n < channels; n++) + if (n != alpha_channel) + encode_buffer[pixel_index + n] *= reciprocal_alpha; + + // We added in a small epsilon to prevent the color channel from being deleted with zero alpha. + // Because we only add it for integer types, it will automatically be discarded on integer + // conversion, so we don't need to subtract it back out (which would be problematic for + // numeric precision reasons). + } + } + + // build a table of all channels that need colorspace correction, so + // we don't perform colorspace correction on channels that don't need it. + for (x = 0, num_nonalpha = 0; x < channels; ++x) + { + if (x != alpha_channel || (stbir_info->flags & STBIR_FLAG_ALPHA_USES_COLORSPACE)) + { + nonalpha[num_nonalpha++] = (stbir_uint16)x; + } + } + + #define STBIR__ROUND_INT(f) ((int) ((f)+0.5)) + #define STBIR__ROUND_UINT(f) ((stbir_uint32) ((f)+0.5)) + + #ifdef STBIR__SATURATE_INT + #define STBIR__ENCODE_LINEAR8(f) stbir__saturate8 (STBIR__ROUND_INT((f) * stbir__max_uint8_as_float )) + #define STBIR__ENCODE_LINEAR16(f) stbir__saturate16(STBIR__ROUND_INT((f) * stbir__max_uint16_as_float)) + #else + #define STBIR__ENCODE_LINEAR8(f) (unsigned char ) STBIR__ROUND_INT(stbir__saturate(f) * stbir__max_uint8_as_float ) + #define STBIR__ENCODE_LINEAR16(f) (unsigned short) STBIR__ROUND_INT(stbir__saturate(f) * stbir__max_uint16_as_float) + #endif + + switch (decode) + { + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned char*)output_buffer)[index] = STBIR__ENCODE_LINEAR8(encode_buffer[index]); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned char*)output_buffer)[index] = stbir__linear_to_srgb_uchar(encode_buffer[index]); + } + + if (!(stbir_info->flags & STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned char *)output_buffer)[pixel_index + alpha_channel] = STBIR__ENCODE_LINEAR8(encode_buffer[pixel_index+alpha_channel]); + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned short*)output_buffer)[index] = STBIR__ENCODE_LINEAR16(encode_buffer[index]); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned short*)output_buffer)[index] = (unsigned short)STBIR__ROUND_INT(stbir__linear_to_srgb(stbir__saturate(encode_buffer[index])) * stbir__max_uint16_as_float); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned short*)output_buffer)[pixel_index + alpha_channel] = STBIR__ENCODE_LINEAR16(encode_buffer[pixel_index + alpha_channel]); + } + + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned int*)output_buffer)[index] = (unsigned int)STBIR__ROUND_UINT(((double)stbir__saturate(encode_buffer[index])) * stbir__max_uint32_as_float); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned int*)output_buffer)[index] = (unsigned int)STBIR__ROUND_UINT(((double)stbir__linear_to_srgb(stbir__saturate(encode_buffer[index]))) * stbir__max_uint32_as_float); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned int*)output_buffer)[pixel_index + alpha_channel] = (unsigned int)STBIR__ROUND_INT(((double)stbir__saturate(encode_buffer[pixel_index + alpha_channel])) * stbir__max_uint32_as_float); + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((float*)output_buffer)[index] = encode_buffer[index]; + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((float*)output_buffer)[index] = stbir__linear_to_srgb(encode_buffer[index]); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((float*)output_buffer)[pixel_index + alpha_channel] = encode_buffer[pixel_index + alpha_channel]; + } + break; + + default: + STBIR_ASSERT(!"Unknown type/colorspace/channels combination."); + break; + } +} + +static void stbir__resample_vertical_upsample(stbir__info* stbir_info, int n) +{ + int x, k; + int output_w = stbir_info->output_w; + stbir__contributors* vertical_contributors = stbir_info->vertical_contributors; + float* vertical_coefficients = stbir_info->vertical_coefficients; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int ring_buffer_entries = stbir_info->ring_buffer_num_entries; + void* output_data = stbir_info->output_data; + float* encode_buffer = stbir_info->encode_buffer; + int decode = STBIR__DECODE(type, colorspace); + int coefficient_width = stbir_info->vertical_coefficient_width; + int coefficient_counter; + int contributor = n; + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_begin_index = stbir_info->ring_buffer_begin_index; + int ring_buffer_first_scanline = stbir_info->ring_buffer_first_scanline; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + + int n0,n1, output_row_start; + int coefficient_group = coefficient_width * contributor; + + n0 = vertical_contributors[contributor].n0; + n1 = vertical_contributors[contributor].n1; + + output_row_start = n * stbir_info->output_stride_bytes; + + STBIR_ASSERT(stbir__use_height_upsampling(stbir_info)); + + memset(encode_buffer, 0, output_w * sizeof(float) * channels); + + // I tried reblocking this for better cache usage of encode_buffer + // (using x_outer, k, x_inner), but it lost speed. -- stb + + coefficient_counter = 0; + switch (channels) { + case 1: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 1; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + } + } + break; + case 2: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 2; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + } + } + break; + case 3: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 3; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + encode_buffer[in_pixel_index + 2] += ring_buffer_entry[in_pixel_index + 2] * coefficient; + } + } + break; + case 4: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 4; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + encode_buffer[in_pixel_index + 2] += ring_buffer_entry[in_pixel_index + 2] * coefficient; + encode_buffer[in_pixel_index + 3] += ring_buffer_entry[in_pixel_index + 3] * coefficient; + } + } + break; + default: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * channels; + int c; + for (c = 0; c < channels; c++) + encode_buffer[in_pixel_index + c] += ring_buffer_entry[in_pixel_index + c] * coefficient; + } + } + break; + } + stbir__encode_scanline(stbir_info, output_w, (char *) output_data + output_row_start, encode_buffer, channels, alpha_channel, decode); +} + +static void stbir__resample_vertical_downsample(stbir__info* stbir_info, int n) +{ + int x, k; + int output_w = stbir_info->output_w; + stbir__contributors* vertical_contributors = stbir_info->vertical_contributors; + float* vertical_coefficients = stbir_info->vertical_coefficients; + int channels = stbir_info->channels; + int ring_buffer_entries = stbir_info->ring_buffer_num_entries; + float* horizontal_buffer = stbir_info->horizontal_buffer; + int coefficient_width = stbir_info->vertical_coefficient_width; + int contributor = n + stbir_info->vertical_filter_pixel_margin; + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_begin_index = stbir_info->ring_buffer_begin_index; + int ring_buffer_first_scanline = stbir_info->ring_buffer_first_scanline; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + int n0,n1; + + n0 = vertical_contributors[contributor].n0; + n1 = vertical_contributors[contributor].n1; + + STBIR_ASSERT(!stbir__use_height_upsampling(stbir_info)); + + for (k = n0; k <= n1; k++) + { + int coefficient_index = k - n0; + int coefficient_group = coefficient_width * contributor; + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, ring_buffer_entries, ring_buffer_length); + + switch (channels) { + case 1: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 1; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + } + break; + case 2: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 2; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + } + break; + case 3: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 3; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + ring_buffer_entry[in_pixel_index + 2] += horizontal_buffer[in_pixel_index + 2] * coefficient; + } + break; + case 4: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 4; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + ring_buffer_entry[in_pixel_index + 2] += horizontal_buffer[in_pixel_index + 2] * coefficient; + ring_buffer_entry[in_pixel_index + 3] += horizontal_buffer[in_pixel_index + 3] * coefficient; + } + break; + default: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * channels; + + int c; + for (c = 0; c < channels; c++) + ring_buffer_entry[in_pixel_index + c] += horizontal_buffer[in_pixel_index + c] * coefficient; + } + break; + } + } +} + +static void stbir__buffer_loop_upsample(stbir__info* stbir_info) +{ + int y; + float scale_ratio = stbir_info->vertical_scale; + float out_scanlines_radius = stbir__filter_info_table[stbir_info->vertical_filter].support(1/scale_ratio) * scale_ratio; + + STBIR_ASSERT(stbir__use_height_upsampling(stbir_info)); + + for (y = 0; y < stbir_info->output_h; y++) + { + float in_center_of_out = 0; // Center of the current out scanline in the in scanline space + int in_first_scanline = 0, in_last_scanline = 0; + + stbir__calculate_sample_range_upsample(y, out_scanlines_radius, scale_ratio, stbir_info->vertical_shift, &in_first_scanline, &in_last_scanline, &in_center_of_out); + + STBIR_ASSERT(in_last_scanline - in_first_scanline + 1 <= stbir_info->ring_buffer_num_entries); + + if (stbir_info->ring_buffer_begin_index >= 0) + { + // Get rid of whatever we don't need anymore. + while (in_first_scanline > stbir_info->ring_buffer_first_scanline) + { + if (stbir_info->ring_buffer_first_scanline == stbir_info->ring_buffer_last_scanline) + { + // We just popped the last scanline off the ring buffer. + // Reset it to the empty state. + stbir_info->ring_buffer_begin_index = -1; + stbir_info->ring_buffer_first_scanline = 0; + stbir_info->ring_buffer_last_scanline = 0; + break; + } + else + { + stbir_info->ring_buffer_first_scanline++; + stbir_info->ring_buffer_begin_index = (stbir_info->ring_buffer_begin_index + 1) % stbir_info->ring_buffer_num_entries; + } + } + } + + // Load in new ones. + if (stbir_info->ring_buffer_begin_index < 0) + stbir__decode_and_resample_upsample(stbir_info, in_first_scanline); + + while (in_last_scanline > stbir_info->ring_buffer_last_scanline) + stbir__decode_and_resample_upsample(stbir_info, stbir_info->ring_buffer_last_scanline + 1); + + // Now all buffers should be ready to write a row of vertical sampling. + stbir__resample_vertical_upsample(stbir_info, y); + + STBIR_PROGRESS_REPORT((float)y / stbir_info->output_h); + } +} + +static void stbir__empty_ring_buffer(stbir__info* stbir_info, int first_necessary_scanline) +{ + int output_stride_bytes = stbir_info->output_stride_bytes; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int output_w = stbir_info->output_w; + void* output_data = stbir_info->output_data; + int decode = STBIR__DECODE(type, colorspace); + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + + if (stbir_info->ring_buffer_begin_index >= 0) + { + // Get rid of whatever we don't need anymore. + while (first_necessary_scanline > stbir_info->ring_buffer_first_scanline) + { + if (stbir_info->ring_buffer_first_scanline >= 0 && stbir_info->ring_buffer_first_scanline < stbir_info->output_h) + { + int output_row_start = stbir_info->ring_buffer_first_scanline * output_stride_bytes; + float* ring_buffer_entry = stbir__get_ring_buffer_entry(ring_buffer, stbir_info->ring_buffer_begin_index, ring_buffer_length); + stbir__encode_scanline(stbir_info, output_w, (char *) output_data + output_row_start, ring_buffer_entry, channels, alpha_channel, decode); + STBIR_PROGRESS_REPORT((float)stbir_info->ring_buffer_first_scanline / stbir_info->output_h); + } + + if (stbir_info->ring_buffer_first_scanline == stbir_info->ring_buffer_last_scanline) + { + // We just popped the last scanline off the ring buffer. + // Reset it to the empty state. + stbir_info->ring_buffer_begin_index = -1; + stbir_info->ring_buffer_first_scanline = 0; + stbir_info->ring_buffer_last_scanline = 0; + break; + } + else + { + stbir_info->ring_buffer_first_scanline++; + stbir_info->ring_buffer_begin_index = (stbir_info->ring_buffer_begin_index + 1) % stbir_info->ring_buffer_num_entries; + } + } + } +} + +static void stbir__buffer_loop_downsample(stbir__info* stbir_info) +{ + int y; + float scale_ratio = stbir_info->vertical_scale; + int output_h = stbir_info->output_h; + float in_pixels_radius = stbir__filter_info_table[stbir_info->vertical_filter].support(scale_ratio) / scale_ratio; + int pixel_margin = stbir_info->vertical_filter_pixel_margin; + int max_y = stbir_info->input_h + pixel_margin; + + STBIR_ASSERT(!stbir__use_height_upsampling(stbir_info)); + + for (y = -pixel_margin; y < max_y; y++) + { + float out_center_of_in; // Center of the current out scanline in the in scanline space + int out_first_scanline, out_last_scanline; + + stbir__calculate_sample_range_downsample(y, in_pixels_radius, scale_ratio, stbir_info->vertical_shift, &out_first_scanline, &out_last_scanline, &out_center_of_in); + + STBIR_ASSERT(out_last_scanline - out_first_scanline + 1 <= stbir_info->ring_buffer_num_entries); + + if (out_last_scanline < 0 || out_first_scanline >= output_h) + continue; + + stbir__empty_ring_buffer(stbir_info, out_first_scanline); + + stbir__decode_and_resample_downsample(stbir_info, y); + + // Load in new ones. + if (stbir_info->ring_buffer_begin_index < 0) + stbir__add_empty_ring_buffer_entry(stbir_info, out_first_scanline); + + while (out_last_scanline > stbir_info->ring_buffer_last_scanline) + stbir__add_empty_ring_buffer_entry(stbir_info, stbir_info->ring_buffer_last_scanline + 1); + + // Now the horizontal buffer is ready to write to all ring buffer rows. + stbir__resample_vertical_downsample(stbir_info, y); + } + + stbir__empty_ring_buffer(stbir_info, stbir_info->output_h); +} + +static void stbir__setup(stbir__info *info, int input_w, int input_h, int output_w, int output_h, int channels) +{ + info->input_w = input_w; + info->input_h = input_h; + info->output_w = output_w; + info->output_h = output_h; + info->channels = channels; +} + +static void stbir__calculate_transform(stbir__info *info, float s0, float t0, float s1, float t1, float *transform) +{ + info->s0 = s0; + info->t0 = t0; + info->s1 = s1; + info->t1 = t1; + + if (transform) + { + info->horizontal_scale = transform[0]; + info->vertical_scale = transform[1]; + info->horizontal_shift = transform[2]; + info->vertical_shift = transform[3]; + } + else + { + info->horizontal_scale = ((float)info->output_w / info->input_w) / (s1 - s0); + info->vertical_scale = ((float)info->output_h / info->input_h) / (t1 - t0); + + info->horizontal_shift = s0 * info->output_w / (s1 - s0); + info->vertical_shift = t0 * info->output_h / (t1 - t0); + } +} + +static void stbir__choose_filter(stbir__info *info, stbir_filter h_filter, stbir_filter v_filter) +{ + if (h_filter == 0) + h_filter = stbir__use_upsampling(info->horizontal_scale) ? STBIR_DEFAULT_FILTER_UPSAMPLE : STBIR_DEFAULT_FILTER_DOWNSAMPLE; + if (v_filter == 0) + v_filter = stbir__use_upsampling(info->vertical_scale) ? STBIR_DEFAULT_FILTER_UPSAMPLE : STBIR_DEFAULT_FILTER_DOWNSAMPLE; + info->horizontal_filter = h_filter; + info->vertical_filter = v_filter; +} + +static stbir_uint32 stbir__calculate_memory(stbir__info *info) +{ + int pixel_margin = stbir__get_filter_pixel_margin(info->horizontal_filter, info->horizontal_scale); + int filter_height = stbir__get_filter_pixel_width(info->vertical_filter, info->vertical_scale); + + info->horizontal_num_contributors = stbir__get_contributors(info->horizontal_scale, info->horizontal_filter, info->input_w, info->output_w); + info->vertical_num_contributors = stbir__get_contributors(info->vertical_scale , info->vertical_filter , info->input_h, info->output_h); + + // One extra entry because floating point precision problems sometimes cause an extra to be necessary. + info->ring_buffer_num_entries = filter_height + 1; + + info->horizontal_contributors_size = info->horizontal_num_contributors * sizeof(stbir__contributors); + info->horizontal_coefficients_size = stbir__get_total_horizontal_coefficients(info) * sizeof(float); + info->vertical_contributors_size = info->vertical_num_contributors * sizeof(stbir__contributors); + info->vertical_coefficients_size = stbir__get_total_vertical_coefficients(info) * sizeof(float); + info->decode_buffer_size = (info->input_w + pixel_margin * 2) * info->channels * sizeof(float); + info->horizontal_buffer_size = info->output_w * info->channels * sizeof(float); + info->ring_buffer_size = info->output_w * info->channels * info->ring_buffer_num_entries * sizeof(float); + info->encode_buffer_size = info->output_w * info->channels * sizeof(float); + + STBIR_ASSERT(info->horizontal_filter != 0); + STBIR_ASSERT(info->horizontal_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); // this now happens too late + STBIR_ASSERT(info->vertical_filter != 0); + STBIR_ASSERT(info->vertical_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); // this now happens too late + + if (stbir__use_height_upsampling(info)) + // The horizontal buffer is for when we're downsampling the height and we + // can't output the result of sampling the decode buffer directly into the + // ring buffers. + info->horizontal_buffer_size = 0; + else + // The encode buffer is to retain precision in the height upsampling method + // and isn't used when height downsampling. + info->encode_buffer_size = 0; + + return info->horizontal_contributors_size + info->horizontal_coefficients_size + + info->vertical_contributors_size + info->vertical_coefficients_size + + info->decode_buffer_size + info->horizontal_buffer_size + + info->ring_buffer_size + info->encode_buffer_size; +} + +static int stbir__resize_allocated(stbir__info *info, + const void* input_data, int input_stride_in_bytes, + void* output_data, int output_stride_in_bytes, + int alpha_channel, stbir_uint32 flags, stbir_datatype type, + stbir_edge edge_horizontal, stbir_edge edge_vertical, stbir_colorspace colorspace, + void* tempmem, size_t tempmem_size_in_bytes) +{ + size_t memory_required = stbir__calculate_memory(info); + + int width_stride_input = input_stride_in_bytes ? input_stride_in_bytes : info->channels * info->input_w * stbir__type_size[type]; + int width_stride_output = output_stride_in_bytes ? output_stride_in_bytes : info->channels * info->output_w * stbir__type_size[type]; + +#ifdef STBIR_DEBUG_OVERWRITE_TEST +#define OVERWRITE_ARRAY_SIZE 8 + unsigned char overwrite_output_before_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_tempmem_before_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_output_after_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_tempmem_after_pre[OVERWRITE_ARRAY_SIZE]; + + size_t begin_forbidden = width_stride_output * (info->output_h - 1) + info->output_w * info->channels * stbir__type_size[type]; + memcpy(overwrite_output_before_pre, &((unsigned char*)output_data)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_output_after_pre, &((unsigned char*)output_data)[begin_forbidden], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_tempmem_before_pre, &((unsigned char*)tempmem)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_tempmem_after_pre, &((unsigned char*)tempmem)[tempmem_size_in_bytes], OVERWRITE_ARRAY_SIZE); +#endif + + STBIR_ASSERT(info->channels >= 0); + STBIR_ASSERT(info->channels <= STBIR_MAX_CHANNELS); + + if (info->channels < 0 || info->channels > STBIR_MAX_CHANNELS) + return 0; + + STBIR_ASSERT(info->horizontal_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + STBIR_ASSERT(info->vertical_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + + if (info->horizontal_filter >= STBIR__ARRAY_SIZE(stbir__filter_info_table)) + return 0; + if (info->vertical_filter >= STBIR__ARRAY_SIZE(stbir__filter_info_table)) + return 0; + + if (alpha_channel < 0) + flags |= STBIR_FLAG_ALPHA_USES_COLORSPACE | STBIR_FLAG_ALPHA_PREMULTIPLIED; + + if (!(flags&STBIR_FLAG_ALPHA_USES_COLORSPACE) || !(flags&STBIR_FLAG_ALPHA_PREMULTIPLIED)) { + STBIR_ASSERT(alpha_channel >= 0 && alpha_channel < info->channels); + } + + if (alpha_channel >= info->channels) + return 0; + + STBIR_ASSERT(tempmem); + + if (!tempmem) + return 0; + + STBIR_ASSERT(tempmem_size_in_bytes >= memory_required); + + if (tempmem_size_in_bytes < memory_required) + return 0; + + memset(tempmem, 0, tempmem_size_in_bytes); + + info->input_data = input_data; + info->input_stride_bytes = width_stride_input; + + info->output_data = output_data; + info->output_stride_bytes = width_stride_output; + + info->alpha_channel = alpha_channel; + info->flags = flags; + info->type = type; + info->edge_horizontal = edge_horizontal; + info->edge_vertical = edge_vertical; + info->colorspace = colorspace; + + info->horizontal_coefficient_width = stbir__get_coefficient_width (info->horizontal_filter, info->horizontal_scale); + info->vertical_coefficient_width = stbir__get_coefficient_width (info->vertical_filter , info->vertical_scale ); + info->horizontal_filter_pixel_width = stbir__get_filter_pixel_width (info->horizontal_filter, info->horizontal_scale); + info->vertical_filter_pixel_width = stbir__get_filter_pixel_width (info->vertical_filter , info->vertical_scale ); + info->horizontal_filter_pixel_margin = stbir__get_filter_pixel_margin(info->horizontal_filter, info->horizontal_scale); + info->vertical_filter_pixel_margin = stbir__get_filter_pixel_margin(info->vertical_filter , info->vertical_scale ); + + info->ring_buffer_length_bytes = info->output_w * info->channels * sizeof(float); + info->decode_buffer_pixels = info->input_w + info->horizontal_filter_pixel_margin * 2; + +#define STBIR__NEXT_MEMPTR(current, newtype) (newtype*)(((unsigned char*)current) + current##_size) + + info->horizontal_contributors = (stbir__contributors *) tempmem; + info->horizontal_coefficients = STBIR__NEXT_MEMPTR(info->horizontal_contributors, float); + info->vertical_contributors = STBIR__NEXT_MEMPTR(info->horizontal_coefficients, stbir__contributors); + info->vertical_coefficients = STBIR__NEXT_MEMPTR(info->vertical_contributors, float); + info->decode_buffer = STBIR__NEXT_MEMPTR(info->vertical_coefficients, float); + + if (stbir__use_height_upsampling(info)) + { + info->horizontal_buffer = NULL; + info->ring_buffer = STBIR__NEXT_MEMPTR(info->decode_buffer, float); + info->encode_buffer = STBIR__NEXT_MEMPTR(info->ring_buffer, float); + + STBIR_ASSERT((size_t)STBIR__NEXT_MEMPTR(info->encode_buffer, unsigned char) == (size_t)tempmem + tempmem_size_in_bytes); + } + else + { + info->horizontal_buffer = STBIR__NEXT_MEMPTR(info->decode_buffer, float); + info->ring_buffer = STBIR__NEXT_MEMPTR(info->horizontal_buffer, float); + info->encode_buffer = NULL; + + STBIR_ASSERT((size_t)STBIR__NEXT_MEMPTR(info->ring_buffer, unsigned char) == (size_t)tempmem + tempmem_size_in_bytes); + } + +#undef STBIR__NEXT_MEMPTR + + // This signals that the ring buffer is empty + info->ring_buffer_begin_index = -1; + + stbir__calculate_filters(info->horizontal_contributors, info->horizontal_coefficients, info->horizontal_filter, info->horizontal_scale, info->horizontal_shift, info->input_w, info->output_w); + stbir__calculate_filters(info->vertical_contributors, info->vertical_coefficients, info->vertical_filter, info->vertical_scale, info->vertical_shift, info->input_h, info->output_h); + + STBIR_PROGRESS_REPORT(0); + + if (stbir__use_height_upsampling(info)) + stbir__buffer_loop_upsample(info); + else + stbir__buffer_loop_downsample(info); + + STBIR_PROGRESS_REPORT(1); + +#ifdef STBIR_DEBUG_OVERWRITE_TEST + STBIR_ASSERT(memcmp(overwrite_output_before_pre, &((unsigned char*)output_data)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE) == 0); + STBIR_ASSERT(memcmp(overwrite_output_after_pre, &((unsigned char*)output_data)[begin_forbidden], OVERWRITE_ARRAY_SIZE) == 0); + STBIR_ASSERT(memcmp(overwrite_tempmem_before_pre, &((unsigned char*)tempmem)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE) == 0); + STBIR_ASSERT(memcmp(overwrite_tempmem_after_pre, &((unsigned char*)tempmem)[tempmem_size_in_bytes], OVERWRITE_ARRAY_SIZE) == 0); +#endif + + return 1; +} + + +static int stbir__resize_arbitrary( + void *alloc_context, + const void* input_data, int input_w, int input_h, int input_stride_in_bytes, + void* output_data, int output_w, int output_h, int output_stride_in_bytes, + float s0, float t0, float s1, float t1, float *transform, + int channels, int alpha_channel, stbir_uint32 flags, stbir_datatype type, + stbir_filter h_filter, stbir_filter v_filter, + stbir_edge edge_horizontal, stbir_edge edge_vertical, stbir_colorspace colorspace) +{ + stbir__info info; + int result; + size_t memory_required; + void* extra_memory; + + stbir__setup(&info, input_w, input_h, output_w, output_h, channels); + stbir__calculate_transform(&info, s0,t0,s1,t1,transform); + stbir__choose_filter(&info, h_filter, v_filter); + memory_required = stbir__calculate_memory(&info); + extra_memory = STBIR_MALLOC(memory_required, alloc_context); + + if (!extra_memory) + return 0; + + result = stbir__resize_allocated(&info, input_data, input_stride_in_bytes, + output_data, output_stride_in_bytes, + alpha_channel, flags, type, + edge_horizontal, edge_vertical, + colorspace, extra_memory, memory_required); + + STBIR_FREE(extra_memory, alloc_context); + + return result; +} + +STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,-1,0, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_LINEAR); +} + +STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,-1,0, STBIR_TYPE_FLOAT, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_LINEAR); +} + +STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_SRGB); +} + +STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + edge_wrap_mode, edge_wrap_mode, STBIR_COLORSPACE_SRGB); +} + +STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + +STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + stbir_uint16 *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT16, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + + +STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_FLOAT, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + + +STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + + +STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float x_scale, float y_scale, + float x_offset, float y_offset) +{ + float transform[4]; + transform[0] = x_scale; + transform[1] = y_scale; + transform[2] = x_offset; + transform[3] = y_offset; + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,transform,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + +STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float s0, float t0, float s1, float t1) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + s0,t0,s1,t1,NULL,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + +#endif // STB_IMAGE_RESIZE_IMPLEMENTATION + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/src/io/audio.h b/src/io/audio.h index 01aa189..6e2b77a 100644 --- a/src/io/audio.h +++ b/src/io/audio.h @@ -9,34 +9,128 @@ namespace io { class AudioSink { public: + enum { + MONO, + STEREO, + _TYPE_COUNT + }; + + struct AudioDevice_t { + std::string name; + int index; + int channels; + std::vector sampleRates; + std::string txtSampleRates; + }; + AudioSink() { } - AudioSink(dsp::stream* in, int bufferSize) { + AudioSink(int bufferSize) { _bufferSize = bufferSize; - _input = in; - buffer = new float[_bufferSize * 2]; - _volume = 1.0f; - Pa_Initialize(); - } - - void init(dsp::stream* in, int bufferSize) { - _bufferSize = bufferSize; - _input = in; - buffer = new float[_bufferSize * 2]; + monoBuffer = new float[_bufferSize]; + stereoBuffer = new dsp::StereoFloat_t[_bufferSize]; _volume = 1.0f; Pa_Initialize(); devTxtList = ""; int devCount = Pa_GetDeviceCount(); + devIndex = Pa_GetDefaultOutputDevice(); const PaDeviceInfo *deviceInfo; + PaStreamParameters outputParams; + outputParams.sampleFormat = paFloat32; + outputParams.hostApiSpecificStreamInfo = NULL; for(int i = 0; i < devCount; i++) { deviceInfo = Pa_GetDeviceInfo(i); + if (deviceInfo->maxOutputChannels < 1) { + continue; + } + AudioDevice_t dev; + dev.name = deviceInfo->name; + dev.index = i; + dev.channels = std::min(deviceInfo->maxOutputChannels, 2); + dev.sampleRates.clear(); + dev.txtSampleRates = ""; + for (int j = 0; j < 6; j++) { + outputParams.channelCount = dev.channels; + outputParams.device = dev.index; + outputParams.suggestedLatency = deviceInfo->defaultLowOutputLatency; + PaError err = Pa_IsFormatSupported(NULL, &outputParams, POSSIBLE_SAMP_RATE[j]); + if (err != paFormatIsSupported) { + continue; + } + dev.sampleRates.push_back(POSSIBLE_SAMP_RATE[j]); + dev.txtSampleRates += std::to_string((int)POSSIBLE_SAMP_RATE[j]); + dev.txtSampleRates += '\0'; + } + if (dev.sampleRates.size() == 0) { + continue; + } + if (i == devIndex) { + devListIndex = devices.size(); + } + devices.push_back(dev); devTxtList += deviceInfo->name; devTxtList += '\0'; } + } + + void init(int bufferSize) { + _bufferSize = bufferSize; + monoBuffer = new float[_bufferSize]; + stereoBuffer = new dsp::StereoFloat_t[_bufferSize]; + _volume = 1.0f; + Pa_Initialize(); + + devTxtList = ""; + int devCount = Pa_GetDeviceCount(); devIndex = Pa_GetDefaultOutputDevice(); + const PaDeviceInfo *deviceInfo; + PaStreamParameters outputParams; + outputParams.sampleFormat = paFloat32; + outputParams.hostApiSpecificStreamInfo = NULL; + for(int i = 0; i < devCount; i++) { + deviceInfo = Pa_GetDeviceInfo(i); + if (deviceInfo->maxOutputChannels < 1) { + continue; + } + AudioDevice_t dev; + dev.name = deviceInfo->name; + dev.index = i; + dev.channels = std::min(deviceInfo->maxOutputChannels, 2); + dev.sampleRates.clear(); + dev.txtSampleRates = ""; + for (int j = 0; j < 6; j++) { + outputParams.channelCount = dev.channels; + outputParams.device = dev.index; + outputParams.suggestedLatency = deviceInfo->defaultLowOutputLatency; + PaError err = Pa_IsFormatSupported(NULL, &outputParams, POSSIBLE_SAMP_RATE[j]); + if (err != paFormatIsSupported) { + continue; + } + dev.sampleRates.push_back(POSSIBLE_SAMP_RATE[j]); + dev.txtSampleRates += std::to_string((int)POSSIBLE_SAMP_RATE[j]); + dev.txtSampleRates += '\0'; + } + if (dev.sampleRates.size() == 0) { + continue; + } + if (i == devIndex) { + devListIndex = devices.size(); + } + devices.push_back(dev); + devTxtList += deviceInfo->name; + devTxtList += '\0'; + } + } + + void setMonoInput(dsp::stream* input) { + _monoInput = input; + } + + void setStereoInput(dsp::stream* input) { + _stereoInput = input; } void setVolume(float volume) { @@ -47,13 +141,25 @@ namespace io { if (running) { return; } + const PaDeviceInfo *deviceInfo; + AudioDevice_t dev = devices[devListIndex]; PaStreamParameters outputParams; + deviceInfo = Pa_GetDeviceInfo(dev.index); outputParams.channelCount = 2; outputParams.sampleFormat = paFloat32; outputParams.hostApiSpecificStreamInfo = NULL; - outputParams.device = devIndex; + outputParams.device = dev.index; outputParams.suggestedLatency = Pa_GetDeviceInfo(outputParams.device)->defaultLowOutputLatency; - PaError err = Pa_OpenStream(&stream, NULL, &outputParams, 48000.0f, _bufferSize, paClipOff, _callback, this); + PaError err; + if (streamType == MONO) { + err = Pa_OpenStream(&stream, NULL, &outputParams, _sampleRate, _bufferSize, NULL, + (dev.channels == 2) ? _mono_to_stereo_callback : _mono_to_mono_callback, this); + } + else { + err = Pa_OpenStream(&stream, NULL, &outputParams, _sampleRate, _bufferSize, NULL, + (dev.channels == 2) ? _stereo_to_stereo_callback : _stereo_to_mono_callback, this); + } + if (err != 0) { spdlog::error("Error while opening audio stream: ({0}) => {1}", err, Pa_GetErrorText(err)); return; @@ -81,47 +187,128 @@ namespace io { return; } _bufferSize = blockSize; + delete[] monoBuffer; + delete[] stereoBuffer; + monoBuffer = new float[_bufferSize]; + stereoBuffer = new dsp::StereoFloat_t[_bufferSize]; + } + + void setSampleRate(float sampleRate) { + _sampleRate = sampleRate; } void setDevice(int id) { - if (devIndex == id) { + if (devListIndex == id) { return; } if (running) { return; } - devIndex = id; + devListIndex = id; + devIndex = devices[id].index; } int getDeviceId() { - return devIndex; + return devListIndex; + } + + void setStreamType(int type) { + streamType = type; } std::string devTxtList; + std::vector devices; private: - static int _callback(const void *input, + static int _mono_to_mono_callback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData ) { AudioSink* _this = (AudioSink*)userData; float* outbuf = (float*)output; - _this->_input->read(_this->buffer, frameCount); + _this->_monoInput->read(_this->monoBuffer, frameCount); float vol = powf(_this->_volume, 2); for (int i = 0; i < frameCount; i++) { - outbuf[(i * 2) + 0] = _this->buffer[i] * vol; - outbuf[(i * 2) + 1] = _this->buffer[i] * vol; + outbuf[i] = _this->monoBuffer[i] * vol; } return 0; } + static int _stereo_to_stereo_callback(const void *input, + void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void *userData ) { + AudioSink* _this = (AudioSink*)userData; + dsp::StereoFloat_t* outbuf = (dsp::StereoFloat_t*)output; + _this->_stereoInput->read(_this->stereoBuffer, frameCount); + + // Note: Calculate the power in the UI instead of here + + float vol = powf(_this->_volume, 2); + for (int i = 0; i < frameCount; i++) { + outbuf[i].l = _this->stereoBuffer[i].l * vol; + outbuf[i].r = _this->stereoBuffer[i].r * vol; + } + return 0; + } + + static int _mono_to_stereo_callback(const void *input, + void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void *userData ) { + AudioSink* _this = (AudioSink*)userData; + dsp::StereoFloat_t* outbuf = (dsp::StereoFloat_t*)output; + _this->_monoInput->read(_this->monoBuffer, frameCount); + + float vol = powf(_this->_volume, 2); + for (int i = 0; i < frameCount; i++) { + outbuf[i].l = _this->monoBuffer[i] * vol; + outbuf[i].r = _this->monoBuffer[i] * vol; + } + return 0; + } + + static int _stereo_to_mono_callback(const void *input, + void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void *userData ) { + AudioSink* _this = (AudioSink*)userData; + float* outbuf = (float*)output; + _this->_stereoInput->read(_this->stereoBuffer, frameCount); + + // Note: Calculate the power in the UI instead of here + + float vol = powf(_this->_volume, 2); + for (int i = 0; i < frameCount; i++) { + outbuf[i] = ((_this->stereoBuffer[i].l + _this->stereoBuffer[i].r) / 2.0f) * vol; + } + return 0; + } + + float POSSIBLE_SAMP_RATE[6] = { + 48000.0f, + 44100.0f, + 24000.0f, + 22050.0f, + 12000.0f, + 11025.0f + }; + + int streamType; int devIndex; + int devListIndex; + float _sampleRate; int _bufferSize; - dsp::stream* _input; - float* buffer; - float _volume; + dsp::stream* _monoInput; + dsp::stream* _stereoInput; + float* monoBuffer; + dsp::StereoFloat_t* stereoBuffer; + float _volume = 1.0f; PaStream *stream; bool running = false; }; diff --git a/src/io/soapy.h b/src/io/soapy.h index cf48186..16ddb7a 100644 --- a/src/io/soapy.h +++ b/src/io/soapy.h @@ -52,6 +52,7 @@ namespace io { devList = SoapySDR::Device::enumerate(); txtDevList = ""; + devNameList.clear(); if (devList.size() == 0) { txtDevList += '\0'; return; @@ -59,6 +60,7 @@ namespace io { for (int i = 0; i < devList.size(); i++) { txtDevList += devList[i]["label"]; txtDevList += '\0'; + devNameList.push_back(devList[i]["label"]); } } @@ -74,6 +76,7 @@ namespace io { txtSampleRateList += std::to_string((int)sampleRates[i]); txtSampleRateList += '\0'; } + _sampleRate = sampleRates[0]; gainList = dev->listGains(SOAPY_SDR_RX, 0); gainRanges.clear(); @@ -84,7 +87,11 @@ namespace io { currentGains = new float[gainList.size()]; for (int i = 0; i < gainList.size(); i++) { gainRanges.push_back(dev->getGainRange(SOAPY_SDR_RX, 0, gainList[i])); - currentGains[i] = dev->getGain(SOAPY_SDR_RX, 0, gainList[i]); + SoapySDR::Range rng = dev->getGainRange(SOAPY_SDR_RX, 0, gainList[i]); + + spdlog::info("{0}: {1} -> {2} (Step: {3})", gainList[i], rng.minimum(), rng.maximum(), rng.step()); + + currentGains[i] = rng.minimum(); } } @@ -109,8 +116,14 @@ namespace io { return running; } + float getSampleRate() { + return _sampleRate; + } + SoapySDR::KwargsList devList; + std::vector devNameList; std::string txtDevList; + std::vector sampleRates; std::string txtSampleRateList; diff --git a/src/main.cpp b/src/main.cpp index 507bab9..6c290b6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,12 +5,17 @@ #include #include #include -#include +#include #include #include #include #include #include +#include +#include + +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include #ifdef _WIN32 #include @@ -37,6 +42,7 @@ int main() { glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac + // Create window with graphics context GLFWwindow* window = glfwCreateWindow(1280, 720, "SDR++ v" VERSION_STR " (Built at " __TIME__ ", " __DATE__ ")", NULL, NULL); @@ -45,6 +51,33 @@ int main() { glfwMakeContextCurrent(window); glfwSwapInterval(1); // Enable vsync + // Load app icon + GLFWimage icons[10]; + icons[0].pixels = stbi_load("res/icons/sdrpp.png", &icons[0].width, &icons[0].height, 0, 4); + icons[1].pixels = (unsigned char*)malloc(16 * 16 * 4); icons[1].width = icons[1].height = 16; + icons[2].pixels = (unsigned char*)malloc(24 * 24 * 4); icons[2].width = icons[2].height = 24; + icons[3].pixels = (unsigned char*)malloc(32 * 32 * 4); icons[3].width = icons[3].height = 32; + icons[4].pixels = (unsigned char*)malloc(48 * 48 * 4); icons[4].width = icons[4].height = 48; + icons[5].pixels = (unsigned char*)malloc(64 * 64 * 4); icons[5].width = icons[5].height = 64; + icons[6].pixels = (unsigned char*)malloc(96 * 96 * 4); icons[6].width = icons[6].height = 96; + icons[7].pixels = (unsigned char*)malloc(128 * 128 * 4); icons[7].width = icons[7].height = 128; + icons[8].pixels = (unsigned char*)malloc(196 * 196 * 4); icons[8].width = icons[8].height = 196; + icons[9].pixels = (unsigned char*)malloc(256 * 256 * 4); icons[9].width = icons[9].height = 256; + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[1].pixels, 16, 16, 16 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[2].pixels, 24, 24, 24 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[3].pixels, 32, 32, 32 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[4].pixels, 48, 48, 48 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[5].pixels, 64, 64, 64 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[6].pixels, 96, 96, 96 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[7].pixels, 128, 128, 128 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[8].pixels, 196, 196, 196 * 4, 4); + stbir_resize_uint8(icons[0].pixels, icons[0].width, icons[0].height, icons[0].width * 4, icons[9].pixels, 256, 256, 256 * 4, 4); + glfwSetWindowIcon(window, 10, icons); + stbi_image_free(icons[0].pixels); + for (int i = 1; i < 10; i++) { + free(icons[i].pixels); + } + if (glewInit() != GLEW_OK) { spdlog::error("Failed to initialize OpenGL loader!"); return 1; @@ -60,9 +93,12 @@ int main() { ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init("#version 150"); - setImguiStyle(io); + // Load config + spdlog::info("Loading config"); + config::load("config.json"); + config::startAutoSave(); - windowInit(); + style::setDefaultStyle(); spdlog::info("Loading icons"); icons::load(); @@ -73,6 +109,8 @@ int main() { spdlog::info("Loading band plans color table"); bandplan::loadColorTable("band_colors.json"); + windowInit(); + spdlog::info("Ready."); // Main loop @@ -88,12 +126,9 @@ int main() { glfwGetWindowSize(window, &wwidth, &wheight); ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(wwidth, wheight)); - ImGui::Begin("Main", NULL, WINDOW_FLAGS); - + drawWindow(); - ImGui::End(); - // Rendering ImGui::Render(); int display_w, display_h; diff --git a/src/main_window.cpp b/src/main_window.cpp index a20eaca..ed48f9d 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -9,6 +9,7 @@ fftwf_plan p; float* tempData; float* uiGains; char buf[1024]; +ImFont* bigFont; int fftSize = 8192 * 8; @@ -37,14 +38,68 @@ void fftHandler(dsp::complex_t* samples) { } dsp::NullSink sink; +int devId = 0; +int srId = 0; +watcher bandplanId(0, true); +watcher freq(90500000L); +int demod = 1; +watcher vfoFreq(92000000.0f); +float dummyVolume = 1.0f; +float* volume = &dummyVolume; +float fftMin = -70.0f; +float fftMax = 0.0f; +watcher offset(0.0f, true); +watcher bw(8000000.0f, true); +int sampleRate = 8000000; +bool playing = false; +watcher dcbias(false, false); +watcher bandPlanEnabled(true, false); +bool showCredits = false; +std::string audioStreamName = ""; +std::string sourceName = ""; + +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]; + + // Set sample rate + + spdlog::warn("Type {0}", sourceSettings.contains("sampleRate")); + + sampleRate = sourceSettings["sampleRate"]; + + sigPath.setSampleRate(sampleRate); + soapy.setSampleRate(sampleRate); + auto _srIt = std::find(soapy.sampleRates.begin(), soapy.sampleRates.end(), sampleRate); + srId = std::distance(soapy.sampleRates.begin(), _srIt); + spdlog::warn("sr {0}", srId); + + // Set gains + delete uiGains; + uiGains = new float[soapy.gainList.size()]; + int i = 0; + for (std::string gainName : soapy.gainList) { + uiGains[i] = sourceSettings["gains"][gainName]; + soapy.setGain(i, uiGains[i]); + i++; + } + + // Update GUI + wtf.setBandwidth(sampleRate); + wtf.setViewBandwidth(sampleRate); + bw.val = sampleRate; +} void windowInit() { - int sampleRate = 8000000; - wtf.setBandwidth(sampleRate); - wtf.setCenterFrequency(90500000); - fSel.init(); - fSel.setFrequency(90500000); fft_in = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * fftSize); fft_out = (fftwf_complex*) fftwf_malloc(sizeof(fftwf_complex) * fftSize); @@ -60,24 +115,92 @@ void windowInit() { spdlog::info("Loading modules"); mod::initAPI(&wtf); mod::loadFromList("module_list.json"); -} -watcher devId(0, true); -watcher srId(0, true); -watcher bandplanId(0, true); -watcher freq(90500000L); -int demod = 1; -watcher vfoFreq(92000000.0f); -float dummyVolume = 1.0f; -float* volume = &dummyVolume; -float fftMin = -70.0f; -float fftMax = 0.0f; -watcher offset(0.0f, true); -watcher bw(8000000.0f, true); -int sampleRate = 1000000; -bool playing = false; -watcher dcbias(false, false); -watcher bandPlanEnabled(true, false); + bigFont = ImGui::GetIO().Fonts->AddFontFromFileTTF("res/fonts/Roboto-Medium.ttf", 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) { + sampleRate = soapy.getSampleRate(); + } + // 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 for audio streams + window size/fullscreen + + + // Update UI settings + fftMin = config::config["min"]; + fftMax = config::config["max"]; + wtf.setFFTMin(fftMin); + wtf.setWaterfallMin(fftMin); + wtf.setFFTMax(fftMax); + wtf.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)); + spdlog::warn("{0} => {1}", bandplanId.val, bandPlanName); + + if (bandPlanEnabled.val) { + wtf.bandplan = &bandplan::bandplans[bandPlanName]; + } + else { + wtf.bandplan = NULL; + } + } + else { + bandplanId.val = 0; + } + bandplanId.markAsChanged(); + + + fSel.setFrequency(frequency); + fSel.frequencyChanged = false; + soapy.setFrequency(frequency); + wtf.setCenterFrequency(frequency); + wtf.setBandwidth(sampleRate); + wtf.setViewBandwidth(sampleRate); + bw.val = sampleRate; + wtf.vfoFreqChanged = false; + wtf.centerFreqMoved = false; + wtf.selectFirstVFO(); + + + audioStreamName = audio::getNameFromVFO(wtf.selectedVFO); + if (audioStreamName != "") { + volume = &audio::streams[audioStreamName]->volume; + } +} void setVFO(float freq) { ImGui::WaterfallVFO* vfo = wtf.vfos[wtf.selectedVFO]; @@ -165,14 +288,15 @@ void setVFO(float freq) { } void drawWindow() { - if (wtf.selectedVFO == "" && wtf.vfos.size() > 0) { - wtf.selectFirstVFO(); - } + ImGui::Begin("Main", NULL, WINDOW_FLAGS); ImGui::WaterfallVFO* vfo = wtf.vfos[wtf.selectedVFO]; if (vfo->centerOffsetChanged) { fSel.setFrequency(wtf.getCenterFrequency() + vfo->generalOffset); + fSel.frequencyChanged = false; + config::config["frequency"] = fSel.frequency; + config::configModified = true; } vfoman::updateFromWaterfall(); @@ -180,7 +304,14 @@ void drawWindow() { if (wtf.selectedVFOChanged) { wtf.selectedVFOChanged = false; fSel.setFrequency(vfo->generalOffset + wtf.getCenterFrequency()); + fSel.frequencyChanged = false; mod::broadcastEvent(mod::EVENT_SELECTED_VFO_CHANGED); + audioStreamName = audio::getNameFromVFO(wtf.selectedVFO); + if (audioStreamName != "") { + volume = &audio::streams[audioStreamName]->volume; + } + config::config["frequency"] = fSel.frequency; + config::configModified = true; } if (fSel.frequencyChanged) { @@ -189,36 +320,16 @@ void drawWindow() { vfo->centerOffsetChanged = false; vfo->lowerOffsetChanged = false; vfo->upperOffsetChanged = false; + config::config["frequency"] = fSel.frequency; + config::configModified = true; } if (wtf.centerFreqMoved) { wtf.centerFreqMoved = false; soapy.setFrequency(wtf.getCenterFrequency()); fSel.setFrequency(wtf.getCenterFrequency() + vfo->generalOffset); - } - - if (devId.changed() && soapy.devList.size() > 0) { - spdlog::info("Changed input device: {0}", devId.val); - soapy.setDevice(soapy.devList[devId.val]); - srId.markAsChanged(); - if (soapy.gainList.size() == 0) { - return; - } - delete[] uiGains; - uiGains = new float[soapy.gainList.size()]; - for (int i = 0; i < soapy.gainList.size(); i++) { - uiGains[i] = soapy.currentGains[i]; - } - } - - if (srId.changed() && soapy.devList.size() > 0) { - spdlog::info("Changed sample rate: {0}", srId.val); - sampleRate = soapy.sampleRates[srId.val]; - soapy.setSampleRate(sampleRate); - wtf.setBandwidth(sampleRate); - wtf.setViewBandwidth(sampleRate); - sigPath.setSampleRate(sampleRate); - bw.val = sampleRate; + config::config["frequency"] = fSel.frequency; + config::configModified = true; } if (dcbias.changed()) { @@ -248,13 +359,13 @@ void drawWindow() { // To Bar if (playing) { - if (ImGui::ImageButton(icons::STOP_RAW, ImVec2(30, 30))) { + 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_RAW, ImVec2(30, 30)) && soapy.devList.size() > 0) { + if (ImGui::ImageButton(icons::PLAY, ImVec2(40, 40), ImVec2(0, 0), ImVec2(1, 1), 0) && soapy.devList.size() > 0) { soapy.start(); soapy.setFrequency(wtf.getCenterFrequency()); playing = true; @@ -265,12 +376,31 @@ void drawWindow() { ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8); ImGui::SetNextItemWidth(200); - ImGui::SliderFloat("##_2_", volume, 0.0f, 1.0f, ""); + if (ImGui::SliderFloat("##_2_", volume, 0.0f, 1.0f, "")) { + if (audioStreamName != "") { + audio::streams[audioStreamName]->audio->setVolume(*volume); + } + } ImGui::SameLine(); fSel.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; + } + ImGui::Columns(3, "WindowColumns", false); ImVec2 winSize = ImGui::GetWindowSize(); ImGui::SetColumnWidth(0, 300); @@ -279,88 +409,223 @@ void drawWindow() { // Left Column ImGui::BeginChild("Left Column"); + float menuColumnWidth = ImGui::GetContentRegionAvailWidth(); - if (ImGui::CollapsingHeader("Source")) { - ImGui::PushItemWidth(ImGui::GetWindowSize().x); - ImGui::Combo("##_0_", &devId.val, soapy.txtDevList.c_str()); + 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 (soapy.gainList.size() == 0) { + return; + } + delete[] uiGains; + uiGains = new float[soapy.gainList.size()]; + for (int i = 0; i < soapy.gainList.size(); i++) { + uiGains[i] = soapy.currentGains[i]; + } + + if (config::config["sourceSettings"].contains(sourceName)) { + loadSourceConfig(sourceName); + } + else { + srId = 0; + sampleRate = soapy.getSampleRate(); + bw.val = sampleRate; + wtf.setBandwidth(sampleRate); + wtf.setViewBandwidth(sampleRate); + sigPath.setSampleRate(sampleRate); + for (int i = 0; i < soapy.gainList.size(); i++) { + uiGains[i] = soapy.gainRanges[i].minimum(); + } + } + setVFO(fSel.frequency); + config::config["source"] = sourceName; + config::configModified = true; + } ImGui::PopItemWidth(); - if (!playing) { - ImGui::Combo("##_1_", &srId.val, soapy.txtSampleRateList.c_str()); - } - else { - ImGui::Text("%.0f Samples/s", soapy.sampleRates[srId.val]); + if (ImGui::Combo("##_1_", &srId, soapy.txtSampleRateList.c_str())) { + spdlog::info("Changed sample rate: {0}", srId); + sampleRate = soapy.sampleRates[srId]; + soapy.setSampleRate(sampleRate); + wtf.setBandwidth(sampleRate); + wtf.setViewBandwidth(sampleRate); + sigPath.setSampleRate(sampleRate); + bw.val = sampleRate; + + if (!config::config["sourceSettings"].contains(sourceName)) { + saveCurrentSource(); + } + config::config["sourceSettings"][sourceName]["sampleRate"] = sampleRate; + config::configModified = true; } ImGui::SameLine(); - if (ImGui::Button("Refresh")) { + if (ImGui::Button("Refresh", ImVec2(menuColumnWidth - ImGui::GetCursorPosX(), 0.0f))) { soapy.refresh(); } + 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::SliderFloat(buf, &uiGains[i], soapy.gainRanges[i].minimum(), soapy.gainRanges[i].maximum()); + + 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]); + config::config["sourceSettings"][sourceName]["gains"][soapy.gainList[i]] = uiGains[i]; + config::configModified = true; + } + ImGui::PopItemWidth(); if (uiGains[i] != soapy.currentGains[i]) { - soapy.setGain(i, uiGains[i]); + } } + + ImGui::Spacing(); } for (int i = 0; i < modCount; i++) { - if (ImGui::CollapsingHeader(mod::moduleNames[i].c_str())) { + if (ImGui::CollapsingHeader(mod::moduleNames[i].c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { mod = mod::modules[mod::moduleNames[i]]; mod._DRAW_MENU_(mod.ctx); + ImGui::Spacing(); } } - ImGui::CollapsingHeader("Audio"); - if (ImGui::CollapsingHeader("Band Plan")) { - ImGui::PushItemWidth(ImGui::GetWindowSize().x); - ImGui::Combo("##_4_", &bandplanId.val, bandplan::bandplanNameTxt.c_str()); + 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); + 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]); + audio::startStream(name); + stream->sampleRateId = 0; + } + 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]); + audio::startStream(name); + } + if (ImGui::SliderFloat(("##_audio_vol_0_" + name).c_str(), &stream->volume, 0.0f, 1.0f, "")) { + stream->audio->setVolume(stream->volume); + } + 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(); - ImGui::Checkbox("Enabled", &bandPlanEnabled.val); + 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, plan.countryCode); ImGui::Text("Author: %s", plan.authorName); + ImGui::Spacing(); } - ImGui::CollapsingHeader("Display"); + if (ImGui::CollapsingHeader("Display")) { + ImGui::Spacing(); + } - ImGui::CollapsingHeader("Recording"); + if (ImGui::CollapsingHeader("Recording")) { + 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: %.1f FPS", wtf.getCenterFrequency()); + ImGui::Text("Center Frequency: %.0f Hz", wtf.getCenterFrequency()); + + ImGui::Spacing(); } ImGui::EndChild(); // Right Column + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGui::NextColumn(); - ImGui::BeginChild("Waterfall"); + wtf.draw(); + ImGui::EndChild(); + ImGui::PopStyleVar(); 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::VSliderFloat("##_8_", ImVec2(20.0f, 150.0f), &fftMax, 0.0f, -100.0f, ""); + ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - 10); + if (ImGui::VSliderFloat("##_8_", ImVec2(20.0f, 150.0f), &fftMax, 0.0f, -100.0f, "")) { + 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::VSliderFloat("##_9_", ImVec2(20.0f, 150.0f), &fftMin, 0.0f, -100.0f, ""); + ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.0f) - 10); + if (ImGui::VSliderFloat("##_9_", ImVec2(20.0f, 150.0f), &fftMin, 0.0f, -100.0f, "")) { + config::config["min"] = fftMin; + config::configModified = true; + } + + ImGui::EndChild(); if (bw.changed()) { wtf.setViewBandwidth(bw.val); @@ -371,6 +636,52 @@ void drawWindow() { wtf.setFFTMax(fftMax); wtf.setWaterfallMin(fftMin); wtf.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"); + + // 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) { diff --git a/src/main_window.h b/src/main_window.h index 2cf7d24..2b7fe92 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -23,6 +23,9 @@ #include #include #include +#include +#include +#include #define WINDOW_FLAGS ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground diff --git a/src/module.cpp b/src/module.cpp index d19c150..ef0fe3e 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace mod { API_t API; @@ -14,6 +15,8 @@ namespace mod { void initAPI(ImGui::WaterFall* wtf) { _wtf = wtf; + + // VFO Manager API.registerVFO = vfoman::create; API.setVFOOffset = vfoman::setOffset; API.setVFOCenterOffset = vfoman::setCenterOffset; @@ -22,9 +25,23 @@ namespace mod { API.getVFOOutputBlockSize = vfoman::getOutputBlockSize; API.setVFOReference = vfoman::setReference; API.removeVFO = vfoman::remove; + + // GUI API.getSelectedVFOName = api_getSelectedVFOName; API.bindVolumeVariable = bindVolumeVariable; API.unbindVolumeVariable = unbindVolumeVariable; + + // Audio + API.registerMonoStream = audio::registerMonoStream; + API.registerStereoStream = audio::registerStereoStream; + API.startStream = audio::startStream; + API.stopStream = audio::stopStream; + API.removeStream = audio::removeStream; + API.bindToStreamMono = audio::bindToStreamMono; + API.bindToStreamStereo = audio::bindToStreamStereo; + API.setBlockSize = audio::setBlockSize; + API.unbindFromStreamMono = audio::unbindFromStreamMono; + API.unbindFromStreamStereo = audio::unbindFromStreamStereo; } void loadModule(std::string path, std::string name) { diff --git a/src/module.h b/src/module.h index ff57504..2bde085 100644 --- a/src/module.h +++ b/src/module.h @@ -29,10 +29,22 @@ namespace mod { int (*getVFOOutputBlockSize)(std::string name); void (*setVFOReference)(std::string name, int ref); void (*removeVFO)(std::string name); + std::string (*getSelectedVFOName)(void); void (*bindVolumeVariable)(float* vol); void (*unbindVolumeVariable)(void); + float (*registerMonoStream)(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx); + float (*registerStereoStream)(dsp::stream* stream, std::string name, std::string vfoName, int (*sampleRateChangeHandler)(void* ctx, float sampleRate), void* ctx); + void (*startStream)(std::string name); + void (*stopStream)(std::string name); + void (*removeStream)(std::string name); + dsp::stream* (*bindToStreamMono)(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx); + dsp::stream* (*bindToStreamStereo)(std::string name, void (*streamRemovedHandler)(void* ctx), void (*sampleRateChangeHandler)(void* ctx, float sampleRate, int blockSize), void* ctx); + void (*setBlockSize)(std::string name, int blockSize); + void (*unbindFromStreamMono)(std::string name, dsp::stream* stream); + void (*unbindFromStreamStereo)(std::string name, dsp::stream* stream); + enum { REF_LOWER, REF_CENTER, diff --git a/src/signal_path.h b/src/signal_path.h index b7998f4..929adb1 100644 --- a/src/signal_path.h +++ b/src/signal_path.h @@ -37,7 +37,7 @@ private: dsp::HandlerSink fftHandlerSink; // VFO - dsp::DynamicSplitter dynSplit; + dsp::DynamicSplitter dynSplit; std::map vfos; float sampleRate; diff --git a/src/style.h b/src/style.h new file mode 100644 index 0000000..3ce378a --- /dev/null +++ b/src/style.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace style { + void setDefaultStyle(); + void beginDisabled(); + void endDisabled(); +} \ No newline at end of file diff --git a/src/styles.cpp b/src/styles.cpp new file mode 100644 index 0000000..6eb344f --- /dev/null +++ b/src/styles.cpp @@ -0,0 +1,28 @@ +#include + +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("res/fonts/Roboto-Medium.ttf", 16.0f); + + ImGui::StyleColorsDark(); + //ImGui::StyleColorsLight(); + } + + void beginDisabled() { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.35f, 0.35f, 0.35f, 0.35f)); + } + + void endDisabled() { + ImGui::PopItemFlag(); + ImGui::PopStyleColor(2); + } +} \ No newline at end of file diff --git a/src/styles.h b/src/styles.h deleted file mode 100644 index b3ba260..0000000 --- a/src/styles.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -#include - -void setImguiStyle(ImGuiIO& io) { - 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; - - io.Fonts->AddFontFromFileTTF("res/fonts/Roboto-Medium.ttf", 16.0f); - - ImGui::StyleColorsDark(); - //ImGui::StyleColorsLight(); -} \ No newline at end of file diff --git a/src/waterfall.cpp b/src/waterfall.cpp index 4f91e2c..9fe0556 100644 --- a/src/waterfall.cpp +++ b/src/waterfall.cpp @@ -195,13 +195,19 @@ namespace ImGui { ImVec2 mousePos = ImGui::GetMousePos(); ImVec2 drag = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left); ImVec2 dragOrigin(mousePos.x - drag.x, mousePos.y - drag.y); - bool draging = ImGui::IsMouseDragging(ImGuiMouseButton_Left); + + 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 (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (mouseClicked) { for (auto const& [name, _vfo] : vfos) { if (name == selectedVFO) { continue; @@ -264,9 +270,11 @@ namespace ImGui { float* tempData = new float[dataWidth]; float pixel; float dataRange = waterfallMax - waterfallMin; + int size; for (int i = 0; i < count; i++) { - drawDataSize = (viewBandwidth / wholeBandwidth) * rawFFTs[i].size(); - drawDataStart = (((float)rawFFTs[i].size() / 2.0f) * (offsetRatio + 1)) - (drawDataSize / 2); + 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(tempData[j], waterfallMin, waterfallMax) - waterfallMin) / dataRange; @@ -365,8 +373,8 @@ namespace ImGui { freqAreaMin = ImVec2(widgetPos.x + 50, widgetPos.y + fftHeight + 11); freqAreaMax = ImVec2(widgetPos.x + dataWidth + 50, widgetPos.y + fftHeight + 50); - maxHSteps = dataWidth / 50; - maxVSteps = fftHeight / 15; + maxHSteps = dataWidth / (ImGui::CalcTextSize("000.000").x + 10); + maxVSteps = fftHeight / (ImGui::CalcTextSize("000.000").y); range = findBestRange(viewBandwidth, maxHSteps); vRange = findBestRange(fftMax - fftMin, maxVSteps); diff --git a/win32/resources.rc b/win32/resources.rc new file mode 100644 index 0000000..6e89f0f --- /dev/null +++ b/win32/resources.rc @@ -0,0 +1 @@ +IDR_MAINFRAME ICON "../res/icons/sdrpp.ico" \ No newline at end of file