]> git.sesse.net Git - nageru/commitdiff
Support changing overall playing speed at runtime, using a slider or a MIDI controller.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Tue, 15 Jan 2019 22:09:50 +0000 (23:09 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Tue, 15 Jan 2019 22:09:50 +0000 (23:09 +0100)
futatabi/behringer_cmd_pl1.midimapping
futatabi/futatabi_midi_mapping.proto
futatabi/mainwindow.cpp
futatabi/mainwindow.h
futatabi/mainwindow.ui
futatabi/midi_mapper.cpp
futatabi/midi_mapper.h
futatabi/player.cpp
futatabi/player.h
nageru/midi_mapper.cpp
shared/midi_mapper_util.h

index ade7595694a20c267cb424edfc0474f165d867c7..9b6e439ec4b5f005502206f498345a41d71b7599 100644 (file)
@@ -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 }
index d51b40e3b56ecb0fc4b7763a79208b069bc3e823..c1415cd1ca429e96fb0f4aa18470dd4db635c6d9 100644 (file)
@@ -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;
index 350b1b2966d5327b20f0980b21e3eda31de401d3..c69f67a17ad0cc43ab209b0b7c07ad4f8d2360c5 100644 (file)
@@ -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(); });
index 43c9e78232f1d3edcff3375b450b24017e4bc835..c7d0ac756f78ae4c800a4dc0598396b07f8a438c 100644 (file)
@@ -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<uint64_t, double> &progress, double time_remaining);
        void set_output_status(const std::string &status);
index 36d8a5ec9ffb0340b843591d15fa8ce655a6461a..e0a4680277d41c2d4d7369a23ccd15549106c287 100644 (file)
            </widget>
           </item>
           <item>
-           <spacer name="horizontalSpacer">
+           <widget class="QSlider" name="speed_slider">
+            <property name="minimum">
+             <number>10</number>
+            </property>
+            <property name="maximum">
+             <number>200</number>
+            </property>
+            <property name="value">
+             <number>100</number>
+            </property>
             <property name="orientation">
              <enum>Qt::Horizontal</enum>
             </property>
-            <property name="sizeHint" stdset="0">
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="speed_label">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="minimumSize">
              <size>
               <width>40</width>
-              <height>20</height>
+              <height>0</height>
              </size>
             </property>
-           </spacer>
+            <property name="text">
+             <string>100%</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
+            </property>
+           </widget>
           </item>
           <item>
            <widget class="QPushButton" name="stop_btn">
index a76e36cb04f83c96ed5c49149078115c23a2cd13..1e26985a01537e226f21cedb7f9e789698648be6 100644 (file)
@@ -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)
index 96e9f198cd12f37035c6f8e1cad2fe9e0ac98288..0dd14cec2ecabd059298e8931703909afc7d7723 100644 (file)
@@ -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;
 
index 6a8612f86e43be0f0f2e1740edd31a0c53ec61d9..30dccc8e4e88132664d26cd4e8f7058586c6c430 100644 (file)
@@ -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;
                        }
index a097fd963c23f42476e1fa292c3d0644b565cbaf..e64abd79f7b834b0f6521829576da8e4ef8bc9f5 100644 (file)
@@ -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<void()>;
@@ -90,6 +95,7 @@ private:
 
        std::thread player_thread;
        std::atomic<bool> should_quit{ false };
+       std::atomic<float> change_master_speed{ 0.0f / 0.0f };
 
        JPEGFrameView *destination;
        done_callback_func done_callback;
index fe62546569c4defca910c6a06f917244bcaf7b37..d111a7a650dab499c7102f7e37e75a3663a10ffa 100644 (file)
@@ -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)
 {
index 39ccd84e43f7d1ec1e707e2de730ea1d08ad59d9..5b66b83710f863e74bf5ab8f28cc0d37928a69ea 100644 (file)
@@ -2,6 +2,7 @@
 #define _MIDI_MAPPER_UTIL_H 1
 
 #include "midi_mapping.pb.h"
+#include "shared/midi_device.h"
 
 #include <google/protobuf/descriptor.h>
 
@@ -61,4 +62,31 @@ void activate_mapped_light(const Proto &msg, int field_number, std::set<unsigned
        active_lights->insert(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)