From 2e793141e5434043205763c70d3a597cf2d78eeb Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 4 Jan 2025 17:51:59 +0100 Subject: Separate clocks --- Click.cpp | 2 - Click.h | 75 ------------------------- ClockClick.cpp | 22 ++++++++ ClockClick.h | 27 +++++++++ InternalClick.cpp | 19 +++++++ InternalClick.h | 28 ++++++++++ MainLoop.cpp | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ MainLoop.h | 33 +++++++++++ Makefile | 7 ++- NoteClick.cpp | 20 +++++++ NoteClick.h | 27 +++++++++ PCM.cpp | 4 +- PCM.h | 2 +- UI.cpp | 87 ++++++++++++++++++++++++++--- UI.h | 13 +++-- main.cpp | 140 ++--------------------------------------------- 16 files changed, 435 insertions(+), 231 deletions(-) delete mode 100644 Click.cpp delete mode 100644 Click.h create mode 100644 ClockClick.cpp create mode 100644 ClockClick.h create mode 100644 InternalClick.cpp create mode 100644 InternalClick.h create mode 100644 MainLoop.cpp create mode 100644 MainLoop.h create mode 100644 NoteClick.cpp create mode 100644 NoteClick.h diff --git a/Click.cpp b/Click.cpp deleted file mode 100644 index 4083e0d..0000000 --- a/Click.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "Click.h" - diff --git a/Click.h b/Click.h deleted file mode 100644 index 6d3d871..0000000 --- a/Click.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -#include - -#include "config.h" -#include "BPMDetect.h" - -// Internally generated -// Configured via: BPM -class InternalClick -{ -public: - InternalClick(Config& config): m_config(config){} - -private: - Config& m_config; -}; - -// Generated from MIDI notes -// Configured via channel and note to listen to -class NoteClick -{ -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_detect(1) - { - m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); - } - - // slots - void receive_note(int channel, int note, uint64_t timestamp) - { - (void) timestamp; - - 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: - ClockClick(): m_detect(24) - { - m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); - } - - // signals - boost::signals2::signal signal_bpm; - - // slots - void receive_clock() - { - m_detect.receive_event(); - } - -private: - BPMDetect m_detect; -}; - diff --git a/ClockClick.cpp b/ClockClick.cpp new file mode 100644 index 0000000..c9f6938 --- /dev/null +++ b/ClockClick.cpp @@ -0,0 +1,22 @@ +#include "ClockClick.h" + +ClockClick::ClockClick(): m_divider(24), m_count{}, m_detect(24) +{ + m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); +} + +// slots +void ClockClick::receive_clock() +{ + m_detect.receive_event(); + + // guard by divider + ++m_count; + if (m_count < m_divider) { + return; + } else { + m_count = 0; + } + + signal_click(); +} diff --git a/ClockClick.h b/ClockClick.h new file mode 100644 index 0000000..2c0ef19 --- /dev/null +++ b/ClockClick.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "config.h" +#include "BPMDetect.h" + +// Generated from MIDI Clock +class ClockClick +{ +public: + ClockClick(); + + // signals + boost::signals2::signal signal_click; + boost::signals2::signal signal_bpm; + + // slots + void receive_clock(); + +private: + const int m_divider; + int m_count; + + BPMDetect m_detect; +}; + diff --git a/InternalClick.cpp b/InternalClick.cpp new file mode 100644 index 0000000..33654fd --- /dev/null +++ b/InternalClick.cpp @@ -0,0 +1,19 @@ +#include "InternalClick.h" + +InternalClick::InternalClick(Config& config): + m_config{config}, + m_timestamp{clock_type::now()} +{ +} + +void InternalClick::run_cyclic_50ms() +{ + std::chrono::time_point now = clock_type::now(); + + std::chrono::duration duration{60.0 / static_cast(m_config.get_bpm())}; + if (now - m_timestamp > duration) { + m_timestamp = now; + signal_click(0); // offset + } +} + diff --git a/InternalClick.h b/InternalClick.h new file mode 100644 index 0000000..1fffb6c --- /dev/null +++ b/InternalClick.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "config.h" +#include "BPMDetect.h" + +using clock_type = std::chrono::high_resolution_clock; + +// Internally generated +// Configured via: BPM +class InternalClick +{ +public: + InternalClick(Config& config); + + // signals + boost::signals2::signal signal_click; + + // slots + void run_cyclic_50ms(); + +private: + Config& m_config; + + std::chrono::time_point m_timestamp; +}; + diff --git a/MainLoop.cpp b/MainLoop.cpp new file mode 100644 index 0000000..1bd0e1d --- /dev/null +++ b/MainLoop.cpp @@ -0,0 +1,160 @@ +#include "MainLoop.h" + +#include "Timer.h" +#include "config.h" +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; +using namespace std::string_literals; + +MainLoop::MainLoop(): + m_config{}, + m_note_click{m_config}, + m_clock_click{}, + m_internal_click{m_config}, + m_midi{}, + m_pcm{}, + m_ui{m_config} +{ +} + +bool run_flag = true; + +void signal_handler(int) { + run_flag = false; + std::cout << "Signal received. Terminating." << std::endl; +} + +void MainLoop::reconfigure_mode() { + debug_cout << "reconfiguring mode" << std::endl; + + m_click_connection.disconnect(); + + int mode = m_config.get_mode(); + + if (mode == 0) { + m_click_connection = m_note_click.signal_click.connect([&](){m_pcm.click(0);}); + } else if (mode == 1) { + m_click_connection = m_clock_click.signal_click.connect([&](){m_pcm.click(0);}); + } else if (mode == 2) { + m_click_connection = m_internal_click.signal_click.connect([&](int64_t offset){m_pcm.click(offset);}); + } else { + log_cout << fmt::format("Error: Unknown mode: {}", mode) << std::endl; + } +} + +int MainLoop::run() +{ + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + + try { + //debug_cout.activate(); + log_cout.activate(); + log_cout.log_lines(log_lines); + + m_pcm.write(); + + Timer timer_50ms(50ms, true); + timer_50ms.start(); + + Timer timer_500ms(500ms, true); + timer_500ms.start(); + + Timer timer_10min(10min, true); + timer_10min.start(); + + // Main signals + boost::signals2::signal signal_count_loops; + + // + // Signal-Slot Connections: + // + m_midi.signal_note.connect([&](int channel, int note, uint64_t timestamp){m_note_click.receive_note(channel, note, timestamp);}); + //m_click_connection = m_note_click.signal_click.connect([&](){m_pcm.click(0);}); + reconfigure_mode(); + m_note_click.signal_bpm.connect([&](int bpm){m_ui.slot_note_bpm(bpm);}); + m_clock_click.signal_bpm.connect([&](int bpm){m_ui.slot_clock_bpm(bpm);}); + m_midi.signal_active_sensing.connect([&](){m_ui.slot_active_sensing();}); + timer_500ms.elapsed.connect([&](){m_ui.draw();}); + signal_count_loops.connect([&](){m_ui.count_main_loops();}); + m_midi.signal_count_events.connect([&](){m_ui.count_midi_events();}); + timer_10min.elapsed.connect([&](){m_config.persist();}); + m_midi.signal_note.connect([&](int channel, int note, uint64_t timestamp){m_ui.slot_midi_note(channel, note, timestamp);}); + m_midi.signal_clock.connect([&](){m_clock_click.receive_clock();}); + m_config.signal_mode.connect([&](int mode){reconfigure_mode();}); + timer_50ms.elapsed.connect([&](){m_internal_click.run_cyclic_50ms();}); + + m_midi.flush(); + + while (run_flag) { + debug_cout << "Main loop entered." << std::endl; + signal_count_loops(); + + fd_set read_set; + FD_ZERO(&read_set); + FD_SET(m_midi.fd(), &read_set); + FD_SET(0, &read_set); + + if constexpr (0) { + // PCM fd almost always writeable: for single frames at high speed + fd_set write_set; + FD_ZERO(&write_set); + FD_SET(m_pcm.fd(), &write_set); + } + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 20000; + + int result = select(FD_SETSIZE, &read_set, nullptr/*&write_set*/, nullptr, &timeout); + if (result < 0) { + throw std::runtime_error("select() failed"); + } else if (result == 0) { + debug_cout << "select() timeout" << std::endl; + } + + while (m_midi.event_ready()) + { + //std::cout << "read..." << std::endl; + auto event = m_midi.read(); + //std::cout << "process..." << std::endl; + m_midi.process(event); + } + + if (m_pcm.write_available()) { + //std::cout << "DEBUG: WRITE" << std::endl; + m_pcm.write(); + } + + if (m_ui.key_available()) { + m_ui.handle_input(); + } + + // handle timers, TODO: make updates more efficient at scale + timer_50ms.update(); + timer_500ms.update(); + timer_10min.update(); + } + } catch (const std::exception& ex) { + std::cerr << "Error: " << ex.what() << std::endl; + } + + return 0; +} + diff --git a/MainLoop.h b/MainLoop.h new file mode 100644 index 0000000..0aac227 --- /dev/null +++ b/MainLoop.h @@ -0,0 +1,33 @@ +#pragma once + +#include "NoteClick.h" +#include "ClockClick.h" +#include "InternalClick.h" +#include "MIDI.h" +#include "PCM.h" +#include "UI.h" + +#include + +class MainLoop +{ +public: + MainLoop(); + int run(); + +private: + void reconfigure_mode(); + + boost::signals2::connection m_click_connection; + + Config m_config; + + NoteClick m_note_click; + ClockClick m_clock_click; + InternalClick m_internal_click; + + MIDI m_midi; + PCM m_pcm; + UI m_ui; +}; + diff --git a/Makefile b/Makefile index faac1fe..93e322a 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ TARGET=click default: $(TARGET) SRCS= \ + MainLoop.cpp \ main.cpp \ MIDI.cpp \ PCM.cpp \ @@ -12,8 +13,10 @@ SRCS= \ debug.cpp \ cpuload.cpp \ log.cpp \ - Click.cpp \ - BPMDetect.cpp + NoteClick.cpp \ + ClockClick.cpp \ + InternalClick.cpp \ + BPMDetect.cpp \ HEADERS=$(SRCS:.cpp=.h) diff --git a/NoteClick.cpp b/NoteClick.cpp new file mode 100644 index 0000000..657a893 --- /dev/null +++ b/NoteClick.cpp @@ -0,0 +1,20 @@ +#include "NoteClick.h" + +NoteClick::NoteClick(Config& config): + m_config(config), + m_detect(1) +{ + m_detect.signal_bpm.connect([&](int bpm){signal_bpm(bpm);}); +} + +// slots +void NoteClick::receive_note(int channel, int note, uint64_t timestamp) +{ + (void) timestamp; + + if (channel == m_config.get_midi_channel() && note == m_config.get_midi_note()) { + signal_click(); + m_detect.receive_event(); + } +} + diff --git a/NoteClick.h b/NoteClick.h new file mode 100644 index 0000000..754329f --- /dev/null +++ b/NoteClick.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "config.h" +#include "BPMDetect.h" + +// Generated from MIDI notes +// Configured via channel and note to listen to +class NoteClick +{ +public: + NoteClick(Config& config); + + // signals + boost::signals2::signal signal_click; + boost::signals2::signal signal_bpm; + + // slots + void receive_note(int channel, int note, uint64_t timestamp); + +private: + Config& m_config; + + BPMDetect m_detect; +}; + diff --git a/PCM.cpp b/PCM.cpp index e6d915f..218ba59 100644 --- a/PCM.cpp +++ b/PCM.cpp @@ -59,13 +59,13 @@ PCM::~PCM() free(pfd); } -void PCM::click() +void PCM::click(int64_t offset_us) { snd_pcm_sframes_t delay; if (0 > snd_pcm_delay(handle, &delay)) { } - m_phase = - click_latency_frames + delay; + m_phase = - click_latency_frames + delay + offset_us; } // generate 1 buffer size diff --git a/PCM.h b/PCM.h index 73cdcc6..90abc3d 100644 --- a/PCM.h +++ b/PCM.h @@ -21,7 +21,7 @@ public: PCM(); ~PCM(); - void click(); + void click(int64_t offset_us); // generate 1 buffer size void generate(); diff --git a/UI.cpp b/UI.cpp index 0adb48b..933f2de 100644 --- a/UI.cpp +++ b/UI.cpp @@ -11,6 +11,9 @@ #include #include +#include +#include + const int midi_monitor_max_size = 3; using namespace std::chrono_literals; @@ -62,6 +65,31 @@ UI::UI(Config& config): { } +bool UI::key_available() { + struct pollfd fds{}; + int ret; + fds.fd = 0; // stdin + fds.events = POLLIN; + ret = poll(&fds, 1, 0); + if (ret == 0) + return false; + else if (ret == 1) + return true; + else + log_cout << "Error" << std::endl; + return false; +} + +int read_key() { + char buf[1]; + int got = read(0, buf, 1); + if (got != 1) { + log_cout << "Bad input size: " << std::to_string(got) << std::endl; + } + + return buf[0]; +} + void UI::draw() { std::vector cpuloads = get_cpu_loads(); @@ -71,28 +99,40 @@ void UI::draw() 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 internal_bpm = m_config.get_bpm(); + int bpm; + if (mode == 0) { + bpm = m_note_bpm; + } else if (mode == 1) { + bpm = m_clock_bpm; + } else if (mode == 2) { + bpm = m_config.get_bpm(); + } + int output = m_config.get_output(); int active_sensing_detected = (clock_type::now() - m_active_sensing_timestamp) < 2s ? 1 : 0; + fmt::text_style editable_color{fg(fmt::color::crimson) | fmt::emphasis::bold}; + fmt::text_style value_color{fg(fmt::color::blue) | fmt::emphasis::bold}; + // clear screen std::cout << "\x1B[2J\x1B[H"; //std::cout << std::endl; - std::cout << fmt::format("- {:3} BPM +", bpm) << std::endl; + std::cout << "- " << fmt::format(editable_color, "{:3}", bpm) << " BPM +" << std::endl; std::cout << "Mode: "; for (int i = 0; i < mode_names.size(); ++i) { if (i == mode) { - std::cout << fmt::format(fg(fmt::color::crimson) | fmt::emphasis::bold, " {}", mode_names[i]); + std::cout << fmt::format(editable_color, " {}", mode_names[i]); } else { std::cout << fmt::format(" {}", mode_names[i]); } } std::cout << std::endl; - std::cout << fmt::format("MIDI Note: {}/{}", channel, note); + std::cout << "MIDI Note: " << fmt::format(editable_color, "{}/{}", channel, note); std::cout << std::endl; - std::cout << "Audio Output: " << fmt::format(fg(fmt::color::crimson) | fmt::emphasis::bold, "{}", output_names[output]); + std::cout << "Audio Output: " << fmt::format(editable_color, "{}", output_names[output]); std::cout << std::endl; std::cout << std::endl; std::cout << "Status:" << std::endl; @@ -111,7 +151,7 @@ void UI::draw() int count{}; for (const auto& i: m_midi_monitor) { if (count == 0) { - std::cout << fmt::format(fg(fmt::color::crimson) | fmt::emphasis::bold, " {}/{}", i.first, i.second); + std::cout << fmt::format(editable_color, " {}/{}", i.first, i.second); } else { std::cout << fmt::format(" {}/{}", i.first, i.second); } @@ -123,11 +163,11 @@ void UI::draw() } std::cout << std::endl; std::cout << fmt::format(" MIDI Timestamp: {}", m_midi_timestamp) << 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 << " MIDI Active Sensing: " << fmt::format(value_color, "{}", 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(" Internal: {:3} BPM", internal_bpm) << std::endl; std::cout << fmt::format(" Main loops/s: {}", main_loops_per_second) << std::endl; @@ -135,6 +175,37 @@ void UI::draw() std::cout << log_cout.get_log() << std::endl; } +void UI::handle_input() +{ + if (key_available()) { + char c; + std::cin >> c; + + debug_cout << "Key: " << std::to_string(c) << std::endl; + + if (c == '+') { + if (m_config.get_bpm() < 240) { + m_config.set_bpm(m_config.get_bpm() + 1); + } + } else if (c == '-') { + if (m_config.get_bpm() > 10) { + m_config.set_bpm(m_config.get_bpm() - 1); + } + } else if (c == 'm') { + m_config.set_mode((m_config.get_mode() + 1) % 3); + } else if (c == 'n') { + if (!m_midi_monitor.empty()) { + m_config.set_midi_channel(m_midi_monitor.front().first); + m_config.set_midi_note(m_midi_monitor.front().second); + } + } else if (c == 'o') { + m_config.set_output(1 - m_config.get_output()); + } else { + log_cout << fmt::format("Unknown key: {}", c) << std::endl; + } + } +} + void UI::count_main_loops() { m_main_loops.count(); diff --git a/UI.h b/UI.h index db2f2cc..e8b1031 100644 --- a/UI.h +++ b/UI.h @@ -31,12 +31,15 @@ public: void draw(); + void handle_input(); + + bool key_available(); + // 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; + boost::signals2::signal signal_bpm; + boost::signals2::signal signal_note_set_from_midi; + boost::signals2::signal signal_mode; + boost::signals2::signal signal_output; // slots void count_main_loops(); diff --git a/main.cpp b/main.cpp index 4a5f698..548b8c5 100644 --- a/main.cpp +++ b/main.cpp @@ -1,138 +1,6 @@ -#include "Click.h" -#include "MIDI.h" -#include "PCM.h" -#include "Timer.h" -#include "UI.h" -#include "config.h" -#include "log.h" +#include "MainLoop.h" -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -using namespace std::chrono_literals; -using namespace std::string_literals; - -double diff_timespec(const struct timespec *time1, const struct timespec *time0) { - return (time1->tv_sec - time0->tv_sec) - + (time1->tv_nsec - time0->tv_nsec) / 1000000000.0; -} - -bool run_flag = true; - -void signal_handler(int) { - run_flag = false; - std::cout << "Signal received. Terminating." << std::endl; -} - -int main(void) -{ - signal(SIGTERM, signal_handler); - signal(SIGINT, signal_handler); - - try { - //debug_cout.activate(); - log_cout.activate(); - log_cout.log_lines(log_lines); - - Config config; - - ClockClick clock_click; - NoteClick note_click(config); - InternalClick internal_click(config); - - MIDI midi; - PCM pcm; - UI ui(config); - - pcm.write(); - - Timer timer_500ms(500ms, true); - timer_500ms.start(); - - Timer timer_10min(10min, true); - timer_10min.start(); - - // Main signals - boost::signals2::signal signal_count_loops; - - // - // Signal-Slot Connections: - // - 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(); - - while (run_flag) { - debug_cout << "Main loop entered." << std::endl; - signal_count_loops(); - - fd_set read_set; - FD_ZERO(&read_set); - FD_SET(midi.fd(), &read_set); - -#if 0 - // PCM fd almost always writeable: for single frames at high speed - fd_set write_set; - FD_ZERO(&write_set); - FD_SET(pcm.fd(), &write_set); -#endif - - struct timeval timeout; - timeout.tv_sec = 0; - timeout.tv_usec = 20000; - - int result = select(FD_SETSIZE, &read_set, nullptr/*&write_set*/, nullptr, &timeout); - if (result < 0) { - throw std::runtime_error("select() failed"); - } else if (result == 0) { - debug_cout << "select() timeout" << std::endl; - } - - while (midi.event_ready()) - { - //std::cout << "read..." << std::endl; - auto event = midi.read(); - //std::cout << "process..." << std::endl; - midi.process(event); - } - - if (pcm.write_available()) { - //std::cout << "DEBUG: WRITE" << std::endl; - pcm.write(); - } - - // handle timers, TODO: make updates more efficient at scale - timer_500ms.update(); - timer_10min.update(); - } - } catch (const std::exception& ex) { - std::cerr << "Error: " << ex.what() << std::endl; - } - - return 0; +int main() { + MainLoop main_loop; + return main_loop.run(); } - -- cgit v1.2.3