diff options
-rw-r--r-- | Click.cpp | 2 | ||||
-rw-r--r-- | Click.h | 75 | ||||
-rw-r--r-- | ClockClick.cpp | 22 | ||||
-rw-r--r-- | ClockClick.h | 27 | ||||
-rw-r--r-- | InternalClick.cpp | 19 | ||||
-rw-r--r-- | InternalClick.h | 28 | ||||
-rw-r--r-- | MainLoop.cpp | 160 | ||||
-rw-r--r-- | MainLoop.h | 33 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | NoteClick.cpp | 20 | ||||
-rw-r--r-- | NoteClick.h | 27 | ||||
-rw-r--r-- | PCM.cpp | 4 | ||||
-rw-r--r-- | PCM.h | 2 | ||||
-rw-r--r-- | UI.cpp | 87 | ||||
-rw-r--r-- | UI.h | 13 | ||||
-rw-r--r-- | main.cpp | 140 |
16 files changed, 435 insertions, 231 deletions
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 <boost/signals2.hpp> - -#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<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_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<void(int)> 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 <boost/signals2.hpp> + +#include "config.h" +#include "BPMDetect.h" + +// Generated from MIDI Clock +class ClockClick +{ +public: + ClockClick(); + + // signals + boost::signals2::signal<void()> signal_click; + boost::signals2::signal<void(int)> 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<clock_type> now = clock_type::now(); + + std::chrono::duration<double> duration{60.0 / static_cast<double>(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 <boost/signals2.hpp> + +#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<void(int64_t)> signal_click; + + // slots + void run_cyclic_50ms(); + +private: + Config& m_config; + + std::chrono::time_point<clock_type> 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 <chrono> +#include <cmath> +#include <cstdint> +#include <exception> +#include <iostream> +#include <limits> +#include <memory> +#include <stdexcept> + +#include <stdio.h> +#include <sys/types.h> +#include <sys/time.h> +#include <time.h> +#include <signal.h> + +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<void()> 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 <boost/signals2.hpp> + +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; +}; + @@ -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 <boost/signals2.hpp> + +#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<void()> signal_click; + boost::signals2::signal<void(int)> signal_bpm; + + // slots + void receive_note(int channel, int note, uint64_t timestamp); + +private: + Config& m_config; + + BPMDetect m_detect; +}; + @@ -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 @@ -21,7 +21,7 @@ public: PCM(); ~PCM(); - void click(); + void click(int64_t offset_us); // generate 1 buffer size void generate(); @@ -11,6 +11,9 @@ #include <fmt/color.h> #include <fmt/format.h> +#include <stdio.h> +#include <sys/poll.h> + 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<int> 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(); @@ -31,12 +31,15 @@ public: void draw(); + void handle_input(); + + bool key_available(); + // 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; + boost::signals2::signal<void(int)> signal_bpm; + boost::signals2::signal<void(int, int)> signal_note_set_from_midi; + boost::signals2::signal<void(int)> signal_mode; + boost::signals2::signal<void(int)> signal_output; // slots void count_main_loops(); @@ -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 <chrono> -#include <cmath> -#include <cstdint> -#include <exception> -#include <iostream> -#include <limits> -#include <memory> -#include <stdexcept> - -#include <stdio.h> -#include <sys/types.h> -#include <sys/time.h> -#include <time.h> -#include <signal.h> - -#include <boost/signals2.hpp> - -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<void()> 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(); } - |