859 lines
21 KiB
C++
859 lines
21 KiB
C++
#pragma once
|
|
|
|
#include <libremidi/error_handler.hpp>
|
|
#include <libremidi/message.hpp>
|
|
|
|
#include <cmath>
|
|
|
|
#include <array>
|
|
#include <cinttypes>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <functional>
|
|
#include <span>
|
|
|
|
NAMESPACE_LIBREMIDI
|
|
{
|
|
static constexpr auto to_underlying(auto e)
|
|
{
|
|
return static_cast<std::underlying_type_t<decltype(e)>>(e);
|
|
}
|
|
|
|
// A clean-room reverse-engineered remote control protocol compatible with many hardware devices.
|
|
// Thanks https://github.com/NicoG60/TouchMCU !
|
|
struct remote_control_protocol
|
|
{
|
|
enum class device_type : uint8_t
|
|
{
|
|
logic_control = 0x10,
|
|
logic_control_xt = 0x11,
|
|
mackie_control = 0x14
|
|
};
|
|
|
|
enum class command_to_device : uint8_t
|
|
{
|
|
device_query = 0x00,
|
|
host_connection_reply = 0x02,
|
|
|
|
transport_click = 0x0A,
|
|
lcd_backlight_save = 0x0B,
|
|
touchless_movable_fader = 0x0C,
|
|
|
|
faders_touch_sensitivity = 0x0E,
|
|
go_offline = 0x0F,
|
|
update_tc_display = 0x10,
|
|
update_assignment_display = 0x11,
|
|
update_lcd = 0x12,
|
|
firmware_version_request = 0x13,
|
|
version_reply = 0x14,
|
|
|
|
firmware_update = 0x18,
|
|
|
|
channel_meter_mode = 0x20,
|
|
global_lcd_meter_mode = 0x21,
|
|
|
|
faders_to_minimum = 0x61,
|
|
all_leds_off = 0x62,
|
|
reset = 0x63,
|
|
};
|
|
|
|
enum class command_from_device : uint8_t
|
|
{
|
|
host_connection_query = 0x01,
|
|
host_connection_confirmation = 0x03,
|
|
host_connection_error = 0x04,
|
|
version_reply = 0x14,
|
|
};
|
|
|
|
enum class lcd_meter_mode : uint8_t
|
|
{
|
|
horizontal = 0x00,
|
|
vertical = 0x01,
|
|
};
|
|
|
|
enum class fader_sensitivity : uint8_t
|
|
{
|
|
sensitivity_0 = 0x00,
|
|
sensitivity_1 = 0x01,
|
|
sensitivity_2 = 0x02,
|
|
sensitivity_3 = 0x03,
|
|
sensitivity_4 = 0x04,
|
|
|
|
sensitivity_default = sensitivity_3,
|
|
};
|
|
|
|
// CC Message:
|
|
// 0b0LMMVVVV
|
|
// L: toggle underneath LED
|
|
// MM: mode as led_ring_mode
|
|
// VVVV: value
|
|
enum class led_ring_mode : uint8_t
|
|
{
|
|
mode_0 = 0b00, // one led only
|
|
mode_1 = 0b01, // pan pot
|
|
mode_2 = 0b10, // fill leds from left
|
|
mode_3 = 0b11, // fill leds from middle
|
|
};
|
|
|
|
enum class pot : uint8_t
|
|
{
|
|
pot_0 = 0x00,
|
|
pot_1 = 0x01,
|
|
pot_2 = 0x02,
|
|
pot_3 = 0x03,
|
|
pot_4 = 0x04,
|
|
pot_5 = 0x05,
|
|
pot_6 = 0x06,
|
|
pot_7 = 0x07,
|
|
};
|
|
|
|
enum class fader : uint8_t
|
|
{
|
|
fader_0 = 0x00,
|
|
fader_1 = 0x01,
|
|
fader_2 = 0x02,
|
|
fader_3 = 0x03,
|
|
fader_4 = 0x04,
|
|
fader_5 = 0x05,
|
|
fader_6 = 0x06,
|
|
fader_7 = 0x07,
|
|
fader_master = 0x08,
|
|
};
|
|
|
|
// control changes
|
|
enum class mixer_control : uint8_t
|
|
{
|
|
// rotation: CC
|
|
// 0b00000001 : clockwise
|
|
// 0b01000001 : counter-clockwise
|
|
vpot_rotation_0 = 0x10 + 0x00,
|
|
vpot_rotation_1 = 0x10 + 0x01,
|
|
vpot_rotation_2 = 0x10 + 0x02,
|
|
vpot_rotation_3 = 0x10 + 0x03,
|
|
vpot_rotation_4 = 0x10 + 0x04,
|
|
vpot_rotation_5 = 0x10 + 0x05,
|
|
vpot_rotation_6 = 0x10 + 0x06,
|
|
vpot_rotation_7 = 0x10 + 0x07,
|
|
|
|
external_control = 0x2E,
|
|
|
|
// led ring: CC
|
|
vpot_led_0 = 0x30 + 0x00,
|
|
vpot_led_1 = 0x30 + 0x01,
|
|
vpot_led_2 = 0x30 + 0x02,
|
|
vpot_led_3 = 0x30 + 0x03,
|
|
vpot_led_4 = 0x30 + 0x04,
|
|
vpot_led_5 = 0x30 + 0x05,
|
|
vpot_led_6 = 0x30 + 0x06,
|
|
vpot_led_7 = 0x30 + 0x07,
|
|
|
|
jog_wheel = 0x3C,
|
|
|
|
timecode_digit_0 = 0x40 + 0x00,
|
|
timecode_digit_1 = 0x40 + 0x01,
|
|
timecode_digit_2 = 0x40 + 0x02,
|
|
timecode_digit_3 = 0x40 + 0x03,
|
|
timecode_digit_4 = 0x40 + 0x04,
|
|
timecode_digit_5 = 0x40 + 0x05,
|
|
timecode_digit_6 = 0x40 + 0x06,
|
|
timecode_digit_7 = 0x40 + 0x07,
|
|
timecode_digit_8 = 0x40 + 0x08,
|
|
timecode_digit_9 = 0x40 + 0x09,
|
|
|
|
assignment_digit_0 = 0x4A,
|
|
assignment_digit_1 = 0x4B,
|
|
};
|
|
|
|
// note events
|
|
enum class mixer_command : uint8_t
|
|
{
|
|
vpot_click_0 = 0x20 + 0x00,
|
|
vpot_click_1 = 0x20 + 0x01,
|
|
vpot_click_2 = 0x20 + 0x02,
|
|
vpot_click_3 = 0x20 + 0x03,
|
|
vpot_click_4 = 0x20 + 0x04,
|
|
vpot_click_5 = 0x20 + 0x05,
|
|
vpot_click_6 = 0x20 + 0x06,
|
|
vpot_click_7 = 0x20 + 0x07,
|
|
|
|
rec_0 = 0x00 + 0x00,
|
|
rec_1 = 0x00 + 0x01,
|
|
rec_2 = 0x00 + 0x02,
|
|
rec_3 = 0x00 + 0x03,
|
|
rec_4 = 0x00 + 0x04,
|
|
rec_5 = 0x00 + 0x05,
|
|
rec_6 = 0x00 + 0x06,
|
|
rec_7 = 0x00 + 0x07,
|
|
|
|
solo_0 = 0x08 + 0x00,
|
|
solo_1 = 0x08 + 0x01,
|
|
solo_2 = 0x08 + 0x02,
|
|
solo_3 = 0x08 + 0x03,
|
|
solo_4 = 0x08 + 0x04,
|
|
solo_5 = 0x08 + 0x05,
|
|
solo_6 = 0x08 + 0x06,
|
|
solo_7 = 0x08 + 0x07,
|
|
|
|
mute_0 = 0x10 + 0x00,
|
|
mute_1 = 0x10 + 0x01,
|
|
mute_2 = 0x10 + 0x02,
|
|
mute_3 = 0x10 + 0x03,
|
|
mute_4 = 0x10 + 0x04,
|
|
mute_5 = 0x10 + 0x05,
|
|
mute_6 = 0x10 + 0x06,
|
|
mute_7 = 0x10 + 0x07,
|
|
|
|
sel_0 = 0x18 + 0x00,
|
|
sel_1 = 0x18 + 0x01,
|
|
sel_2 = 0x18 + 0x02,
|
|
sel_3 = 0x18 + 0x03,
|
|
sel_4 = 0x18 + 0x04,
|
|
sel_5 = 0x18 + 0x05,
|
|
sel_6 = 0x18 + 0x06,
|
|
sel_7 = 0x18 + 0x07,
|
|
|
|
// TODO metering
|
|
assign_track = 0x28,
|
|
assign_send = 0x29,
|
|
assign_pan = 0x2A,
|
|
assign_plugin = 0x2B,
|
|
assign_eq = 0x2C,
|
|
assign_instrument = 0x2D,
|
|
|
|
bank_left = 0x2E,
|
|
bank_right = 0x2F,
|
|
channel_left = 0x30,
|
|
channel_right = 0x31,
|
|
flip = 0x32,
|
|
global = 0x33,
|
|
|
|
name_value_button = 0x34,
|
|
smpte_beats_button = 0x35,
|
|
|
|
f1 = 0x36 + 0x00,
|
|
f2 = 0x36 + 0x01,
|
|
f3 = 0x36 + 0x02,
|
|
f4 = 0x36 + 0x03,
|
|
f5 = 0x36 + 0x04,
|
|
f6 = 0x36 + 0x05,
|
|
f7 = 0x36 + 0x06,
|
|
f8 = 0x36 + 0x07,
|
|
|
|
midi_tracks = 0x3E,
|
|
inputs = 0x3F,
|
|
audio_tracks = 0x40,
|
|
audio_instruments = 0x41,
|
|
aux = 0x42,
|
|
busses = 0x43,
|
|
outputs = 0x44,
|
|
user = 0x45,
|
|
|
|
shift = 0x46,
|
|
option = 0x47,
|
|
control = 0x48,
|
|
alt = 0x49,
|
|
|
|
save = 0x50,
|
|
undo = 0x51,
|
|
cancel = 0x52,
|
|
enter = 0x53,
|
|
|
|
markers = 0x54,
|
|
nudge = 0x55,
|
|
cycle = 0x56,
|
|
drop = 0x57,
|
|
replace = 0x58,
|
|
click = 0x59,
|
|
solo = 0x5a,
|
|
|
|
rewind = 0x5b,
|
|
forward = 0x5c,
|
|
stop = 0x5d,
|
|
play = 0x5e,
|
|
record = 0x5f,
|
|
|
|
up = 0x60,
|
|
down = 0x61,
|
|
left = 0x62,
|
|
right = 0x63,
|
|
zoom = 0x64,
|
|
scrub = 0x65,
|
|
|
|
user_switch_1 = 0x66,
|
|
user_switch_2 = 0x67,
|
|
|
|
fader_touched_0 = 0x68,
|
|
fader_touched_1 = 0x69,
|
|
fader_touched_2 = 0x6a,
|
|
fader_touched_3 = 0x6b,
|
|
fader_touched_4 = 0x6c,
|
|
fader_touched_5 = 0x6d,
|
|
fader_touched_6 = 0x6e,
|
|
fader_touched_7 = 0x6f,
|
|
fader_touched_master = 0x70,
|
|
|
|
smpte_led = 0x71,
|
|
beats_led = 0x72,
|
|
rude_solo_led = 0x73,
|
|
|
|
relay_click = 0x76,
|
|
};
|
|
|
|
template <std::size_t N>
|
|
using arr = std::array<uint8_t, N>;
|
|
|
|
device_type type = device_type::mackie_control;
|
|
|
|
static libremidi::message make_command_impl(auto&&... data)
|
|
{
|
|
using namespace std;
|
|
libremidi::message m;
|
|
m.bytes.reserve((std::ssize(data) + ...));
|
|
(m.bytes.insert(m.bytes.end(), begin(data), end(data)), ...);
|
|
return m;
|
|
}
|
|
|
|
libremidi::message make_command(command_to_device c, auto&&... data)
|
|
{
|
|
using namespace std;
|
|
const auto type = to_underlying(this->type);
|
|
const auto cmd = to_underlying(c);
|
|
const uint8_t header[6]{0xF0, 0x00, 0x00, 0x66, type, cmd};
|
|
const uint8_t footer[1]{0xF7};
|
|
return make_command_impl(header, data..., footer);
|
|
}
|
|
|
|
auto device_query() { return make_command(command_to_device::device_query); }
|
|
|
|
auto response_to_challenge(arr<4> c)
|
|
{
|
|
arr<4> r;
|
|
|
|
r[0] = 0x7F & (c[0] + (c[1] ^ 0x0A) - c[3]);
|
|
r[1] = 0x7F & ((c[2] >> 4) ^ (c[0] + c[3]));
|
|
r[2] = 0x7F & (c[3] - (c[2] << 2) ^ (c[0] | c[1]));
|
|
r[3] = 0x7F & (c[1] - c[2] + (0xF0 ^ (c[3] << 4)));
|
|
|
|
return r;
|
|
}
|
|
|
|
auto host_connection_reply(arr<7> serial, arr<4> challenge_code)
|
|
{
|
|
const auto res = response_to_challenge(challenge_code);
|
|
return make_command(command_to_device::host_connection_reply, serial, res);
|
|
}
|
|
|
|
auto transport_click(bool enabled)
|
|
{
|
|
return make_command(
|
|
command_to_device::transport_click, arr<1>{uint8_t(enabled ? 0x01 : 0x00)});
|
|
}
|
|
|
|
auto lcd_backlight_save(uint8_t timeout)
|
|
{
|
|
// 0: instant off otherwise timeout in minutes
|
|
return make_command(command_to_device::lcd_backlight_save, arr<1>{timeout});
|
|
}
|
|
|
|
auto touchless_movable_fader(bool enabled)
|
|
{
|
|
return make_command(
|
|
command_to_device::touchless_movable_fader, arr<1>{uint8_t(enabled ? 0x01 : 0x00)});
|
|
}
|
|
|
|
auto faders_touch_sensitivity(uint8_t fader_id, fader_sensitivity sens)
|
|
{
|
|
return make_command(
|
|
command_to_device::faders_touch_sensitivity, arr<2>{fader_id, to_underlying(sens)});
|
|
}
|
|
|
|
auto go_offline() { return make_command(command_to_device::go_offline, arr<1>{0x7F}); }
|
|
|
|
auto update_tc_display()
|
|
{
|
|
// FIXME 1 .. 10
|
|
return make_command(command_to_device::update_tc_display, arr<10>{});
|
|
}
|
|
|
|
auto update_assignment_display()
|
|
{
|
|
// FIXME 1 .. 2
|
|
return make_command(command_to_device::update_assignment_display, arr<2>{});
|
|
}
|
|
|
|
auto update_lcd(std::string_view txt, int pos)
|
|
{
|
|
// FIXME
|
|
if (pos < 0 || pos >= 112)
|
|
return libremidi::message{};
|
|
|
|
int len = int(std::ssize(txt));
|
|
|
|
if (len > (112 - pos))
|
|
{
|
|
txt = txt.substr(0, 112 - pos);
|
|
len = 112 - pos;
|
|
}
|
|
|
|
uint8_t buf[128];
|
|
const int N = std::min(len, 112 - pos);
|
|
for (int i = 0; i < N; i++)
|
|
{
|
|
buf[i + pos] = charmap_lcd(txt[i]);
|
|
}
|
|
buf[55] = '\n';
|
|
buf[111] = '\n';
|
|
|
|
uint8_t cmd_pos = pos;
|
|
|
|
return make_command(command_to_device::update_lcd, arr<1>{cmd_pos}, std::span(buf + pos, len));
|
|
}
|
|
|
|
auto update_lcd(std::string_view txt)
|
|
{
|
|
uint8_t buf[112] = {};
|
|
for (int i = 0; i < std::min(int(std::ssize(txt)), 112); i++)
|
|
{
|
|
buf[i] = charmap_lcd(txt[i]);
|
|
}
|
|
buf[55] = '\n';
|
|
buf[111] = '\n';
|
|
return make_command(command_to_device::update_lcd, arr<1>{0}, std::span(buf, 112));
|
|
}
|
|
|
|
auto firmware_version_request()
|
|
{
|
|
return make_command(command_to_device::firmware_version_request, arr<1>{0});
|
|
}
|
|
|
|
auto firmware_update(std::span<uint8_t> firmware)
|
|
{
|
|
return make_command(command_to_device::firmware_update, firmware);
|
|
}
|
|
|
|
auto channel_meter_mode(uint8_t fader_id, bool level_meter, bool peak_hold, bool signal_led)
|
|
{
|
|
uint8_t mode = 0;
|
|
|
|
if (signal_led)
|
|
mode |= 0b1;
|
|
if (peak_hold)
|
|
mode |= 0b10;
|
|
if (level_meter)
|
|
mode |= 0b100;
|
|
|
|
return make_command(command_to_device::channel_meter_mode, arr<2>{fader_id, mode});
|
|
}
|
|
|
|
auto global_lcd_meter_mode(lcd_meter_mode mode)
|
|
{
|
|
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); }
|
|
|
|
auto reset() { return make_command(command_to_device::reset); }
|
|
|
|
static auto timecode(int hi, int mi, int si, int framei)
|
|
{
|
|
std::vector<libremidi::message> msg;
|
|
auto h = std::to_string(hi);
|
|
while (h.size() < 3)
|
|
h.insert(h.begin(), '0');
|
|
auto m = std::to_string(mi);
|
|
while (m.size() < 2)
|
|
m.insert(m.begin(), '0');
|
|
auto s = std::to_string(si);
|
|
while (s.size() < 2)
|
|
s.insert(s.begin(), '0');
|
|
auto f = std::to_string(framei);
|
|
while (f.size() < 3)
|
|
f.insert(f.begin(), '0');
|
|
|
|
using ce = libremidi::channel_events;
|
|
msg.push_back(ce::control_change(1, 0x49, charmap_7segment(h[0])));
|
|
msg.push_back(ce::control_change(1, 0x48, charmap_7segment(h[1])));
|
|
msg.push_back(ce::control_change(1, 0x47, charmap_7segment(h[2])));
|
|
|
|
msg.push_back(ce::control_change(1, 0x46, charmap_7segment(m[0])));
|
|
msg.push_back(ce::control_change(1, 0x45, charmap_7segment(m[1])));
|
|
|
|
msg.push_back(ce::control_change(1, 0x44, charmap_7segment(s[0])));
|
|
msg.push_back(ce::control_change(1, 0x43, charmap_7segment(s[1])));
|
|
|
|
msg.push_back(ce::control_change(1, 0x42, charmap_7segment(f[0])));
|
|
msg.push_back(ce::control_change(1, 0x41, charmap_7segment(f[1])));
|
|
msg.push_back(ce::control_change(1, 0x40, charmap_7segment(f[2])));
|
|
|
|
return msg;
|
|
}
|
|
|
|
static uint8_t charmap_7segment(char c, bool dot)
|
|
{
|
|
uint8_t res = charmap_7segment(c);
|
|
if (dot)
|
|
res |= 0b00100000;
|
|
return res;
|
|
};
|
|
|
|
static uint8_t charmap_7segment(char c)
|
|
{
|
|
// FIXME there are some more characters but what to map them to ? :)
|
|
if (c >= 'a' && c <= 'z')
|
|
return c - 'a' + 1;
|
|
else if (c >= 'A' && c <= 'Z')
|
|
return c - 'A' + 1;
|
|
else if (c >= '0' && c <= '9')
|
|
return c - '0' + 0x30;
|
|
else
|
|
switch (c)
|
|
{
|
|
case '[':
|
|
case '{':
|
|
return 0x1B;
|
|
case '\\':
|
|
case '~': // yen too ?
|
|
return 0x1C;
|
|
case ']':
|
|
case '}':
|
|
return 0x1D;
|
|
case '^':
|
|
return 0x1E;
|
|
case '_':
|
|
return 0x1F;
|
|
|
|
case '!':
|
|
return 0x21;
|
|
case '"':
|
|
return 0x22;
|
|
case '#':
|
|
return 0x23;
|
|
case '$':
|
|
return 0x24;
|
|
case '%':
|
|
return 0x25;
|
|
case '&':
|
|
return 0x26;
|
|
case '\'':
|
|
return 0x27;
|
|
case '(':
|
|
return 0x28;
|
|
case ')':
|
|
return 0x29;
|
|
case '*':
|
|
return 0x2A;
|
|
case '+':
|
|
return 0x2B;
|
|
case ',':
|
|
return 0x2C;
|
|
case '-':
|
|
return 0x2D;
|
|
case '.':
|
|
return 0x2E;
|
|
case '/':
|
|
return 0x2F;
|
|
|
|
case ':':
|
|
return 0x3A;
|
|
case ';':
|
|
return 0x3B;
|
|
case '<':
|
|
return 0x3C;
|
|
case '=':
|
|
return 0x3D;
|
|
case '>':
|
|
return 0x3E;
|
|
case '?':
|
|
return 0x3F;
|
|
|
|
default:
|
|
return 0x00;
|
|
}
|
|
}
|
|
static uint8_t charmap_lcd(char c)
|
|
{
|
|
// FIXME there are some more characters but what to map them to ? :)
|
|
if (c >= 'a' && c <= 'z')
|
|
return c - 'a' + 0x61;
|
|
else if (c >= 'A' && c <= 'Z')
|
|
return c - 'A' + 0x41;
|
|
else if (c >= '0' && c <= '9')
|
|
return c - '0' + 0x30;
|
|
else
|
|
switch (c)
|
|
{
|
|
case '!':
|
|
return 0x21;
|
|
case '"':
|
|
return 0x22;
|
|
case '#':
|
|
return 0x23;
|
|
case '$':
|
|
return 0x24;
|
|
case '%':
|
|
return 0x25;
|
|
case '&':
|
|
return 0x26;
|
|
case '\'':
|
|
return 0x27;
|
|
case '(':
|
|
return 0x28;
|
|
case ')':
|
|
return 0x29;
|
|
case '*':
|
|
return 0x2A;
|
|
case '+':
|
|
return 0x2B;
|
|
case ',':
|
|
return 0x2C;
|
|
case '-':
|
|
return 0x2D;
|
|
case '.':
|
|
return 0x2E;
|
|
case '/':
|
|
return 0x2F;
|
|
|
|
case ':':
|
|
return 0x3A;
|
|
case ';':
|
|
return 0x3B;
|
|
case '<':
|
|
return 0x3C;
|
|
case '=':
|
|
return 0x3D;
|
|
case '>':
|
|
return 0x3E;
|
|
case '?':
|
|
return 0x3F;
|
|
|
|
case '@':
|
|
return 0x40;
|
|
case '[':
|
|
return 0x5B;
|
|
case '~': // Yen symbol... builtin mojibake?
|
|
return 0x5C;
|
|
case ']':
|
|
return 0x5D;
|
|
case '^':
|
|
return 0x5E;
|
|
case '_':
|
|
return 0x5F;
|
|
case '`':
|
|
return 0x60;
|
|
case '{':
|
|
return 0x7B;
|
|
case '|':
|
|
return 0x7C;
|
|
case '}':
|
|
return 0x7D;
|
|
case '\u000E':
|
|
return 0x7E;
|
|
case '\u000F':
|
|
return 0x7F;
|
|
default:
|
|
return c; // gives access to the bubble first row 0x00 > 0x0F
|
|
}
|
|
}
|
|
};
|
|
|
|
struct rcp_configuration
|
|
{
|
|
//! How to send MIDI messages to the device.
|
|
//! Note: this function *will* be called from different thread,
|
|
//! thus it has to be thread-safe, for instance
|
|
//! by storing the message in an event queue.
|
|
std::function<void(libremidi::message&&)> midi_out;
|
|
|
|
std::function<void(libremidi::remote_control_protocol::device_type)> on_connected;
|
|
std::function<void(libremidi::remote_control_protocol::mixer_command, bool)> on_command;
|
|
std::function<void(libremidi::remote_control_protocol::mixer_control, int)> on_control;
|
|
std::function<void(libremidi::remote_control_protocol::fader, uint16_t)> on_fader;
|
|
|
|
libremidi::midi_error_callback on_error{};
|
|
};
|
|
|
|
struct remote_control_processor : libremidi::error_handler
|
|
{
|
|
using rcp = libremidi::remote_control_protocol;
|
|
rcp_configuration configuration;
|
|
rcp impl;
|
|
|
|
explicit remote_control_processor(rcp_configuration conf)
|
|
: configuration{std::move(conf)}
|
|
{
|
|
assert(configuration.midi_out);
|
|
|
|
if (!configuration.on_error)
|
|
configuration.on_error = [](std::string_view s, auto&&...) {
|
|
std::fprintf(stderr, "libremidi: rcp error: %s\n", s.data());
|
|
};
|
|
|
|
if (!configuration.on_connected)
|
|
configuration.on_connected
|
|
= [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_connected"); };
|
|
|
|
if (!configuration.on_command)
|
|
configuration.on_command
|
|
= [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_command"); };
|
|
|
|
if (!configuration.on_control)
|
|
configuration.on_control
|
|
= [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_control"); };
|
|
|
|
if (!configuration.on_fader)
|
|
configuration.on_fader
|
|
= [this](auto&&...) { libremidi_handle_error(configuration, "Unhandled on_fader"); };
|
|
}
|
|
|
|
void start()
|
|
{
|
|
current_state = waiting_for_query;
|
|
configuration.midi_out(impl.device_query());
|
|
}
|
|
|
|
void on_midi(const libremidi::message& message)
|
|
{
|
|
switch (message.get_message_type())
|
|
{
|
|
case libremidi::message_type::SYSTEM_EXCLUSIVE:
|
|
if (auto N = message.size(); N >= 7)
|
|
{
|
|
const uint8_t* bytes = message.bytes.data();
|
|
|
|
// strip 0xF0 & 0xF7
|
|
bytes += 1;
|
|
N -= 2;
|
|
|
|
// Mackie manufacturer ID check
|
|
if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0x66)
|
|
{
|
|
impl.type = static_cast<rcp::device_type>(bytes[3]);
|
|
|
|
// strip header
|
|
bytes += 4;
|
|
N -= 4;
|
|
on_rcp_command(std::span(bytes, N));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
libremidi_handle_error(configuration, "Invalid sysex");
|
|
}
|
|
break;
|
|
case libremidi::message_type::NOTE_ON:
|
|
configuration.on_command(static_cast<rcp::mixer_command>(message[1]), message[2] > 0);
|
|
break;
|
|
case libremidi::message_type::NOTE_OFF:
|
|
break;
|
|
case libremidi::message_type::CONTROL_CHANGE:
|
|
configuration.on_control(static_cast<rcp::mixer_control>(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<rcp::fader>(uint8_t(message.get_channel() - 1)), value);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void on_rcp_command(std::span<const uint8_t> cmd)
|
|
{
|
|
if (cmd.empty())
|
|
{
|
|
libremidi_handle_error(configuration, "on_rcp_command: empty command");
|
|
return;
|
|
}
|
|
|
|
auto command = static_cast<rcp::command_from_device>(cmd[0]);
|
|
cmd = cmd.subspan(1);
|
|
switch (command)
|
|
{
|
|
case rcp::command_from_device::host_connection_query: {
|
|
if (cmd.size() == 11)
|
|
{
|
|
current_state = got_query;
|
|
|
|
std::array<uint8_t, 7> serial;
|
|
std::array<uint8_t, 4> challenge;
|
|
std::copy_n(cmd.data(), 7, serial.begin());
|
|
std::copy_n(cmd.data() + 7, 4, challenge.begin());
|
|
|
|
configuration.midi_out(impl.host_connection_reply(serial, challenge));
|
|
}
|
|
else
|
|
libremidi_handle_error(configuration, "host_connection_query: invalid size");
|
|
break;
|
|
}
|
|
case rcp::command_from_device::host_connection_confirmation:
|
|
current_state = connected;
|
|
configuration.on_connected(impl.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
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void update_timecode(int h, int m, int s, int f)
|
|
{
|
|
for (auto&& m : rcp::timecode(h, m, s, f))
|
|
configuration.midi_out(std::move(m));
|
|
}
|
|
|
|
void update_lcd(std::string_view v)
|
|
{
|
|
auto res = 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 command(remote_control_protocol::mixer_command c, bool press)
|
|
{
|
|
using ce = libremidi::channel_events;
|
|
configuration.midi_out(ce::note_on(1, to_underlying(c), press ? 127 : 0));
|
|
configuration.midi_out(ce::note_off(1, to_underlying(c), press ? 127 : 0));
|
|
}
|
|
|
|
void control(remote_control_protocol::mixer_control c, int value)
|
|
{
|
|
using ce = libremidi::channel_events;
|
|
configuration.midi_out(ce::control_change(1, to_underlying(c), value));
|
|
}
|
|
|
|
void fader(remote_control_protocol::fader c, uint16_t value)
|
|
{
|
|
int idx = to_underlying(c);
|
|
|
|
using ce = libremidi::channel_events;
|
|
configuration.midi_out(ce::pitch_bend(idx + 1, value));
|
|
}
|
|
|
|
// State machine
|
|
enum
|
|
{
|
|
waiting_for_query,
|
|
got_query,
|
|
connected,
|
|
errored
|
|
} current_state{waiting_for_query};
|
|
};
|
|
}
|