loading/rendering audio
haldean
1 year, 1 month ago
61 | 61 | qbshim.hpp |
62 | 62 | nodeui.cpp |
63 | 63 | nodeui.hpp |
64 | task.cpp | |
65 | task.hpp | |
66 | AudioFile/AudioFile.h | |
64 | 67 | ) |
65 | 68 | if(CMAKE_SYSTEM_NAME STREQUAL Windows) |
66 | 69 | add_executable(qb WIN32 ${QB_SOURCES}) |
0 | 0 | #include "qb.hpp" |
1 | 1 | #include "nodeui.hpp" |
2 | 2 | |
3 | #include "AudioFile/AudioFile.h" | |
3 | 4 | #include "cimgui/imgui/imgui.h" |
4 | 5 | |
5 | 6 | namespace qb { |
6 | 7 | |
7 | 8 | void draw(const std::shared_ptr<qb::audio_node> &node) |
8 | 9 | { |
9 | if (!node->loaded()) { | |
10 | ImGui::Text("failed to load file %s", node->path().c_str()); | |
10 | ImGui::Text(node->path().c_str()); | |
11 | ||
12 | switch (node->loadstate().stage) { | |
13 | case load_state::loading: | |
14 | ImGui::Text("loading: %s", node->loadstate().message.c_str()); | |
11 | 15 | return; |
16 | ||
17 | case load_state::failed: | |
18 | ImGui::Text("failed to load file"); | |
19 | return; | |
20 | ||
21 | case load_state::loaded: { | |
22 | qb::audio_file_cptr data = node->data(); | |
23 | if (!data) { | |
24 | ImGui::Text("audio file is invalid\n"); | |
25 | } | |
26 | double seconds = data->getLengthInSeconds(); | |
27 | const int minutes = (int) floor(seconds / 60.0); | |
28 | seconds -= 60 * minutes; | |
29 | ||
30 | ImGui::Text("sample rate %d depth %d samples %d channels %d length %dm%0.3fs", | |
31 | data->getSampleRate(), | |
32 | data->getBitDepth(), | |
33 | data->getNumSamplesPerChannel(), | |
34 | data->getNumChannels(), | |
35 | minutes, seconds); | |
36 | ||
37 | static int len = 500; | |
38 | static int off = 0; | |
39 | ImGui::DragInt("len", &len, 1.0, 0, INT_MAX); | |
40 | ImGui::DragInt("off", &off, 1.0, 0, INT_MAX); | |
41 | ||
42 | const std::vector<float> &r = node->rendered(); | |
43 | ImGui::PlotLines("samples", r.data(), | |
44 | std::min((int)r.size() - off, len), | |
45 | std::min((int)r.size() - 1, off)); | |
12 | 46 | } |
13 | ImGui::Text(node->path().c_str()); | |
47 | } | |
14 | 48 | } |
15 | 49 | |
16 | 50 | template <class To, class From> |
0 | 0 | #include "qb.hpp" |
1 | 1 | #include "nodeui.hpp" |
2 | #include "task.hpp" | |
3 | #include "AudioFile/AudioFile.h" | |
2 | 4 | |
3 | 5 | #include "cimgui/imgui/imgui.h" |
4 | 6 | |
5 | 7 | namespace qb { |
6 | 8 | context::context() { |
7 | _nodes.push_back(std::make_shared<qb::audio_node>("test.wav")); | |
9 | _nodes.push_back(std::make_shared<qb::audio_node>(this, "test/coc-stereo.wav")); | |
8 | 10 | } |
9 | 11 | |
10 | 12 | void context::frame(int width, int height) { |
13 | taskpool::get().frame(); | |
14 | ||
11 | 15 | ImGui::SetNextWindowPos(ImVec2(0, 0)); |
12 | 16 | ImGui::SetNextWindowSize(ImVec2((float) width, (float) height)); |
13 | 17 | ImGui::Begin("MainWindow", nullptr, ImGuiWindowFlags_NoDecoration); |
21 | 25 | void context::shutdown() { |
22 | 26 | } |
23 | 27 | |
24 | node::node(const std::string &n, node_type t) : _name(n), _type(t) {} | |
28 | node::node(const context *c, const std::string &n, node_type t) | |
29 | : _c(c), _name(n), _type(t) {} | |
25 | 30 | |
26 | /* | |
27 | class audio_file : public AudioFile<float> { | |
28 | using AudioFile::AudioFile; | |
29 | }; | |
30 | */ | |
31 | class audio_file { | |
32 | public: | |
33 | bool load(const std::string &path) { return false; } | |
34 | }; | |
31 | audio_node::audio_node(const context *c, const std::string &path) | |
32 | : node(c, path, qb::node_type::audio) | |
33 | , _path(path) | |
34 | , _loadstate(load_state::loading) | |
35 | { | |
36 | taskpool::get().submit_async([this]() { this->load_async(); }); | |
37 | } | |
35 | 38 | |
36 | audio_node::audio_node(const std::string &path) | |
37 | : node(path, qb::node_type::audio) | |
38 | , _path(path) | |
39 | { | |
39 | void audio_node::load_async() { | |
40 | taskpool::get().submit_frame([this]() { | |
41 | this->_loadstate = load_state(load_state::loading, "loading file"); | |
42 | }); | |
40 | 43 | _data.reset(new audio_file()); |
41 | _loaded = _data->load(path); | |
44 | const bool success = _data->load(_path); | |
45 | if (!success) { | |
46 | taskpool::get().submit_frame([this]() { | |
47 | this->_loadstate = load_state(load_state::failed); | |
48 | }); | |
49 | return; | |
50 | } | |
51 | ||
52 | taskpool::get().submit_frame([this]() { | |
53 | this->_loadstate = load_state(load_state::loading, "generating preview"); | |
54 | }); | |
55 | render_samples(); | |
56 | ||
57 | // update load state on main thread: this is what governs whether the UI | |
58 | // accesses the rest of the state here or not. | |
59 | taskpool::get().submit_frame([this]() { | |
60 | this->_loadstate = load_state(load_state::loaded); | |
61 | }); | |
62 | } | |
63 | ||
64 | void audio_node::render_samples() { | |
65 | const size_t samples = _data->getNumSamplesPerChannel(); | |
66 | const double framerate = _c->framerate(); | |
67 | ||
68 | const double spf = (double)_data->getSampleRate() / framerate; | |
69 | const int frames = 8 * (int) ceil(samples / spf); | |
70 | const int channels = _data->getNumChannels(); | |
71 | ||
72 | _rendered.resize(frames); | |
73 | for (int i = 0; i < frames; i++) { | |
74 | const double s1 = i * spf; | |
75 | const double s2 = (i + 1) * spf; | |
76 | float sum = 0; | |
77 | for (int chan = 0; chan < channels; chan++) { | |
78 | for (size_t s = (size_t)floor(s1); s < (size_t)ceil(s2); s++) { | |
79 | const float sample = s >= samples ? 0 : _data->samples[chan][s]; | |
80 | if (s == floor(s1) && (double)s != s1) { | |
81 | sum += sample * (ceil(s1) - s1); | |
82 | } else if (s == ceil(s2) && (double)s != s2) { | |
83 | sum += sample * (s2 - floor(s2)); | |
84 | } else { | |
85 | sum += sample; | |
86 | } | |
87 | } | |
88 | } | |
89 | _rendered[i] = (float)(sum / (spf * (double)channels)); | |
90 | } | |
42 | 91 | } |
43 | 92 | |
44 | 93 | audio_node::~audio_node() = default; |
4 | 4 | #include <string> |
5 | 5 | #include <vector> |
6 | 6 | |
7 | #include "AudioFile/AudioFile.h" | |
8 | ||
7 | 9 | namespace qb { |
8 | 10 | enum class node_type { |
9 | 11 | audio, |
10 | 12 | midi, |
11 | 13 | osc, |
12 | 14 | }; |
15 | ||
16 | struct load_state { | |
17 | enum stage_kind { | |
18 | loading, | |
19 | loaded, | |
20 | failed, | |
21 | }; | |
22 | stage_kind stage; | |
23 | std::string message; | |
24 | ||
25 | load_state(stage_kind s) : stage(s), message("") {}; | |
26 | load_state(stage_kind s, const std::string &m) : stage(s), message(m) {}; | |
27 | }; | |
28 | ||
29 | struct context; | |
13 | 30 | |
14 | 31 | struct node { |
15 | 32 | node(const node&) = delete; |
19 | 36 | |
20 | 37 | virtual ~node() = default; |
21 | 38 | |
22 | const std::string &name() const { | |
23 | return _name; | |
24 | } | |
25 | node_type type() const { | |
26 | return _type; | |
27 | } | |
39 | const std::string &name() const { return _name; } | |
40 | node_type type() const { return _type; } | |
28 | 41 | |
29 | 42 | protected: |
30 | node(const std::string &n, node_type t); | |
43 | node(const context *c, const std::string &n, node_type t); | |
44 | const context *_c; | |
31 | 45 | |
32 | 46 | private: |
33 | 47 | std::string _name; |
35 | 49 | }; |
36 | 50 | using node_ptr = std::shared_ptr<node>; |
37 | 51 | |
38 | class audio_file; | |
52 | using audio_file = AudioFile<float>; | |
53 | using audio_file_ptr = std::shared_ptr<audio_file>; | |
54 | using audio_file_cptr = std::shared_ptr<const audio_file>; | |
55 | ||
39 | 56 | struct audio_node : public node { |
40 | explicit audio_node(const std::string &path); | |
57 | audio_node(const context *c, const std::string &path); | |
41 | 58 | ~audio_node() override; |
42 | 59 | |
43 | const std::string &path() const { | |
44 | return _path; | |
45 | } | |
46 | ||
47 | bool loaded() const { | |
48 | return _loaded; | |
49 | } | |
60 | const std::string &path() const { return _path; } | |
61 | load_state loadstate() const { return _loadstate; } | |
62 | const audio_file_cptr data() const { return _data; } | |
63 | const std::vector<float>& rendered() const { return _rendered; } | |
50 | 64 | |
51 | 65 | private: |
66 | void load_async(); | |
67 | void render_samples(); | |
68 | ||
52 | 69 | const std::string _path; |
53 | std::unique_ptr<audio_file> _data; | |
54 | bool _loaded; | |
70 | audio_file_ptr _data; | |
71 | std::vector<float> _rendered; | |
72 | load_state _loadstate; | |
55 | 73 | }; |
56 | 74 | |
57 | 75 | struct context |
64 | 82 | context &operator=(const context &) = delete; |
65 | 83 | context &operator=(context &&) = delete; |
66 | 84 | |
85 | int framerate() const { return _framerate; } | |
86 | void framerate(int fr) { _framerate = fr; } | |
87 | ||
67 | 88 | void frame(int width, int height); |
68 | 89 | void shutdown(); |
69 | 90 | |
70 | 91 | private: |
92 | int _framerate = 24; | |
71 | 93 | std::vector<qb::node_ptr> _nodes; |
72 | 94 | }; |
73 | 95 |
0 | #include "task.hpp" | |
1 | ||
2 | namespace qb { | |
3 | taskpool& taskpool::get() { | |
4 | static taskpool tp; | |
5 | return tp; | |
6 | } | |
7 | ||
8 | void taskpool::submit_async(const task &t) { | |
9 | std::thread(t).detach(); | |
10 | } | |
11 | ||
12 | void taskpool::submit_frame(const task &t) { | |
13 | std::lock_guard<std::mutex> guard(_locktasks); | |
14 | _frametasks.push_back(t); | |
15 | } | |
16 | ||
17 | void taskpool::frame() { | |
18 | std::lock_guard<std::mutex> guard(_locktasks); | |
19 | for (const task &t : _frametasks) { | |
20 | t(); | |
21 | } | |
22 | _frametasks.clear(); | |
23 | } | |
24 | } |
0 | #include <functional> | |
1 | #include <mutex> | |
2 | #include <thread> | |
3 | #include <vector> | |
4 | ||
5 | namespace qb | |
6 | { | |
7 | ||
8 | using task = std::function<void(void)>; | |
9 | struct taskpool { | |
10 | static taskpool& get(); | |
11 | void submit_async(const task &t); | |
12 | void submit_frame(const task &t); | |
13 | void frame(); | |
14 | ||
15 | private: | |
16 | std::vector<task> _frametasks; | |
17 | std::mutex _locktasks; | |
18 | }; | |
19 | ||
20 | } |