diff --git a/.gitmodules b/.gitmodules index 84e9e94..1434591 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "implot"] path = implot url = https://github.com/epezent/implot +[submodule "AudioFFT"] + path = AudioFFT + url = https://github.com/HiFi-LoFi/AudioFFT diff --git a/AudioFFT b/AudioFFT new file mode 160000 index 0000000..0893b53 --- /dev/null +++ b/AudioFFT @@ -0,0 +1 @@ +Subproject commit 0893b532dd357c7270609425f5ae9d9b5ae7d725 diff --git a/CMakeLists.txt b/CMakeLists.txt index 750ad67..d0794b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,9 +55,16 @@ target_link_libraries(sokol PUBLIC cimgui) target_include_directories(sokol INTERFACE sokol) +#=== LIBRARY: AudioFFT +add_library(audiofft STATIC + AudioFFT/AudioFFT.h + AudioFFT/AudioFFT.cpp) +target_include_directories(cimgui INTERFACE AudioFFT) + #=== EXECUTABLE: qb set(QB_SOURCES launch.c + audionode.cpp colors.cpp colors.hpp qb.cpp @@ -75,8 +82,8 @@ else() add_executable(qb ${QB_SOURCES}) endif() -target_include_directories(qb PRIVATE cimgui/imgui implot) -target_link_libraries(qb sokol) +target_include_directories(qb PRIVATE cimgui/imgui implot AudioFFT) +target_link_libraries(qb sokol audiofft) # explicitly strip dead code if (CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_SYSTEM_NAME STREQUAL Emscripten) diff --git a/audionode.cpp b/audionode.cpp new file mode 100644 index 0000000..ea15df4 --- /dev/null +++ b/audionode.cpp @@ -0,0 +1,126 @@ +#include "qb.hpp" +#include "task.hpp" +#include "AudioFile/AudioFile.h" + +namespace qb { + audio_node::audio_node(const context *c, const std::string &path) + : node(c, path, qb::node_type::audio) + , _path(path) + , _loadstate(load_state::loading) + { + taskpool::get().submit_async([this]() { this->load_async(); }); + } + + const std::vector& audio_node::packed() const { + if (!_data || _data->getNumChannels() != 1) { + return _packed; + } + return _data->samples[0]; + } + + static constexpr double render_rate_hi = 16; + static constexpr double render_rate_lo = 4; + static constexpr int hires_frame_limit = 1000; + + const std::vector& audio_node::rendered() const { + double visframes = 2 * _c->visrad; + return visframes > hires_frame_limit ? _rlo : _rhi; + } + + double audio_node::rsamplerate() const { + double visframes = 2 * _c->visrad; + return (visframes > hires_frame_limit ? render_rate_lo : render_rate_hi); + } + + void audio_node::load_async() { + taskpool::get().submit_frame([this]() { + this->_loadstate = load_state(load_state::loading, "loading file"); + }); + _data.reset(new audio_file()); + const bool success = _data->load(_path); + if (!success) { + taskpool::get().submit_frame([this]() { + this->_loadstate = load_state(load_state::failed); + }); + return; + } + + taskpool::get().submit_frame([this]() { + this->_loadstate = + load_state(load_state::loading, "generating hi-res preview"); + }); + _rhi = render_samples(render_rate_hi); + + taskpool::get().submit_frame([this]() { + this->_loadstate = + load_state(load_state::loading, "generating lo-res preview"); + }); + _rlo = render_samples(render_rate_lo); + + taskpool::get().submit_frame([this]() { + this->_loadstate = + load_state(load_state::loading, "packing samples for playback"); + }); + if (_data->getNumChannels() != 1) { + const size_t channels = _data->getNumChannels(); + const size_t n = _data->getNumSamplesPerChannel(); + _packed.reserve(channels * n); + for (size_t i = 0; i < n; i++) { + for (size_t c = 0; c < channels; c++) { + _packed.push_back(_data->samples[c][i]); + } + } + } + + // update load state on main thread: this is what governs whether the UI + // accesses the rest of the state here or not. + taskpool::get().submit_frame([this]() { + this->_loadstate = load_state(load_state::loaded); + }); + } + + std::vector audio_node::render_samples(double framesamples) { + const size_t samples = _data->getNumSamplesPerChannel(); + const double spr = + (double)_data->getSampleRate() / (framesamples * _c->framerate()); + const size_t rsamples = (size_t) ceil((double)samples / spr); + const int channels = _data->getNumChannels(); + + std::vector rendered; + rendered.resize(rsamples); + float absmax = 0; + for (int i = 0; i < rsamples; i++) { + const double s1 = i * spr; + const double s2 = (i + 1) * spr; + double sum = 0; + for (int chan = 0; chan < channels; chan++) { + for (size_t s = (size_t)floor(s1); + s < (size_t)ceil(s2) && s < _data->samples[chan].size(); + s++) { + double x = _data->samples[chan][s]; + sum += x; + } + } + const float r = (float) (sum / (spr * channels)); + absmax = std::max(absmax, fabsf(r)); + rendered[i] = r; + } + for (float &f : rendered) { + f /= absmax; + } + return rendered; + } + + int audio_node::minframe() const { + return 0; + } + + int audio_node::maxframe() const { + if (_loadstate.stage != load_state::loaded) { + return 0; + } + return (int) ceil(_data->getLengthInSeconds() * _c->framerate()); + } + + audio_node::~audio_node() = default; +} diff --git a/nodeui.cpp b/nodeui.cpp index 5bcd96d..f61005c 100644 --- a/nodeui.cpp +++ b/nodeui.cpp @@ -43,7 +43,7 @@ const std::vector &r = node->rendered(); const context *c = node->ctx(); - const int rad = 100; + const int rad = 40; const double rsamplerate = node->rsamplerate(); const int64_t s0 = (long) floor(rsamplerate * (c->playhead - c->visrad)); const int64_t s1 = (long) ceil(rsamplerate * (c->playhead + c->visrad)); diff --git a/qb.cpp b/qb.cpp index d4b6aa2..e9387e6 100644 --- a/qb.cpp +++ b/qb.cpp @@ -2,7 +2,6 @@ #include "colors.hpp" #include "nodeui.hpp" #include "task.hpp" -#include "AudioFile/AudioFile.h" #include "sokol/sokol_app.h" #include "sokol/sokol_audio.h" @@ -196,126 +195,5 @@ node::node(const context *c, const std::string &n, node_type t) : _c(c), _name(n), _type(t) {} - - audio_node::audio_node(const context *c, const std::string &path) - : node(c, path, qb::node_type::audio) - , _path(path) - , _loadstate(load_state::loading) - { - taskpool::get().submit_async([this]() { this->load_async(); }); - } - - const std::vector& audio_node::packed() const { - if (!_data || _data->getNumChannels() != 1) { - return _packed; - } - return _data->samples[0]; - } - - static constexpr double render_rate_hi = 8; - static constexpr double render_rate_lo = 2; - static constexpr int hires_frame_limit = 1000; - - const std::vector& audio_node::rendered() const { - double visframes = 2 * _c->visrad; - return visframes > hires_frame_limit ? _rlo : _rhi; - } - - double audio_node::rsamplerate() const { - double visframes = 2 * _c->visrad; - return (visframes > hires_frame_limit ? render_rate_lo : render_rate_hi); - } - - void audio_node::load_async() { - taskpool::get().submit_frame([this]() { - this->_loadstate = load_state(load_state::loading, "loading file"); - }); - _data.reset(new audio_file()); - const bool success = _data->load(_path); - if (!success) { - taskpool::get().submit_frame([this]() { - this->_loadstate = load_state(load_state::failed); - }); - return; - } - - taskpool::get().submit_frame([this]() { - this->_loadstate = - load_state(load_state::loading, "generating hi-res preview"); - }); - _rhi = render_samples(render_rate_hi); - - taskpool::get().submit_frame([this]() { - this->_loadstate = - load_state(load_state::loading, "generating lo-res preview"); - }); - _rlo = render_samples(render_rate_lo); - - taskpool::get().submit_frame([this]() { - this->_loadstate = - load_state(load_state::loading, "packing samples for playback"); - }); - if (_data->getNumChannels() != 1) { - const size_t channels = _data->getNumChannels(); - const size_t n = _data->getNumSamplesPerChannel(); - _packed.reserve(channels * n); - for (size_t i = 0; i < n; i++) { - for (size_t c = 0; c < channels; c++) { - _packed.push_back(_data->samples[c][i]); - } - } - } - - // update load state on main thread: this is what governs whether the UI - // accesses the rest of the state here or not. - taskpool::get().submit_frame([this]() { - this->_loadstate = load_state(load_state::loaded); - }); - } - - std::vector audio_node::render_samples(double framesamples) { - const size_t samples = _data->getNumSamplesPerChannel(); - const double spr = - (double)_data->getSampleRate() / (framesamples * _c->framerate()); - const size_t rsamples = (size_t) ceil((double)samples / spr); - const int channels = _data->getNumChannels(); - - std::vector rendered; - rendered.resize(rsamples); - float absmax = 0; - for (int i = 0; i < rsamples; i++) { - const double s1 = i * spr; - const double s2 = (i + 1) * spr; - double sum = 0; - for (int chan = 0; chan < channels; chan++) { - for (size_t s = (size_t)floor(s1); - s < (size_t)ceil(s2) && s < _data->samples[chan].size(); - s++) { - double x = _data->samples[chan][s]; - sum += (x < 0 ? -1.0 : 1.0) * sqrt(fabs(x)); - } - } - const float r = (float) (sum / (spr * channels)); - absmax = std::max(absmax, fabsf(r)); - rendered[i] = r; - } - for (float &f : rendered) { - f /= absmax; - } - return rendered; - } - - int audio_node::minframe() const { - return 0; - } - - int audio_node::maxframe() const { - if (_loadstate.stage != load_state::loaded) { - return 0; - } - return (int) ceil(_data->getLengthInSeconds() * _c->framerate()); - } - - audio_node::~audio_node() = default; }