From 39ec820c931b07bc0cec98add36f106a5965e137 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 4 Jan 2025 13:48:34 +0100 Subject: BPM detect --- BPMDetect.cpp | 37 +++++++++++++++++++++++++ BPMDetect.h | 27 ++++++++++++++++++ Click.h | 33 ++++++++++++++++++---- MIDI.cpp | 2 ++ MIDI.h | 1 + Makefile | 5 ++-- UI.cpp | 88 +++++++++++++++++++++++++++++++++++++++++++---------------- UI.h | 32 +++++++++++++++++----- config.cpp | 22 +++++++++++++-- config.h | 6 ++++ main.cpp | 7 +++-- 11 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 BPMDetect.cpp create mode 100644 BPMDetect.h 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(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 + +#include +#include + +class BPMDetect +{ +public: + BPMDetect(int divider); + void receive_event(); + + // signal + boost::signals2::signal signal_bpm; + +private: + using clock_type = std::chrono::high_resolution_clock; + using time_point = std::chrono::time_point; + + const int m_divider; + + int m_count; + + std::deque m_timestamps; +}; + diff --git a/Click.h b/Click.h index 7861d33..4af693e 100644 --- a/Click.h +++ b/Click.h @@ -3,6 +3,7 @@ #include #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 signal_click; + boost::signals2::signal 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 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 signal_bpm; + + // slots + void receive_clock() + { + m_detect.receive_event(); + } + +private: + BPMDetect m_detect; }; diff --git a/MIDI.cpp b/MIDI.cpp index 5e625bd..dedd94a 100644 --- a/MIDI.cpp +++ b/MIDI.cpp @@ -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() diff --git a/MIDI.h b/MIDI.h index 177fd59..8045684 100644 --- a/MIDI.h +++ b/MIDI.h @@ -19,6 +19,7 @@ public: boost::signals2::signal signal_note; boost::signals2::signal signal_active_sensing; boost::signals2::signal signal_clock; + boost::signals2::signal signal_count_events; int fd(); diff --git a/Makefile b/Makefile index 2cebb56..faac1fe 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/UI.cpp b/UI.cpp index 4e063d0..0adb48b 100644 --- a/UI.cpp +++ b/UI.cpp @@ -15,44 +15,66 @@ const int midi_monitor_max_size = 3; using namespace std::chrono_literals; -static std::vector mode_names{{ +static std::vector 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 output_names{ + "Off", "On" +}; + +static std::vector 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 now = clock_type::now(); - uint64_t diff_ms = std::chrono::duration_cast(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(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 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; +} + diff --git a/UI.h b/UI.h index a3c56e0..db2f2cc 100644 --- a/UI.h +++ b/UI.h @@ -8,38 +8,56 @@ #include +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 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 bpm_plus; boost::signals2::signal bpm_minus; boost::signals2::signal note_set_from_midi; boost::signals2::signal mode; + boost::signals2::signal 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 m_main_loops_timestamp; + IntervalCounter m_main_loops; + IntervalCounter m_midi_events; std::chrono::time_point m_active_sensing_timestamp; uint64_t m_midi_timestamp; std::deque> m_midi_monitor; + + int m_note_bpm; + int m_clock_bpm; }; diff --git a/config.cpp b/config.cpp index 04736ea..94e881d 100644 --- a/config.cpp +++ b/config.cpp @@ -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); +} + diff --git a/config.h b/config.h index 5d2a1d0..e52e9f5 100644 --- a/config.h +++ b/config.h @@ -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 signal_channel; boost::signals2::signal signal_note; boost::signals2::signal signal_bpm; + boost::signals2::signal 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; }; diff --git a/main.cpp b/main.cpp index 337fa96..5c8d345 100644 --- a/main.cpp +++ b/main.cpp @@ -54,9 +54,6 @@ int main(void) std::shared_ptr note_click = std::make_shared(config); std::shared_ptr internal_click = std::make_shared(config); - // Active Mode - std::shared_ptr 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(); -- cgit v1.2.3