diff options
-rw-r--r-- | BPMDetect.cpp | 37 | ||||
-rw-r--r-- | BPMDetect.h | 27 | ||||
-rw-r--r-- | Click.h | 33 | ||||
-rw-r--r-- | MIDI.cpp | 2 | ||||
-rw-r--r-- | MIDI.h | 1 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | UI.cpp | 88 | ||||
-rw-r--r-- | UI.h | 32 | ||||
-rw-r--r-- | config.cpp | 22 | ||||
-rw-r--r-- | config.h | 6 | ||||
-rw-r--r-- | main.cpp | 7 |
11 files changed, 217 insertions, 43 deletions
diff --git a/BPMDetect.cpp b/BPMDetect.cpp new file mode 100644 index 0000000..756747d --- /dev/null +++ b/BPMDetect.cpp @@ -0,0 +1,37 @@ +#include "BPMDetect.h" + +#include "log.h" + +BPMDetect::BPMDetect(int divider): + m_divider{divider}, + m_count{} +{ +} + +// Strategy: evaluate last 3 valid beats +// always skip m_divider-1 beats +void BPMDetect::receive_event() +{ + // guard by divider + ++m_count; + if (m_count < m_divider) { + return; + } else { + m_count = 0; + } + + // calculate bpm + time_point now = clock_type::now(); + + m_timestamps.push_back(now); + + while (m_timestamps.size() > 3) { + m_timestamps.pop_front(); + } + + if (m_timestamps.size() == 3) { + int bpm = 60 * 2 / std::chrono::duration<double>(m_timestamps.back() - m_timestamps.front()).count(); + + signal_bpm(bpm); + } +} diff --git a/BPMDetect.h b/BPMDetect.h new file mode 100644 index 0000000..7cd926e --- /dev/null +++ b/BPMDetect.h @@ -0,0 +1,27 @@ +#pragma once + +#include <boost/signals2.hpp> + +#include <chrono> +#include <deque> + +class BPMDetect +{ +public: + BPMDetect(int divider); + void receive_event(); + + // signal + boost::signals2::signal<void(int)> signal_bpm; + +private: + using clock_type = std::chrono::high_resolution_clock; + using time_point = std::chrono::time_point<clock_type>; + + const int m_divider; + + int m_count; + + std::deque<time_point> m_timestamps; +}; + @@ -3,6 +3,7 @@ #include <boost/signals2.hpp> #include "config.h" +#include "BPMDetect.h" // Virtual base class // Abstraction of BPM detection / setting @@ -30,17 +31,20 @@ private: class NoteClick: public Click { public: + // signals + boost::signals2::signal<void()> signal_click; + boost::signals2::signal<void(int)> signal_bpm; + NoteClick(Config& config): m_channel(config.get_midi_channel()), - m_note(config.get_midi_note()) + m_note(config.get_midi_note()), + m_detect(1) { + m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); } virtual ~NoteClick(){} - // signals - boost::signals2::signal<void()> signal_click; - // slots void receive_note(int channel, int note, uint64_t timestamp) { @@ -48,19 +52,38 @@ public: if (true || (channel == m_channel && note == m_note)) { signal_click(); + m_detect.receive_event(); } } private: int m_channel; int m_note; + + BPMDetect m_detect; }; // Generated from MIDI Clock class ClockClick: public Click { public: - ClockClick(){} + ClockClick(): m_detect(24) + { + m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); + } + virtual ~ClockClick(){} + + // signals + boost::signals2::signal<void(int)> signal_bpm; + + // slots + void receive_clock() + { + m_detect.receive_event(); + } + +private: + BPMDetect m_detect; }; @@ -148,6 +148,8 @@ void MIDI::process(snd_seq_event_t *ev) { log_cout << fmt::format("[{}] Unknown MIDI event: {}\n", timestamp_from_event(ev), ev->type) << std::endl; } + + signal_count_events(); } void MIDI::flush() @@ -19,6 +19,7 @@ public: boost::signals2::signal<void(int, int, uint64_t)> signal_note; boost::signals2::signal<void()> signal_active_sensing; boost::signals2::signal<void()> signal_clock; + boost::signals2::signal<void()> signal_count_events; int fd(); @@ -12,7 +12,8 @@ SRCS= \ debug.cpp \ cpuload.cpp \ log.cpp \ - Click.cpp + Click.cpp \ + BPMDetect.cpp HEADERS=$(SRCS:.cpp=.h) @@ -25,7 +26,7 @@ $(TARGET): $(OBJS) $(CXX) $^ -o $@ $(CXXLIBS) %.o: %.cpp - $(CXX) $(CXXFLAGS) -std=c++20 -O2 -g -Wall -o $@ -c $< + $(CXX) $(CXXFLAGS) -std=c++20 -O2 -g -Wall -Wpedantic -o $@ -c $< clean: rm -f $(TARGET) $(OBJS) @@ -15,44 +15,66 @@ const int midi_monitor_max_size = 3; using namespace std::chrono_literals; -static std::vector<std::string> mode_names{{ +static std::vector<std::string> mode_names{ "NoteClick", "Clock", "Internal" -}}; +}; -UI::UI(Config& config): - m_config(config), - m_main_loops{}, - m_main_loops_checkpoint{}, - m_main_loops_timestamp{}, - m_active_sensing_timestamp{}, - m_midi_timestamp{} +static std::vector<std::string> output_names{ + "Off", "On" +}; + +static std::vector<std::string> active_sensing_names{ + "Not Detected", "Detected" +}; + +IntervalCounter::IntervalCounter() { } -int UI::get_main_loops_per_second() +int IntervalCounter::get_count_per_second() { // calculate result std::chrono::time_point<clock_type> now = clock_type::now(); - uint64_t diff_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_main_loops_timestamp).count(); - uint64_t loops_per_second = (diff_ms == 0 || m_main_loops_checkpoint == 0) ? 0 : ((m_main_loops - m_main_loops_checkpoint) * 1000 / diff_ms); + + uint64_t diff_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_checkpoint_timestamp).count(); + uint64_t count_per_second = (diff_ms == 0 || m_count_checkpoint == 0) ? 0 : ((m_count - m_count_checkpoint) * 1000 / diff_ms); // update state - m_main_loops_timestamp = now; - m_main_loops_checkpoint = m_main_loops; + m_checkpoint_timestamp = now; + m_count_checkpoint = m_count; - return loops_per_second; + return count_per_second; +} + +void IntervalCounter::count() +{ + m_count++; +} + +UI::UI(Config& config): + m_config(config), + m_main_loops{}, + m_midi_events{}, + m_active_sensing_timestamp{}, + m_midi_timestamp{}, + m_note_bpm{}, + m_clock_bpm{} +{ } void UI::draw() { std::vector<int> cpuloads = get_cpu_loads(); - int main_loops_per_second = get_main_loops_per_second(); + int main_loops_per_second = m_main_loops.get_count_per_second(); + int midi_events_per_second = m_midi_events.get_count_per_second(); + int mode = m_config.get_mode(); int channel = m_config.get_midi_channel(); int note = m_config.get_midi_note(); int bpm = m_config.get_bpm(); + int output = m_config.get_output(); - bool active_sensing_detected = (clock_type::now() - m_active_sensing_timestamp) < 2s; + int active_sensing_detected = (clock_type::now() - m_active_sensing_timestamp) < 2s ? 1 : 0; // clear screen std::cout << "\x1B[2J\x1B[H"; @@ -70,6 +92,8 @@ void UI::draw() std::cout << std::endl; std::cout << fmt::format("MIDI Note: {}/{}", channel, note); std::cout << std::endl; + std::cout << "Audio Output: " << fmt::format(fg(fmt::color::crimson) | fmt::emphasis::bold, "{}", output_names[output]); + std::cout << std::endl; std::cout << std::endl; std::cout << "Status:" << std::endl; @@ -80,7 +104,7 @@ void UI::draw() int max = *std::max_element(cpuloads.begin(), cpuloads.end()); std::cout << fmt::format(", max. {:2}%", max) << std::endl; - std::cout << " MIDI Activity: "; + std::cout << " MIDI Notes: "; if (m_midi_monitor.empty()) { std::cout << "--"; } else { @@ -99,10 +123,11 @@ void UI::draw() } std::cout << std::endl; std::cout << fmt::format(" MIDI Timestamp: {}", m_midi_timestamp) << std::endl; - std::cout << fmt::format(" Active sensing: {}", active_sensing_detected) << std::endl; - std::cout << " Clock: ____ BPM" << std::endl; - std::cout << " Click: ____ BPM" << std::endl; - std::cout << fmt::format(" Internal: {:3} BPM", bpm) << std::endl; + std::cout << " MIDI Active Sensing: " << fmt::format(fg(fmt::color::blue) | fmt::emphasis::bold, "{}", active_sensing_names[active_sensing_detected]) << std::endl; + std::cout << fmt::format(" MIDI Events/s: {}", midi_events_per_second) << std::endl; + std::cout << fmt::format(" MIDI Clock: {:3} BPM", m_clock_bpm) << std::endl; + std::cout << fmt::format(" MIDI Click: {:3} BPM", m_note_bpm) << std::endl; + std::cout << fmt::format(" Internal: {:3} BPM", bpm) << std::endl; std::cout << fmt::format(" Main loops/s: {}", main_loops_per_second) << std::endl; @@ -112,8 +137,12 @@ void UI::draw() void UI::count_main_loops() { - ++m_main_loops; - debug_cout << "DEBUG:" << m_main_loops << std::endl; + m_main_loops.count(); +} + +void UI::count_midi_events() +{ + m_midi_events.count(); } void UI::slot_active_sensing() @@ -131,3 +160,14 @@ void UI::slot_midi_note(int channel, int note, uint64_t timestamp) m_midi_monitor.pop_back(); } } + +void UI::slot_note_bpm(int bpm) +{ + m_note_bpm = bpm; +} + +void UI::slot_clock_bpm(int bpm) +{ + m_clock_bpm = bpm; +} + @@ -8,38 +8,56 @@ #include <boost/signals2.hpp> +using clock_type = std::chrono::high_resolution_clock; + +struct IntervalCounter +{ +public: + IntervalCounter(); + int get_count_per_second(); + void count(); + +private: + uint64_t m_count{}; + uint64_t m_count_checkpoint{}; + std::chrono::time_point<clock_type> m_checkpoint_timestamp{}; +}; + class UI { public: - using clock_type = std::chrono::high_resolution_clock; UI(Config& config); void draw(); - // signals + // signals, from user input boost::signals2::signal<void()> bpm_plus; boost::signals2::signal<void()> bpm_minus; boost::signals2::signal<void()> note_set_from_midi; boost::signals2::signal<void()> mode; + boost::signals2::signal<void()> output; // slots void count_main_loops(); + void count_midi_events(); void slot_active_sensing(); void slot_midi_note(int channel, int note, uint64_t timestamp); + void slot_note_bpm(int bpm); + void slot_clock_bpm(int bpm); private: Config& m_config; - int get_main_loops_per_second(); - - uint64_t m_main_loops; - uint64_t m_main_loops_checkpoint; - std::chrono::time_point<clock_type> m_main_loops_timestamp; + IntervalCounter m_main_loops; + IntervalCounter m_midi_events; std::chrono::time_point<clock_type> m_active_sensing_timestamp; uint64_t m_midi_timestamp; std::deque<std::pair<int,int>> m_midi_monitor; + + int m_note_bpm; + int m_clock_bpm; }; @@ -31,6 +31,7 @@ void Config::recover() m_midi_note = CLICK_NOTE; m_bpm = default_bpm; m_mode = default_mode; + m_output = default_output; std::string config = Reichwein::File::getFile(config_filename); @@ -53,6 +54,10 @@ void Config::recover() m_mode = stoul(i.substr(5)); signal_mode(m_mode); } + if (i.starts_with("output=")) { + m_output = stoul(i.substr(7)); + signal_output(m_output); + } } } catch (const std::exception& ex) { log_cout << "Config not found. Setting config to defaults." << std::endl; @@ -61,10 +66,12 @@ void Config::recover() void Config::persist() { - std::string config = fmt::format("midi_channel={}\n", m_midi_channel) + + std::string config = + fmt::format("midi_channel={}\n", m_midi_channel) + fmt::format("midi_note={}\n", m_midi_note) + fmt::format("bpm={}\n", m_bpm) + - fmt::format("mode={}\n", m_mode); + fmt::format("mode={}\n", m_mode) + + fmt::format("output={}\n", m_output); Reichwein::File::setFile(config_filename, config); } @@ -113,3 +120,14 @@ void Config::set_mode(int mode) signal_mode(mode); } +int Config::get_output() +{ + return m_output; +} + +void Config::set_output(int output) +{ + m_output = output; + signal_output(output); +} + @@ -14,6 +14,7 @@ const int default_bpm = 120; const int pcm_latency_us = 100000; const int click_latency_frames = 10000; const int default_mode = 0; // 0 = note, 1 = clock, 2 = internal +const int default_output = 1; // 0 = off, 1 = on class Config { @@ -26,6 +27,7 @@ public: boost::signals2::signal<void(int)> signal_channel; boost::signals2::signal<void(int)> signal_note; boost::signals2::signal<void(int)> signal_bpm; + boost::signals2::signal<void(int)> signal_output; int get_midi_channel(); void set_midi_channel(int channel); @@ -39,6 +41,9 @@ public: int get_mode(); void set_mode(int mode); + int get_output(); + void set_output(int output); + void recover(); void persist(); @@ -47,4 +52,5 @@ private: int m_midi_note; int m_bpm; int m_mode; + int m_output; }; @@ -54,9 +54,6 @@ int main(void) std::shared_ptr<NoteClick> note_click = std::make_shared<NoteClick>(config); std::shared_ptr<InternalClick> internal_click = std::make_shared<InternalClick>(config); - // Active Mode - std::shared_ptr<Click> click = note_click; - MIDI midi; PCM pcm; UI ui(config); @@ -77,11 +74,15 @@ int main(void) // midi.signal_note.connect([&](int channel, int note, uint64_t timestamp){note_click->receive_note(channel, note, timestamp);}); note_click->signal_click.connect([&](){pcm.click();}); + note_click->signal_bpm.connect([&](int bpm){ui.slot_note_bpm(bpm);}); + clock_click->signal_bpm.connect([&](int bpm){ui.slot_clock_bpm(bpm);}); midi.signal_active_sensing.connect([&](){ui.slot_active_sensing();}); timer_500ms.elapsed.connect([&](){ui.draw();}); signal_count_loops.connect([&](){ui.count_main_loops();}); + midi.signal_count_events.connect([&](){ui.count_midi_events();}); timer_10min.elapsed.connect([&](){config.persist();}); midi.signal_note.connect([&](int channel, int note, uint64_t timestamp){ui.slot_midi_note(channel, note, timestamp);}); + midi.signal_clock.connect([&](){clock_click->receive_clock();}); midi.flush(); |