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..e61782d1 --- /dev/null +++ b/examples/protocols/coremidi_mcu_xtouch.cpp @@ -0,0 +1,489 @@ +#include "../utils.hpp" + +#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 +#else +#error This example was written for CoreMIDI, so you will have to adapt it for your own system +#endif + +#include +#include +#include +#include +#include +#include + +// for debugging/testing purposes +const bool observe_incoming_messages = false; +const bool observe_outgoing_messages = false; + + +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 + }; + + struct vpot_st + { + uint8_t index; + bool state = false; + xt::led_ring_mode mode = xt::led_ring_mode::mode_0; + uint8_t value = 0; + void change_value(int change){ + if (change > 0 && value < xt::vpot_max_value[(uint8_t)mode]) + value++; + else if (change < 0 && value > xt::vpot_min_value[(uint8_t)mode]) + value--; + } + void toggle_led_state(){ + state = !state; + } + }; + + + State state = State::Off; + + MIDIClientRef handle; + + libremidi::midi_out * midi_out; + libremidi::midi_in * midi_in; + + libremidi::remote_control_client_processor_xtouch * xtouch; + + 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 = 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() + { + 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 (xtouch) + delete xtouch; + if (midi_in) + delete midi_in; + if (midi_out) + delete midi_out; + + std::cout << "Disposing of MIDIClient" << std::endl; + MIDIClientDispose(handle); + } + + void _update_buttons(int i = -1) + { + assert(xtouch); + + if (i == -1){ + 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++){ + 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(xtouch); + + if (i != -1){ + xtouch->vpot(vpots[i].index, vpots[i].state, vpots[i].mode, vpots[i].value); + } else { + for (i = 0; i < 8; i++){ + xtouch->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; + + +#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 { + { + + .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"); + } + + xtouch->on_midi(message); + } + }, + api + }; + + // Set-up the remote control API. + // Here we only do some logging, this is where commands sqall be handled. +// struct libremidi::remote_control_client_processor_xtouch::configuration conf = ; + + 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::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; + } + }, + .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 + 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 + 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 + xtouch->start(); + + // reset interface + _update_buttons(); + _update_vpots(); + + // Blast messages :) + + 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 + }; + 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"}; + + 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); + xtouch->update_timecode(ctime->tm_hour, ctime->tm_min, ctime->tm_sec, 0); + + 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); + + xtouch->update_channel_color(i % mcu::channel_count, color_palette[(i/8 + i%8) % 8]); + + xtouch->channel_meter(i % mcu::channel_count, i % mcu::channel_meter_max_value); + + xtouch->fader(i % mcu::channel_count, (200 * i) % 16384); + } + + state = State::Off; + } + + void start(){ + try { + _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() +{ + setupSignalHandler(); + + my_app_instance = new my_xtouch_app(); + + try { + my_app_instance->start(); + } catch (std::exception e){ + std::cerr << "Failed to start: " << e.what() << std::endl; + } + + 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 (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 + // 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 EXIT_SUCCESS; +} \ No newline at end of file diff --git a/examples/protocols/remote_control.cpp b/examples/protocols/remote_control.cpp index 5ab77941..94655d3e 100644 --- a/examples/protocols/remote_control.cpp +++ b/examples/protocols/remote_control.cpp @@ -28,34 +28,74 @@ 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 - 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; + 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") + if (p.port_name == port_name) ip = p; for (auto& p : observer.get_output_ports()) - if (p.port_name == "TouchOSC") + 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 @@ -63,20 +103,67 @@ 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_client_processor rcp{{ + .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"; + }, + .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"; + + // 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{ - {.on_message = [&](const libremidi::message& message) { rcp.on_midi(message); }}, api}; + { + // 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); + } + }, +api + }; // Open the ports if (auto err = midi_in.open_port(ip); err != stdx::error{}) @@ -90,18 +177,31 @@ int main() // 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); - rcp.fader(static_cast(i % 8), (200 * i) % 16384); - i++; + + // 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); + } - return 0; + 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 dd6edfd1..d1064caa 100644 --- a/include/libremidi/protocols/remote_control.hpp +++ b/include/libremidi/protocols/remote_control.hpp @@ -10,21 +10,44 @@ #include #include #include +#include #include #include 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 ! + * + * @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}; + enum class device_type : uint8_t { + no_type = 0, + mackie_hui = 0x05, logic_control = 0x10, logic_control_xt = 0x11, - mackie_control = 0x14 + 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 { @@ -50,7 +73,7 @@ struct remote_control_protocol faders_to_minimum = 0x61, all_leds_off = 0x62, - reset = 0x63, + reset = 0x63 }; enum class command_from_device : uint8_t @@ -61,12 +84,31 @@ 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, 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}, + }; + enum class fader_sensitivity : uint8_t { sensitivity_0 = 0x00, @@ -82,39 +124,27 @@ struct remote_control_protocol // 0b0LMMVVVV // L: toggle underneath LED // MM: mode as led_ring_mode - // VVVV: value - enum class led_ring_mode : uint8_t - { - mode_0 = 0b00, // one led only - mode_1 = 0b01, // pan pot - mode_2 = 0b10, // fill leds from left - mode_3 = 0b11, // fill leds from middle - }; + // VVVV: value range depends on mode (0 = off, 1 -> at least one led, max -> see below) + + static constexpr uint8_t vpot_mask_state = 0b01000000; + static constexpr uint8_t vpot_mask_mode = 0b00110000; + static constexpr uint8_t vpot_mask_value = 0b00001111; - enum class pot : uint8_t + 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_ring_mode : 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, + 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 }; - 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 int channel_count = 8; // control changes enum class mixer_control : uint8_t @@ -130,6 +160,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, @@ -142,6 +173,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, @@ -155,14 +187,51 @@ 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 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) + { + return (uint8_t)ctl - (uint8_t)type; + } + + 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){ + 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 : uint8_t { + // vpot_click vpot_click_0 = 0x20 + 0x00, vpot_click_1 = 0x20 + 0x01, vpot_click_2 = 0x20 + 0x02, @@ -171,7 +240,9 @@ 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, rec_1 = 0x00 + 0x01, rec_2 = 0x00 + 0x02, @@ -180,7 +251,9 @@ 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, solo_1 = 0x08 + 0x01, solo_2 = 0x08 + 0x02, @@ -189,7 +262,9 @@ 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, mute_1 = 0x10 + 0x01, mute_2 = 0x10 + 0x02, @@ -198,7 +273,9 @@ 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, sel_1 = 0x18 + 0x01, sel_2 = 0x18 + 0x02, @@ -207,25 +284,31 @@ struct remote_control_protocol sel_5 = 0x18 + 0x05, sel_6 = 0x18 + 0x06, sel_7 = 0x18 + 0x07, + type_sel = sel_0, // TODO metering + // assign assign_track = 0x28, assign_send = 0x29, assign_pan = 0x2A, assign_plugin = 0x2B, assign_eq = 0x2C, assign_instrument = 0x2D, + type_assign = assign_track, + // channels bank_left = 0x2E, bank_right = 0x2F, channel_left = 0x30, channel_right = 0x31, flip = 0x32, global = 0x33, + type_channel = bank_left, name_value_button = 0x34, smpte_beats_button = 0x35, + // f(unction) f1 = 0x36 + 0x00, f2 = 0x36 + 0x01, f3 = 0x36 + 0x02, @@ -234,7 +317,9 @@ struct remote_control_protocol f6 = 0x36 + 0x05, f7 = 0x36 + 0x06, f8 = 0x36 + 0x07, + type_f = f1, + // page midi_tracks = 0x3E, inputs = 0x3F, audio_tracks = 0x40, @@ -243,17 +328,23 @@ 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, nudge = 0x55, cycle = 0x56, @@ -275,9 +366,14 @@ 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, fader_touched_1 = 0x69, fader_touched_2 = 0x6a, @@ -287,18 +383,56 @@ 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 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_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; + device_type type; static libremidi::message make_command_impl(auto&&... data) { @@ -379,26 +513,25 @@ struct remote_control_protocol auto update_lcd(std::string_view txt, int pos) { - // FIXME - if (pos < 0 || pos >= 112) + // valid length? + if (pos < 0 || lcd_total_len <= pos) return libremidi::message{}; int len = int(std::ssize(txt)); - if (len > (112 - pos)) + // length sanity check + 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); - for (int i = 0; i < N; i++) + + for (int i = 0; i < len; i++) { - buf[i + pos] = charmap_lcd(txt[i]); + buf[i + pos] = txt[i] & 0x7F; } - buf[55] = '\n'; - buf[111] = '\n'; uint8_t cmd_pos = pos; @@ -407,16 +540,17 @@ 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[i] = txt[i] & 0x7F; } - buf[55] = '\n'; - buf[111] = '\n'; - return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, 112)); + + return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, lcd_total_len)); } + auto firmware_version_request() { return make_command(command_to_device::firmware_version_request, arr<1>{0}); @@ -446,6 +580,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); } @@ -568,94 +703,27 @@ 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 - } - } +// 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; + //! 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 @@ -665,7 +733,9 @@ 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_version = nullptr; libremidi::midi_error_callback on_error{}; }; @@ -673,14 +743,20 @@ struct rcp_configuration struct remote_control_processor : libremidi::error_handler { using rcp = libremidi::remote_control_protocol; + 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)} { assert(configuration.midi_out); + // no sanity checking of device type + 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()); @@ -703,16 +779,43 @@ struct remote_control_processor : libremidi::error_handler = [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); }; } - void start() + + 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..) + * @see firmware_version_request() + * @see rcp_configuration::on_version() + */ + std::span & get_version() { - current_state = waiting_for_query; - configuration.midi_out(impl.device_query()); + return version; } void on_midi(const libremidi::message& message) { 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) { @@ -723,14 +826,17 @@ 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]); + + 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 @@ -738,25 +844,68 @@ struct remote_control_processor : libremidi::error_handler libremidi_handle_error(configuration, "Invalid sysex"); } break; - case libremidi::message_type::NOTE_ON: - configuration.on_command(static_cast(message[1]), message[2] > 0); - break; - case libremidi::message_type::NOTE_OFF: - break; - case libremidi::message_type::CONTROL_CHANGE: - configuration.on_control(static_cast(message[1]), message[2]); - break; - case libremidi::message_type::PITCH_BEND: { - uint16_t value = message.bytes[2] * 128 + message.bytes[1]; - configuration.on_fader(static_cast(uint8_t(message.get_channel() - 1)), value); - break; - } default: break; } } - void on_rcp_command(std::span cmd) + 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()) { @@ -786,21 +935,98 @@ 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: + std::cerr << "Unhandled MIDI message" << std::endl; break; } } + + void device_query() + { + configuration.midi_out(impl.device_query()); + } + + void firmware_version_request() + { + configuration.midi_out(impl.firmware_version_request()); + } + + 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)); + } + + 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))); + } + void update_timecode(int h, int m, int s, int f) { for (auto&& m : rcp::timecode(h, m, s, f)) @@ -821,34 +1047,29 @@ struct remote_control_processor : libremidi::error_handler configuration.midi_out(std::move(res)); } - void command(remote_control_protocol::mixer_command c, bool press) + void update_lcd_ch_line(std::string_view v, uint ch, uint line) { - 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)); - } + // 8 channels, 2 lines + // Show error msg? + if (7 < ch || remote_control_protocol::lcd_channel_line_n < line) + return; - 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)); - } + // Prefill segment with spaces + char buf[8] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0}; - void fader(remote_control_protocol::fader c, uint16_t value) - { - int idx = to_underlying(c); + int len = int(std::ssize(v)); + if (len > remote_control_protocol::lcd_channel_line_len) + len = remote_control_protocol::lcd_channel_line_len; - using ce = libremidi::channel_events; - configuration.midi_out(ce::pitch_bend(idx + 1, value)); + 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)); } - // State machine - enum - { - waiting_for_query, - got_query, - connected, - errored - } current_state{waiting_for_query}; }; + + }