From 2c9a83aeae44dae6a0bbfbae33719976e6d527af Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Tue, 15 Jan 2019 23:09:50 +0100 Subject: [PATCH] Support changing overall playing speed at runtime, using a slider or a MIDI controller. --- futatabi/behringer_cmd_pl1.midimapping | 3 +++ futatabi/futatabi_midi_mapping.proto | 3 +++ futatabi/mainwindow.cpp | 24 +++++++++++++++++++ futatabi/mainwindow.h | 2 ++ futatabi/mainwindow.ui | 33 ++++++++++++++++++++++---- futatabi/midi_mapper.cpp | 5 ++++ futatabi/midi_mapper.h | 1 + futatabi/player.cpp | 10 +++++++- futatabi/player.h | 6 +++++ nageru/midi_mapper.cpp | 31 ------------------------ shared/midi_mapper_util.h | 28 ++++++++++++++++++++++ 11 files changed, 110 insertions(+), 36 deletions(-) diff --git a/futatabi/behringer_cmd_pl1.midimapping b/futatabi/behringer_cmd_pl1.midimapping index ade7595..9b6e439 100644 --- a/futatabi/behringer_cmd_pl1.midimapping +++ b/futatabi/behringer_cmd_pl1.midimapping @@ -36,3 +36,6 @@ cue_out_enabled: { note_number: 33 } # but it's also unclear where else it would be. preview: { note_number: 27 } preview_enabled: { note_number: 27 } + +# The slider (pitch bend) is mapped to master speed. +master_speed: { controller_number: 128 } diff --git a/futatabi/futatabi_midi_mapping.proto b/futatabi/futatabi_midi_mapping.proto index d51b40e..c1415cd 100644 --- a/futatabi/futatabi_midi_mapping.proto +++ b/futatabi/futatabi_midi_mapping.proto @@ -29,6 +29,9 @@ message MIDIMappingProto { optional MIDIControllerProto jog = 16; optional int32 jog_bank = 17; + optional MIDIControllerProto master_speed = 34; + optional int32 master_speed_bank = 35; + // Buttons. optional MIDIButtonProto preview = 18; optional int32 preview_bank = 19; diff --git a/futatabi/mainwindow.cpp b/futatabi/mainwindow.cpp index 350b1b2..c69f67a 100644 --- a/futatabi/mainwindow.cpp +++ b/futatabi/mainwindow.cpp @@ -181,6 +181,8 @@ MainWindow::MainWindow() connect(ui->stop_btn, &QPushButton::clicked, this, &MainWindow::stop_clicked); ui->stop_btn->setEnabled(false); + connect(ui->speed_slider, &QAbstractSlider::valueChanged, this, &MainWindow::speed_slider_changed); + connect(ui->playlist_duplicate_btn, &QPushButton::clicked, this, &MainWindow::playlist_duplicate); connect(ui->playlist_remove_btn, &QPushButton::clicked, this, &MainWindow::playlist_remove); @@ -624,6 +626,13 @@ void MainWindow::stop_clicked() ui->stop_btn->setEnabled(false); } +void MainWindow::speed_slider_changed(int percent) +{ + float speed = percent / 100.0f; + ui->speed_label->setText(QString::fromStdString(to_string(percent) + "%")); + live_player->set_master_speed(speed); +} + void MainWindow::live_player_done() { playlist_selection_changed(); @@ -1225,6 +1234,21 @@ void MainWindow::switch_camera(unsigned camera_idx) }); } +void MainWindow::set_master_speed(float speed) +{ + speed = min(max(speed, 0.1f), 2.0f); + + post_to_main_thread([this, speed] { + int percent = lrintf(speed * 100.0f); + ui->speed_slider->blockSignals(true); + ui->speed_slider->setValue(percent); + ui->speed_slider->blockSignals(false); + ui->speed_label->setText(QString::fromStdString(to_string(percent) + "%")); + }); + + live_player->set_master_speed(speed); +} + void MainWindow::cue_in() { post_to_main_thread([this] { cue_in_clicked(); }); diff --git a/futatabi/mainwindow.h b/futatabi/mainwindow.h index 43c9e78..c7d0ac7 100644 --- a/futatabi/mainwindow.h +++ b/futatabi/mainwindow.h @@ -45,6 +45,7 @@ public: void play() override; void jog(int delta) override; void switch_camera(unsigned camera_idx) override; + void set_master_speed(float speed) override; void cue_in() override; void cue_out() override; @@ -128,6 +129,7 @@ private: void preview_angle_clicked(unsigned stream_idx); void play_clicked(); void stop_clicked(); + void speed_slider_changed(int percent); void live_player_done(); void live_player_clip_progress(const std::map &progress, double time_remaining); void set_output_status(const std::string &status); diff --git a/futatabi/mainwindow.ui b/futatabi/mainwindow.ui index 36d8a5e..e0a4680 100644 --- a/futatabi/mainwindow.ui +++ b/futatabi/mainwindow.ui @@ -141,17 +141,42 @@ - + + + 10 + + + 200 + + + 100 + Qt::Horizontal - + + + + + + + 0 + 0 + + + 40 - 20 + 0 - + + 100% + + + Qt::AlignCenter + + diff --git a/futatabi/midi_mapper.cpp b/futatabi/midi_mapper.cpp index a76e36c..1e26985 100644 --- a/futatabi/midi_mapper.cpp +++ b/futatabi/midi_mapper.cpp @@ -78,11 +78,16 @@ ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver) void MIDIMapper::controller_received(int controller, int value_int) { int delta_value = value_int - 64; // For infinite controllers such as jog. + float value = map_controller_to_float(controller, value_int); receiver->controller_changed(controller); match_controller(controller, MIDIMappingProto::kJogFieldNumber, MIDIMappingProto::kJogBankFieldNumber, delta_value, bind(&ControllerReceiver::jog, receiver, _1)); + + // Speed goes from 0.0 to 2.0 (the receiver will clamp). + match_controller(controller, MIDIMappingProto::kMasterSpeedFieldNumber, MIDIMappingProto::kMasterSpeedBankFieldNumber, + value * 2.0, bind(&ControllerReceiver::set_master_speed, receiver, _1)); } void MIDIMapper::note_on_received(int note) diff --git a/futatabi/midi_mapper.h b/futatabi/midi_mapper.h index 96e9f19..0dd14ce 100644 --- a/futatabi/midi_mapper.h +++ b/futatabi/midi_mapper.h @@ -34,6 +34,7 @@ public: virtual void play() = 0; virtual void jog(int delta) = 0; virtual void switch_camera(unsigned camera_idx) = 0; + virtual void set_master_speed(float speed) = 0; virtual void cue_in() = 0; virtual void cue_out() = 0; diff --git a/futatabi/player.cpp b/futatabi/player.cpp index 6a8612f..30dccc8 100644 --- a/futatabi/player.cpp +++ b/futatabi/player.cpp @@ -114,6 +114,7 @@ void Player::play_playlist_once() bool clip_ready; steady_clock::time_point before_sleep = steady_clock::now(); string pause_status; + float master_speed = 1.0f; // Wait until we're supposed to play something. { @@ -184,9 +185,16 @@ void Player::play_playlist_once() double out_pts = out_pts_origin + TIMEBASE * frameno / global_flags.output_framerate; next_frame_start = origin + microseconds(lrint((out_pts - out_pts_origin) * 1e6 / TIMEBASE)); - int64_t in_pts = lrint(in_pts_origin + TIMEBASE * frameno * clip->speed / global_flags.output_framerate); + int64_t in_pts = lrint(in_pts_origin + TIMEBASE * frameno * clip->speed * master_speed / global_flags.output_framerate); pts = lrint(out_pts); + float new_master_speed = change_master_speed.exchange(0.0f / 0.0f); + if (!std::isnan(new_master_speed)) { + master_speed = new_master_speed; + in_pts_origin = in_pts - TIMEBASE * frameno * clip->speed * master_speed / global_flags.output_framerate; + out_pts_origin = out_pts - TIMEBASE * frameno / global_flags.output_framerate; + } + if (in_pts >= clip->pts_out) { break; } diff --git a/futatabi/player.h b/futatabi/player.h index a097fd9..e64abd7 100644 --- a/futatabi/player.h +++ b/futatabi/player.h @@ -61,6 +61,11 @@ public: pause_status = status; } + void set_master_speed(float speed) + { + change_master_speed = speed; + } + // Not thread-safe to set concurrently with playing. // Will be called back from the player thread. using done_callback_func = std::function; @@ -90,6 +95,7 @@ private: std::thread player_thread; std::atomic should_quit{ false }; + std::atomic change_master_speed{ 0.0f / 0.0f }; JPEGFrameView *destination; done_callback_func done_callback; diff --git a/nageru/midi_mapper.cpp b/nageru/midi_mapper.cpp index fe62546..d111a7a 100644 --- a/nageru/midi_mapper.cpp +++ b/nageru/midi_mapper.cpp @@ -28,37 +28,6 @@ using namespace google::protobuf; using namespace std; using namespace std::placeholders; -namespace { - -double map_controller_to_float(int controller, int val) -{ - if (controller == MIDIReceiver::PITCH_BEND_CONTROLLER) { - // We supposedly go from -8192 to 8191 (inclusive), but there are - // controllers that only have 10-bit precision and do the upconversion - // to 14-bit wrong (just padding with zeros), making 8176 the highest - // attainable value. We solve this by making the effective range - // -8176..8176 (inclusive). - if (val <= -8176) { - return 0.0; - } else if (val >= 8176) { - return 1.0; - } else { - return 0.5 * (double(val) / 8176.0) + 0.5; - } - } - - // Slightly hackish mapping so that we can represent exactly 0.0, 0.5 and 1.0. - if (val <= 0) { - return 0.0; - } else if (val >= 127) { - return 1.0; - } else { - return (val + 0.5) / 127.0; - } -} - -} // namespace - MIDIMapper::MIDIMapper(ControllerReceiver *receiver) : receiver(receiver), mapping_proto(new MIDIMappingProto), midi_device(this) { diff --git a/shared/midi_mapper_util.h b/shared/midi_mapper_util.h index 39ccd84..5b66b83 100644 --- a/shared/midi_mapper_util.h +++ b/shared/midi_mapper_util.h @@ -2,6 +2,7 @@ #define _MIDI_MAPPER_UTIL_H 1 #include "midi_mapping.pb.h" +#include "shared/midi_device.h" #include @@ -61,4 +62,31 @@ void activate_mapped_light(const Proto &msg, int field_number, std::setinsert(light_proto.note_number()); } +inline double map_controller_to_float(int controller, int val) +{ + if (controller == MIDIReceiver::PITCH_BEND_CONTROLLER) { + // We supposedly go from -8192 to 8191 (inclusive), but there are + // controllers that only have 10-bit precision and do the upconversion + // to 14-bit wrong (just padding with zeros), making 8176 the highest + // attainable value. We solve this by making the effective range + // -8176..8176 (inclusive). + if (val <= -8176) { + return 0.0; + } else if (val >= 8176) { + return 1.0; + } else { + return 0.5 * (double(val) / 8176.0) + 0.5; + } + } + + // Slightly hackish mapping so that we can represent exactly 0.0, 0.5 and 1.0. + if (val <= 0) { + return 0.0; + } else if (val >= 127) { + return 1.0; + } else { + return (val + 0.5) / 127.0; + } +} + #endif // !defined(_MIDI_MAPPER_UTIL_H) -- 2.39.2