Created
August 22, 2020 20:25
-
-
Save liquidev/6007ff8169768bc9dd02cfe29c3a9916 to your computer and use it in GitHub Desktop.
Novation Launchpad S to keyboard/mouse controls script, for torturing yourself
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <string.h> | |
#include <alsa/asoundlib.h> | |
#include <math.h> | |
#include <unistd.h> | |
#include <xdo.h> | |
#define LOG(...) do { \ | |
fprintf(stderr, __VA_ARGS__); \ | |
fflush(stderr); \ | |
} while (0) | |
#define LEN(x) (sizeof((x)) / sizeof((x)[0])) | |
enum action_type | |
{ | |
action_key, | |
action_mouse_move, | |
action_mouse_click | |
}; | |
enum mouse_button | |
{ | |
mouse_button_left = 1, | |
mouse_button_middle = 2, | |
mouse_button_right = 3, | |
mouse_wheel_up = 4, | |
mouse_wheel_down = 5, | |
}; | |
enum direction | |
{ | |
direction_right, | |
direction_down, | |
direction_left, | |
direction_up, | |
direction_len, | |
}; | |
struct color | |
{ | |
unsigned char r, g; | |
}; | |
struct action | |
{ | |
int note; | |
enum action_type type; | |
union { | |
const char *key; | |
enum direction dir; | |
enum mouse_button click; | |
} data; | |
struct color normal, pressed; | |
}; | |
const struct action action_list[] = { | |
// W, S, A, D, Ctrl, Space | |
{ 50, action_key, { .key = "w" }, {1, 0}, {3, 0} }, | |
{ 66, action_key, { .key = "s" }, {1, 0}, {3, 0} }, | |
{ 65, action_key, { .key = "a" }, {1, 0}, {3, 0} }, | |
{ 67, action_key, { .key = "d" }, {1, 0}, {3, 0} }, | |
{ 51, action_key, { .key = "e" }, {1, 0}, {3, 0} }, | |
{ 80, action_key, { .key = "Control_L" }, {1, 0}, {3, 0} }, | |
{ 116, action_key, { .key = "space" }, {1, 0}, {3, 0} }, | |
{ 32, action_key, { .key = "Escape" }, {1, 0}, {3, 0} }, | |
// mouse up, down, left, right | |
{ 54, action_mouse_move, { .dir = direction_up }, {0, 1}, {0, 3} }, | |
{ 70, action_mouse_move, { .dir = direction_down }, {0, 1}, {0, 3} }, | |
{ 69, action_mouse_move, { .dir = direction_left }, {0, 1}, {0, 3} }, | |
{ 71, action_mouse_move, { .dir = direction_right }, {0, 1}, {0, 3} }, | |
// mouse click left, right, middle | |
{ 53, action_mouse_click, { .click = mouse_button_left }, {2, 1}, {3, 2} }, | |
{ 55, action_mouse_click, { .click = mouse_button_right }, {2, 1}, {3, 2} }, | |
{ 38, action_mouse_click, { .click = mouse_button_middle }, {2, 1}, {3, 2} }, | |
{ 56, action_mouse_click, { .click = mouse_wheel_up }, {1, 2}, {1, 3} }, | |
{ 72, action_mouse_click, { .click = mouse_wheel_down }, {1, 2}, {1, 3} }, | |
}; | |
struct action_table { | |
const struct action *key[128]; | |
const struct action *mouse_move[128]; | |
const struct action *mouse_click[128]; | |
}; | |
unsigned char color_to_velocity(struct color c) | |
{ | |
return (c.r & 0x3) | (c.g & 0x3) << 4; | |
} | |
int get_launchpad_client_id(snd_seq_t *seq_handle) | |
{ | |
snd_seq_client_info_t *client_info; | |
int error; | |
int own_id = snd_seq_client_id(seq_handle); | |
int launchpad_id = -1; | |
snd_seq_client_info_malloc(&client_info); | |
snd_seq_client_info_set_client(client_info, -1); | |
while ((error = snd_seq_query_next_client(seq_handle, client_info)) >= 0) { | |
int id = snd_seq_client_info_get_client(client_info); | |
const char *name = snd_seq_client_info_get_name(client_info); | |
LOG("client %i: %s", id, name); | |
if (id == own_id) { | |
LOG(" -- that's me!"); | |
} | |
if (strcmp(name, "Launchpad S") == 0) { | |
LOG(" -- that's the launchpad\n"); | |
return id; | |
} | |
LOG("\n"); | |
} | |
snd_seq_client_info_free(client_info); | |
return -1; | |
} | |
int get_launchpad_port_id(snd_seq_t *seq_handle, int launchpad_id) | |
{ | |
snd_seq_port_info_t *port_info; | |
int error; | |
snd_seq_port_info_malloc(&port_info); | |
snd_seq_port_info_set_client(port_info, launchpad_id); | |
snd_seq_port_info_set_port(port_info, -1); | |
while ((error = snd_seq_query_next_port(seq_handle, port_info)) >= 0) { | |
int id = snd_seq_port_info_get_port(port_info); | |
const char *name = snd_seq_port_info_get_name(port_info); | |
LOG("port %i: %s", id, name); | |
if (strcmp(name, "Launchpad S MIDI 1") == 0) { | |
LOG(" -- that's the port\n"); | |
return id; | |
} | |
LOG("\n"); | |
} | |
snd_seq_port_info_free(port_info); | |
return -1; | |
} | |
void setup_launchpad_io( | |
snd_seq_t *seq_handle, | |
snd_seq_addr_t *lp_address, | |
int input_port, int output_port, | |
int input_event_queue, int output_event_queue | |
) | |
{ | |
snd_seq_addr_t seq_input = { | |
.client = snd_seq_client_id(seq_handle), | |
.port = input_port, | |
}; | |
snd_seq_addr_t seq_output = { | |
.client = snd_seq_client_id(seq_handle), | |
.port = output_port, | |
}; | |
snd_seq_port_subscribe_t *sub_i, *sub_o; | |
// input | |
LOG( | |
"routing input from launchpad - %i:%i -> %i:%i\n", | |
lp_address->client, lp_address->port, | |
seq_input.client, seq_input.port | |
); | |
snd_seq_port_subscribe_malloc(&sub_i); | |
snd_seq_port_subscribe_set_sender(sub_i, lp_address); | |
snd_seq_port_subscribe_set_dest(sub_i, &seq_input); | |
snd_seq_port_subscribe_set_queue(sub_i, input_event_queue); | |
snd_seq_port_subscribe_set_time_update(sub_i, 1); | |
snd_seq_port_subscribe_set_time_real(sub_i, 1); | |
snd_seq_subscribe_port(seq_handle, sub_i); | |
snd_seq_port_subscribe_free(sub_i); | |
// output | |
LOG( | |
"routing output to launchpad - %i:%i -> %i:%i\n", | |
seq_output.client, seq_output.port, | |
lp_address->client, lp_address->port | |
); | |
snd_seq_port_subscribe_malloc(&sub_o); | |
snd_seq_port_subscribe_set_sender(sub_o, &seq_output); | |
snd_seq_port_subscribe_set_dest(sub_o, lp_address); | |
snd_seq_port_subscribe_set_queue(sub_o, output_event_queue); | |
snd_seq_subscribe_port(seq_handle, sub_o); | |
snd_seq_port_subscribe_free(sub_o); | |
} | |
void load_actions( | |
const struct action action_list[], int n_actions, | |
struct action_table *out_actions | |
) | |
{ | |
memset(out_actions, 0, sizeof(*out_actions)); | |
for (int i = 0; i < n_actions; ++i) { | |
const struct action *action = &action_list[i]; | |
switch (action->type) { | |
case action_key: | |
out_actions->key[action->note] = action; | |
break; | |
case action_mouse_move: | |
out_actions->mouse_move[action->note] = action; | |
break; | |
case action_mouse_click: | |
out_actions->mouse_click[action->note] = action; | |
break; | |
} | |
} | |
} | |
void initial_lights( | |
const struct action action_list[], int n_actions, | |
snd_seq_t *seq_handle, int output_port | |
) | |
{ | |
snd_seq_event_t event; | |
snd_seq_ev_set_source(&event, output_port); | |
snd_seq_ev_set_subs(&event); | |
snd_seq_ev_set_direct(&event); | |
// reset | |
snd_seq_ev_set_controller(&event, 0, 0, 0); | |
snd_seq_event_output(seq_handle, &event); | |
for (int i = 0; i < n_actions; ++i) { | |
const struct action *action = &action_list[i]; | |
snd_seq_ev_set_noteon( | |
&event, | |
/* channel */ 0, | |
action->note, | |
color_to_velocity(action->normal) | |
); | |
snd_seq_event_output(seq_handle, &event); | |
} | |
snd_seq_drain_output(seq_handle); | |
} | |
void process_event( | |
struct action_table *actions, snd_seq_event_t *in_event, | |
snd_seq_t *seq_handle, int output_port, | |
xdo_t *xdo, char mouse_moves[] | |
) | |
{ | |
if (in_event->type & SND_SEQ_EVENT_NOTEON) { | |
snd_seq_event_t out_event; | |
snd_seq_ev_note_t *note_event = &in_event->data.note; | |
int note = note_event->note; | |
int velocity = note_event->velocity; | |
char down = velocity > 0; | |
const struct action *action; | |
if (note_event->channel != 0) return; | |
LOG("-- N%i V%i\n", note, velocity); | |
if ((action = actions->key[note]) != 0) { | |
const char *key = action->data.key; | |
LOG("key action\n"); | |
if (down) { | |
xdo_send_keysequence_window_down(xdo, CURRENTWINDOW, key, 0); | |
} else { | |
xdo_send_keysequence_window_up(xdo, CURRENTWINDOW, key, 0); | |
} | |
} else if ((action = actions->mouse_move[note]) != 0) { | |
LOG("mouse move action\n"); | |
mouse_moves[action->data.dir] = down; | |
} else if ((action = actions->mouse_click[note]) != 0) { | |
int mouse_button = action->data.click; | |
LOG("mouse click action\n"); | |
if (mouse_button == mouse_wheel_up || mouse_button == mouse_wheel_down) { | |
xdo_click_window(xdo, CURRENTWINDOW, mouse_button); | |
} else { | |
if (down) { | |
xdo_mouse_down(xdo, CURRENTWINDOW, mouse_button); | |
} else { | |
xdo_mouse_up(xdo, CURRENTWINDOW, mouse_button); | |
} | |
} | |
} else { | |
LOG("no action\n"); | |
return; | |
} | |
snd_seq_ev_set_source(&out_event, output_port); | |
snd_seq_ev_set_subs(&out_event); | |
snd_seq_ev_set_direct(&out_event); | |
unsigned char color = | |
color_to_velocity(down ? action->pressed : action->normal); | |
snd_seq_ev_set_noteon(&out_event, 0, note, color); | |
snd_seq_event_output(seq_handle, &out_event); | |
snd_seq_drain_output(seq_handle); | |
LOG("- A* %p\n", action); | |
} | |
} | |
int main(void) | |
{ | |
snd_seq_t *seq_handle; | |
int error; | |
int input_port, output_port; | |
int input_event_queue, output_event_queue; | |
snd_seq_addr_t lp_address; | |
struct action_table actions; | |
xdo_t *xdo = xdo_new(0); | |
char mouse_moves[direction_len]; | |
float mouse_move_speed = 0.0; | |
load_actions(action_list, LEN(action_list), &actions); | |
memset(mouse_moves, 0, sizeof(mouse_moves)); | |
LOG("opening sequencer\n"); | |
error = snd_seq_open( | |
&seq_handle, "default", | |
SND_SEQ_OPEN_DUPLEX, | |
SND_SEQ_NONBLOCK | |
); | |
if (error < 0) { | |
return 1; | |
} | |
LOG("setting client name\n"); | |
if (snd_seq_set_client_name(seq_handle, "ALSA MIDI testing")) { | |
return 2; | |
} | |
lp_address.client = get_launchpad_client_id(seq_handle); | |
lp_address.port = get_launchpad_port_id(seq_handle, lp_address.client); | |
LOG("launchpad address - %i:%i\n", lp_address.client, lp_address.port); | |
LOG("creating IO ports\n"); | |
input_port = snd_seq_create_simple_port( | |
seq_handle, "launchpad_input", | |
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, | |
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION | |
); | |
if (input_port < 0) { | |
return 3; | |
} | |
output_port = snd_seq_create_simple_port( | |
seq_handle, "launchpad_output", | |
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, | |
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION | |
); | |
if (output_port < 0) { | |
return 3; | |
} | |
LOG("creating event queues\n"); | |
input_event_queue = snd_seq_alloc_named_queue(seq_handle, "input_events"); | |
if (input_event_queue < 0) { | |
return 4; | |
} | |
output_event_queue = snd_seq_alloc_named_queue(seq_handle, "output_events"); | |
if (output_event_queue < 0) { | |
return 4; | |
} | |
setup_launchpad_io( | |
seq_handle, &lp_address, | |
input_port, output_port, | |
input_event_queue, output_event_queue | |
); | |
initial_lights(action_list, LEN(action_list), seq_handle, output_port); | |
LOG("listening for events\n"); | |
for (;;) { | |
snd_seq_event_t *in_event = 0; | |
if (snd_seq_event_input(seq_handle, &in_event) >= 0) { | |
process_event( | |
&actions, | |
in_event, | |
seq_handle, output_port, | |
xdo, mouse_moves | |
); | |
} | |
char mouse_moving = 0; | |
for (int i = 0; i < direction_len; ++i) { | |
if (mouse_moves[i]) { | |
mouse_moving = 1; | |
break; | |
} | |
} | |
mouse_move_speed = mouse_moving ? fmax(3.0, mouse_move_speed + 0.1) : 0.0; | |
if (mouse_moves[direction_right]) { | |
xdo_move_mouse_relative(xdo, mouse_move_speed, 0); | |
} | |
if (mouse_moves[direction_down]) { | |
xdo_move_mouse_relative(xdo, 0, mouse_move_speed); | |
} | |
if (mouse_moves[direction_left]) { | |
xdo_move_mouse_relative(xdo, -mouse_move_speed, 0); | |
} | |
if (mouse_moves[direction_up]) { | |
xdo_move_mouse_relative(xdo, 0, -mouse_move_speed); | |
} | |
usleep(16666); | |
} | |
xdo_free(xdo); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
compile with: