From a6f3a2275ad116e6ab338e583ab8ef1b1141b468 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Wed, 9 Jan 2019 23:54:13 +0100 Subject: [PATCH] Add basic MIDI input support (jog, cue in/out, play, camera switch) to Futatabi. No editor yet. --- futatabi/flags.cpp | 8 +- futatabi/flags.h | 1 + futatabi/futatabi_midi_mapping.proto | 55 ++++++ futatabi/mainwindow.cpp | 242 ++++++++++++++++++++------- futatabi/mainwindow.h | 31 +++- futatabi/midi_mapper.cpp | 236 ++++++++++++++++++++++++++ futatabi/midi_mapper.h | 109 ++++++++++++ meson.build | 6 +- 8 files changed, 620 insertions(+), 68 deletions(-) create mode 100644 futatabi/futatabi_midi_mapping.proto create mode 100644 futatabi/midi_mapper.cpp create mode 100644 futatabi/midi_mapper.h diff --git a/futatabi/flags.cpp b/futatabi/flags.cpp index 2115098..c5bc722 100644 --- a/futatabi/flags.cpp +++ b/futatabi/flags.cpp @@ -17,7 +17,8 @@ enum LongOption { OPTION_SLOW_DOWN_INPUT = 1001, OPTION_HTTP_PORT = 1002, OPTION_TALLY_URL = 1003, - OPTION_CUE_POINT_PADDING = 1004 + OPTION_CUE_POINT_PADDING = 1004, + OPTION_MIDI_MAPPING = 1005 }; void usage() @@ -40,6 +41,7 @@ void usage() fprintf(stderr, " -d, --working-directory DIR where to store frames and database\n"); fprintf(stderr, " --http-port PORT which port to listen on for output\n"); fprintf(stderr, " --tally-url URL URL to get tally color from (polled every 100 ms)\n"); + fprintf(stderr, " --midi-mapping=FILE start with the given MIDI controller mapping\n"); } void parse_flags(int argc, char *const argv[]) @@ -55,6 +57,7 @@ void parse_flags(int argc, char *const argv[]) { "http-port", required_argument, 0, OPTION_HTTP_PORT }, { "tally-url", required_argument, 0, OPTION_TALLY_URL }, { "cue-point-padding", required_argument, 0, OPTION_CUE_POINT_PADDING }, + { "midi-mapping", required_argument, 0, OPTION_MIDI_MAPPING }, { 0, 0, 0, 0 } }; for (;;) { @@ -103,6 +106,9 @@ void parse_flags(int argc, char *const argv[]) global_flags.cue_point_padding_seconds = atof(optarg); global_flags.cue_point_padding_set = true; break; + case OPTION_MIDI_MAPPING: + global_flags.midi_mapping_filename = optarg; + break; case OPTION_HELP: usage(); exit(0); diff --git a/futatabi/flags.h b/futatabi/flags.h index 1e0284b..4252235 100644 --- a/futatabi/flags.h +++ b/futatabi/flags.h @@ -17,6 +17,7 @@ struct Flags { std::string tally_url; double cue_point_padding_seconds = 0.0; // Can be changed in the menus. bool cue_point_padding_set = false; + std::string midi_mapping_filename; // Empty for none. }; extern Flags global_flags; diff --git a/futatabi/futatabi_midi_mapping.proto b/futatabi/futatabi_midi_mapping.proto new file mode 100644 index 0000000..d51b40e --- /dev/null +++ b/futatabi/futatabi_midi_mapping.proto @@ -0,0 +1,55 @@ +syntax = "proto2"; + +import "midi_mapping.proto"; + +message CameraMIDIMappingProto { + optional MIDIButtonProto button = 1; + optional int32 bank = 2; + optional MIDILightProto is_current = 3; +} + +message MIDIMappingProto { + optional int32 num_controller_banks = 1 [default = 0]; // Max 5. + + // Bank switching. + optional MIDIButtonProto prev_bank = 2; + optional MIDIButtonProto next_bank = 3; + optional MIDIButtonProto select_bank_1 = 4; + optional MIDIButtonProto select_bank_2 = 5; + optional MIDIButtonProto select_bank_3 = 6; + optional MIDIButtonProto select_bank_4 = 7; + optional MIDIButtonProto select_bank_5 = 8; + optional MIDILightProto bank_1_is_selected = 9; + optional MIDILightProto bank_2_is_selected = 10; + optional MIDILightProto bank_3_is_selected = 11; + optional MIDILightProto bank_4_is_selected = 12; + optional MIDILightProto bank_5_is_selected = 13; + + // Controllers. + optional MIDIControllerProto jog = 16; + optional int32 jog_bank = 17; + + // Buttons. + optional MIDIButtonProto preview = 18; + optional int32 preview_bank = 19; + optional MIDILightProto preview_enabled = 20; + + optional MIDIButtonProto queue = 21; + optional int32 queue_bank = 22; + optional MIDILightProto queue_enabled = 23; + + optional MIDIButtonProto play = 24; + optional int32 play_bank = 25; + optional MIDILightProto play_enabled = 26; + + optional MIDIButtonProto cue_in = 27; + optional int32 cue_in_bank = 28; + optional MIDILightProto cue_in_enabled = 29; // In practice always true currently. + + optional MIDIButtonProto cue_out = 30; + optional int32 cue_out_bank = 31; + optional MIDILightProto cue_out_enabled = 32; // In practice always true currently. + + // Camera buttons. + repeated CameraMIDIMappingProto camera = 33; +} diff --git a/futatabi/mainwindow.cpp b/futatabi/mainwindow.cpp index b39d8d4..350b1b2 100644 --- a/futatabi/mainwindow.cpp +++ b/futatabi/mainwindow.cpp @@ -5,6 +5,7 @@ #include "flags.h" #include "frame_on_disk.h" #include "player.h" +#include "futatabi_midi_mapping.pb.h" #include "shared/aboutdialog.h" #include "shared/disk_space_estimator.h" #include "shared/post_to_main_thread.h" @@ -33,9 +34,25 @@ static PlayList *playlist_clips; extern int64_t current_pts; +namespace { + +void set_pts_in(int64_t pts, int64_t current_pts, ClipProxy &clip) +{ + pts = std::max(pts, 0); + if (clip->pts_out == -1) { + pts = std::min(pts, current_pts); + } else { + pts = std::min(pts, clip->pts_out); + } + clip->pts_in = pts; +} + +} // namespace + MainWindow::MainWindow() : ui(new Ui::MainWindow), - db(global_flags.working_directory + "/futatabi.db") + db(global_flags.working_directory + "/futatabi.db"), + midi_mapper(this) { global_mainwindow = this; ui->setupUi(this); @@ -195,6 +212,7 @@ MainWindow::MainWindow() }); }); set_output_status("paused"); + enable_or_disable_queue_button(); defer_timeout = new QTimer(this); defer_timeout->setSingleShot(true); @@ -221,6 +239,18 @@ MainWindow::MainWindow() if (!global_flags.tally_url.empty()) { start_tally(); } + + if (!global_flags.midi_mapping_filename.empty()) { + MIDIMappingProto midi_mapping; + if (!load_midi_mapping_from_file(global_flags.midi_mapping_filename, &midi_mapping)) { + fprintf(stderr, "Couldn't load MIDI mapping '%s'; exiting.\n", + global_flags.midi_mapping_filename.c_str()); + exit(1); + } + midi_mapper.set_midi_mapping(midi_mapping); + } + midi_mapper.refresh_lights(); + midi_mapper.start_thread(); } void MainWindow::change_num_cameras() @@ -443,6 +473,64 @@ void MainWindow::playlist_move(int delta) playlist_selection_changed(); } +void MainWindow::jog_internal(JogDestination jog_destination, int row, int column, int stream_idx, int pts_delta) +{ + constexpr int camera_pts_per_pixel = 1500; // One click of most mice (15 degrees), multiplied by the default wheel_sensitivity. + + int in_column, out_column, camera_column; + if (jog_destination == JOG_CLIP_LIST) { + in_column = int(ClipList::Column::IN); + out_column = int(ClipList::Column::OUT); + camera_column = -1; + } else if (jog_destination == JOG_PLAYLIST) { + in_column = int(PlayList::Column::IN); + out_column = int(PlayList::Column::OUT); + camera_column = int(PlayList::Column::CAMERA); + } else { + assert(false); + } + + currently_deferring_model_changes = true; + { + current_change_id = (jog_destination == JOG_CLIP_LIST) ? "cliplist:" : "playlist:"; + ClipProxy clip = (jog_destination == JOG_CLIP_LIST) ? cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row); + if (jog_destination == JOG_PLAYLIST) { + stream_idx = clip->stream_idx; + } + + if (column == in_column) { + current_change_id += "in:" + to_string(row); + int64_t pts = clip->pts_in + pts_delta; + set_pts_in(pts, current_pts, clip); + preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); + } else if (column == out_column) { + current_change_id += "out:" + to_string(row); + int64_t pts = clip->pts_out + pts_delta; + pts = std::max(pts, clip->pts_in); + pts = std::min(pts, current_pts); + clip->pts_out = pts; + preview_single_frame(pts, stream_idx, LAST_BEFORE); + } else if (column == camera_column) { + current_change_id += "camera:" + to_string(row); + int angle_degrees = pts_delta; + if (last_mousewheel_camera_row == row) { + angle_degrees += leftover_angle_degrees; + } + + int stream_idx = clip->stream_idx + angle_degrees / camera_pts_per_pixel; + stream_idx = std::max(stream_idx, 0); + stream_idx = std::min(stream_idx, num_cameras - 1); + clip->stream_idx = stream_idx; + + last_mousewheel_camera_row = row; + leftover_angle_degrees = angle_degrees % camera_pts_per_pixel; + + // Don't update the live view, that's rarely what the operator wants. + } + } + currently_deferring_model_changes = false; +} + void MainWindow::defer_timer_expired() { state_changed(deferred_state); @@ -564,26 +652,15 @@ void MainWindow::relayout() ui->preview_display->setMinimumWidth(ui->preview_display->height() * 16 / 9); } -void set_pts_in(int64_t pts, int64_t current_pts, ClipProxy &clip) -{ - pts = std::max(pts, 0); - if (clip->pts_out == -1) { - pts = std::min(pts, current_pts); - } else { - pts = std::min(pts, clip->pts_out); - } - clip->pts_in = pts; -} - bool MainWindow::eventFilter(QObject *watched, QEvent *event) { constexpr int dead_zone_pixels = 3; // To avoid that simple clicks get misinterpreted. - constexpr int camera_degrees_per_pixel = 15; // One click of most mice. int scrub_sensitivity = 100; // pts units per pixel. int wheel_sensitivity = 100; // pts units per degree. if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) { enable_or_disable_preview_button(); + hidden_jog_column = -1; } unsigned stream_idx = ui->preview_display->get_stream_idx(); @@ -714,22 +791,22 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) } QTableView *destination; - int in_column, out_column, camera_column; + JogDestination jog_destination; if (watched == ui->clip_list->viewport()) { destination = ui->clip_list; - in_column = int(ClipList::Column::IN); - out_column = int(ClipList::Column::OUT); - camera_column = -1; + jog_destination = JOG_CLIP_LIST; last_mousewheel_camera_row = -1; } else if (watched == ui->playlist->viewport()) { destination = ui->playlist; - in_column = int(PlayList::Column::IN); - out_column = int(PlayList::Column::OUT); - camera_column = int(PlayList::Column::CAMERA); + jog_destination = JOG_PLAYLIST; + if (destination->columnAt(wheel->x()) != int(PlayList::Column::CAMERA)) { + last_mousewheel_camera_row = -1; + } } else { last_mousewheel_camera_row = -1; return false; } + int column = destination->columnAt(wheel->x()); int row = destination->rowAt(wheel->y()); if (column == -1 || row == -1) @@ -741,48 +818,7 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) return false; } - currently_deferring_model_changes = true; - { - current_change_id = (watched == ui->clip_list->viewport()) ? "cliplist:" : "playlist:"; - ClipProxy clip = (watched == ui->clip_list->viewport()) ? cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row); - if (watched == ui->playlist->viewport()) { - stream_idx = clip->stream_idx; - } - - if (column != camera_column) { - last_mousewheel_camera_row = -1; - } - if (column == in_column) { - current_change_id += "in:" + to_string(row); - int64_t pts = clip->pts_in + angle_delta * wheel_sensitivity; - set_pts_in(pts, current_pts, clip); - preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER); - } else if (column == out_column) { - current_change_id += "out:" + to_string(row); - int64_t pts = clip->pts_out + angle_delta * wheel_sensitivity; - pts = std::max(pts, clip->pts_in); - pts = std::min(pts, current_pts); - clip->pts_out = pts; - preview_single_frame(pts, stream_idx, LAST_BEFORE); - } else if (column == camera_column) { - current_change_id += "camera:" + to_string(row); - int angle_degrees = angle_delta; - if (last_mousewheel_camera_row == row) { - angle_degrees += leftover_angle_degrees; - } - - int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel; - stream_idx = std::max(stream_idx, 0); - stream_idx = std::min(stream_idx, num_cameras - 1); - clip->stream_idx = stream_idx; - - last_mousewheel_camera_row = row; - leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel; - - // Don't update the live view, that's rarely what the operator wants. - } - } - currently_deferring_model_changes = false; + jog_internal(jog_destination, row, column, stream_idx, angle_delta * wheel_sensitivity); return true; // Don't scroll. } else if (event->type() == QEvent::MouseButtonRelease) { scrubbing = false; @@ -830,7 +866,9 @@ void MainWindow::playlist_selection_changed() any_selected && selected->selectedRows().front().row() > 0); ui->playlist_move_down_btn->setEnabled( any_selected && selected->selectedRows().back().row() < int(playlist_clips->size()) - 1); + ui->play_btn->setEnabled(!playlist_clips->empty()); + midi_mapper.set_play_enabled(!playlist_clips->empty()); if (!any_selected) { set_output_status("paused"); @@ -844,11 +882,20 @@ void MainWindow::playlist_selection_changed() } } -void MainWindow::clip_list_selection_changed(const QModelIndex ¤t, const QModelIndex &) +void MainWindow::clip_list_selection_changed(const QModelIndex ¤t, const QModelIndex &previous) { int camera_selected = -1; if (cliplist_clips->is_camera_column(current.column())) { camera_selected = current.column() - int(ClipList::Column::CAMERA_1); + + // See the comment on hidden_jog_column. + if (current.row() != previous.row()) { + hidden_jog_column = -1; + } else if (hidden_jog_column == -1) { + hidden_jog_column = previous.column(); + } + } else { + hidden_jog_column = -1; } highlight_camera_input(camera_selected); enable_or_disable_queue_button(); @@ -1043,6 +1090,7 @@ void MainWindow::highlight_camera_input(int stream_idx) displays[i].frame->setStyleSheet(""); } } + midi_mapper.highlight_camera_input(stream_idx); } void MainWindow::enable_or_disable_preview_button() @@ -1055,12 +1103,14 @@ void MainWindow::enable_or_disable_preview_button() QItemSelectionModel *selected = ui->playlist->selectionModel(); if (selected->hasSelection()) { ui->preview_btn->setEnabled(true); + midi_mapper.set_preview_enabled(true); return; } } // TODO: Perhaps only enable this if something is actually selected. ui->preview_btn->setEnabled(!cliplist_clips->empty()); + midi_mapper.set_preview_enabled(!cliplist_clips->empty()); } void MainWindow::enable_or_disable_queue_button() @@ -1085,6 +1135,7 @@ void MainWindow::enable_or_disable_queue_button() } ui->queue_btn->setEnabled(enabled); + midi_mapper.set_queue_enabled(enabled); } void MainWindow::set_output_status(const string &status) @@ -1119,6 +1170,71 @@ void MainWindow::display_frame(unsigned stream_idx, const FrameOnDisk &frame) displays[stream_idx].display->setFrame(stream_idx, frame); } +void MainWindow::preview() +{ + post_to_main_thread([this] { + preview_clicked(); + }); +} + +void MainWindow::queue() +{ + post_to_main_thread([this] { + queue_clicked(); + }); +} + +void MainWindow::play() +{ + post_to_main_thread([this] { + play_clicked(); + }); +} + +void MainWindow::jog(int delta) +{ + post_to_main_thread([this, delta] { + int64_t pts_delta = delta * (TIMEBASE / 60); // One click = frame at 60 fps. + if (ui->playlist->hasFocus()) { + QModelIndex selected = ui->playlist->selectionModel()->currentIndex(); + if (selected.column() != -1 && selected.row() != -1) { + jog_internal(JOG_PLAYLIST, selected.row(), selected.column(), /*stream_idx=*/-1, pts_delta); + } + } else if (ui->clip_list->hasFocus()) { + QModelIndex selected = ui->clip_list->selectionModel()->currentIndex(); + if (cliplist_clips->is_camera_column(selected.column()) && + hidden_jog_column != -1) { + // See the definition on hidden_jog_column. + selected = selected.sibling(selected.row(), hidden_jog_column); + ui->clip_list->selectionModel()->setCurrentIndex(selected, QItemSelectionModel::ClearAndSelect); + hidden_jog_column = -1; + } + if (selected.column() != -1 && selected.row() != -1) { + jog_internal(JOG_CLIP_LIST, selected.row(), selected.column(), ui->preview_display->get_stream_idx(), pts_delta); + } + } + }); +} + +void MainWindow::switch_camera(unsigned camera_idx) +{ + post_to_main_thread([this, camera_idx] { + if (camera_idx < num_cameras) { // TODO: Also make this change a highlighted clip? + preview_angle_clicked(camera_idx); + } + }); +} + +void MainWindow::cue_in() +{ + post_to_main_thread([this] { cue_in_clicked(); }); +} + +void MainWindow::cue_out() +{ + post_to_main_thread([this] { cue_out_clicked(); }); +} + template void MainWindow::replace_model(QTableView *view, Model **model, Model *new_model) { diff --git a/futatabi/mainwindow.h b/futatabi/mainwindow.h index 3628bf4..43c9e78 100644 --- a/futatabi/mainwindow.h +++ b/futatabi/mainwindow.h @@ -3,6 +3,7 @@ #include "clip_list.h" #include "db.h" +#include "midi_mapper.h" #include "state.pb.h" #include @@ -26,7 +27,7 @@ class Player; class QPushButton; class QTableView; -class MainWindow : public QMainWindow { +class MainWindow : public QMainWindow, public ControllerReceiver { Q_OBJECT public: @@ -38,6 +39,19 @@ public: void display_frame(unsigned stream_idx, const FrameOnDisk &frame); + // ControllerReceiver interface. + void preview() override; + void queue() override; + void play() override; + void jog(int delta) override; + void switch_camera(unsigned camera_idx) override; + void cue_in() override; + void cue_out() override; + + // Raw receivers are not used. + void controller_changed(unsigned controller) override {} + void note_on(unsigned note) override {} + private: Ui::MainWindow *ui; @@ -61,6 +75,16 @@ private: int last_mousewheel_camera_row = -1; int leftover_angle_degrees = 0; + // Normally, jog is only allowed if in the focus (well, selection) is + // on the in or out pts columns. However, changing camera (even when + // using a MIDI button) on the clip list changes the highlight, + // and we'd like to keep on jogging. Thus, as a special case, if you + // change to a camera column on the clip list (and don't change which + // clip you're looking at), the last column you were at will be stored here. + // If you then try to jog, we'll fetch the value from here and highlight it. + // Doing pretty much anything else is going to reset it back to -1, though. + int hidden_jog_column = -1; + // Some operations, notably scrubbing and scrolling, happen in so large increments // that we want to group them instead of saving to disk every single time. // If they happen (ie., we get a callback from the model that it's changed) while @@ -94,6 +118,8 @@ private: QNetworkAccessManager http; QNetworkReply *http_reply = nullptr; + MIDIMapper midi_mapper; + void change_num_cameras(); void cue_in_clicked(); void cue_out_clicked(); @@ -109,6 +135,9 @@ private: void playlist_remove(); void playlist_move(int delta); + enum JogDestination { JOG_CLIP_LIST, JOG_PLAYLIST }; + void jog_internal(JogDestination jog_destination, int column, int row, int stream_idx, int pts_delta); + void defer_timer_expired(); void content_changed(); // In clip_list or play_list. void state_changed(const StateProto &state); // Called post-filtering. diff --git a/futatabi/midi_mapper.cpp b/futatabi/midi_mapper.cpp new file mode 100644 index 0000000..a76e36c --- /dev/null +++ b/futatabi/midi_mapper.cpp @@ -0,0 +1,236 @@ +#include "midi_mapper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" +#include "futatabi_midi_mapping.pb.h" +#include "shared/midi_mapper_util.h" +#include "shared/text_proto.h" + +using namespace google::protobuf; +using namespace std; +using namespace std::placeholders; + +MIDIMapper::MIDIMapper(ControllerReceiver *receiver) + : receiver(receiver), mapping_proto(new MIDIMappingProto), midi_device(this) +{ +} + +MIDIMapper::~MIDIMapper() {} + +bool load_midi_mapping_from_file(const string &filename, MIDIMappingProto *new_mapping) +{ + return load_proto_from_file(filename, new_mapping); +} + +bool save_midi_mapping_to_file(const MIDIMappingProto &mapping_proto, const string &filename) +{ + return save_proto_to_file(mapping_proto, filename); +} + +void MIDIMapper::set_midi_mapping(const MIDIMappingProto &new_mapping) +{ + lock_guard lock(mu); + if (mapping_proto) { + mapping_proto->CopyFrom(new_mapping); + } else { + mapping_proto.reset(new MIDIMappingProto(new_mapping)); + } + + num_controller_banks = min(max(mapping_proto->num_controller_banks(), 1), 5); + current_controller_bank = 0; +} + +void MIDIMapper::start_thread() +{ + midi_device.start_thread(); +} + +const MIDIMappingProto &MIDIMapper::get_current_mapping() const +{ + lock_guard lock(mu); + return *mapping_proto; +} + +ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver) +{ + lock_guard lock(mu); + swap(receiver, new_receiver); + return new_receiver; // Now old receiver. +} + +void MIDIMapper::controller_received(int controller, int value_int) +{ + int delta_value = value_int - 64; // For infinite controllers such as jog. + + receiver->controller_changed(controller); + + match_controller(controller, MIDIMappingProto::kJogFieldNumber, MIDIMappingProto::kJogBankFieldNumber, + delta_value, bind(&ControllerReceiver::jog, receiver, _1)); +} + +void MIDIMapper::note_on_received(int note) +{ + lock_guard lock(mu); + receiver->note_on(note); + + if (mapping_proto->has_prev_bank() && + mapping_proto->prev_bank().note_number() == note) { + current_controller_bank = (current_controller_bank + num_controller_banks - 1) % num_controller_banks; + update_lights_lock_held(); + } + if (mapping_proto->has_next_bank() && + mapping_proto->next_bank().note_number() == note) { + current_controller_bank = (current_controller_bank + 1) % num_controller_banks; + update_lights_lock_held(); + } + if (mapping_proto->has_select_bank_1() && + mapping_proto->select_bank_1().note_number() == note) { + current_controller_bank = 0; + update_lights_lock_held(); + } + if (mapping_proto->has_select_bank_2() && + mapping_proto->select_bank_2().note_number() == note && + num_controller_banks >= 2) { + current_controller_bank = 1; + update_lights_lock_held(); + } + if (mapping_proto->has_select_bank_3() && + mapping_proto->select_bank_3().note_number() == note && + num_controller_banks >= 3) { + current_controller_bank = 2; + update_lights_lock_held(); + } + if (mapping_proto->has_select_bank_4() && + mapping_proto->select_bank_4().note_number() == note && + num_controller_banks >= 4) { + current_controller_bank = 3; + update_lights_lock_held(); + } + if (mapping_proto->has_select_bank_5() && + mapping_proto->select_bank_5().note_number() == note && + num_controller_banks >= 5) { + current_controller_bank = 4; + update_lights_lock_held(); + } + + match_button(note, MIDIMappingProto::kPreviewFieldNumber, MIDIMappingProto::kPreviewBankFieldNumber, + bind(&ControllerReceiver::preview, receiver)); + match_button(note, MIDIMappingProto::kQueueFieldNumber, MIDIMappingProto::kQueueBankFieldNumber, + bind(&ControllerReceiver::queue, receiver)); + match_button(note, MIDIMappingProto::kPlayFieldNumber, MIDIMappingProto::kPlayBankFieldNumber, + bind(&ControllerReceiver::play, receiver)); + + unsigned num_cameras = std::min(MAX_STREAMS, mapping_proto->camera_size()); + for (unsigned camera_idx = 0; camera_idx < num_cameras; ++camera_idx) { + const CameraMIDIMappingProto &camera = mapping_proto->camera(camera_idx); + if (match_bank_helper(camera, CameraMIDIMappingProto::kBankFieldNumber, current_controller_bank) && + match_button_helper(camera, CameraMIDIMappingProto::kButtonFieldNumber, note)) { + receiver->switch_camera(camera_idx); + } + } + + match_button(note, MIDIMappingProto::kCueInFieldNumber, MIDIMappingProto::kCueInBankFieldNumber, + bind(&ControllerReceiver::cue_in, receiver)); + match_button(note, MIDIMappingProto::kCueOutFieldNumber, MIDIMappingProto::kCueOutBankFieldNumber, + bind(&ControllerReceiver::cue_out, receiver)); +} + +void MIDIMapper::match_controller(int controller, int field_number, int bank_field_number, float value, function func) +{ + if (bank_mismatch(bank_field_number)) { + return; + } + + if (match_controller_helper(*mapping_proto, field_number, controller)) { + func(value); + } +} + +void MIDIMapper::match_button(int note, int field_number, int bank_field_number, function func) +{ + if (bank_mismatch(bank_field_number)) { + return; + } + + if (match_button_helper(*mapping_proto, field_number, note)) { + func(); + } +} + +bool MIDIMapper::has_active_controller(int field_number, int bank_field_number) +{ + if (bank_mismatch(bank_field_number)) { + return false; + } + + const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(field_number); + const Reflection *reflection = mapping_proto->GetReflection(); + return reflection->HasField(*mapping_proto, descriptor); +} + +bool MIDIMapper::bank_mismatch(int bank_field_number) +{ + return !match_bank_helper(*mapping_proto, bank_field_number, current_controller_bank); +} + +void MIDIMapper::refresh_lights() +{ + lock_guard lock(mu); + update_lights_lock_held(); +} + +void MIDIMapper::update_lights_lock_held() +{ + set active_lights; // Desired state. + if (current_controller_bank == 0) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kBank1IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 1) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kBank2IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 2) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kBank3IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 3) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kBank4IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 4) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kBank5IsSelectedFieldNumber, &active_lights); + } + if (preview_enabled_light) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kPreviewEnabledFieldNumber, &active_lights); + } + if (queue_enabled_light) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kQueueEnabledFieldNumber, &active_lights); + } + if (play_enabled_light) { + activate_mapped_light(*mapping_proto, MIDIMappingProto::kPlayEnabledFieldNumber, &active_lights); + } + if (current_highlighted_camera >= 0 && current_highlighted_camera < mapping_proto->camera_size()) { + const CameraMIDIMappingProto &camera = mapping_proto->camera(current_highlighted_camera); + activate_mapped_light(camera, CameraMIDIMappingProto::kIsCurrentFieldNumber, &active_lights); + } + + // These are always enabled right now. + activate_mapped_light(*mapping_proto, MIDIMappingProto::kCueInFieldNumber, &active_lights); + activate_mapped_light(*mapping_proto, MIDIMappingProto::kCueOutFieldNumber, &active_lights); + + midi_device.update_lights(active_lights); +} diff --git a/futatabi/midi_mapper.h b/futatabi/midi_mapper.h new file mode 100644 index 0000000..96e9f19 --- /dev/null +++ b/futatabi/midi_mapper.h @@ -0,0 +1,109 @@ +#ifndef _MIDI_MAPPER_H +#define _MIDI_MAPPER_H 1 + +// MIDIMapper in Futatabi is much the same as MIDIMapper in Nageru +// (it incoming MIDI messages from mixer controllers interprets them +// according to a user-defined mapping, and calls back into a receiver), +// and shares a fair amount of support code with it. However, it is +// also somewhat different; there are no audio buses, in particular. +// Also, DJ controllers typically have more buttons than audio controllers +// since there's only one (or maybe two) channels, so banks are less +// important, and thus, there's no highlighting. Also, the controllers +// are somewhat different, e.g., you have jog to deal with. + +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" +#include "shared/midi_device.h" + +class MIDIMappingProto; + +// Interface for receiving interpreted controller messages. +class ControllerReceiver { +public: + virtual ~ControllerReceiver() {} + + virtual void preview() = 0; + virtual void queue() = 0; + virtual void play() = 0; + virtual void jog(int delta) = 0; + virtual void switch_camera(unsigned camera_idx) = 0; + virtual void cue_in() = 0; + virtual void cue_out() = 0; + + // Raw events; used for the editor dialog only. + virtual void controller_changed(unsigned controller) = 0; + virtual void note_on(unsigned note) = 0; +}; + +class MIDIMapper : public MIDIReceiver { +public: + MIDIMapper(ControllerReceiver *receiver); + virtual ~MIDIMapper(); + void set_midi_mapping(const MIDIMappingProto &new_mapping); + void start_thread(); + const MIDIMappingProto &get_current_mapping() const; + + // Overwrites and returns the previous value. + ControllerReceiver *set_receiver(ControllerReceiver *new_receiver); + + void refresh_lights(); + + void set_preview_enabled(bool enabled) { + preview_enabled_light = enabled; + refresh_lights(); + } + void set_queue_enabled(bool enabled) { + queue_enabled_light = enabled; + refresh_lights(); + } + void set_play_enabled(bool enabled) { + play_enabled_light = enabled; + refresh_lights(); + } + void highlight_camera_input(int stream_idx) { // -1 for none. + current_highlighted_camera = stream_idx; + refresh_lights(); + } + + // MIDIReceiver. + void controller_received(int controller, int value) override; + void note_on_received(int note) override; + void update_num_subscribers(unsigned num_subscribers) override {} + +private: + void match_controller(int controller, int field_number, int bank_field_number, float value, std::function func); + void match_button(int note, int field_number, int bank_field_number, std::function func); + bool has_active_controller(int field_number, int bank_field_number); // Also works for buttons. + bool bank_mismatch(int bank_field_number); + + void update_lights_lock_held(); + void activate_lights_all_buses(int field_number, std::set *active_lights); + + std::atomic should_quit{false}; + int should_quit_fd; + + mutable std::mutex mu; + ControllerReceiver *receiver; // Under . + std::unique_ptr mapping_proto; // Under . + int num_controller_banks; // Under . + std::atomic current_controller_bank{0}; + + std::atomic preview_enabled_light{false}; + std::atomic queue_enabled_light{false}; + std::atomic play_enabled_light{false}; + std::atomic current_highlighted_camera{-1}; + + MIDIDevice midi_device; +}; + +bool load_midi_mapping_from_file(const std::string &filename, MIDIMappingProto *new_mapping); +bool save_midi_mapping_to_file(const MIDIMappingProto &mapping_proto, const std::string &filename); + +#endif // !defined(_MIDI_MAPPER_H) diff --git a/meson.build b/meson.build index fa6d40f..ac82c3e 100644 --- a/meson.build +++ b/meson.build @@ -268,8 +268,8 @@ endforeach # Protobuf compilation. gen = generator(protoc, \ output : ['@BASENAME@.pb.cc', '@BASENAME@.pb.h'], - arguments : ['--proto_path=@CURRENT_SOURCE_DIR@/futatabi', '--cpp_out=@BUILD_DIR@', '@INPUT@']) -proto_generated = gen.process('futatabi/state.proto', 'futatabi/frame.proto') + arguments : ['--proto_path=@CURRENT_SOURCE_DIR@/futatabi', '--cpp_out=@BUILD_DIR@', '-I@CURRENT_SOURCE_DIR@/shared', '@INPUT@']) +proto_generated = gen.process('futatabi/state.proto', 'futatabi/frame.proto', 'futatabi/futatabi_midi_mapping.proto') # Preprocess Qt as needed. moc_files = qt5.preprocess( @@ -284,7 +284,7 @@ futatabi_srcs = ['futatabi/flow.cpp', 'futatabi/gpu_timers.cpp'] futatabi_srcs += ['futatabi/main.cpp', 'futatabi/player.cpp', 'futatabi/video_stream.cpp', 'futatabi/chroma_subsampler.cpp'] futatabi_srcs += ['futatabi/vaapi_jpeg_decoder.cpp', 'futatabi/db.cpp', 'futatabi/ycbcr_converter.cpp', 'futatabi/flags.cpp'] futatabi_srcs += ['futatabi/mainwindow.cpp', 'futatabi/jpeg_frame_view.cpp', 'futatabi/clip_list.cpp', 'futatabi/frame_on_disk.cpp'] -futatabi_srcs += ['futatabi/export.cpp'] +futatabi_srcs += ['futatabi/export.cpp', 'futatabi/midi_mapper.cpp'] futatabi_srcs += moc_files futatabi_srcs += proto_generated -- 2.39.2