From ebbaa73e24ed186ad08e91047621261cc1cbfe05 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Thu, 9 Jul 2026 08:28:28 +0200 Subject: [PATCH 1/9] Initial commit of files and refactoring --- cmake/libremidi.examples.cmake | 1 + examples/protocols/coremidi_mcu_xtouch.cpp | 345 ++++++++++++++++++ .../libremidi/protocols/remote_control.hpp | 242 +++++++++++- 3 files changed, 569 insertions(+), 19 deletions(-) create mode 100644 examples/protocols/coremidi_mcu_xtouch.cpp diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index 0eb53f50..1af82094 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -91,6 +91,7 @@ endif() if(LIBREMIDI_HAS_COREMIDI) add_example(coremidi_share) + add_example(protocols/coremidi_mcu_xtouch) endif() if(LIBREMIDI_HAS_EMSCRIPTEN) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp new file mode 100644 index 00000000..048e3108 --- /dev/null +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -0,0 +1,345 @@ +#include "../utils.hpp" + +#include +#include + +#if defined(_WIN32) && __has_include() + #include +#endif + +#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 + +#if defined(__APPLE__) + #include + #include +#endif + +#include +#include +#include +#include +#include + +using mcu = libremidi::remote_control_protocol; +using mcu_proc = libremidi::remote_control_processor; + +struct my_xtouch_app +{ + static constexpr auto api = libremidi::API::COREMIDI; + static constexpr char kDeviceName[] = "X-TOUCH_INT"; + + enum class State : uint8_t + { + Off,Starting, Running, Stopping + }; + + struct vpot_st + { + mcu::pot index; + mcu::led_state state = mcu::led_state::off; + mcu::led_ring_mode mode = mcu::led_ring_mode::mode_0; + uint8_t value = 0; + void change_value(int change){ + if (change > 0 && value < mcu::vpot_max_value) value++; + else if (change < 0 && value > 0) value--; + } + }; + + + State state = State::Off; + + MIDIClientRef handle; + + +// std::vector midiin; +// std::vector midiout; + + libremidi::midi_out * midi_out; + libremidi::midi_in * midi_in; + + libremidi::remote_control_processor * rcp; + + struct { + bool rec[8] = {false,false,false,false,false,false,false,false}; + bool mute[8] = {false,false,false,false,false,false,false,false}; + } buttons; + + struct vpot_st vpots[8] = { + {.index = mcu::pot::pot_0, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, + {.index = mcu::pot::pot_1, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, + {.index = mcu::pot::pot_2, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, + {.index = mcu::pot::pot_3, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, + {.index = mcu::pot::pot_4, .mode = mcu::led_ring_mode::mode_0, .value = 6}, + {.index = mcu::pot::pot_5, .mode = mcu::led_ring_mode::mode_1, .value = 6}, + {.index = mcu::pot::pot_6, .mode = mcu::led_ring_mode::mode_2, .value = 6}, + {.index = mcu::pot::pot_7, .mode = mcu::led_ring_mode::mode_3, .value = 6} + }; + + my_xtouch_app() + { + std::cout << "Creating MIDIClient.." << std::endl; + + auto res = MIDIClientCreate(CFSTR("My App"), 0, 0, &handle); + if (res != noErr) + throw std::runtime_error("Could not start CoreMIDI"); + + + } + + ~my_xtouch_app() + { + if (rcp) + delete rcp; + if (midi_in) + delete midi_in; + if (midi_out) + delete midi_out; + + std::cout << "Disposing of MIDIClient" << std::endl; + MIDIClientDispose(handle); + } + + void _update_buttons() + { + assert(rcp); + + for (int i = 0; i < 8; i++){ + rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::rec_0) + i), buttons.rec[i]); + rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::mute_0) + i), buttons.mute[i]); + } + } + + void _update_vpots() + { + assert(rcp); + + for (int i = 0; i < 8; i++){ + rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + } + } + + void start_impl() + { + std::cout << "Starting app.." << std::endl; + state = State::Starting; + +// auto callback = [&](int port, const libremidi::message& msg) { +// std::cout << msg << std::endl; +// midiout[port].send_message(msg); +// }; + + + +#if defined(_WIN32) && __has_include() + winrt::init_apartment(); +#endif + + + // find devices + libremidi::observer observer{{.track_any = true}, api}; + if (observer.get_input_ports().empty()) + return; + if (observer.get_output_ports().empty()) + return; + + // scan for wanted device + 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 == kDeviceName) + ip = p; + for (auto& p : observer.get_output_ports()) + if (p.port_name == kDeviceName) + op = p; + + if (ip.port_name.empty() || op.port_name.empty()) + { + throw std::runtime_error("No device found"); + } + + // Create the midi out port + midi_out = new libremidi::midi_out {{}, api}; + midi_in = new libremidi::midi_in { + { + .on_message = [&](const libremidi::message& message) { + rcp->on_midi(message); + } + }, + 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{ + rcp = new libremidi::remote_control_processor{ + { + .midi_out = [&](libremidi::message&& msg){ + midi_out->send_message(msg); + }, + .on_command = [&](mcu::mixer_command cmd, bool pressed){ + std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " << (pressed ? "pressed" : "released") << "\n"; + auto type = mcu::which_mixer_command_type(cmd); + auto i = 0; + switch(type){ + case mcu::mixer_command_type::rec: + if (!pressed) return; + i = (int)cmd - (int)mcu::mixer_command::rec_0; + buttons.rec[i] = !buttons.rec[i]; + _update_buttons(); + break; + case mcu::mixer_command_type::mute: + if (!pressed) return; + i = (int)cmd - (int)mcu::mixer_command::mute_0; + buttons.mute[i] = !buttons.mute[i]; + _update_buttons(); + break; + case mcu::mixer_command_type::solo: + case mcu::mixer_command_type::sel: + rcp->command(cmd, pressed); + break; + break; + default: + break; + } + + }, + .on_control = [&](mcu::mixer_control ctl, int v) { + std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; + + auto type = mcu::which_mixer_control_type(ctl); + auto i = 0; + auto val = 0; + switch(type) + { + case mcu::mixer_control_type::vpot_rotation: + i = (int)ctl - (int)mcu::mixer_control::vpot_rotation_0; + + val = mcu::relative_midi_to_value(v); + std::cerr << "relative value= " << val << std::endl; + + vpots[i].change_value(val); + + _update_vpots(); + break; + + default: + break; + } + }, + .on_fader = [](mcu::fader f, uint16_t v) { + std::cerr << "fader: " << magic_enum::enum_name(f) << " -> " << v << "\n"; + } + } + }; +//rcp.command() + + // 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(); + + + state = State::Running; + + // Start communication + rcp->start(); + + + // reset interface + _update_buttons(); + _update_vpots(); + + // Blast messages :) + + mcu::channel_color_xt colors[8] = { + mcu::channel_color_xt::black, mcu::channel_color_xt::red, mcu::channel_color_xt::yellow, + mcu::channel_color_xt::green, mcu::channel_color_xt::cyan, mcu::channel_color_xt::blue, + mcu::channel_color_xt::magenta, mcu::channel_color_xt::white + }; + std::string labels1[8] = {"1", "21", "321", "4321", "54321", "654321", "7654321", "87654321"}; + std::string labels2[8] = {"ch1", "ch2", "ch3", "ch4", "ch5", "ch6", "ch7", "ch8"}; + + for (uint8_t i = 0; state == State::Running;i++) + { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + 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_ch_line(labels1[ (i/8 + i%8) % 8 ], i % 8, 0); + rcp->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % 8, 1); + + rcp->set_channel_color(i % 8, colors[(i/8 + i%8) % 8]); + rcp->update_channel_colors(); + + rcp->fader(static_cast(i % 8), (200 * i) % 16384); + + } + + state = State::Off; + } + + void start(){ + try { + start_impl(); + } catch (std::exception e){ + state = State::Off; + throw e; + } + } + +}; + +int main() +{ + my_xtouch_app app{}; + + try { + app.start(); + } catch (std::exception e){ + std::cerr << "Failed to start: " << e.what() << std::endl; + } + + while( app.state == my_xtouch_app::State::Starting); + // wait for app to not be starting.. + + // if failed to start.. + if (app.state == my_xtouch_app::State::Off){ + return 1; + } + +#if defined(__APPLE__) + // On macOS, observation can *only* be done in the main thread + // with an active CFRunLoop. + CFRunLoopRun(); +#else + // int c; + // while ((c = getchar()) != '\n' && c != EOF) + // ; + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +#endif + + return 0; +} \ No newline at end of file diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index dd6edfd1..6a789357 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -19,11 +19,15 @@ NAMESPACE_LIBREMIDI // Thanks https://github.com/NicoG60/TouchMCU ! struct remote_control_protocol { + static constexpr int channel_count = 8; + enum class device_type : uint8_t { + mackie_hui = 0x05, logic_control = 0x10, logic_control_xt = 0x11, - mackie_control = 0x14 + mackie_control = 0x14, + mackie_control_xt = 0x15 }; enum class command_to_device : uint8_t @@ -51,6 +55,8 @@ struct remote_control_protocol faders_to_minimum = 0x61, all_leds_off = 0x62, reset = 0x63, + + update_channel_colors_xt = 0x72 // xtouch specific (?) }; enum class command_from_device : uint8_t @@ -67,6 +73,42 @@ struct remote_control_protocol vertical = 0x01, }; + static constexpr uint8_t lcd_channel_line_n = 2; + static constexpr uint8_t lcd_channel_line_len = 7; + static constexpr int lcd_total_len = 112; + + static constexpr uint8_t lcd_channel_offset[8][2] = { + {0x00, 0x38}, + {0x00 + 7, 0x38 + 7}, + {0x00 + 14, 0x38 + 14}, + {0x00 + 21, 0x38 + 21}, + {0x00 + 28, 0x38 + 28}, + {0x00 + 35, 0x38 + 35}, + {0x00 + 42, 0x38 + 42}, + {0x00 + 49, 0x38 + 49}, + }; + + /** + * X-Touch specific + * 3-bit RGB encoding + */ + enum class channel_color_xt : uint8_t + { + black = 0b000, + red = 0b001, + green = 0b010, + yellow = 0b011, + blue = 0b100, + magenta = 0b101, + cyan = 0b110, + white = 0b111 + }; + + static inline bool channel_color_is_valid(channel_color_xt color) + { + return channel_color_xt::black <= color && color <= channel_color_xt::white; + }; + enum class fader_sensitivity : uint8_t { sensitivity_0 = 0x00, @@ -82,13 +124,20 @@ struct remote_control_protocol // 0b0LMMVVVV // L: toggle underneath LED // MM: mode as led_ring_mode - // VVVV: value + // VVVV: value in [0,11] + + enum class led_state : uint8_t + { + off = 0, + on = 0b01000000 + }; + 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 + mode_0 = 0b000000, // one led only + mode_1 = 0b010000, // pan pot + mode_2 = 0b100000, // fill leds from left + mode_3 = 0b110000, // fill leds from middle }; enum class pot : uint8_t @@ -103,6 +152,16 @@ struct remote_control_protocol pot_7 = 0x07, }; +// struct vpot +// { +// pot index; +// led_state state; +// led_ring_mode mode; +// uint8_t value; +// }; + + static constexpr int vpot_max_value = 11; + enum class fader : uint8_t { fader_0 = 0x00, @@ -116,6 +175,17 @@ struct remote_control_protocol fader_master = 0x08, }; + enum class mixer_control_type : uint16_t + { + vpot_rotation = 0x10, + vpot_led = 0x30, + jog_wheel = 0x3C, + timecode_digit = 0x40, + assignment_digit = 0x5A, + other = 0xFFFF + }; + + // control changes enum class mixer_control : uint8_t { @@ -160,9 +230,53 @@ struct remote_control_protocol assignment_digit_1 = 0x4B, }; + static mixer_control_type which_mixer_control_type(mixer_control ctl) + { + if (mixer_control::vpot_rotation_0 <= ctl && ctl <= mixer_control::vpot_rotation_7) return mixer_control_type::vpot_rotation; + if (mixer_control::vpot_led_0 <= ctl && ctl <= mixer_control::vpot_led_7) return mixer_control_type::vpot_led; + if (mixer_control::timecode_digit_0 <= ctl && ctl <= mixer_control::timecode_digit_9) return mixer_control_type::timecode_digit; + if (mixer_control::assignment_digit_0 <= ctl && ctl <= mixer_control::assignment_digit_1) return mixer_control_type::assignment_digit; + + return mixer_control_type::other; + } + + static inline int relative_value_to_midi(int value){ + if (value < 0) + return 0b01000000 - value; + return value; + } + static inline int relative_midi_to_value(int midi){ + if (midi < 0b010000) + return midi; + + return (0b01000000 - midi); + } + // note events + + enum class mixer_command_type : uint16_t + { + vpot_click = 0x20, + rec = 0x00, + solo = 0x08, + mute = 0x10, + sel = 0x18, + assign = 0x28, + channels = 0x2E, + f = 0x36, + page = 0x3E, + meta = 0x46, + control = 0x50, + transport = 0x54, + user = 0x66, + fader_touched = 0x68, + leds = 0x71, + other = 0xffff + }; + enum class mixer_command : uint8_t { + // vpot_click vpot_click_0 = 0x20 + 0x00, vpot_click_1 = 0x20 + 0x01, vpot_click_2 = 0x20 + 0x02, @@ -172,6 +286,7 @@ struct remote_control_protocol vpot_click_6 = 0x20 + 0x06, vpot_click_7 = 0x20 + 0x07, + // record arm rec_0 = 0x00 + 0x00, rec_1 = 0x00 + 0x01, rec_2 = 0x00 + 0x02, @@ -181,6 +296,7 @@ struct remote_control_protocol rec_6 = 0x00 + 0x06, rec_7 = 0x00 + 0x07, + // solo solo_0 = 0x08 + 0x00, solo_1 = 0x08 + 0x01, solo_2 = 0x08 + 0x02, @@ -190,6 +306,7 @@ struct remote_control_protocol solo_6 = 0x08 + 0x06, solo_7 = 0x08 + 0x07, + // mute mute_0 = 0x10 + 0x00, mute_1 = 0x10 + 0x01, mute_2 = 0x10 + 0x02, @@ -199,6 +316,7 @@ struct remote_control_protocol mute_6 = 0x10 + 0x06, mute_7 = 0x10 + 0x07, + // sel(ection) sel_0 = 0x18 + 0x00, sel_1 = 0x18 + 0x01, sel_2 = 0x18 + 0x02, @@ -209,6 +327,7 @@ struct remote_control_protocol sel_7 = 0x18 + 0x07, // TODO metering + // assign assign_track = 0x28, assign_send = 0x29, assign_pan = 0x2A, @@ -216,6 +335,7 @@ struct remote_control_protocol assign_eq = 0x2C, assign_instrument = 0x2D, + // channels bank_left = 0x2E, bank_right = 0x2F, channel_left = 0x30, @@ -226,6 +346,7 @@ struct remote_control_protocol name_value_button = 0x34, smpte_beats_button = 0x35, + // f(unction) f1 = 0x36 + 0x00, f2 = 0x36 + 0x01, f3 = 0x36 + 0x02, @@ -235,6 +356,7 @@ struct remote_control_protocol f7 = 0x36 + 0x06, f8 = 0x36 + 0x07, + // page midi_tracks = 0x3E, inputs = 0x3F, audio_tracks = 0x40, @@ -244,16 +366,19 @@ struct remote_control_protocol outputs = 0x44, user = 0x45, + // meta shift = 0x46, option = 0x47, control = 0x48, alt = 0x49, + // control save = 0x50, undo = 0x51, cancel = 0x52, enter = 0x53, + // transport markers = 0x54, nudge = 0x55, cycle = 0x56, @@ -275,9 +400,11 @@ struct remote_control_protocol zoom = 0x64, scrub = 0x65, + // user user_switch_1 = 0x66, user_switch_2 = 0x67, + // fader touched fader_touched_0 = 0x68, fader_touched_1 = 0x69, fader_touched_2 = 0x6a, @@ -288,6 +415,7 @@ struct remote_control_protocol fader_touched_7 = 0x6f, fader_touched_master = 0x70, + // leds smpte_led = 0x71, beats_led = 0x72, rude_solo_led = 0x73, @@ -295,10 +423,33 @@ struct remote_control_protocol relay_click = 0x76, }; + static mixer_command_type which_mixer_command_type(mixer_command cmd) + { + if (mixer_command::vpot_click_0 <= cmd && cmd <= mixer_command::vpot_click_7) return mixer_command_type::vpot_click; + if (mixer_command::rec_0 <= cmd && cmd <= mixer_command::rec_7) return mixer_command_type::rec; + if (mixer_command::solo_0 <= cmd && cmd <= mixer_command::solo_7) return mixer_command_type::solo; + if (mixer_command::mute_0 <= cmd && cmd <= mixer_command::mute_7) return mixer_command_type::mute; + if (mixer_command::sel_0 <= cmd && cmd <= mixer_command::sel_7) return mixer_command_type::sel; + if (mixer_command::assign_track <= cmd && cmd <= mixer_command::assign_instrument) return mixer_command_type::assign; + if (mixer_command::bank_left <= cmd && cmd <= mixer_command::global) return mixer_command_type::channels; + if (mixer_command::f1 <= cmd && cmd <= mixer_command::f8) return mixer_command_type::f; + if (mixer_command::midi_tracks <= cmd && cmd <= mixer_command::user) return mixer_command_type::page; + if (mixer_command::shift <= cmd && cmd <= mixer_command::alt) return mixer_command_type::meta; + if (mixer_command::save <= cmd && cmd <= mixer_command::enter) return mixer_command_type::control; + if (mixer_command::markers <= cmd && cmd <= mixer_command::scrub) return mixer_command_type::transport; + if (mixer_command::user_switch_1 <= cmd && cmd <= mixer_command::user_switch_2) return mixer_command_type::user; + if (mixer_command::fader_touched_0 <= cmd && cmd <= mixer_command::fader_touched_master) return mixer_command_type::fader_touched; + if (mixer_command::smpte_led <= cmd && cmd <= mixer_command::rude_solo_led) return mixer_command_type::leds; + + return mixer_command_type::other; + } + template using arr = std::array; - device_type type = device_type::mackie_control; + device_type type = device_type::mackie_control_xt; + + channel_color_xt channel_colors[8] = {channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black}; static libremidi::message make_command_impl(auto&&... data) { @@ -380,25 +531,25 @@ struct remote_control_protocol auto update_lcd(std::string_view txt, int pos) { // FIXME - if (pos < 0 || pos >= 112) + if (pos < 0 || lcd_total_len <= pos) return libremidi::message{}; int len = int(std::ssize(txt)); - if (len > (112 - pos)) + if (len > (lcd_total_len - pos)) { - txt = txt.substr(0, 112 - pos); - len = 112 - pos; + txt = txt.substr(0, lcd_total_len - pos); + len = lcd_total_len - pos; } uint8_t buf[128]; - const int N = std::min(len, 112 - pos); + const int N = std::min(len, lcd_total_len - pos); for (int i = 0; i < N; i++) { buf[i + pos] = charmap_lcd(txt[i]); } - buf[55] = '\n'; - buf[111] = '\n'; +// buf[55] = '\n'; +// buf[111] = '\n'; uint8_t cmd_pos = pos; @@ -407,14 +558,19 @@ struct remote_control_protocol auto update_lcd(std::string_view txt) { - uint8_t buf[112] = {}; - for (int i = 0; i < std::min(int(std::ssize(txt)), 112); i++) + uint8_t buf[lcd_total_len] = {}; + for (int i = 0; i < std::min(int(std::ssize(txt)), lcd_total_len); 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)); +// buf[55] = '\n'; +// buf[111] = '\n'; + return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, lcd_total_len)); + } + + auto update_channel_colors() + { + return make_command(command_to_device::update_channel_colors_xt, std::span((uint8_t*)channel_colors, 8)); } auto firmware_version_request() @@ -707,6 +863,7 @@ struct remote_control_processor : libremidi::error_handler { current_state = waiting_for_query; configuration.midi_out(impl.device_query()); + configuration.midi_out(impl.firmware_version_request()); } void on_midi(const libremidi::message& message) @@ -727,6 +884,8 @@ struct remote_control_processor : libremidi::error_handler { impl.type = static_cast(bytes[3]); + std::cerr << "device type " << (int) impl.type << std::endl; + // strip header bytes += 4; N -= 4; @@ -821,6 +980,43 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(std::move(res)); } + void update_lcd_ch_line(std::string_view v, uint ch, uint line) + { + // 8 channels, 2 lines + // Show error msg? + if (7 < ch || remote_control_protocol::lcd_channel_line_n < line) + return; + + // Prefill segment with spaces + char buf[8] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0}; + + int len = int(std::ssize(v)); + if (len > remote_control_protocol::lcd_channel_line_len) + len = remote_control_protocol::lcd_channel_line_len; + + std::memcpy(buf, v.data(), len); + + auto res = impl.update_lcd(buf, remote_control_protocol::lcd_channel_offset[ch][line]); + + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + void set_channel_color(uint8_t ch, remote_control_protocol::channel_color_xt color) + { + if (7 < ch || ! remote_control_protocol::channel_color_is_valid(color)) + return; + + impl.channel_colors[ch] = color; + } + + void update_channel_colors() + { + auto res = impl.update_channel_colors(); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + void command(remote_control_protocol::mixer_command c, bool press) { using ce = libremidi::channel_events; @@ -834,6 +1030,14 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::control_change(1, to_underlying(c), value)); } + inline void vpot(remote_control_protocol::pot index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) + { + control( + (remote_control_protocol::mixer_control)((int)remote_control_protocol::mixer_control::vpot_led_0 + (int)index), + (int)state | (int)mode | value + ); + } + void fader(remote_control_protocol::fader c, uint16_t value) { int idx = to_underlying(c); From 2233fbbd52665a6c0aed2727d866a7199e937480 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Wed, 15 Jul 2026 13:32:25 +0200 Subject: [PATCH 2/9] refactoring --- examples/protocols/coremidi_mcu_xtouch.cpp | 200 +++++++++---- examples/protocols/remote_control.cpp | 41 ++- .../libremidi/protocols/remote_control.hpp | 264 ++++++++++++------ 3 files changed, 352 insertions(+), 153 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index 048e3108..6b16c480 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -27,6 +27,8 @@ std::string enum_name(auto cmd) #if defined(__APPLE__) #include #include +#else +#error This example was written for CoreMIDI, so you will have to adapt it for your own system #endif #include @@ -34,14 +36,16 @@ std::string enum_name(auto cmd) #include #include #include +#include using mcu = libremidi::remote_control_protocol; using mcu_proc = libremidi::remote_control_processor; struct my_xtouch_app { - static constexpr auto api = libremidi::API::COREMIDI; + static constexpr auto api = libremidi::API::UNSPECIFIED; static constexpr char kDeviceName[] = "X-TOUCH_INT"; + static constexpr mcu::device_type kDeviceType = mcu::device_type::mackie_control_xt; enum class State : uint8_t { @@ -50,14 +54,20 @@ struct my_xtouch_app struct vpot_st { - mcu::pot index; + mcu::channel_index index; mcu::led_state state = mcu::led_state::off; mcu::led_ring_mode mode = mcu::led_ring_mode::mode_0; uint8_t value = 0; void change_value(int change){ - if (change > 0 && value < mcu::vpot_max_value) value++; + if (change > 0 && value < mcu::vpot_max_value-1) value++; else if (change < 0 && value > 0) value--; } + void toggle_led_state(){ + if (state == mcu::led_state::off) + state = mcu::led_state::on; + else + state = mcu::led_state::off; + } }; @@ -65,10 +75,6 @@ struct my_xtouch_app MIDIClientRef handle; - -// std::vector midiin; -// std::vector midiout; - libremidi::midi_out * midi_out; libremidi::midi_in * midi_in; @@ -80,14 +86,14 @@ struct my_xtouch_app } buttons; struct vpot_st vpots[8] = { - {.index = mcu::pot::pot_0, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, - {.index = mcu::pot::pot_1, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, - {.index = mcu::pot::pot_2, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, - {.index = mcu::pot::pot_3, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, - {.index = mcu::pot::pot_4, .mode = mcu::led_ring_mode::mode_0, .value = 6}, - {.index = mcu::pot::pot_5, .mode = mcu::led_ring_mode::mode_1, .value = 6}, - {.index = mcu::pot::pot_6, .mode = mcu::led_ring_mode::mode_2, .value = 6}, - {.index = mcu::pot::pot_7, .mode = mcu::led_ring_mode::mode_3, .value = 6} + {.index = mcu::channel_index::channel_1, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, + {.index = mcu::channel_index::channel_2, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, + {.index = mcu::channel_index::channel_3, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, + {.index = mcu::channel_index::channel_4, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, + {.index = mcu::channel_index::channel_5, .mode = mcu::led_ring_mode::mode_0, .value = 6}, + {.index = mcu::channel_index::channel_6, .mode = mcu::led_ring_mode::mode_1, .value = 6}, + {.index = mcu::channel_index::channel_7, .mode = mcu::led_ring_mode::mode_2, .value = 6}, + {.index = mcu::channel_index::channel_8, .mode = mcu::led_ring_mode::mode_3, .value = 6} }; my_xtouch_app() @@ -114,26 +120,36 @@ struct my_xtouch_app MIDIClientDispose(handle); } - void _update_buttons() + void _update_buttons(int i = -1) { assert(rcp); - for (int i = 0; i < 8; i++){ + if (i == -1){ rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::rec_0) + i), buttons.rec[i]); rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::mute_0) + i), buttons.mute[i]); } + else { + for (int i = 0; i < 8; i++){ + rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::rec_0) + i), buttons.rec[i]); + rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::mute_0) + i), buttons.mute[i]); + } + } } - void _update_vpots() + void _update_vpots(int i = -1) { assert(rcp); - for (int i = 0; i < 8; i++){ + if (i == -1){ rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + } else { + for (i = 0; i < 8; i++){ + rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + } } } - void start_impl() + void _start_impl() { std::cout << "Starting app.." << std::endl; state = State::Starting; @@ -190,31 +206,90 @@ struct my_xtouch_app // libremidi::remote_control_processor rcp{ rcp = new libremidi::remote_control_processor{ { + .device_type = kDeviceType, .midi_out = [&](libremidi::message&& msg){ midi_out->send_message(msg); }, .on_command = [&](mcu::mixer_command cmd, bool pressed){ std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " << (pressed ? "pressed" : "released") << "\n"; + auto type = mcu::which_mixer_command_type(cmd); - auto i = 0; + auto index = mcu::which_mixer_command_index(type, cmd); switch(type){ - case mcu::mixer_command_type::rec: + case mcu::mixer_command::type_rec: if (!pressed) return; - i = (int)cmd - (int)mcu::mixer_command::rec_0; - buttons.rec[i] = !buttons.rec[i]; - _update_buttons(); + // toggle button state + buttons.rec[index] = !buttons.rec[index]; + _update_buttons(index); break; - case mcu::mixer_command_type::mute: + + case mcu::mixer_command::type_mute: if (!pressed) return; - i = (int)cmd - (int)mcu::mixer_command::mute_0; - buttons.mute[i] = !buttons.mute[i]; - _update_buttons(); + // toggle button state + buttons.mute[index] = !buttons.mute[index]; + _update_buttons(index); break; - case mcu::mixer_command_type::solo: - case mcu::mixer_command_type::sel: + + case mcu::mixer_command::type_solo: + case mcu::mixer_command::type_sel: + // just light up button while being pressed rcp->command(cmd, pressed); break; + + case mcu::mixer_command::type_vpot_click: + std::cerr << "-> vpot click " << index << std::endl; + vpots[index].toggle_led_state(); + _update_vpots(index); + break; + + case mcu::mixer_command::type_fader_touched: + std::cerr << "-> fader touched " << index << std::endl; + break; + + case mcu::mixer_command::type_f: + std::cerr << "-> F" << index << std::endl; + break; + + case mcu::mixer_command::type_channel: + switch(cmd) { + case mcu::mixer_command::bank_left: + std::cerr << "-> bank left" << std::endl; + break; + case mcu::mixer_command::bank_right: + std::cerr << "-> bank right" << std::endl; + break; + case mcu::mixer_command::channel_left: + std::cerr << "-> channel left" << std::endl; + break; + case mcu::mixer_command::channel_right: + std::cerr << "-> channel right" << std::endl; + break; + default: + break; + } break; + + case mcu::mixer_command::type_transport: + switch(cmd){ + case mcu::mixer_command::stop: + std::cerr << " -> stop" << std::endl; + break; + case mcu::mixer_command::play: + std::cerr << " -> play" << std::endl; + break; + default: + std::cerr << " -> some transport function" << std::endl; + break; + } + break; + + case mcu::mixer_command::type_leds: + case mcu::mixer_command::type_assign: + case mcu::mixer_command::type_meta: + case mcu::mixer_command::type_control: + case mcu::mixer_command::type_page: + case mcu::mixer_command::type_user: + case mcu::mixer_command::type_other: default: break; } @@ -224,27 +299,25 @@ struct my_xtouch_app std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; auto type = mcu::which_mixer_control_type(ctl); - auto i = 0; + auto index = mcu::which_mixer_control_index(type, ctl); auto val = 0; switch(type) { - case mcu::mixer_control_type::vpot_rotation: - i = (int)ctl - (int)mcu::mixer_control::vpot_rotation_0; - + case mcu::mixer_control::type_vpot_rotation: val = mcu::relative_midi_to_value(v); - std::cerr << "relative value= " << val << std::endl; + std::cerr << "-> vpot " << index << " relative value= " << val << std::endl; - vpots[i].change_value(val); + vpots[index].change_value(val); - _update_vpots(); + _update_vpots(index); break; default: break; } }, - .on_fader = [](mcu::fader f, uint16_t v) { - std::cerr << "fader: " << magic_enum::enum_name(f) << " -> " << v << "\n"; + .on_fader = [](uint8_t fader, uint16_t v) { + std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; } } }; @@ -293,7 +366,7 @@ struct my_xtouch_app rcp->set_channel_color(i % 8, colors[(i/8 + i%8) % 8]); rcp->update_channel_colors(); - rcp->fader(static_cast(i % 8), (200 * i) % 16384); + rcp->fader(i % 8, (200 * i) % 16384); } @@ -302,32 +375,59 @@ struct my_xtouch_app void start(){ try { - start_impl(); + _start_impl(); } catch (std::exception e){ state = State::Off; throw e; } } + void stop(){ + state = State::Stopping; + } + }; +my_xtouch_app * my_app_instance = nullptr; + +void SignalHandler(int signal) +{ + if (!my_app_instance) + exit(EXIT_FAILURE); + + if (my_app_instance->state == my_xtouch_app::State::Running){ + // Attempt to gracefully stop process. + my_app_instance->stop(); + return; + } + + // Force quit if necessary + exit(EXIT_FAILURE); +} + +void setupSignalHandler(){ + // sigintTicks = 0; + signal(SIGINT, SignalHandler); +} + int main() { - my_xtouch_app app{}; + setupSignalHandler(); + + my_app_instance = new my_xtouch_app(); try { - app.start(); + my_app_instance->start(); } catch (std::exception e){ std::cerr << "Failed to start: " << e.what() << std::endl; } - while( app.state == my_xtouch_app::State::Starting); - // wait for app to not be starting.. + while( my_app_instance->state == my_xtouch_app::State::Starting); + // wait for app to change state (Running, or stopped, likely) // if failed to start.. - if (app.state == my_xtouch_app::State::Off){ - return 1; - } + if (my_app_instance->state == my_xtouch_app::State::Off) + return EXIT_FAILURE; #if defined(__APPLE__) // On macOS, observation can *only* be done in the main thread @@ -341,5 +441,5 @@ int main() std::this_thread::sleep_for(std::chrono::milliseconds(50)); #endif - return 0; + return EXIT_SUCCESS; } \ No newline at end of file diff --git a/examples/protocols/remote_control.cpp b/examples/protocols/remote_control.cpp index 5ab77941..a65a04f3 100644 --- a/examples/protocols/remote_control.cpp +++ b/examples/protocols/remote_control.cpp @@ -34,7 +34,7 @@ int main() winrt::init_apartment(); #endif - auto api = libremidi::API::ALSA_SEQ; + auto api = libremidi::API::UNSPECIFIED; libremidi::observer observer{{.track_any = true}, api}; if (observer.get_input_ports().empty()) return 1; @@ -46,10 +46,10 @@ int main() // Tested with https://github.com/NicoG60/TouchMCU for (auto& p : observer.get_input_ports()) - if (p.port_name == "TouchOSC") + if (p.port_name == "TouchOSC" || p.port_name == "TouchOSC Bridge") ip = p; for (auto& p : observer.get_output_ports()) - if (p.port_name == "TouchOSC") + if (p.port_name == "TouchOSC" || p.port_name == "TouchOSC Bridge") op = p; if (ip.port_name.empty() || op.port_name.empty()) @@ -63,20 +63,31 @@ int main() // 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"; - }}}; + libremidi::remote_control_processor rcp{{ + .device_type = libremidi::remote_control_protocol::device_type::mackie_control, + .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 = [](uint8_t fader, uint16_t v) { + std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; + } + }}; // Initialize the midi in port libremidi::midi_in midi_in{ - {.on_message = [&](const libremidi::message& message) { rcp.on_midi(message); }}, api}; + { + .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{}) @@ -99,7 +110,7 @@ int main() 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); + rcp.fader(i % 8, (200 * i) % 16384); i++; } diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 6a789357..6fad833a 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -19,7 +20,7 @@ NAMESPACE_LIBREMIDI // Thanks https://github.com/NicoG60/TouchMCU ! struct remote_control_protocol { - static constexpr int channel_count = 8; + static constexpr uint8_t mackie_manufacturer_id[3] = {0,0,0x66}; enum class device_type : uint8_t { @@ -126,10 +127,14 @@ struct remote_control_protocol // MM: mode as led_ring_mode // VVVV: value in [0,11] + static constexpr int vpot_max_value = 11; + + enum class led_state : uint8_t { off = 0, - on = 0b01000000 + on = 0b01000000, + mask = on }; enum class led_ring_mode : uint8_t @@ -138,19 +143,35 @@ struct remote_control_protocol mode_1 = 0b010000, // pan pot mode_2 = 0b100000, // fill leds from left mode_3 = 0b110000, // fill leds from middle + mask = mode_3, }; - enum class pot : uint8_t + static constexpr int channel_count = 8; + + enum class channel_index : 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, + channel_1 = 0x00, + channel_2 = 0x01, + channel_3 = 0x02, + channel_4 = 0x03, + channel_5 = 0x04, + channel_6 = 0x05, + channel_7 = 0x06, + channel_8 = 0x07, + channel_main = 0x08 }; +// +// 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, +// }; // struct vpot // { @@ -160,31 +181,20 @@ struct remote_control_protocol // uint8_t value; // }; - static constexpr int vpot_max_value = 11; - - 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, - }; - - enum class mixer_control_type : uint16_t - { - vpot_rotation = 0x10, - vpot_led = 0x30, - jog_wheel = 0x3C, - timecode_digit = 0x40, - assignment_digit = 0x5A, - other = 0xFFFF - }; +// 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, +// }; +// static constexpr uint8_t // control changes enum class mixer_control : uint8_t @@ -200,6 +210,7 @@ struct remote_control_protocol vpot_rotation_5 = 0x10 + 0x05, vpot_rotation_6 = 0x10 + 0x06, vpot_rotation_7 = 0x10 + 0x07, + type_vpot_rotation = vpot_rotation_0, external_control = 0x2E, @@ -212,6 +223,7 @@ struct remote_control_protocol vpot_led_5 = 0x30 + 0x05, vpot_led_6 = 0x30 + 0x06, vpot_led_7 = 0x30 + 0x07, + type_vpot_led = vpot_led_0, jog_wheel = 0x3C, @@ -225,19 +237,33 @@ struct remote_control_protocol timecode_digit_7 = 0x40 + 0x07, timecode_digit_8 = 0x40 + 0x08, timecode_digit_9 = 0x40 + 0x09, + type_timecode_digit = timecode_digit_0, assignment_digit_0 = 0x4A, assignment_digit_1 = 0x4B, + type_assignment_digit = assignment_digit_0, + + type_other = external_control, }; - static mixer_control_type which_mixer_control_type(mixer_control ctl) + static mixer_control which_mixer_control_type(mixer_control ctl) + { + if (mixer_control::vpot_rotation_0 <= ctl && ctl <= mixer_control::vpot_rotation_7) return mixer_control::type_vpot_rotation; + if (mixer_control::vpot_led_0 <= ctl && ctl <= mixer_control::vpot_led_7) return mixer_control::type_vpot_led; + if (mixer_control::timecode_digit_0 <= ctl && ctl <= mixer_control::timecode_digit_9) return mixer_control::type_timecode_digit; + if (mixer_control::assignment_digit_0 <= ctl && ctl <= mixer_control::assignment_digit_1) return mixer_control::type_assignment_digit; + + return mixer_control::type_other; + } + + static inline int which_mixer_control_index(mixer_control type, mixer_control ctl) { - if (mixer_control::vpot_rotation_0 <= ctl && ctl <= mixer_control::vpot_rotation_7) return mixer_control_type::vpot_rotation; - if (mixer_control::vpot_led_0 <= ctl && ctl <= mixer_control::vpot_led_7) return mixer_control_type::vpot_led; - if (mixer_control::timecode_digit_0 <= ctl && ctl <= mixer_control::timecode_digit_9) return mixer_control_type::timecode_digit; - if (mixer_control::assignment_digit_0 <= ctl && ctl <= mixer_control::assignment_digit_1) return mixer_control_type::assignment_digit; + return (uint8_t)ctl - (uint8_t)type; + } - return mixer_control_type::other; + static inline int which_mixer_control_index(mixer_control type, uint8_t ctl_byte) + { + return which_mixer_control_index(type, static_cast(ctl_byte)); } static inline int relative_value_to_midi(int value){ @@ -254,25 +280,6 @@ struct remote_control_protocol // note events - enum class mixer_command_type : uint16_t - { - vpot_click = 0x20, - rec = 0x00, - solo = 0x08, - mute = 0x10, - sel = 0x18, - assign = 0x28, - channels = 0x2E, - f = 0x36, - page = 0x3E, - meta = 0x46, - control = 0x50, - transport = 0x54, - user = 0x66, - fader_touched = 0x68, - leds = 0x71, - other = 0xffff - }; enum class mixer_command : uint8_t { @@ -285,6 +292,7 @@ struct remote_control_protocol vpot_click_5 = 0x20 + 0x05, vpot_click_6 = 0x20 + 0x06, vpot_click_7 = 0x20 + 0x07, + type_vpot_click = vpot_click_0, // record arm rec_0 = 0x00 + 0x00, @@ -295,6 +303,7 @@ struct remote_control_protocol rec_5 = 0x00 + 0x05, rec_6 = 0x00 + 0x06, rec_7 = 0x00 + 0x07, + type_rec = rec_0, // solo solo_0 = 0x08 + 0x00, @@ -305,6 +314,7 @@ struct remote_control_protocol solo_5 = 0x08 + 0x05, solo_6 = 0x08 + 0x06, solo_7 = 0x08 + 0x07, + type_solo = solo_0, // mute mute_0 = 0x10 + 0x00, @@ -315,6 +325,7 @@ struct remote_control_protocol mute_5 = 0x10 + 0x05, mute_6 = 0x10 + 0x06, mute_7 = 0x10 + 0x07, + type_mute = mute_0, // sel(ection) sel_0 = 0x18 + 0x00, @@ -325,6 +336,7 @@ struct remote_control_protocol sel_5 = 0x18 + 0x05, sel_6 = 0x18 + 0x06, sel_7 = 0x18 + 0x07, + type_sel = sel_0, // TODO metering // assign @@ -334,6 +346,7 @@ struct remote_control_protocol assign_plugin = 0x2B, assign_eq = 0x2C, assign_instrument = 0x2D, + type_assign = assign_track, // channels bank_left = 0x2E, @@ -342,6 +355,7 @@ struct remote_control_protocol channel_right = 0x31, flip = 0x32, global = 0x33, + type_channel = bank_left, name_value_button = 0x34, smpte_beats_button = 0x35, @@ -355,6 +369,7 @@ struct remote_control_protocol f6 = 0x36 + 0x05, f7 = 0x36 + 0x06, f8 = 0x36 + 0x07, + type_f = f1, // page midi_tracks = 0x3E, @@ -365,18 +380,21 @@ struct remote_control_protocol busses = 0x43, outputs = 0x44, user = 0x45, + type_page = midi_tracks, // meta shift = 0x46, option = 0x47, control = 0x48, alt = 0x49, + type_meta = shift, // control save = 0x50, undo = 0x51, cancel = 0x52, enter = 0x53, + type_control = save, // transport markers = 0x54, @@ -400,9 +418,12 @@ struct remote_control_protocol zoom = 0x64, scrub = 0x65, + type_transport = markers, + // user user_switch_1 = 0x66, user_switch_2 = 0x67, + type_user = user_switch_1, // fader touched fader_touched_0 = 0x68, @@ -414,40 +435,77 @@ struct remote_control_protocol fader_touched_6 = 0x6e, fader_touched_7 = 0x6f, fader_touched_master = 0x70, + type_fader_touched = fader_touched_0, // leds smpte_led = 0x71, beats_led = 0x72, rude_solo_led = 0x73, + type_leds = smpte_led, + // other relay_click = 0x76, + type_other = relay_click }; - static mixer_command_type which_mixer_command_type(mixer_command cmd) + +// enum class mixer_command_type : uint16_t +// { +// vpot_click = (uint16_t)mixer_command::vpot_click_0, +// rec = (uint16_t)mixer_command::rec_0, +// solo = (uint16_t)mixer_command::solo_0, +// mute = (uint16_t)mixer_command::mute_0, +// sel = (uint16_t)mixer_command::sel_0, +// assign = (uint16_t)mixer_command::assign_track, +// channels = (uint16_t)mixer_command::0x2E, +// f = 0x36, +// page = 0x3E, +// meta = 0x46, +// control = 0x50, +// transport = 0x54, +// user = 0x66, +// fader_touched = 0x68, +// leds = 0x71, +// other = 0xffff +// }; + + static mixer_command which_mixer_command_type(mixer_command cmd) { - if (mixer_command::vpot_click_0 <= cmd && cmd <= mixer_command::vpot_click_7) return mixer_command_type::vpot_click; - if (mixer_command::rec_0 <= cmd && cmd <= mixer_command::rec_7) return mixer_command_type::rec; - if (mixer_command::solo_0 <= cmd && cmd <= mixer_command::solo_7) return mixer_command_type::solo; - if (mixer_command::mute_0 <= cmd && cmd <= mixer_command::mute_7) return mixer_command_type::mute; - if (mixer_command::sel_0 <= cmd && cmd <= mixer_command::sel_7) return mixer_command_type::sel; - if (mixer_command::assign_track <= cmd && cmd <= mixer_command::assign_instrument) return mixer_command_type::assign; - if (mixer_command::bank_left <= cmd && cmd <= mixer_command::global) return mixer_command_type::channels; - if (mixer_command::f1 <= cmd && cmd <= mixer_command::f8) return mixer_command_type::f; - if (mixer_command::midi_tracks <= cmd && cmd <= mixer_command::user) return mixer_command_type::page; - if (mixer_command::shift <= cmd && cmd <= mixer_command::alt) return mixer_command_type::meta; - if (mixer_command::save <= cmd && cmd <= mixer_command::enter) return mixer_command_type::control; - if (mixer_command::markers <= cmd && cmd <= mixer_command::scrub) return mixer_command_type::transport; - if (mixer_command::user_switch_1 <= cmd && cmd <= mixer_command::user_switch_2) return mixer_command_type::user; - if (mixer_command::fader_touched_0 <= cmd && cmd <= mixer_command::fader_touched_master) return mixer_command_type::fader_touched; - if (mixer_command::smpte_led <= cmd && cmd <= mixer_command::rude_solo_led) return mixer_command_type::leds; - - return mixer_command_type::other; + if (mixer_command::vpot_click_0 <= cmd && cmd <= mixer_command::vpot_click_7) return mixer_command::type_vpot_click; + if (mixer_command::rec_0 <= cmd && cmd <= mixer_command::rec_7) return mixer_command::type_rec; + if (mixer_command::solo_0 <= cmd && cmd <= mixer_command::solo_7) return mixer_command::type_solo; + if (mixer_command::mute_0 <= cmd && cmd <= mixer_command::mute_7) return mixer_command::type_mute; + if (mixer_command::sel_0 <= cmd && cmd <= mixer_command::sel_7) return mixer_command::type_sel; + if (mixer_command::assign_track <= cmd && cmd <= mixer_command::assign_instrument) return mixer_command::type_assign; + if (mixer_command::bank_left <= cmd && cmd <= mixer_command::global) return mixer_command::type_channel; + if (mixer_command::f1 <= cmd && cmd <= mixer_command::f8) return mixer_command::type_f; + if (mixer_command::midi_tracks <= cmd && cmd <= mixer_command::user) return mixer_command::type_page; + if (mixer_command::shift <= cmd && cmd <= mixer_command::alt) return mixer_command::type_meta; + if (mixer_command::save <= cmd && cmd <= mixer_command::enter) return mixer_command::type_control; + if (mixer_command::markers <= cmd && cmd <= mixer_command::scrub) return mixer_command::type_transport; + if (mixer_command::user_switch_1 <= cmd && cmd <= mixer_command::user_switch_2) return mixer_command::type_user; + if (mixer_command::fader_touched_0 <= cmd && cmd <= mixer_command::fader_touched_master) return mixer_command::type_fader_touched; + if (mixer_command::smpte_led <= cmd && cmd <= mixer_command::rude_solo_led) return mixer_command::type_leds; + + return mixer_command::type_other; + } + + inline static mixer_command which_mixer_command_type(uint8_t cmd_byte){ + return which_mixer_command_type(static_cast(cmd_byte)); + } + + inline static int which_mixer_command_index(mixer_command type, mixer_command cmd){ + return (uint8_t)cmd - (uint8_t)type; } + inline static int which_mixer_command_index(mixer_command type, uint8_t cmd_byte){ + return which_mixer_command_index(type, static_cast(cmd_byte)); + } + template using arr = std::array; - device_type type = device_type::mackie_control_xt; + device_type type = device_type::mackie_control; channel_color_xt channel_colors[8] = {channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black}; @@ -812,6 +870,8 @@ struct remote_control_protocol struct rcp_configuration { + remote_control_protocol::device_type device_type = remote_control_protocol::device_type::mackie_control; + //! 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 @@ -821,7 +881,16 @@ struct rcp_configuration std::function on_connected; std::function on_command; std::function on_control; - std::function on_fader; + std::function on_fader; + +// std::function on_vpot_change = nullptr; +// std::function on_vpot_click = nullptr; +// std::function on_rec_click = nullptr; +// std::function on_solo_click = nullptr; +// std::function on_mute_click = nullptr; +// std::function on_sel_click = nullptr; +//// std::function on_assign_click = nullptr; +// std::function on_function_click = nullptr; libremidi::midi_error_callback on_error{}; }; @@ -837,6 +906,8 @@ struct remote_control_processor : libremidi::error_handler { assert(configuration.midi_out); + impl.type = conf.device_type; + if (!configuration.on_error) configuration.on_error = [](std::string_view s, auto&&...) { std::fprintf(stderr, "libremidi: rcp error: %s\n", s.data()); @@ -880,7 +951,9 @@ struct remote_control_processor : libremidi::error_handler N -= 2; // Mackie manufacturer ID check - if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0x66) + if (bytes[0] == remote_control_protocol::mackie_manufacturer_id[0] && + bytes[1] == remote_control_protocol::mackie_manufacturer_id[1] && + bytes[2] == remote_control_protocol::mackie_manufacturer_id[2]) { impl.type = static_cast(bytes[3]); @@ -898,7 +971,23 @@ struct remote_control_processor : libremidi::error_handler } break; case libremidi::message_type::NOTE_ON: - configuration.on_command(static_cast(message[1]), message[2] > 0); + { +// auto t = rcp::which_mixer_command_type(message[1]); +// auto i = rcp::which_mixer_command_index(t, message[1]); + auto pressed = message[2] > 0; +// +// // first check if particular callbacks have been set for different commands +// if (t == rcp::mixer_command::type_vpot_click && configuration.on_vpot_click) +// configuration.on_vpot_click(i, pressed); +// if (t == rcp::mixer_command::type_ && configuration.on_sel_click) +// configuration.on_sel_click(i, pressed); +// if (t == rcp::mixer_command::type_sel && configuration.on_sel_click) +// configuration.on_sel_click(i, pressed); +// +// // if no particular callbacks have been set, use the on_command callback as fallback +// else + configuration.on_command(static_cast(message[1]), pressed); + } break; case libremidi::message_type::NOTE_OFF: break; @@ -907,7 +996,7 @@ struct remote_control_processor : libremidi::error_handler 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); + configuration.on_fader(message.get_channel() - 1, value); break; } default: @@ -956,6 +1045,7 @@ struct remote_control_processor : libremidi::error_handler break; } default: + std::cerr << "Unhandled MIDI message" << std::endl; break; } } @@ -1030,7 +1120,7 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::control_change(1, to_underlying(c), value)); } - inline void vpot(remote_control_protocol::pot index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) + inline void vpot(remote_control_protocol::channel_index index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) { control( (remote_control_protocol::mixer_control)((int)remote_control_protocol::mixer_control::vpot_led_0 + (int)index), @@ -1038,12 +1128,10 @@ struct remote_control_processor : libremidi::error_handler ); } - void fader(remote_control_protocol::fader c, uint16_t value) + void fader(uint8_t i, uint16_t value) { - int idx = to_underlying(c); - using ce = libremidi::channel_events; - configuration.midi_out(ce::pitch_bend(idx + 1, value)); + configuration.midi_out(ce::pitch_bend(i + 1, value)); } // State machine From e5fadc2c6ee6b2920295df9d99ce00c8def2a0de Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Wed, 15 Jul 2026 13:54:55 +0200 Subject: [PATCH 3/9] some cleanup --- examples/protocols/coremidi_mcu_xtouch.cpp | 26 ++-- .../libremidi/protocols/remote_control.hpp | 128 ++++-------------- 2 files changed, 34 insertions(+), 120 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index 6b16c480..907172b7 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -54,7 +54,7 @@ struct my_xtouch_app struct vpot_st { - mcu::channel_index index; + uint8_t index; mcu::led_state state = mcu::led_state::off; mcu::led_ring_mode mode = mcu::led_ring_mode::mode_0; uint8_t value = 0; @@ -86,14 +86,14 @@ struct my_xtouch_app } buttons; struct vpot_st vpots[8] = { - {.index = mcu::channel_index::channel_1, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, - {.index = mcu::channel_index::channel_2, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, - {.index = mcu::channel_index::channel_3, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, - {.index = mcu::channel_index::channel_4, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, - {.index = mcu::channel_index::channel_5, .mode = mcu::led_ring_mode::mode_0, .value = 6}, - {.index = mcu::channel_index::channel_6, .mode = mcu::led_ring_mode::mode_1, .value = 6}, - {.index = mcu::channel_index::channel_7, .mode = mcu::led_ring_mode::mode_2, .value = 6}, - {.index = mcu::channel_index::channel_8, .mode = mcu::led_ring_mode::mode_3, .value = 6} + {.index = 0, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, + {.index = 1, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, + {.index = 2, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, + {.index = 3, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, + {.index = 4, .mode = mcu::led_ring_mode::mode_0, .value = 6}, + {.index = 5, .mode = mcu::led_ring_mode::mode_1, .value = 6}, + {.index = 6, .mode = mcu::led_ring_mode::mode_2, .value = 6}, + {.index = 7, .mode = mcu::led_ring_mode::mode_3, .value = 6} }; my_xtouch_app() @@ -154,12 +154,6 @@ struct my_xtouch_app std::cout << "Starting app.." << std::endl; state = State::Starting; -// auto callback = [&](int port, const libremidi::message& msg) { -// std::cout << msg << std::endl; -// midiout[port].send_message(msg); -// }; - - #if defined(_WIN32) && __has_include() winrt::init_apartment(); @@ -203,7 +197,6 @@ struct my_xtouch_app // Set-up the remote control API. // Here we only do some logging, this is where commands sqall be handled. -// libremidi::remote_control_processor rcp{ rcp = new libremidi::remote_control_processor{ { .device_type = kDeviceType, @@ -321,7 +314,6 @@ struct my_xtouch_app } } }; -//rcp.command() // Open the ports if (auto err = midi_in->open_port(ip); err != stdx::error{}) diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 6fad833a..88417e13 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -16,8 +16,21 @@ NAMESPACE_LIBREMIDI { -// A clean-room reverse-engineered remote control protocol compatible with many hardware devices. -// Thanks https://github.com/NicoG60/TouchMCU ! +/** + * A clean-room reverse-engineered remote control protocol compatible with many hardware devices. + * Thanks https://github.com/NicoG60/TouchMCU ! + * + * X-Touch screen colors RE from extender unit: + * Command Format := 8* + * := 0x72 + * := [0 - 7] (on-off bitmap of RGB colors LSB = R) + * Example (setting each screen a different color): 15720001020304050607 + * + * @TODO Check with actual hardware having all the control elements + * + * @TODO Desired device support: + * - MIDIPLUS (Waves) FIT Controller + */ struct remote_control_protocol { static constexpr uint8_t mackie_manufacturer_id[3] = {0,0,0x66}; @@ -148,54 +161,6 @@ struct remote_control_protocol static constexpr int channel_count = 8; - enum class channel_index : uint8_t - { - channel_1 = 0x00, - channel_2 = 0x01, - channel_3 = 0x02, - channel_4 = 0x03, - channel_5 = 0x04, - channel_6 = 0x05, - channel_7 = 0x06, - channel_8 = 0x07, - channel_main = 0x08 - }; -// -// 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, -// }; - -// struct vpot -// { -// pot index; -// led_state state; -// led_ring_mode mode; -// uint8_t value; -// }; - - -// 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, -// }; -// static constexpr uint8_t - // control changes enum class mixer_control : uint8_t { @@ -279,8 +244,6 @@ struct remote_control_protocol } // note events - - enum class mixer_command : uint8_t { // vpot_click @@ -448,27 +411,6 @@ struct remote_control_protocol type_other = relay_click }; - -// enum class mixer_command_type : uint16_t -// { -// vpot_click = (uint16_t)mixer_command::vpot_click_0, -// rec = (uint16_t)mixer_command::rec_0, -// solo = (uint16_t)mixer_command::solo_0, -// mute = (uint16_t)mixer_command::mute_0, -// sel = (uint16_t)mixer_command::sel_0, -// assign = (uint16_t)mixer_command::assign_track, -// channels = (uint16_t)mixer_command::0x2E, -// f = 0x36, -// page = 0x3E, -// meta = 0x46, -// control = 0x50, -// transport = 0x54, -// user = 0x66, -// fader_touched = 0x68, -// leds = 0x71, -// other = 0xffff -// }; - static mixer_command which_mixer_command_type(mixer_command cmd) { if (mixer_command::vpot_click_0 <= cmd && cmd <= mixer_command::vpot_click_7) return mixer_command::type_vpot_click; @@ -505,7 +447,7 @@ struct remote_control_protocol template using arr = std::array; - device_type type = device_type::mackie_control; + device_type type; channel_color_xt channel_colors[8] = {channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black}; @@ -589,11 +531,13 @@ struct remote_control_protocol auto update_lcd(std::string_view txt, int pos) { // FIXME + // valid length? if (pos < 0 || lcd_total_len <= pos) return libremidi::message{}; int len = int(std::ssize(txt)); + // length sanity check if (len > (lcd_total_len - pos)) { txt = txt.substr(0, lcd_total_len - pos); @@ -601,13 +545,11 @@ struct remote_control_protocol } uint8_t buf[128]; - const int N = std::min(len, lcd_total_len - pos); - for (int i = 0; i < N; i++) + + for (int i = 0; i < len; i++) { buf[i + pos] = charmap_lcd(txt[i]); } -// buf[55] = '\n'; -// buf[111] = '\n'; uint8_t cmd_pos = pos; @@ -617,12 +559,12 @@ struct remote_control_protocol auto update_lcd(std::string_view txt) { uint8_t buf[lcd_total_len] = {}; + for (int i = 0; i < std::min(int(std::ssize(txt)), lcd_total_len); 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, lcd_total_len)); } @@ -883,15 +825,6 @@ struct rcp_configuration std::function on_control; std::function on_fader; -// std::function on_vpot_change = nullptr; -// std::function on_vpot_click = nullptr; -// std::function on_rec_click = nullptr; -// std::function on_solo_click = nullptr; -// std::function on_mute_click = nullptr; -// std::function on_sel_click = nullptr; -//// std::function on_assign_click = nullptr; -// std::function on_function_click = nullptr; - libremidi::midi_error_callback on_error{}; }; @@ -906,6 +839,8 @@ struct remote_control_processor : libremidi::error_handler { assert(configuration.midi_out); + // no sanity checking of device type + // maybe issue a warning? impl.type = conf.device_type; if (!configuration.on_error) @@ -972,20 +907,7 @@ struct remote_control_processor : libremidi::error_handler break; case libremidi::message_type::NOTE_ON: { -// auto t = rcp::which_mixer_command_type(message[1]); -// auto i = rcp::which_mixer_command_index(t, message[1]); auto pressed = message[2] > 0; -// -// // first check if particular callbacks have been set for different commands -// if (t == rcp::mixer_command::type_vpot_click && configuration.on_vpot_click) -// configuration.on_vpot_click(i, pressed); -// if (t == rcp::mixer_command::type_ && configuration.on_sel_click) -// configuration.on_sel_click(i, pressed); -// if (t == rcp::mixer_command::type_sel && configuration.on_sel_click) -// configuration.on_sel_click(i, pressed); -// -// // if no particular callbacks have been set, use the on_command callback as fallback -// else configuration.on_command(static_cast(message[1]), pressed); } break; @@ -1120,7 +1042,7 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::control_change(1, to_underlying(c), value)); } - inline void vpot(remote_control_protocol::channel_index index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) + inline void vpot(uint8_t index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) { control( (remote_control_protocol::mixer_control)((int)remote_control_protocol::mixer_control::vpot_led_0 + (int)index), From 9c2c2ae420fb5cba427b4d2a74bc7071bf16e2f0 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Wed, 15 Jul 2026 14:50:14 +0200 Subject: [PATCH 4/9] vpot refactoring --- examples/protocols/coremidi_mcu_xtouch.cpp | 27 +++++++++---------- .../libremidi/protocols/remote_control.hpp | 26 ++++++++++-------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index 907172b7..429bba6e 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -55,18 +55,17 @@ struct my_xtouch_app struct vpot_st { uint8_t index; - mcu::led_state state = mcu::led_state::off; + bool state = false; mcu::led_ring_mode mode = mcu::led_ring_mode::mode_0; uint8_t value = 0; void change_value(int change){ - if (change > 0 && value < mcu::vpot_max_value-1) value++; - else if (change < 0 && value > 0) value--; + if (change > 0 && value < mcu::vpot_max_value[(uint8_t)mode]) + value++; + else if (change < 0 && value > mcu::vpot_min_value[(uint8_t)mode]) + value--; } void toggle_led_state(){ - if (state == mcu::led_state::off) - state = mcu::led_state::on; - else - state = mcu::led_state::off; + state = !state; } }; @@ -86,10 +85,10 @@ struct my_xtouch_app } buttons; struct vpot_st vpots[8] = { - {.index = 0, .mode = mcu::led_ring_mode::mode_0, .state = mcu::led_state::off}, - {.index = 1, .mode = mcu::led_ring_mode::mode_1, .state = mcu::led_state::off}, - {.index = 2, .mode = mcu::led_ring_mode::mode_2, .state = mcu::led_state::on}, - {.index = 3, .mode = mcu::led_ring_mode::mode_3, .state = mcu::led_state::on}, + {.index = 0, .mode = mcu::led_ring_mode::mode_0, .value=1, .state = false}, + {.index = 1, .mode = mcu::led_ring_mode::mode_1, .value=1, .state = false}, + {.index = 2, .mode = mcu::led_ring_mode::mode_2, .value=1, .state = true}, + {.index = 3, .mode = mcu::led_ring_mode::mode_3, .value=1, .state = true}, {.index = 4, .mode = mcu::led_ring_mode::mode_0, .value = 6}, {.index = 5, .mode = mcu::led_ring_mode::mode_1, .value = 6}, {.index = 6, .mode = mcu::led_ring_mode::mode_2, .value = 6}, @@ -140,7 +139,7 @@ struct my_xtouch_app { assert(rcp); - if (i == -1){ + if (i != -1){ rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); } else { for (i = 0; i < 8; i++){ @@ -349,7 +348,6 @@ struct my_xtouch_app 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_ch_line(labels1[ (i/8 + i%8) % 8 ], i % 8, 0); @@ -358,8 +356,9 @@ struct my_xtouch_app rcp->set_channel_color(i % 8, colors[(i/8 + i%8) % 8]); rcp->update_channel_colors(); - rcp->fader(i % 8, (200 * i) % 16384); +// rcp->me + rcp->fader(i % 8, (200 * i) % 16384); } state = State::Off; diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 88417e13..2ba8c87d 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -138,25 +138,29 @@ struct remote_control_protocol // 0b0LMMVVVV // L: toggle underneath LED // MM: mode as led_ring_mode - // VVVV: value in [0,11] + // VVVV: value range depends on mode (0 = off, 1 -> at least one led, max -> see below) - static constexpr int vpot_max_value = 11; + static constexpr uint8_t vpot_mask_state = 0b01000000; + static constexpr uint8_t vpot_mask_mode = 0b00110000; + static constexpr uint8_t vpot_mask_value = 0b00001111; + + static constexpr uint8_t vpot_min_value[4] = {1,1,1,1}; + static constexpr uint8_t vpot_max_value[4] = {11,11,11,6}; + static constexpr uint8_t vpot_mode_bits[4] = {0b000000, 0b010000, 0b100000, 0b110000}; enum class led_state : uint8_t { off = 0, on = 0b01000000, - mask = on }; enum class led_ring_mode : uint8_t { - mode_0 = 0b000000, // one led only - mode_1 = 0b010000, // pan pot - mode_2 = 0b100000, // fill leds from left - mode_3 = 0b110000, // fill leds from middle - mask = mode_3, + mode_0 = 0, // one led only + mode_1 = 1, // pan pot + mode_2 = 2, // fill leds from left + mode_3 = 3, // fill leds from middle }; static constexpr int channel_count = 8; @@ -1042,11 +1046,11 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::control_change(1, to_underlying(c), value)); } - inline void vpot(uint8_t index, remote_control_protocol::led_state state, remote_control_protocol::led_ring_mode mode, uint8_t value) + inline void vpot(uint8_t index, bool state, remote_control_protocol::led_ring_mode mode, uint8_t value) { control( - (remote_control_protocol::mixer_control)((int)remote_control_protocol::mixer_control::vpot_led_0 + (int)index), - (int)state | (int)mode | value + (remote_control_protocol::mixer_control)((uint8_t)remote_control_protocol::mixer_control::vpot_led_0 + index), + (state ? remote_control_protocol::vpot_mask_state : 0) | remote_control_protocol::vpot_mode_bits[(uint8_t)mode] | value ); } From 7272ecaa46f33074aa422fab7e0bfa6f2de3f353 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Wed, 15 Jul 2026 15:15:05 +0200 Subject: [PATCH 5/9] added channel metering --- examples/protocols/coremidi_mcu_xtouch.cpp | 10 +++++----- include/libremidi/protocols/remote_control.hpp | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index 429bba6e..4b9c688e 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -350,15 +350,15 @@ struct my_xtouch_app auto ctime = std::localtime(&result); rcp->update_timecode(ctime->tm_hour, ctime->tm_min, ctime->tm_sec, 0); - rcp->update_lcd_ch_line(labels1[ (i/8 + i%8) % 8 ], i % 8, 0); - rcp->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % 8, 1); + rcp->update_lcd_ch_line(labels1[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 0); + rcp->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 1); - rcp->set_channel_color(i % 8, colors[(i/8 + i%8) % 8]); + rcp->set_channel_color(i % mcu::channel_count, colors[(i/8 + i%8) % 8]); rcp->update_channel_colors(); -// rcp->me + rcp->channel_meter(i % mcu::channel_count, i % mcu::channel_meter_max_value); - rcp->fader(i % 8, (200 * i) % 16384); + rcp->fader(i % mcu::channel_count, (200 * i) % 16384); } state = State::Off; diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 2ba8c87d..627db432 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -81,6 +81,10 @@ struct remote_control_protocol version_reply = 0x14, }; + static constexpr uint8_t channel_meter_mask_index = 0b01110000; + static constexpr uint8_t channel_meter_mask_value = 0b00001111; + static constexpr uint8_t channel_meter_max_value = 12; + enum class lcd_meter_mode : uint8_t { horizontal = 0x00, @@ -149,11 +153,6 @@ struct remote_control_protocol static constexpr uint8_t vpot_mode_bits[4] = {0b000000, 0b010000, 0b100000, 0b110000}; - enum class led_state : uint8_t - { - off = 0, - on = 0b01000000, - }; enum class led_ring_mode : uint8_t { @@ -577,6 +576,8 @@ struct remote_control_protocol return make_command(command_to_device::update_channel_colors_xt, std::span((uint8_t*)channel_colors, 8)); } + + auto firmware_version_request() { return make_command(command_to_device::firmware_version_request, arr<1>{0}); @@ -606,6 +607,7 @@ struct remote_control_protocol return make_command(command_to_device::global_lcd_meter_mode, arr<1>{to_underlying(mode)}); } + auto faders_to_minimum() { return make_command(command_to_device::faders_to_minimum); } auto all_leds_off() { return make_command(command_to_device::all_leds_off); } @@ -1060,6 +1062,12 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::pitch_bend(i + 1, value)); } + void channel_meter(uint8_t i, uint8_t value) + { + using ce = libremidi::channel_events; + configuration.midi_out(ce::aftertouch(1, ((i<<4) & rcp::channel_meter_mask_index) | (value & rcp::channel_meter_mask_value))); + } + // State machine enum { From a5e08685497f88cd99dbfb789ee10ec08a6a7da7 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Wed, 15 Jul 2026 16:50:29 +0200 Subject: [PATCH 6/9] some notes and charmap tests --- .../libremidi/protocols/remote_control.hpp | 200 +++++++++++------- 1 file changed, 118 insertions(+), 82 deletions(-) diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 627db432..7045d672 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -730,90 +730,124 @@ struct remote_control_protocol 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 - } + +// // lookup table according to https://github.com/NicoG60/TouchMCU/blob/main/doc/mackie_control_protocol.md#lcd-bitmap-font-character-table +// static constexpr uint8_t charmap_lcd_lut[256] = { +// 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // special signs +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // like ASCII SPACE, ! ... / +// 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // like ASCII 0, 1 ... ? +// 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // like ASCII @, A ... O +// 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, // +// 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // +// 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, // +// +// // map anything above 0x7F to space +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only +// }; +// + // NOTE + static inline uint8_t charmap_lcd(char c) + { +// return charmap_lcd_lut[c]; + return c & 0x7F; } + +// static uint8_t charmap_lcd(char c) +// { +// return c & 0x7f; +// +// // 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 @@ -846,7 +880,6 @@ struct remote_control_processor : libremidi::error_handler assert(configuration.midi_out); // no sanity checking of device type - // maybe issue a warning? impl.type = conf.device_type; if (!configuration.on_error) @@ -874,6 +907,8 @@ struct remote_control_processor : libremidi::error_handler void start() { current_state = waiting_for_query; + + // NOTE on x-touch hardware device did not observe any response to these queries configuration.midi_out(impl.device_query()); configuration.midi_out(impl.firmware_version_request()); } @@ -896,6 +931,7 @@ struct remote_control_processor : libremidi::error_handler bytes[1] == remote_control_protocol::mackie_manufacturer_id[1] && bytes[2] == remote_control_protocol::mackie_manufacturer_id[2]) { + //should this be updated automatically? impl.type = static_cast(bytes[3]); std::cerr << "device type " << (int) impl.type << std::endl; From 25660595be3faf7e9b9bacbe5f74bf3874f838e0 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Thu, 16 Jul 2026 09:31:36 +0200 Subject: [PATCH 7/9] moved color state out of protocol+processor, to be maintained by app logic --- examples/protocols/coremidi_mcu_xtouch.cpp | 8 ++++--- .../libremidi/protocols/remote_control.hpp | 21 +++++++------------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index 4b9c688e..c54b6a2a 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -334,11 +334,13 @@ struct my_xtouch_app // Blast messages :) - mcu::channel_color_xt colors[8] = { + mcu::channel_color_xt color_palette[8] = { mcu::channel_color_xt::black, mcu::channel_color_xt::red, mcu::channel_color_xt::yellow, mcu::channel_color_xt::green, mcu::channel_color_xt::cyan, mcu::channel_color_xt::blue, mcu::channel_color_xt::magenta, mcu::channel_color_xt::white }; + mcu::channel_color_list channel_colors = {mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black}; + std::string labels1[8] = {"1", "21", "321", "4321", "54321", "654321", "7654321", "87654321"}; std::string labels2[8] = {"ch1", "ch2", "ch3", "ch4", "ch5", "ch6", "ch7", "ch8"}; @@ -353,8 +355,8 @@ struct my_xtouch_app rcp->update_lcd_ch_line(labels1[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 0); rcp->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 1); - rcp->set_channel_color(i % mcu::channel_count, colors[(i/8 + i%8) % 8]); - rcp->update_channel_colors(); + channel_colors[i % mcu::channel_count] = color_palette[(i/8 + i%8) % 8]; + rcp->update_channel_colors(channel_colors); rcp->channel_meter(i % mcu::channel_count, i % mcu::channel_meter_max_value); diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 7045d672..1f2a7cf8 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -122,6 +122,8 @@ struct remote_control_protocol white = 0b111 }; + typedef channel_color_xt channel_color_list[8]; + static inline bool channel_color_is_valid(channel_color_xt color) { return channel_color_xt::black <= color && color <= channel_color_xt::white; @@ -452,8 +454,6 @@ struct remote_control_protocol device_type type; - channel_color_xt channel_colors[8] = {channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black, channel_color_xt::black}; - static libremidi::message make_command_impl(auto&&... data) { using namespace std; @@ -571,7 +571,7 @@ struct remote_control_protocol return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, lcd_total_len)); } - auto update_channel_colors() + auto update_channel_colors(channel_color_list &channel_colors) { return make_command(command_to_device::update_channel_colors_xt, std::span((uint8_t*)channel_colors, 8)); } @@ -874,6 +874,9 @@ struct remote_control_processor : libremidi::error_handler rcp_configuration configuration; rcp impl; + + + explicit remote_control_processor(rcp_configuration conf) : configuration{std::move(conf)} { @@ -1056,17 +1059,9 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(std::move(res)); } - void set_channel_color(uint8_t ch, remote_control_protocol::channel_color_xt color) - { - if (7 < ch || ! remote_control_protocol::channel_color_is_valid(color)) - return; - - impl.channel_colors[ch] = color; - } - - void update_channel_colors() + void update_channel_colors(rcp::channel_color_list &channel_colors) { - auto res = impl.update_channel_colors(); + auto res = impl.update_channel_colors(channel_colors); if (!res.empty()) configuration.midi_out(std::move(res)); } From 53c083626a6c7cf96fd2bc801a2ca2bc9471f089 Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Fri, 24 Jul 2026 13:32:00 +0200 Subject: [PATCH 8/9] - made more mixer commands accessible - removed lcd charmap - added version callback - sysex filter fix --- examples/protocols/coremidi_mcu_xtouch.cpp | 43 ++- examples/protocols/remote_control.cpp | 118 +++++++- .../libremidi/protocols/remote_control.hpp | 283 ++++++++---------- 3 files changed, 273 insertions(+), 171 deletions(-) diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index c54b6a2a..b4d53d83 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -38,12 +38,17 @@ std::string enum_name(auto cmd) #include #include +// for debugging/testing purposes +const bool observe_incoming_messages = false; +const bool observe_outgoing_messages = false; + + using mcu = libremidi::remote_control_protocol; using mcu_proc = libremidi::remote_control_processor; struct my_xtouch_app { - static constexpr auto api = libremidi::API::UNSPECIFIED; + static constexpr auto api = libremidi::API::COREMIDI; static constexpr char kDeviceName[] = "X-TOUCH_INT"; static constexpr mcu::device_type kDeviceType = mcu::device_type::mackie_control_xt; @@ -187,7 +192,17 @@ struct my_xtouch_app midi_out = new libremidi::midi_out {{}, api}; midi_in = new libremidi::midi_in { { + + .ignore_sysex = false, .on_message = [&](const libremidi::message& message) { + + if (observe_incoming_messages){ + fprintf(stderr, "MIDI IN (%zu) ", message.size()); + for(int i = 0; i < message.size(); i++) + fprintf(stderr, "%02x ", message.bytes[i]); + fprintf(stderr, "\n"); + } + rcp->on_midi(message); } }, @@ -199,8 +214,25 @@ struct my_xtouch_app rcp = new libremidi::remote_control_processor{ { .device_type = kDeviceType, - .midi_out = [&](libremidi::message&& msg){ - midi_out->send_message(msg); +// .device_type = mcu::device_type::logic_control, + .midi_out = [&](libremidi::message&& message){ + + if (observe_outgoing_messages){ + fprintf(stderr, "MIDI OUT (%zu) ", message.size()); + for(int i = 0; i < message.size(); i++) + fprintf(stderr, "%02x ", message.bytes[i]); + fprintf(stderr, "\n"); + } + + midi_out->send_message(message); + }, + + .on_version = [&](libremidi::remote_control_protocol::device_type device_type, std::span version){ + std::cerr << "version reply: "; + for (int i = 1; i < version.size(); i++){ + fprintf(stderr, "%02X", version[i]); + } + std::cerr << std::endl; }, .on_command = [&](mcu::mixer_command cmd, bool pressed){ std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " << (pressed ? "pressed" : "released") << "\n"; @@ -308,8 +340,10 @@ struct my_xtouch_app break; } }, - .on_fader = [](uint8_t fader, uint16_t v) { + .on_fader = [&](uint8_t fader, uint16_t v) { std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; + // echo back + rcp->fader(fader, v); } } }; @@ -327,7 +361,6 @@ struct my_xtouch_app // Start communication rcp->start(); - // reset interface _update_buttons(); _update_vpots(); diff --git a/examples/protocols/remote_control.cpp b/examples/protocols/remote_control.cpp index a65a04f3..7a4c23de 100644 --- a/examples/protocols/remote_control.cpp +++ b/examples/protocols/remote_control.cpp @@ -28,8 +28,48 @@ std::string enum_name(auto cmd) #include #include -int main() +// for debugging/testing purposes +const bool observe_incoming_messages = false; +const bool observe_outgoing_messages = false; + +void help(char * argv0){ + + printf( + "Usage: %s []\n" + "\nOptions:\n" + " Also see example midiprobe for currently connected MIDI ports and their names\n" + " Type of device (in hex) to control, will not work if wrong (default = 14)\n" + " 05 (mackie hui)\n" + " 10 (logic control)\n" + " 11 (logic control xt)\n" + " 14 (mackie control)\n" + " 15 (mackie control xt)\n" + , argv0); +} + +int main(int argc, char * argv[]) { + if (argc < 2 || 3 < argc){ + help(argv[0]); + return EXIT_FAILURE; + } + + char * port_name = argv[1]; + libremidi::remote_control_protocol::device_type device_type = libremidi::remote_control_protocol::device_type::mackie_control; + + if (argc == 3){ + int i = 0; + if (!sscanf(argv[2], "%02X", &i)){ + std::cerr << "Error: argument invalid" << std::endl; + return EXIT_FAILURE; + } else if (!libremidi::remote_control_protocol::device_type_is_valid(i)){ + std::cerr << "Error: argument not a recognized type" << std::endl; + help(argv[0]); + return EXIT_FAILURE; + } + device_type = static_cast(i); + } + #if defined(_WIN32) && __has_include() winrt::init_apartment(); #endif @@ -37,25 +77,25 @@ int main() auto api = libremidi::API::UNSPECIFIED; libremidi::observer observer{{.track_any = true}, api}; if (observer.get_input_ports().empty()) - return 1; + return EXIT_FAILURE; if (observer.get_output_ports().empty()) - return 1; + return EXIT_FAILURE; 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" || p.port_name == "TouchOSC Bridge") + if (p.port_name == port_name) ip = p; for (auto& p : observer.get_output_ports()) - if (p.port_name == "TouchOSC" || p.port_name == "TouchOSC Bridge") + if (p.port_name == port_name) op = p; if (ip.port_name.empty() || op.port_name.empty()) { std::cerr << "No device found !"; - return 1; + return EXIT_FAILURE; } // Create the midi out port @@ -64,9 +104,20 @@ int main() // Set-up the remote control API. // Here we only do some logging, this is where commands sqall be handled. libremidi::remote_control_processor rcp{{ - .device_type = libremidi::remote_control_protocol::device_type::mackie_control, - .midi_out = [&](libremidi::message&& msg) { - midi_out.send_message(msg); + .device_type = device_type, + .midi_out = [&](libremidi::message&& message) { + + if (observe_outgoing_messages){ + fprintf(stderr, "MIDI OUT (%zu) ", message.size()); + for(int i = 0; i < message.size(); i++) + fprintf(stderr, "%02x ", message.bytes[i]); + fprintf(stderr, "\n"); + } + + midi_out.send_message(message); + }, + .on_connected = [](libremidi::remote_control_protocol::device_type deviceType){ + fprintf(stderr, "connected: device type: %02X\n", libremidi::to_underlying(deviceType)); }, .on_command = [](libremidi::remote_control_protocol::mixer_command cmd, bool pressed) { std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " << (pressed ? "pressed" : "released") << "\n"; @@ -74,15 +125,40 @@ int main() .on_control = [](libremidi::remote_control_protocol::mixer_control ctl, int v) { std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; }, - .on_fader = [](uint8_t fader, uint16_t v) { + .on_fader = [&](uint8_t fader, uint16_t v) { std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; + + // NOTE + // device are generally do not change their state by themselves, so unless the fader level + // is confirmed (or sent to the device) the fader may return to the last sent state + // you might also want to rely on the fader_touched released command (although the touch + // detection doesn't seem to always react) +// rcp.fader(fader, v); + }, + .on_version = [&](libremidi::remote_control_protocol::device_type device_type, std::span version){ + std::cerr << "version reply: "; + for (int i = 1; i < version.size(); i++){ + fprintf(stderr, "%02X", version[i]); + } + std::cerr << std::endl; } }}; // Initialize the midi in port libremidi::midi_in midi_in{ { + // Note, it's important to allow for sysex to pass through, otherwise + // not all messages from the device come through + .ignore_sysex = false, .on_message = [&](const libremidi::message& message) { + + if (observe_incoming_messages){ + fprintf(stderr, "MIDI IN (%zu) ", message.size()); + for(int i = 0; i < message.size(); i++) + fprintf(stderr, "%02x ", message.bytes[i]); + fprintf(stderr, "\n"); + } + rcp.on_midi(message); } }, @@ -101,18 +177,30 @@ api // Blast messages :) using proto = libremidi::remote_control_protocol; - unsigned i = 0; - for (;;) + + for (unsigned i = 0;;i++) { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); 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); + + // light up buttons :) + proto::mixer_command mc[4] = { + static_cast(libremidi::to_underlying(proto::mixer_command::rec_0) + (i%8)), + static_cast(libremidi::to_underlying(proto::mixer_command::solo_0) + (i%8)), + static_cast(libremidi::to_underlying(proto::mixer_command::mute_0) + (i%8)), + static_cast(libremidi::to_underlying(proto::mixer_command::sel_0) + (i%8)), + }; + for (int j = 0; j < sizeof(mc); j++) + rcp.command(mc[j], i%3); + rcp.fader(i % 8, (200 * i) % 16384); - i++; + } - return 0; + return EXIT_SUCCESS; } diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index 1f2a7cf8..f4c3984b 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -43,6 +43,14 @@ struct remote_control_protocol mackie_control = 0x14, mackie_control_xt = 0x15 }; + static bool device_type_is_valid( uint8_t type ) + { + return (type == libremidi::to_underlying(device_type::mackie_hui) || + type == libremidi::to_underlying(device_type::logic_control) || + type == libremidi::to_underlying(device_type::logic_control_xt) || + type == libremidi::to_underlying(device_type::mackie_control) || + type == libremidi::to_underlying(device_type::mackie_control_xt)); + } enum class command_to_device : uint8_t { @@ -158,10 +166,10 @@ struct remote_control_protocol enum class led_ring_mode : uint8_t { - mode_0 = 0, // one led only - mode_1 = 1, // pan pot - mode_2 = 2, // fill leds from left - mode_3 = 3, // fill leds from middle + mode_0 = 0, ///< one led only + mode_1 = 1, ///< pan pot + mode_2 = 2, ///< fill leds from left + mode_3 = 3, ///< fill leds from middle }; static constexpr int channel_count = 8; @@ -533,7 +541,6 @@ struct remote_control_protocol auto update_lcd(std::string_view txt, int pos) { - // FIXME // valid length? if (pos < 0 || lcd_total_len <= pos) return libremidi::message{}; @@ -551,7 +558,7 @@ struct remote_control_protocol for (int i = 0; i < len; i++) { - buf[i + pos] = charmap_lcd(txt[i]); + buf[i + pos] = txt[i] & 0x7F; } uint8_t cmd_pos = pos; @@ -565,7 +572,7 @@ struct remote_control_protocol for (int i = 0; i < std::min(int(std::ssize(txt)), lcd_total_len); i++) { - buf[i] = charmap_lcd(txt[i]); + buf[i] = txt[i] & 0x7F; } return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, lcd_total_len)); @@ -731,123 +738,6 @@ struct remote_control_protocol } } - - -// // lookup table according to https://github.com/NicoG60/TouchMCU/blob/main/doc/mackie_control_protocol.md#lcd-bitmap-font-character-table -// static constexpr uint8_t charmap_lcd_lut[256] = { -// 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // special signs -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // like ASCII SPACE, ! ... / -// 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // like ASCII 0, 1 ... ? -// 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // like ASCII @, A ... O -// 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, // -// 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // -// 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, // -// -// // map anything above 0x7F to space -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // spaces only -// }; -// - // NOTE - static inline uint8_t charmap_lcd(char c) - { -// return charmap_lcd_lut[c]; - return c & 0x7F; - } - -// static uint8_t charmap_lcd(char c) -// { -// return c & 0x7f; -// -// // 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 @@ -865,6 +755,8 @@ struct rcp_configuration std::function on_control; std::function on_fader; + std::function)> on_version = nullptr; + libremidi::midi_error_callback on_error{}; }; @@ -874,8 +766,7 @@ struct remote_control_processor : libremidi::error_handler rcp_configuration configuration; rcp impl; - - + std::span version = std::array{0,0,0,0,0}; explicit remote_control_processor(rcp_configuration conf) : configuration{std::move(conf)} @@ -907,11 +798,26 @@ struct remote_control_processor : libremidi::error_handler = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); }; } + /** + * + * @return if version never received version it will be all zeros (also if received all zeros as version..) + * @see firmware_version_request() + * @see rcp_configuration::on_version() + */ + std::span & get_version() + { + return version; + } + + void firmware_version_request() + { + configuration.midi_out(impl.firmware_version_request()); + } + void start() { current_state = waiting_for_query; - // NOTE on x-touch hardware device did not observe any response to these queries configuration.midi_out(impl.device_query()); configuration.midi_out(impl.firmware_version_request()); } @@ -920,6 +826,23 @@ struct remote_control_processor : libremidi::error_handler { switch (message.get_message_type()) { + case libremidi::message_type::NOTE_ON: + { + auto pressed = message[2] > 0; + configuration.on_command(static_cast(message[1]), pressed); + 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(message.get_channel() - 1, value); + break; + } case libremidi::message_type::SYSTEM_EXCLUSIVE: if (auto N = message.size(); N >= 7) { @@ -934,15 +857,13 @@ struct remote_control_processor : libremidi::error_handler bytes[1] == remote_control_protocol::mackie_manufacturer_id[1] && bytes[2] == remote_control_protocol::mackie_manufacturer_id[2]) { - //should this be updated automatically? - impl.type = static_cast(bytes[3]); - std::cerr << "device type " << (int) impl.type << std::endl; + remote_control_protocol::device_type device_type = static_cast(bytes[3]); // strip header bytes += 4; N -= 4; - on_rcp_command(std::span(bytes, N)); + on_rcp_command(device_type,std::span(bytes, N)); } } else @@ -950,28 +871,12 @@ struct remote_control_processor : libremidi::error_handler libremidi_handle_error(configuration, "Invalid sysex"); } break; - case libremidi::message_type::NOTE_ON: - { - auto pressed = message[2] > 0; - configuration.on_command(static_cast(message[1]), pressed); - } - 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(message.get_channel() - 1, value); - break; - } default: break; } } - void on_rcp_command(std::span cmd) + void on_rcp_command(remote_control_protocol::device_type device_type, std::span cmd) { if (cmd.empty()) { @@ -1001,14 +906,23 @@ struct remote_control_processor : libremidi::error_handler } case rcp::command_from_device::host_connection_confirmation: current_state = connected; - configuration.on_connected(impl.type); + configuration.on_connected(device_type); 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 + + if (cmd.size() != 5) + std::cerr << "malformed version reply, should be 5" << std::endl; + + version = std::span(cmd); + + // if on_version callback exists, call it + if (configuration.on_version) + configuration.on_version(device_type, version); + break; } default: @@ -1082,11 +996,17 @@ struct remote_control_processor : libremidi::error_handler inline void vpot(uint8_t index, bool state, remote_control_protocol::led_ring_mode mode, uint8_t value) { control( - (remote_control_protocol::mixer_control)((uint8_t)remote_control_protocol::mixer_control::vpot_led_0 + index), + (remote_control_protocol::mixer_control)(libremidi::to_underlying(remote_control_protocol::mixer_control::vpot_led_0) + index), (state ? remote_control_protocol::vpot_mask_state : 0) | remote_control_protocol::vpot_mode_bits[(uint8_t)mode] | value ); } + /** + * Move fader to position/level. + * + * @param i index of fader (0-8 for channel 1-9) + * @param value position/level of fader + */ void fader(uint8_t i, uint16_t value) { using ce = libremidi::channel_events; @@ -1099,6 +1019,67 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(ce::aftertouch(1, ((i<<4) & rcp::channel_meter_mask_index) | (value & rcp::channel_meter_mask_value))); } + /** + * Send a device query + */ + void device_query() + { + configuration.midi_out(impl.device_query()); + } + + + + void reset() + { + configuration.midi_out(impl.reset()); + } + + void all_leds_off() + { + configuration.midi_out(impl.all_leds_off()); + } + + void faders_to_minimum() + { + configuration.midi_out(impl.faders_to_minimum()); + } + + void go_offline() + { + configuration.midi_out(impl.go_offline()); + } + + void global_lcd_meter_mode(rcp::lcd_meter_mode mode) + { + configuration.midi_out(impl.global_lcd_meter_mode(mode)); + } + + void channel_meter_mode(uint8_t fader_id, bool level_meter, bool peak_hold, bool signal_led) + { + configuration.midi_out(impl.channel_meter_mode(fader_id,level_meter,peak_hold,signal_led)); + } + + auto transport_click(bool enabled) + { + configuration.midi_out(impl.transport_click(enabled)); + } + + auto lcd_backlight_save(uint8_t timeout) + { + configuration.midi_out(impl.lcd_backlight_save(timeout)); + } + + auto touchless_movable_fader(bool enabled) + { + configuration.midi_out(impl.touchless_movable_fader(enabled)); + } + + void faders_touch_sensitivity(uint8_t fader_id, rcp::fader_sensitivity sens) + { + configuration.midi_out(impl.faders_touch_sensitivity(fader_id, sens)); + } + + // State machine enum { From 185812f3019e790b81f2362fd873ef520b889b9a Mon Sep 17 00:00:00 2001 From: Philip Tschiemer Date: Tue, 28 Jul 2026 12:28:34 +0200 Subject: [PATCH 9/9] refactoring remote control + xtouch --- examples/protocols/coremidi_mcu_xtouch.cpp | 342 +++++++++--------- examples/protocols/remote_control.cpp | 3 +- .../libremidi/protocols/devices/xtouch.hpp | 151 ++++++++ .../libremidi/protocols/remote_control.hpp | 281 +++++++------- 4 files changed, 465 insertions(+), 312 deletions(-) create mode 100644 include/libremidi/protocols/devices/xtouch.hpp diff --git a/examples/protocols/coremidi_mcu_xtouch.cpp b/examples/protocols/coremidi_mcu_xtouch.cpp index b4d53d83..e61782d1 100644 --- a/examples/protocols/coremidi_mcu_xtouch.cpp +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -1,7 +1,6 @@ #include "../utils.hpp" -#include -#include +#include #if defined(_WIN32) && __has_include() #include @@ -43,30 +42,31 @@ const bool observe_incoming_messages = false; const bool observe_outgoing_messages = false; -using mcu = libremidi::remote_control_protocol; -using mcu_proc = libremidi::remote_control_processor; - struct my_xtouch_app { + using mcu = libremidi::remote_control_protocol; + using xt = libremidi::remote_control_protocol_xtouch; + static constexpr auto api = libremidi::API::COREMIDI; static constexpr char kDeviceName[] = "X-TOUCH_INT"; static constexpr mcu::device_type kDeviceType = mcu::device_type::mackie_control_xt; + static constexpr xt::model_type kModelType = xt::model_type::extender; enum class State : uint8_t { - Off,Starting, Running, Stopping + Off, Starting, Running, Stopping }; struct vpot_st { uint8_t index; bool state = false; - mcu::led_ring_mode mode = mcu::led_ring_mode::mode_0; + xt::led_ring_mode mode = xt::led_ring_mode::mode_0; uint8_t value = 0; void change_value(int change){ - if (change > 0 && value < mcu::vpot_max_value[(uint8_t)mode]) + if (change > 0 && value < xt::vpot_max_value[(uint8_t)mode]) value++; - else if (change < 0 && value > mcu::vpot_min_value[(uint8_t)mode]) + else if (change < 0 && value > xt::vpot_min_value[(uint8_t)mode]) value--; } void toggle_led_state(){ @@ -82,7 +82,7 @@ struct my_xtouch_app libremidi::midi_out * midi_out; libremidi::midi_in * midi_in; - libremidi::remote_control_processor * rcp; + libremidi::remote_control_client_processor_xtouch * xtouch; struct { bool rec[8] = {false,false,false,false,false,false,false,false}; @@ -90,14 +90,14 @@ struct my_xtouch_app } buttons; struct vpot_st vpots[8] = { - {.index = 0, .mode = mcu::led_ring_mode::mode_0, .value=1, .state = false}, - {.index = 1, .mode = mcu::led_ring_mode::mode_1, .value=1, .state = false}, - {.index = 2, .mode = mcu::led_ring_mode::mode_2, .value=1, .state = true}, - {.index = 3, .mode = mcu::led_ring_mode::mode_3, .value=1, .state = true}, - {.index = 4, .mode = mcu::led_ring_mode::mode_0, .value = 6}, - {.index = 5, .mode = mcu::led_ring_mode::mode_1, .value = 6}, - {.index = 6, .mode = mcu::led_ring_mode::mode_2, .value = 6}, - {.index = 7, .mode = mcu::led_ring_mode::mode_3, .value = 6} + {.index = 0, .mode = xt::led_ring_mode::mode_0, .value=1, .state = false}, + {.index = 1, .mode = xt::led_ring_mode::mode_1, .value=1, .state = false}, + {.index = 2, .mode = xt::led_ring_mode::mode_2, .value=1, .state = true}, + {.index = 3, .mode = xt::led_ring_mode::mode_3, .value=1, .state = true}, + {.index = 4, .mode = xt::led_ring_mode::mode_0, .value = 6}, + {.index = 5, .mode = xt::led_ring_mode::mode_1, .value = 6}, + {.index = 6, .mode = xt::led_ring_mode::mode_2, .value = 6}, + {.index = 7, .mode = xt::led_ring_mode::mode_3, .value = 6} }; my_xtouch_app() @@ -113,8 +113,8 @@ struct my_xtouch_app ~my_xtouch_app() { - if (rcp) - delete rcp; + if (xtouch) + delete xtouch; if (midi_in) delete midi_in; if (midi_out) @@ -126,29 +126,29 @@ struct my_xtouch_app void _update_buttons(int i = -1) { - assert(rcp); + assert(xtouch); if (i == -1){ - rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::rec_0) + i), buttons.rec[i]); - rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::mute_0) + i), buttons.mute[i]); + xtouch->command((xt::mixer_command)((int)(xt::mixer_command::rec_0) + i), buttons.rec[i]); + xtouch->command((xt::mixer_command)((int)(xt::mixer_command::mute_0) + i), buttons.mute[i]); } else { for (int i = 0; i < 8; i++){ - rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::rec_0) + i), buttons.rec[i]); - rcp->command((mcu::mixer_command)((int)(mcu::mixer_command::mute_0) + i), buttons.mute[i]); + xtouch->command((xt::mixer_command)((int)(xt::mixer_command::rec_0) + i), buttons.rec[i]); + xtouch->command((xt::mixer_command)((int)(xt::mixer_command::mute_0) + i), buttons.mute[i]); } } } void _update_vpots(int i = -1) { - assert(rcp); + assert(xtouch); if (i != -1){ - rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + xtouch->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); } else { for (i = 0; i < 8; i++){ - rcp->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + xtouch->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); } } } @@ -203,7 +203,7 @@ struct my_xtouch_app fprintf(stderr, "\n"); } - rcp->on_midi(message); + xtouch->on_midi(message); } }, api @@ -211,141 +211,158 @@ struct my_xtouch_app // Set-up the remote control API. // Here we only do some logging, this is where commands sqall be handled. - rcp = new libremidi::remote_control_processor{ - { - .device_type = kDeviceType, -// .device_type = mcu::device_type::logic_control, - .midi_out = [&](libremidi::message&& message){ - - if (observe_outgoing_messages){ - fprintf(stderr, "MIDI OUT (%zu) ", message.size()); - for(int i = 0; i < message.size(); i++) - fprintf(stderr, "%02x ", message.bytes[i]); - fprintf(stderr, "\n"); - } - - midi_out->send_message(message); - }, +// struct libremidi::remote_control_client_processor_xtouch::configuration conf = ; - .on_version = [&](libremidi::remote_control_protocol::device_type device_type, std::span version){ - std::cerr << "version reply: "; - for (int i = 1; i < version.size(); i++){ - fprintf(stderr, "%02X", version[i]); - } - std::cerr << std::endl; - }, - .on_command = [&](mcu::mixer_command cmd, bool pressed){ - std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " << (pressed ? "pressed" : "released") << "\n"; - - auto type = mcu::which_mixer_command_type(cmd); - auto index = mcu::which_mixer_command_index(type, cmd); - switch(type){ - case mcu::mixer_command::type_rec: - if (!pressed) return; - // toggle button state - buttons.rec[index] = !buttons.rec[index]; - _update_buttons(index); - break; - - case mcu::mixer_command::type_mute: - if (!pressed) return; - // toggle button state - buttons.mute[index] = !buttons.mute[index]; - _update_buttons(index); - break; - - case mcu::mixer_command::type_solo: - case mcu::mixer_command::type_sel: - // just light up button while being pressed - rcp->command(cmd, pressed); - break; - - case mcu::mixer_command::type_vpot_click: - std::cerr << "-> vpot click " << index << std::endl; - vpots[index].toggle_led_state(); - _update_vpots(index); - break; - - case mcu::mixer_command::type_fader_touched: - std::cerr << "-> fader touched " << index << std::endl; + xtouch = new libremidi::remote_control_client_processor_xtouch + { + { + {.device_type = kDeviceType, + + .midi_out = + [&](libremidi::message&& message) { + if (observe_outgoing_messages) + { + fprintf(stderr, "MIDI OUT (%zu) ", message.size()); + for (int i = 0; i < message.size(); i++) + fprintf(stderr, "%02x ", message.bytes[i]); + fprintf(stderr, "\n"); + } + + midi_out->send_message(message); + }, + + .on_command = + [&](mcu::mixer_command cmd, bool pressed) { + std::cerr << "command: " << magic_enum::enum_name(cmd) << " -> " + << (pressed ? "pressed" : "released") << "\n"; + + auto type = mcu::which_mixer_command_type(cmd); + auto index = mcu::which_mixer_command_index(type, cmd); + switch (type) + { + case mcu::mixer_command::type_rec: + if (!pressed) + return; + // toggle button state + buttons.rec[index] = !buttons.rec[index]; + _update_buttons(index); + break; + + case mcu::mixer_command::type_mute: + if (!pressed) + return; + // toggle button state + buttons.mute[index] = !buttons.mute[index]; + _update_buttons(index); + break; + + case mcu::mixer_command::type_solo: + case mcu::mixer_command::type_sel: + // just light up button while being pressed + xtouch->command(cmd, pressed); + break; + + case mcu::mixer_command::type_vpot_click: + std::cerr << "-> vpot click " << index << std::endl; + vpots[index].toggle_led_state(); + _update_vpots(index); + break; + + case mcu::mixer_command::type_fader_touched: + std::cerr << "-> fader touched " << index << std::endl; + break; + + case mcu::mixer_command::type_f: + std::cerr << "-> F" << index << std::endl; + break; + + case mcu::mixer_command::type_channel: + switch (cmd) + { + case mcu::mixer_command::bank_left: + std::cerr << "-> bank left" << std::endl; break; - - case mcu::mixer_command::type_f: - std::cerr << "-> F" << index << std::endl; + case mcu::mixer_command::bank_right: + std::cerr << "-> bank right" << std::endl; break; - - case mcu::mixer_command::type_channel: - switch(cmd) { - case mcu::mixer_command::bank_left: - std::cerr << "-> bank left" << std::endl; - break; - case mcu::mixer_command::bank_right: - std::cerr << "-> bank right" << std::endl; - break; - case mcu::mixer_command::channel_left: - std::cerr << "-> channel left" << std::endl; - break; - case mcu::mixer_command::channel_right: - std::cerr << "-> channel right" << std::endl; - break; - default: - break; - } + case mcu::mixer_command::channel_left: + std::cerr << "-> channel left" << std::endl; break; - - case mcu::mixer_command::type_transport: - switch(cmd){ - case mcu::mixer_command::stop: - std::cerr << " -> stop" << std::endl; - break; - case mcu::mixer_command::play: - std::cerr << " -> play" << std::endl; - break; - default: - std::cerr << " -> some transport function" << std::endl; - break; - } + case mcu::mixer_command::channel_right: + std::cerr << "-> channel right" << std::endl; break; - - case mcu::mixer_command::type_leds: - case mcu::mixer_command::type_assign: - case mcu::mixer_command::type_meta: - case mcu::mixer_command::type_control: - case mcu::mixer_command::type_page: - case mcu::mixer_command::type_user: - case mcu::mixer_command::type_other: default: break; } + break; - }, - .on_control = [&](mcu::mixer_control ctl, int v) { - std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; - - auto type = mcu::which_mixer_control_type(ctl); - auto index = mcu::which_mixer_control_index(type, ctl); - auto val = 0; - switch(type) + case mcu::mixer_command::type_transport: + switch (cmd) { - case mcu::mixer_control::type_vpot_rotation: - val = mcu::relative_midi_to_value(v); - std::cerr << "-> vpot " << index << " relative value= " << val << std::endl; - - vpots[index].change_value(val); - - _update_vpots(index); + case mcu::mixer_command::stop: + std::cerr << " -> stop" << std::endl; + break; + case mcu::mixer_command::play: + std::cerr << " -> play" << std::endl; break; - default: + std::cerr << " -> some transport function" << std::endl; break; } + break; + + case mcu::mixer_command::type_leds: + case mcu::mixer_command::type_assign: + case mcu::mixer_command::type_meta: + case mcu::mixer_command::type_control: + case mcu::mixer_command::type_page: + case mcu::mixer_command::type_user: + case mcu::mixer_command::type_other: + default: + break; + } + }, + .on_control + = [&](mcu::mixer_control ctl, int v) { + std::cerr << "control: " << magic_enum::enum_name(ctl) << " -> " << v << "\n"; + + auto type = mcu::which_mixer_control_type(ctl); + auto index = mcu::which_mixer_control_index(type, ctl); + auto val = 0; + switch (type) + { + case mcu::mixer_control::type_vpot_rotation: + val = mcu::relative_midi_to_value(v); + std::cerr << "-> vpot " << index << " relative value= " << val << std::endl; + + vpots[index].change_value(val); + + _update_vpots(index); + break; + + default: + break; + } }, - .on_fader = [&](uint8_t fader, uint16_t v) { - std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; - // echo back - rcp->fader(fader, v); - } - } + .on_fader + = [&](uint8_t fader, uint16_t v) { + std::cerr << "fader: " << (int)fader << " -> " << v << "\n"; + // echo back + xtouch->fader(fader, v); + }, + .on_version + = [&](libremidi::remote_control_protocol::device_type device_type, + std::span version) { + std::cerr << "version reply: "; + for (int i = 1; i < version.size(); i++) + { + fprintf(stderr, "%02X", version[i]); + } + std::cerr << std::endl; + }}, + + .model_type = kModelType, + } }; // Open the ports @@ -359,7 +376,7 @@ struct my_xtouch_app state = State::Running; // Start communication - rcp->start(); + xtouch->start(); // reset interface _update_buttons(); @@ -367,12 +384,14 @@ struct my_xtouch_app // Blast messages :) - mcu::channel_color_xt color_palette[8] = { - mcu::channel_color_xt::black, mcu::channel_color_xt::red, mcu::channel_color_xt::yellow, - mcu::channel_color_xt::green, mcu::channel_color_xt::cyan, mcu::channel_color_xt::blue, - mcu::channel_color_xt::magenta, mcu::channel_color_xt::white + xt::channel_color color_palette[8] = {xt::channel_color::black, xt::channel_color::red, xt::channel_color::yellow, + xt::channel_color::green, xt::channel_color::cyan, xt::channel_color::blue, + xt::channel_color::magenta, xt::channel_color::white }; - mcu::channel_color_list channel_colors = {mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black, mcu::channel_color_xt::black}; + for (uint8_t i = 0; i < 8; i++){ + xtouch->set_channel_color(i, xt::channel_color::black); + } + xtouch->update_channel_colors(); std::string labels1[8] = {"1", "21", "321", "4321", "54321", "654321", "7654321", "87654321"}; std::string labels2[8] = {"ch1", "ch2", "ch3", "ch4", "ch5", "ch6", "ch7", "ch8"}; @@ -383,17 +402,16 @@ struct my_xtouch_app 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); + xtouch->update_timecode(ctime->tm_hour, ctime->tm_min, ctime->tm_sec, 0); - rcp->update_lcd_ch_line(labels1[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 0); - rcp->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 1); + xtouch->update_lcd_ch_line(labels1[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 0); + xtouch->update_lcd_ch_line(labels2[ (i/8 + i%8) % 8 ], i % mcu::channel_count, 1); - channel_colors[i % mcu::channel_count] = color_palette[(i/8 + i%8) % 8]; - rcp->update_channel_colors(channel_colors); + xtouch->update_channel_color(i % mcu::channel_count, color_palette[(i/8 + i%8) % 8]); - rcp->channel_meter(i % mcu::channel_count, i % mcu::channel_meter_max_value); + xtouch->channel_meter(i % mcu::channel_count, i % mcu::channel_meter_max_value); - rcp->fader(i % mcu::channel_count, (200 * i) % 16384); + xtouch->fader(i % mcu::channel_count, (200 * i) % 16384); } state = State::Off; diff --git a/examples/protocols/remote_control.cpp b/examples/protocols/remote_control.cpp index 7a4c23de..94655d3e 100644 --- a/examples/protocols/remote_control.cpp +++ b/examples/protocols/remote_control.cpp @@ -103,7 +103,7 @@ int main(int argc, char * argv[]) // Set-up the remote control API. // Here we only do some logging, this is where commands sqall be handled. - libremidi::remote_control_processor rcp{{ + libremidi::remote_control_client_processor rcp{{ .device_type = device_type, .midi_out = [&](libremidi::message&& message) { @@ -204,3 +204,4 @@ api return EXIT_SUCCESS; } + diff --git a/include/libremidi/protocols/devices/xtouch.hpp b/include/libremidi/protocols/devices/xtouch.hpp new file mode 100644 index 00000000..f77301fa --- /dev/null +++ b/include/libremidi/protocols/devices/xtouch.hpp @@ -0,0 +1,151 @@ +/** +* libremidi +* Copyright (C) 2026 Philip Tschiemer +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +#include + +NAMESPACE_LIBREMIDI +{ + + /** + * X-Touch screen colors RE from extender unit: + * Command Format := 8* + * := 0x72 + * := [0 - 7] (on-off bitmap of RGB colors LSB = R) + * Example (setting each screen a different color): 15720001020304050607 + */ + struct remote_control_protocol_xtouch : remote_control_protocol { + + enum class model_type : uint8_t + { + base = 0, ///< X-Touch + extender = 1, ///< X-Touch Extender + one = 2, ///< X-Touch One + compact = 3, ///< X-Touch Compact + }; + + static inline bool model_type_is_valid(model_type model) + { + return (model == model_type::base || + model == model_type::extender || + model == model_type::one || + model == model_type::compact); + } + + static constexpr remote_control_protocol::device_type device_type_by_model[4] = { + remote_control_protocol::device_type::mackie_control, // base + remote_control_protocol::device_type::mackie_control_xt, // extender + remote_control_protocol::device_type::mackie_control, // one (guessed) + remote_control_protocol::device_type::mackie_control, // compact (guessed) + }; + + + enum class command_to_device : uint8_t + { + update_channel_colors = 0x72 + }; + + /** + * X-Touch specific + * 3-bit RGB encoding + */ + enum class channel_color : uint8_t + { + black = 0b000, + red = 0b001, + green = 0b010, + yellow = 0b011, + blue = 0b100, + magenta = 0b101, + cyan = 0b110, + white = 0b111 + }; + + typedef channel_color channel_color_list_t[8]; + + static inline bool channel_color_is_valid(channel_color color) + { + return channel_color::black <= color && color <= channel_color::white; + }; + + + auto update_channel_colors(channel_color_list_t list) + { + return make_command( + static_cast(command_to_device::update_channel_colors), + std::span(reinterpret_cast(list),8) + ); + } + + }; + + struct remote_control_client_processor_xtouch : remote_control_client_processor + { + struct configuration : public rcp_configuration + { + remote_control_protocol_xtouch::model_type model_type; + }; + + struct configuration configuration; + + remote_control_protocol_xtouch impl; + + remote_control_protocol_xtouch::model_type model; + + remote_control_protocol_xtouch::channel_color_list_t channel_color_list = { + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white, + remote_control_protocol_xtouch::channel_color::white + }; + + + explicit remote_control_client_processor_xtouch(struct configuration conf) : remote_control_client_processor(conf), configuration(std::move(conf)){ + + if (!remote_control_protocol_xtouch::model_type_is_valid(conf.model_type)) + throw new std::invalid_argument("invalid xtouch model type"); + + impl.type = remote_control_protocol_xtouch::device_type_by_model[libremidi::to_underlying(conf.model_type)]; + } + + void set_channel_color(int channel, remote_control_protocol_xtouch::channel_color color) + { + if (channel < 0 || 7 < channel) + throw new std::invalid_argument("channel index must be in [0,7]]"); + + channel_color_list[channel] = color; + } + + void update_channel_colors() + { + auto res = impl.update_channel_colors(channel_color_list); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + inline void update_channel_color(int channel, remote_control_protocol_xtouch::channel_color color) + { + set_channel_color(channel, color); + update_channel_colors(); + } + + }; +} diff --git a/include/libremidi/protocols/remote_control.hpp b/include/libremidi/protocols/remote_control.hpp index f4c3984b..d1064caa 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -16,16 +16,12 @@ NAMESPACE_LIBREMIDI { + + /** * A clean-room reverse-engineered remote control protocol compatible with many hardware devices. * Thanks https://github.com/NicoG60/TouchMCU ! * - * X-Touch screen colors RE from extender unit: - * Command Format := 8* - * := 0x72 - * := [0 - 7] (on-off bitmap of RGB colors LSB = R) - * Example (setting each screen a different color): 15720001020304050607 - * * @TODO Check with actual hardware having all the control elements * * @TODO Desired device support: @@ -37,6 +33,7 @@ struct remote_control_protocol enum class device_type : uint8_t { + no_type = 0, mackie_hui = 0x05, logic_control = 0x10, logic_control_xt = 0x11, @@ -76,9 +73,7 @@ struct remote_control_protocol faders_to_minimum = 0x61, all_leds_off = 0x62, - reset = 0x63, - - update_channel_colors_xt = 0x72 // xtouch specific (?) + reset = 0x63 }; enum class command_from_device : uint8_t @@ -114,29 +109,6 @@ struct remote_control_protocol {0x00 + 49, 0x38 + 49}, }; - /** - * X-Touch specific - * 3-bit RGB encoding - */ - enum class channel_color_xt : uint8_t - { - black = 0b000, - red = 0b001, - green = 0b010, - yellow = 0b011, - blue = 0b100, - magenta = 0b101, - cyan = 0b110, - white = 0b111 - }; - - typedef channel_color_xt channel_color_list[8]; - - static inline bool channel_color_is_valid(channel_color_xt color) - { - return channel_color_xt::black <= color && color <= channel_color_xt::white; - }; - enum class fader_sensitivity : uint8_t { sensitivity_0 = 0x00, @@ -578,12 +550,6 @@ struct remote_control_protocol return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, lcd_total_len)); } - auto update_channel_colors(channel_color_list &channel_colors) - { - return make_command(command_to_device::update_channel_colors_xt, std::span((uint8_t*)channel_colors, 8)); - } - - auto firmware_version_request() { @@ -738,8 +704,22 @@ struct remote_control_protocol } } +// abstract class Channel_Color { +// enum class Color : uint8_t { +// Black, +// Red, +// Green, +// Yellow, +// Blue, +// Magenta, +// Cyan, +// White +// }; +// }; }; + + struct rcp_configuration { remote_control_protocol::device_type device_type = remote_control_protocol::device_type::mackie_control; @@ -763,6 +743,7 @@ struct rcp_configuration struct remote_control_processor : libremidi::error_handler { using rcp = libremidi::remote_control_protocol; + rcp_configuration configuration; rcp impl; @@ -798,6 +779,11 @@ struct remote_control_processor : libremidi::error_handler = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); }; } + + virtual void start() = 0; + + virtual void on_rcp_command(remote_control_protocol::device_type device_type, std::span cmd) = 0; + /** * * @return if version never received version it will be all zeros (also if received all zeros as version..) @@ -809,19 +795,6 @@ struct remote_control_processor : libremidi::error_handler return version; } - void firmware_version_request() - { - configuration.midi_out(impl.firmware_version_request()); - } - - void start() - { - current_state = waiting_for_query; - - configuration.midi_out(impl.device_query()); - configuration.midi_out(impl.firmware_version_request()); - } - void on_midi(const libremidi::message& message) { switch (message.get_message_type()) @@ -876,6 +849,62 @@ struct remote_control_processor : libremidi::error_handler } } + 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)); + } + + inline void vpot(uint8_t index, bool state, remote_control_protocol::led_ring_mode mode, uint8_t value) + { + control( + (remote_control_protocol::mixer_control)(libremidi::to_underlying(remote_control_protocol::mixer_control::vpot_led_0) + index), + (state ? remote_control_protocol::vpot_mask_state : 0) | remote_control_protocol::vpot_mode_bits[(uint8_t)mode] | value + ); + } + + /** + * Move fader to position/level. + * + * @param i index of fader (0-8 for channel 1-9) + * @param value position/level of fader + */ + void fader(uint8_t i, uint16_t value) + { + using ce = libremidi::channel_events; + configuration.midi_out(ce::pitch_bend(i + 1, value)); + } +}; + +struct remote_control_client_processor : remote_control_processor +{ + // State machine + enum + { + waiting_for_query, + got_query, + connected, + errored + } current_state {waiting_for_query}; + + explicit remote_control_client_processor(rcp_configuration conf) + : remote_control_processor(conf) {} + + void start() + { + current_state = waiting_for_query; + + configuration.midi_out(impl.device_query()); + configuration.midi_out(impl.firmware_version_request()); + } + void on_rcp_command(remote_control_protocol::device_type device_type, std::span cmd) { if (cmd.empty()) @@ -931,103 +960,16 @@ struct remote_control_processor : libremidi::error_handler } } - 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 = impl.update_lcd(v); - if (!res.empty()) - configuration.midi_out(std::move(res)); - } - - void update_lcd(std::string_view v, int pos) - { - auto res = impl.update_lcd(v, pos); - if (!res.empty()) - configuration.midi_out(std::move(res)); - } - - void update_lcd_ch_line(std::string_view v, uint ch, uint line) - { - // 8 channels, 2 lines - // Show error msg? - if (7 < ch || remote_control_protocol::lcd_channel_line_n < line) - return; - - // Prefill segment with spaces - char buf[8] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0}; - - int len = int(std::ssize(v)); - if (len > remote_control_protocol::lcd_channel_line_len) - len = remote_control_protocol::lcd_channel_line_len; - - std::memcpy(buf, v.data(), len); - - auto res = impl.update_lcd(buf, remote_control_protocol::lcd_channel_offset[ch][line]); - - if (!res.empty()) - configuration.midi_out(std::move(res)); - } - - void update_channel_colors(rcp::channel_color_list &channel_colors) - { - auto res = impl.update_channel_colors(channel_colors); - 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)); - } - inline void vpot(uint8_t index, bool state, remote_control_protocol::led_ring_mode mode, uint8_t value) - { - control( - (remote_control_protocol::mixer_control)(libremidi::to_underlying(remote_control_protocol::mixer_control::vpot_led_0) + index), - (state ? remote_control_protocol::vpot_mask_state : 0) | remote_control_protocol::vpot_mode_bits[(uint8_t)mode] | value - ); - } - - /** - * Move fader to position/level. - * - * @param i index of fader (0-8 for channel 1-9) - * @param value position/level of fader - */ - void fader(uint8_t i, uint16_t value) - { - using ce = libremidi::channel_events; - configuration.midi_out(ce::pitch_bend(i + 1, value)); - } - - void channel_meter(uint8_t i, uint8_t value) - { - using ce = libremidi::channel_events; - configuration.midi_out(ce::aftertouch(1, ((i<<4) & rcp::channel_meter_mask_index) | (value & rcp::channel_meter_mask_value))); - } - - /** - * Send a device query - */ void device_query() { configuration.midi_out(impl.device_query()); } - + void firmware_version_request() + { + configuration.midi_out(impl.firmware_version_request()); + } void reset() { @@ -1079,14 +1021,55 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(impl.faders_touch_sensitivity(fader_id, sens)); } + void channel_meter(uint8_t i, uint8_t value) + { + using ce = libremidi::channel_events; + configuration.midi_out(ce::aftertouch(1, ((i<<4) & rcp::channel_meter_mask_index) | (value & rcp::channel_meter_mask_value))); + } - // State machine - enum + void update_timecode(int h, int m, int s, int f) { - waiting_for_query, - got_query, - connected, - errored - } current_state{waiting_for_query}; + for (auto&& m : rcp::timecode(h, m, s, f)) + configuration.midi_out(std::move(m)); + } + + void update_lcd(std::string_view v) + { + auto res = impl.update_lcd(v); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + void update_lcd(std::string_view v, int pos) + { + auto res = impl.update_lcd(v, pos); + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + + void update_lcd_ch_line(std::string_view v, uint ch, uint line) + { + // 8 channels, 2 lines + // Show error msg? + if (7 < ch || remote_control_protocol::lcd_channel_line_n < line) + return; + + // Prefill segment with spaces + char buf[8] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0}; + + int len = int(std::ssize(v)); + if (len > remote_control_protocol::lcd_channel_line_len) + len = remote_control_protocol::lcd_channel_line_len; + + std::memcpy(buf, v.data(), len); + + auto res = impl.update_lcd(buf, remote_control_protocol::lcd_channel_offset[ch][line]); + + if (!res.empty()) + configuration.midi_out(std::move(res)); + } + }; + + }