From 20fc4147d29aa11f45c77fd59fac9a10dca8bae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sat, 21 Dec 2024 17:52:54 -0500 Subject: [PATCH] protocols: implement a remote control protocol based on MIDI Based on reverse engineering done by this wonderful team: https://github.com/NicoG60/TouchMCU --- cmake/libremidi.examples.cmake | 13 +- examples/protocols/remote_control.cpp | 99 +++ .../libremidi/protocols/remote_control.hpp | 832 ++++++++++++++++++ 3 files changed, 939 insertions(+), 5 deletions(-) create mode 100644 examples/protocols/remote_control.cpp create mode 100644 include/libremidi/protocols/remote_control.hpp diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index 288940b..c092cb1 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -1,15 +1,16 @@ macro(setup_example _example) - target_link_libraries(${_example} PRIVATE libremidi) + target_link_libraries("${_example}" PRIVATE libremidi) endmacro() macro(add_example _example) - add_executable(${_example} examples/${_example}.cpp) - setup_example(${_example}) + string(REPLACE "/" "_" _exampleName "${_example}") + add_executable("${_exampleName}" "examples/${_example}.cpp") + setup_example("${_exampleName}") endmacro() macro(add_backend_example _example) - add_executable(${_example} examples/backends/${_example}.cpp) - setup_example(${_example}) + add_executable("${_example}" "examples/backends/${_example}.cpp") + setup_example("${_example}") endmacro() add_example(midiobserve) @@ -27,6 +28,8 @@ add_example(minimal) add_example(midi2_echo) add_example(rawmidiin) +add_example(protocols/remote_control) + if(LIBREMIDI_NI_MIDI2) add_example(midi2_interop) endif() diff --git a/examples/protocols/remote_control.cpp b/examples/protocols/remote_control.cpp new file mode 100644 index 0000000..78a7b66 --- /dev/null +++ b/examples/protocols/remote_control.cpp @@ -0,0 +1,99 @@ +#include +#include + +#if __has_include() + #include +#else + #include + #include + #include +namespace magic_enum +{ +std::string enum_name(auto cmd) +{ + std::stringstream ss; + ss << "0x" << std::setbase(16) << static_cast(cmd); + return ss.str(); +} +} +#endif + +#include +#include +#include +#include +#include + +int main() +{ + auto api = libremidi::API::ALSA_SEQ; + libremidi::observer observer{{.track_any = true}, api}; + if (observer.get_input_ports().empty()) + return 1; + if (observer.get_output_ports().empty()) + return 1; + + libremidi::input_port ip; + libremidi::output_port op; + + // Tested with https://github.com/NicoG60/TouchMCU + for (auto& p : observer.get_input_ports()) + if (p.port_name == "TouchOSC") + ip = p; + for (auto& p : observer.get_output_ports()) + if (p.port_name == "TouchOSC") + op = p; + + if (ip.port_name.empty() || op.port_name.empty()) + { + std::cerr << "No device found !"; + return 1; + } + + // Create the midi out port + libremidi::midi_out midi_out{{}, api}; + + // Set-up the remote control API. + // Here we only do some logging, this is where commands sqall be handled. + libremidi::remote_control_processor rcp{{.midi_out = [&](libremidi::message&& msg) { + midi_out.send_message(msg); + }, .on_command = [](libremidi::remote_control_protocol::mixer_command cmd, bool pressed) { + std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " + << (pressed ? "pressed" : "released") << "\n"; + }, .on_control = [](libremidi::remote_control_protocol::mixer_control ctl, int v) { + std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; + }, .on_fader = [](libremidi::remote_control_protocol::fader f, uint16_t v) { + std::cerr << "fader: " << magic_enum::enum_name(f) << " -> " << v << "\n"; + }}}; + + // Initialize the midi in port + libremidi::midi_in midi_in{ + {.on_message = [&](const libremidi::message& message) { rcp.on_midi(message); }}, api}; + + // Open the ports + if (auto err = midi_in.open_port(ip); err != stdx::error{}) + err.throw_exception(); + + if (auto err = midi_out.open_port(op); err != stdx::error{}) + err.throw_exception(); + + // Start communication + rcp.start(); + + // Blast messages :) + using proto = libremidi::remote_control_protocol; + unsigned i = 0; + for (;;) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + std::time_t result = std::time(nullptr); + auto ctime = std::localtime(&result); + rcp.update_timecode(ctime->tm_hour, ctime->tm_min, ctime->tm_sec, 0); + rcp.update_lcd(std::string(1, '\0' + i % 127), i % 112); + rcp.fader(static_cast(i % 8), (200 * i) % 16384); + i++; + } + + return 0; +} diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp new file mode 100644 index 0000000..e222b1b --- /dev/null +++ b/include/libremidi/protocols/remote_control.hpp @@ -0,0 +1,832 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace libremidi +{ +static constexpr auto to_underlying(auto e) +{ + return static_cast>(e); +} + +// A clean-room reverse-engineered remote control protocol compatible with many hardware devices. +// Thanks https://github.com/NicoG60/TouchMCU ! +struct remote_control_protocol +{ + enum class command_to_device : uint8_t + { + device_query = 0x00, + host_connection_reply = 0x02, + + transport_click = 0x0A, + lcd_backlight_save = 0x0B, + touchless_movable_fader = 0x0C, + + faders_touch_sensitivity = 0x0E, + go_offline = 0x0F, + update_tc_display = 0x10, + update_assignment_display = 0x11, + update_lcd = 0x12, + firmware_version_request = 0x13, + version_reply = 0x14, + + firmware_update = 0x18, + + channel_meter_mode = 0x20, + global_lcd_meter_mode = 0x21, + + faders_to_minimum = 0x61, + all_leds_off = 0x62, + reset = 0x63, + }; + + enum class command_from_device : uint8_t + { + host_connection_query = 0x01, + host_connection_confirmation = 0x03, + host_connection_error = 0x04, + version_reply = 0x14, + }; + + enum class lcd_meter_mode : uint8_t + { + horizontal = 0x00, + vertical = 0x01, + }; + + enum class fader_sensitivity : uint8_t + { + sensitivity_0 = 0x00, + sensitivity_1 = 0x01, + sensitivity_2 = 0x02, + sensitivity_3 = 0x03, + sensitivity_4 = 0x04, + + sensitivity_default = sensitivity_3, + }; + + // CC Message: + // 0b0LMMVVVV + // L: toggle underneath LED + // MM: mode as led_ring_mode + // VVVV: value + enum class led_ring_mode : uint8_t + { + mode_0 = 0b00, // one led only + mode_1 = 0b01, // pan pot + mode_2 = 0b10, // fill leds from left + mode_3 = 0b11, // fill leds from middle + }; + + enum class pot : uint8_t + { + pot_0 = 0x00, + pot_1 = 0x01, + pot_2 = 0x02, + pot_3 = 0x03, + pot_4 = 0x04, + pot_5 = 0x05, + pot_6 = 0x06, + pot_7 = 0x07, + }; + + enum class fader : uint8_t + { + fader_0 = 0x00, + fader_1 = 0x01, + fader_2 = 0x02, + fader_3 = 0x03, + fader_4 = 0x04, + fader_5 = 0x05, + fader_6 = 0x06, + fader_7 = 0x07, + fader_master = 0x08, + }; + + // control changes + enum class mixer_control : uint8_t + { + // rotation: CC + // 0b00000001 : clockwise + // 0b01000001 : counter-clockwise + vpot_rotation_0 = 0x10 + 0x00, + vpot_rotation_1 = 0x10 + 0x01, + vpot_rotation_2 = 0x10 + 0x02, + vpot_rotation_3 = 0x10 + 0x03, + vpot_rotation_4 = 0x10 + 0x04, + vpot_rotation_5 = 0x10 + 0x05, + vpot_rotation_6 = 0x10 + 0x06, + vpot_rotation_7 = 0x10 + 0x07, + + external_control = 0x2E, + + // led ring: CC + vpot_led_0 = 0x30 + 0x00, + vpot_led_1 = 0x30 + 0x01, + vpot_led_2 = 0x30 + 0x02, + vpot_led_3 = 0x30 + 0x03, + vpot_led_4 = 0x30 + 0x04, + vpot_led_5 = 0x30 + 0x05, + vpot_led_6 = 0x30 + 0x06, + vpot_led_7 = 0x30 + 0x07, + + jog_wheel = 0x3C, + + timecode_digit_0 = 0x40 + 0x00, + timecode_digit_1 = 0x40 + 0x01, + timecode_digit_2 = 0x40 + 0x02, + timecode_digit_3 = 0x40 + 0x03, + timecode_digit_4 = 0x40 + 0x04, + timecode_digit_5 = 0x40 + 0x05, + timecode_digit_6 = 0x40 + 0x06, + timecode_digit_7 = 0x40 + 0x07, + timecode_digit_8 = 0x40 + 0x08, + timecode_digit_9 = 0x40 + 0x09, + + assignment_digit_0 = 0x4A, + assignment_digit_1 = 0x4B, + }; + + // note events + enum class mixer_command : uint8_t + { + vpot_click_0 = 0x20 + 0x00, + vpot_click_1 = 0x20 + 0x01, + vpot_click_2 = 0x20 + 0x02, + vpot_click_3 = 0x20 + 0x03, + vpot_click_4 = 0x20 + 0x04, + vpot_click_5 = 0x20 + 0x05, + vpot_click_6 = 0x20 + 0x06, + vpot_click_7 = 0x20 + 0x07, + + rec_0 = 0x00 + 0x00, + rec_1 = 0x00 + 0x01, + rec_2 = 0x00 + 0x02, + rec_3 = 0x00 + 0x03, + rec_4 = 0x00 + 0x04, + rec_5 = 0x00 + 0x05, + rec_6 = 0x00 + 0x06, + rec_7 = 0x00 + 0x07, + + solo_0 = 0x08 + 0x00, + solo_1 = 0x08 + 0x01, + solo_2 = 0x08 + 0x02, + solo_3 = 0x08 + 0x03, + solo_4 = 0x08 + 0x04, + solo_5 = 0x08 + 0x05, + solo_6 = 0x08 + 0x06, + solo_7 = 0x08 + 0x07, + + mute_0 = 0x10 + 0x00, + mute_1 = 0x10 + 0x01, + mute_2 = 0x10 + 0x02, + mute_3 = 0x10 + 0x03, + mute_4 = 0x10 + 0x04, + mute_5 = 0x10 + 0x05, + mute_6 = 0x10 + 0x06, + mute_7 = 0x10 + 0x07, + + sel_0 = 0x18 + 0x00, + sel_1 = 0x18 + 0x01, + sel_2 = 0x18 + 0x02, + sel_3 = 0x18 + 0x03, + sel_4 = 0x18 + 0x04, + sel_5 = 0x18 + 0x05, + sel_6 = 0x18 + 0x06, + sel_7 = 0x18 + 0x07, + + // TODO metering + assign_track = 0x28, + assign_send = 0x29, + assign_pan = 0x2A, + assign_plugin = 0x2B, + assign_eq = 0x2C, + assign_instrument = 0x2D, + + bank_left = 0x2E, + bank_right = 0x2F, + channel_left = 0x30, + channel_right = 0x31, + flip = 0x32, + global = 0x33, + + name_value_button = 0x34, + smpte_beats_button = 0x35, + + f1 = 0x36 + 0x00, + f2 = 0x36 + 0x01, + f3 = 0x36 + 0x02, + f4 = 0x36 + 0x03, + f5 = 0x36 + 0x04, + f6 = 0x36 + 0x05, + f7 = 0x36 + 0x06, + f8 = 0x36 + 0x07, + + midi_tracks = 0x3E, + inputs = 0x3F, + audio_tracks = 0x40, + audio_instruments = 0x41, + aux = 0x42, + busses = 0x43, + outputs = 0x44, + user = 0x45, + + shift = 0x46, + option = 0x47, + control = 0x48, + alt = 0x49, + + save = 0x50, + undo = 0x51, + cancel = 0x52, + enter = 0x53, + + markers = 0x54, + nudge = 0x55, + cycle = 0x56, + drop = 0x57, + replace = 0x58, + click = 0x59, + solo = 0x5a, + + rewind = 0x5b, + forward = 0x5c, + stop = 0x5d, + play = 0x5e, + record = 0x5f, + + up = 0x60, + down = 0x61, + left = 0x62, + right = 0x63, + zoom = 0x64, + scrub = 0x65, + + user_switch_1 = 0x66, + user_switch_2 = 0x67, + + fader_touched_0 = 0x68, + fader_touched_1 = 0x69, + fader_touched_2 = 0x6a, + fader_touched_3 = 0x6b, + fader_touched_4 = 0x6c, + fader_touched_5 = 0x6d, + fader_touched_6 = 0x6e, + fader_touched_7 = 0x6f, + fader_touched_master = 0x70, + + smpte_led = 0x71, + beats_led = 0x72, + rude_solo_led = 0x73, + + relay_click = 0x76, + }; + + template + using arr = std::array; + + static libremidi::message make_command_impl(auto&&... data) + { + using namespace std; + libremidi::message m; + m.bytes.reserve((std::ssize(data) + ...)); + (m.bytes.insert(m.bytes.end(), begin(data), end(data)), ...); + return m; + } + + static libremidi::message make_command(command_to_device c, auto&&... data) + { + using namespace std; + const auto cmd = to_underlying(c); + const uint8_t header[6]{0xF0, 0x00, 0x00, 0x66, 0x14, cmd}; + const uint8_t footer[1]{0xF7}; + return make_command_impl(header, data..., footer); + } + + static auto device_query() { return make_command(command_to_device::device_query); } + + static auto response_to_challenge(arr<4> c) + { + arr<4> r; + + r[0] = 0x7F & (c[0] + (c[1] ^ 0x0A) - c[3]); + r[1] = 0x7F & ((c[2] >> 4) ^ (c[0] + c[3])); + r[2] = 0x7F & (c[3] - (c[2] << 2) ^ (c[0] | c[1])); + r[3] = 0x7F & (c[1] - c[2] + (0xF0 ^ (c[3] << 4))); + + return r; + } + + static auto host_connection_reply(arr<7> serial, arr<4> challenge_code) + { + const auto res = response_to_challenge(challenge_code); + return make_command(command_to_device::host_connection_reply, serial, res); + } + + static auto transport_click(bool enabled) + { + return make_command( + command_to_device::transport_click, arr<1>{uint8_t(enabled ? 0x01 : 0x00)}); + } + + static auto lcd_backlight_save(uint8_t timeout) + { + // 0: instant off otherwise timeout in minutes + return make_command(command_to_device::lcd_backlight_save, arr<1>{timeout}); + } + + static auto touchless_movable_fader(bool enabled) + { + return make_command( + command_to_device::touchless_movable_fader, arr<1>{uint8_t(enabled ? 0x01 : 0x00)}); + } + + static auto faders_touch_sensitivity(uint8_t fader_id, fader_sensitivity sens) + { + return make_command( + command_to_device::faders_touch_sensitivity, arr<2>{fader_id, to_underlying(sens)}); + } + + static auto go_offline() { return make_command(command_to_device::go_offline, arr<1>{0x7F}); } + + static auto update_tc_display() + { + // FIXME 1 .. 10 + return make_command(command_to_device::update_tc_display, arr<10>{}); + } + + static auto update_assignment_display() + { + // FIXME 1 .. 2 + return make_command(command_to_device::update_assignment_display, arr<2>{}); + } + + static auto update_lcd(std::string_view txt, int pos) + { + // FIXME + if (pos < 0 || pos >= 112) + return libremidi::message{}; + + int len = int(std::ssize(txt)); + + if (len > (112 - pos)) + { + txt = txt.substr(0, 112 - pos); + len = 112 - pos; + } + + uint8_t buf[128]; + const int N = std::min(len, 112 - pos); + for (int i = 0; i < N; i++) + { + buf[i + pos] = charmap_lcd(txt[i]); + } + buf[55] = '\n'; + buf[111] = '\n'; + + uint8_t cmd_pos = pos; + + return make_command(command_to_device::update_lcd, arr<1>{cmd_pos}, std::span(buf + pos, len)); + } + + static auto update_lcd(std::string_view txt) + { + uint8_t buf[112] = {}; + for (int i = 0; i < std::min(int(std::ssize(txt)), 112); i++) + { + buf[i] = charmap_lcd(txt[i]); + } + buf[55] = '\n'; + buf[111] = '\n'; + return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, 112)); + } + + static auto firmware_version_request() + { + return make_command(command_to_device::firmware_version_request, arr<1>{0}); + } + + static auto firmware_update(std::span firmware) + { + return make_command(command_to_device::firmware_update, firmware); + } + + static auto + channel_meter_mode(uint8_t fader_id, bool level_meter, bool peak_hold, bool signal_led) + { + uint8_t mode = 0; + + if (signal_led) + mode |= 0b1; + if (peak_hold) + mode |= 0b10; + if (level_meter) + mode |= 0b100; + + return make_command(command_to_device::channel_meter_mode, arr<2>{fader_id, mode}); + } + + static auto global_lcd_meter_mode(lcd_meter_mode mode) + { + return make_command(command_to_device::global_lcd_meter_mode, arr<1>{to_underlying(mode)}); + } + + static auto faders_to_minimum() { return make_command(command_to_device::faders_to_minimum); } + + static auto all_leds_off() { return make_command(command_to_device::all_leds_off); } + + static auto reset() { return make_command(command_to_device::reset); } + + static auto timecode(int hi, int mi, int si, int framei) + { + std::vector msg; + auto h = std::to_string(hi); + while (h.size() < 3) + h.insert(h.begin(), '0'); + auto m = std::to_string(mi); + while (m.size() < 2) + m.insert(m.begin(), '0'); + auto s = std::to_string(si); + while (s.size() < 2) + s.insert(s.begin(), '0'); + auto f = std::to_string(framei); + while (f.size() < 3) + f.insert(f.begin(), '0'); + + using ce = libremidi::channel_events; + msg.push_back(ce::control_change(1, 0x49, charmap_7segment(h[0]))); + msg.push_back(ce::control_change(1, 0x48, charmap_7segment(h[1]))); + msg.push_back(ce::control_change(1, 0x47, charmap_7segment(h[2]))); + + msg.push_back(ce::control_change(1, 0x46, charmap_7segment(m[0]))); + msg.push_back(ce::control_change(1, 0x45, charmap_7segment(m[1]))); + + msg.push_back(ce::control_change(1, 0x44, charmap_7segment(s[0]))); + msg.push_back(ce::control_change(1, 0x43, charmap_7segment(s[1]))); + + msg.push_back(ce::control_change(1, 0x42, charmap_7segment(f[0]))); + msg.push_back(ce::control_change(1, 0x41, charmap_7segment(f[1]))); + msg.push_back(ce::control_change(1, 0x40, charmap_7segment(f[2]))); + + return msg; + } + + static uint8_t charmap_7segment(char c, bool dot) + { + uint8_t res = charmap_7segment(c); + if (dot) + res |= 0b00100000; + return res; + }; + + static uint8_t charmap_7segment(char c) + { + // FIXME there are some more characters but what to map them to ? :) + if (c >= 'a' && c <= 'z') + return c - 'a' + 1; + else if (c >= 'A' && c <= 'Z') + return c - 'A' + 1; + else if (c >= '0' && c <= '9') + return c - '0' + 0x30; + else + switch (c) + { + case '[': + case '{': + return 0x1B; + case '\\': + case '~': // yen too ? + return 0x1C; + case ']': + case '}': + return 0x1D; + case '^': + return 0x1E; + case '_': + return 0x1F; + + case '!': + return 0x21; + case '"': + return 0x22; + case '#': + return 0x23; + case '$': + return 0x24; + case '%': + return 0x25; + case '&': + return 0x26; + case '\'': + return 0x27; + case '(': + return 0x28; + case ')': + return 0x29; + case '*': + return 0x2A; + case '+': + return 0x2B; + case ',': + return 0x2C; + case '-': + return 0x2D; + case '.': + return 0x2E; + case '/': + return 0x2F; + + case ':': + return 0x3A; + case ';': + return 0x3B; + case '<': + return 0x3C; + case '=': + return 0x3D; + case '>': + return 0x3E; + case '?': + return 0x3F; + + default: + return 0x00; + } + } + static uint8_t charmap_lcd(char c) + { + // FIXME there are some more characters but what to map them to ? :) + if (c >= 'a' && c <= 'z') + return c - 'a' + 0x61; + else if (c >= 'A' && c <= 'Z') + return c - 'A' + 0x41; + else if (c >= '0' && c <= '9') + return c - '0' + 0x30; + else + switch (c) + { + case '!': + return 0x21; + case '"': + return 0x22; + case '#': + return 0x23; + case '$': + return 0x24; + case '%': + return 0x25; + case '&': + return 0x26; + case '\'': + return 0x27; + case '(': + return 0x28; + case ')': + return 0x29; + case '*': + return 0x2A; + case '+': + return 0x2B; + case ',': + return 0x2C; + case '-': + return 0x2D; + case '.': + return 0x2E; + case '/': + return 0x2F; + + case ':': + return 0x3A; + case ';': + return 0x3B; + case '<': + return 0x3C; + case '=': + return 0x3D; + case '>': + return 0x3E; + case '?': + return 0x3F; + + case '@': + return 0x40; + case '[': + return 0x5B; + case '~': // Yen symbol... builtin mojibake? + return 0x5C; + case ']': + return 0x5D; + case '^': + return 0x5E; + case '_': + return 0x5F; + case '`': + return 0x60; + case '{': + return 0x7B; + case '|': + return 0x7C; + case '}': + return 0x7D; + case '\u000E': + return 0x7E; + case '\u000F': + return 0x7F; + default: + return c; // gives access to the bubble first row 0x00 > 0x0F + } + } +}; + +struct rcp_configuration +{ + //! How to send MIDI messages to the device. + //! Note: this function *will* be called from different thread, + //! thus it has to be thread-safe, for instance + //! by storing the message in an event queue. + std::function midi_out; + + std::function on_command; + std::function on_control; + std::function on_fader; + + libremidi::midi_error_callback on_error{}; +}; + +struct remote_control_processor : libremidi::error_handler +{ + using rcp = libremidi::remote_control_protocol; + rcp_configuration configuration; + + explicit remote_control_processor(rcp_configuration conf) + : configuration{std::move(conf)} + { + assert(configuration.midi_out); + + if (!configuration.on_error) + configuration.on_error = [](std::string_view s, auto&&...) { + std::fprintf(stderr, "libremidi: rcp error: %s\n", s.data()); + }; + + if (!configuration.on_command) + configuration.on_command + = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_command"); }; + + if (!configuration.on_control) + configuration.on_control + = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_control"); }; + + if (!configuration.on_fader) + configuration.on_fader + = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); }; + } + + void start() + { + current_state = waiting_for_query; + configuration.midi_out(rcp::device_query()); + } + + void on_midi(const libremidi::message& message) + { + switch (message.get_message_type()) + { + case libremidi::message_type::SYSTEM_EXCLUSIVE: + if (int N = message.size(); N >= 7) + { + const uint8_t* bytes = message.bytes.data(); + // strip 0xF0 & 0xF7 + bytes += 1; + N -= 2; + + // strip header + bytes += 4; + N -= 4; + on_rcp_command(std::span(bytes, N)); + } + else + { + libremidi_handle_error(configuration, "Invalid sysex"); + } + break; + case libremidi::message_type::NOTE_ON: + configuration.on_command(static_cast(message[1]), message[2] > 0); + break; + case libremidi::message_type::NOTE_OFF: + break; + case libremidi::message_type::CONTROL_CHANGE: + configuration.on_control(static_cast(message[1]), message[2]); + break; + case libremidi::message_type::PITCH_BEND: { + uint16_t value = message.bytes[2] * 128 + message.bytes[1]; + configuration.on_fader(static_cast(uint8_t(message.get_channel() - 1)), value); + break; + } + default: + break; + } + } + + void on_rcp_command(std::span cmd) + { + if (cmd.empty()) + { + libremidi_handle_error(configuration, "on_rcp_command: empty command"); + return; + } + + auto command = static_cast(cmd[0]); + cmd = cmd.subspan(1); + switch (command) + { + case rcp::command_from_device::host_connection_query: { + if (cmd.size() == 11) + { + current_state = got_query; + + std::array serial; + std::array challenge; + std::copy_n(cmd.data(), 7, serial.begin()); + std::copy_n(cmd.data() + 7, 4, challenge.begin()); + + configuration.midi_out(rcp::host_connection_reply(serial, challenge)); + } + else + libremidi_handle_error(configuration, "host_connection_query: invalid size"); + break; + } + case rcp::command_from_device::host_connection_confirmation: + current_state = connected; + break; + case rcp::command_from_device::host_connection_error: + current_state = errored; + libremidi_handle_error(configuration, "host_connection_error"); + break; + case rcp::command_from_device::version_reply: { + // TODO + break; + } + default: + break; + } + } + + void update_timecode(int h, int m, int s, int f) + { + for (auto&& m : rcp::timecode(h, m, s, f)) + configuration.midi_out(std::move(m)); + } + + void update_lcd(std::string_view v) + { + auto res = rcp::update_lcd(v); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + void update_lcd(std::string_view v, int pos) + { + auto res = rcp::update_lcd(v, pos); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + void command(remote_control_protocol::mixer_command c, bool press) + { + using ce = libremidi::channel_events; + configuration.midi_out(ce::note_on(1, to_underlying(c), press ? 127 : 0)); + configuration.midi_out(ce::note_off(1, to_underlying(c), press ? 127 : 0)); + } + + void control(remote_control_protocol::mixer_control c, int value) + { + using ce = libremidi::channel_events; + configuration.midi_out(ce::control_change(1, to_underlying(c), value)); + } + + void fader(remote_control_protocol::fader c, uint16_t value) + { + int idx = to_underlying(c); + + using ce = libremidi::channel_events; + configuration.midi_out(ce::pitch_bend(idx + 1, value)); + } + + // State machine + enum + { + waiting_for_query, + got_query, + connected, + errored + } current_state{waiting_for_query}; +}; +}