From 29ae366659cc0f808c5dbbeba7884edaf2e63c0e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Dec 2019 01:19:17 +1100 Subject: [PATCH] improved speed/duration implementation This functionality is almost complete minus the reverse speed and any of the audio implementation. --- app/dialog/speedduration/speedduration.cpp | 342 ++++++++++++++------- app/dialog/speedduration/speedduration.h | 12 +- app/widget/slider/sliderbase.cpp | 23 +- app/widget/slider/sliderbase.h | 5 + 4 files changed, 273 insertions(+), 109 deletions(-) diff --git a/app/dialog/speedduration/speedduration.cpp b/app/dialog/speedduration/speedduration.cpp index a994a526a..5c650ec68 100644 --- a/app/dialog/speedduration/speedduration.cpp +++ b/app/dialog/speedduration/speedduration.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include "common/timecodefunctions.h" #include "undo/undostack.h" @@ -17,145 +19,205 @@ SpeedDurationDialog::SpeedDurationDialog(const rational& timebase, const QListaddWidget(speed_groupbox); + QGridLayout* speed_layout = new QGridLayout(speed_groupbox); - original_speed_ = clips.first()->speed(); - original_length_ = olive::time_to_timestamp(clips.first()->length(), timebase_); + int row = 0; - // Check all clips after the first to see if they all share the same speed or not - for (int i=1;ispeed())) { - original_speed_ = qSNaN(); + // For any other clips that are selected, determine if they share speeds and lengths. If they don't, the UI can't + // show them all as having the same parameters + bool same_speed = true; + bool same_duration = true; + + for (int i=1;ispeed(), this_clip->speed())) { + same_speed = false; + } + + // Check if the durations are different + if (same_duration + && prev_clip->length() != this_clip->length()) { + same_duration = false; + } + + // If we've already determined both are different, no need to continue + if (!same_speed && !same_duration) { + break; + } } - if (original_length_ != olive::time_to_timestamp(clips.at(i)->length(), timebase_)) { - original_length_ = -1; + speed_layout->addWidget(new QLabel(tr("Speed:")), row, 0); + + // Create "Speed" slider + speed_slider_ = new FloatSlider(); + speed_slider_->SetMinimum(0); + speed_layout->addWidget(speed_slider_, row, 1); + + if (same_speed) { + // All clips share the same speed so we can show the value (converted to a percentage) + speed_slider_->SetValue(clips_.first()->speed() * 100.0); + } else { + // Else, we show an invalid initial state + speed_slider_->SetTristate(); } - if (original_length_ == -1 && qIsNaN(original_speed_)) { - break; + row++; + + speed_layout->addWidget(new QLabel(tr("Duration:")), row, 0); + + // Create "Duration" slider + duration_slider_ = new TimeSlider(); + duration_slider_->SetTimebase(timebase_); + duration_slider_->SetMinimum(1); + speed_layout->addWidget(duration_slider_, row, 1); + + if (same_duration) { + duration_slider_->SetValue(olive::time_to_timestamp(clips_.first()->length(), timebase_)); + } else { + duration_slider_->SetTristate(); } + + row++; + + link_speed_and_duration_ = new QCheckBox(tr("Link Speed and Duration")); + link_speed_and_duration_->setChecked(true); + speed_layout->addWidget(link_speed_and_duration_, row, 0, 1, 2); + + // Pick up when the speed or duration slider changes so we can programmatically link them + connect(speed_slider_, SIGNAL(ValueChanged(double)), this, SLOT(SpeedChanged())); + connect(duration_slider_, SIGNAL(ValueChanged(int64_t)), this, SLOT(DurationChanged())); } - layout->addWidget(new QLabel(tr("Speed:")), row, 0); - - speed_slider_ = new FloatSlider(); - - // Convert speed to percentage value - speed_slider_->SetValue(original_speed_ * 100.0); - connect(speed_slider_, SIGNAL(ValueChanged(double)), this, SLOT(SpeedChanged())); - layout->addWidget(speed_slider_, row, 1); - - row++; - - layout->addWidget(new QLabel(tr("Duration:")), row, 0); - - duration_slider_ = new TimeSlider(); - duration_slider_->SetTimebase(timebase_); - duration_slider_->SetValue(original_length_); - layout->addWidget(duration_slider_, row, 1); - - if (!qIsNaN(original_speed_)) { - // Convert internally stored original length value to 1x speed for later calculations - original_length_ = qRound(static_cast(original_length_) * original_speed_); - } - - row++; - reverse_speed_checkbox_ = new QCheckBox(tr("Reverse Speed")); - layout->addWidget(reverse_speed_checkbox_, row, 0, 1, 2); - - row++; + layout->addWidget(reverse_speed_checkbox_); maintain_audio_pitch_checkbox_ = new QCheckBox(tr("Maintain Audio Pitch")); - layout->addWidget(maintain_audio_pitch_checkbox_, row, 0, 1, 2); - - row++; + layout->addWidget(maintain_audio_pitch_checkbox_); ripple_clips_checkbox_ = new QCheckBox(tr("Ripple Clips")); - layout->addWidget(ripple_clips_checkbox_, row, 0, 1, 2); - - row++; + layout->addWidget(ripple_clips_checkbox_); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->setCenterButtons(true); - layout->addWidget(buttons, row, 0, 1, 2); + layout->addWidget(buttons); connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); } void SpeedDurationDialog::accept() { - // FIXME: Support multiple incompatible speeds/lengths + if (duration_slider_->IsTristate() && speed_slider_->IsTristate()) { + // Nothing to be done + QDialog::accept(); + return; + } QUndoCommand* command = new QUndoCommand(); - rational new_clip_length = olive::timestamp_to_time(duration_slider_->GetValue(), timebase_); - double new_speed = speed_slider_->GetValue() * 0.01; - foreach (ClipBlock* clip, clips_) { - rational this_clip_new_length = new_clip_length; + bool change_duration = !duration_slider_->IsTristate() || link_speed_and_duration_->isChecked(); + bool change_speed = !speed_slider_->IsTristate() || link_speed_and_duration_->isChecked(); - Block* next_block = clip->next(); + double new_speed = speed_slider_->GetValue()*0.01; - if (this_clip_new_length != clip->length() - && !ripple_clips_checkbox_->isChecked() - && next_block) { + rational new_clip_length = clip->length(); - if (this_clip_new_length > clip->length()) { + if (change_duration) { + // Change the duration + int64_t current_duration = olive::time_to_timestamp(clip->length(), timebase_); + int64_t new_duration = current_duration; - // Check if next clip is a gap, and if so we can take it all up - if (next_block->type() == Block::kGap) { - this_clip_new_length = qMin(next_block->out(), clip->in() + this_clip_new_length); + // Check if we're getting the duration value directly from the slider or calculating it from the speed + if (duration_slider_->IsTristate()) { + // Calculate duration from speed + new_duration = GetAdjustedDuration(clip, new_speed); + } else { + // Get duration directly from slider + new_duration = duration_slider_->GetValue(); - // If we're taking up the entire clip, we'll just remove it - if (this_clip_new_length == next_block->out()) { - new TrackRippleRemoveBlockCommand(TrackOutput::TrackFromBlock(next_block), next_block, command); + // Check if we're calculating the speed from this duration + if (speed_slider_->IsTristate() && change_speed) { + // If we're here, the duration is going to override the speed + new_speed = GetAdjustedSpeed(clip, new_duration); + } + } - // Delete node and its exclusive deps - QList gap_and_its_deps; - gap_and_its_deps.append(next_block); - gap_and_its_deps.append(next_block->GetExclusiveDependencies()); - new NodeRemoveCommand(static_cast(next_block->parent()), gap_and_its_deps, command); - } else { - // Otherwise we can just resize it - new BlockResizeCommand(next_block, next_block->out() - this_clip_new_length, command); + if (new_duration != current_duration) { + new_clip_length = olive::timestamp_to_time(new_duration, timebase_); + Block* next_block = clip->next(); + + // If "ripple clips" isn't checked, we need to calculate around the timeline as-is + if (!ripple_clips_checkbox_->isChecked() + && next_block) { + + if (new_clip_length > clip->length()) { + + // Check if next clip is a gap, and if so we can take it all up + if (next_block->type() == Block::kGap) { + new_clip_length = qMin(next_block->out(), clip->in() + new_clip_length); + + // If we're taking up the entire clip, we'll just remove it + if (new_clip_length == next_block->out()) { + new TrackRippleRemoveBlockCommand(TrackOutput::TrackFromBlock(next_block), next_block, command); + + // Delete node and its exclusive deps + QList gap_and_its_deps; + gap_and_its_deps.append(next_block); + gap_and_its_deps.append(next_block->GetExclusiveDependencies()); + new NodeRemoveCommand(static_cast(next_block->parent()), gap_and_its_deps, command); + } else { + // Otherwise we can just resize it + new BlockResizeCommand(next_block, next_block->out() - new_clip_length, command); + } + + } else { + // Otherwise we can't extend any further + new_clip_length = clip->length(); + } + + } else if (new_clip_length < clip->length()) { + + // If we're not rippling these clips, we'll need to insert a gap (unless the clip is already at the end) + rational gap_length = clip->length() - new_clip_length; + + if (next_block->type() == Block::kGap) { + // If we've already got a gap here, we can just resize it + new BlockResizeCommand(next_block, next_block->length() + gap_length, command); + } else { + // Otherwise we have to create a new gap + GapBlock* gap = new GapBlock(); + gap->set_length(gap_length); + new NodeAddCommand(static_cast(clip->parent()), gap, command); + new TrackInsertBlockBetweenBlocksCommand(TrackOutput::TrackFromBlock(clip), gap, clip, next_block, command); + } } - - } else { - // Otherwise we can't extend any further - this_clip_new_length = clip->length(); } - } else if (this_clip_new_length < clip->length()) { - - // If we're not rippling these clips, we'll need to insert a gap (unless the clip is already at the end) - rational gap_length = clip->length() - this_clip_new_length; - - if (next_block->type() == Block::kGap) { - // If we've already got a gap here, we can just resize it - new BlockResizeCommand(next_block, next_block->length() + gap_length, command); - } else { - // Otherwise we have to create a new gap - GapBlock* gap = new GapBlock(); - gap->set_length(gap_length); - new NodeAddCommand(static_cast(clip->parent()), gap, command); - new TrackInsertBlockBetweenBlocksCommand(TrackOutput::TrackFromBlock(clip), gap, clip, next_block, command); + if (new_clip_length != clip->length()) { + new BlockResizeCommand(clip, new_clip_length, command); } } } - if (this_clip_new_length != clip->length()) { - new BlockResizeWithoutMediaOutCommand(clip, this_clip_new_length, command); - } + if (change_speed) { + int64_t new_clip_duration = olive::time_to_timestamp(new_clip_length, timebase_); + int64_t new_media_duration = qRound(static_cast(new_clip_duration) * new_speed); + rational new_media_length = olive::timestamp_to_time(new_media_duration, timebase_); + rational new_media_out = clip->media_in() + new_media_length; - int64_t media_length = qRound(static_cast(olive::time_to_timestamp(this_clip_new_length, timebase_)) * new_speed); - new BlockSetMediaOutCommand(clip, - clip->media_in() + olive::timestamp_to_time(media_length, timebase_), - command); + // Change the speed by calculating the appropriate media out point for this clip + new BlockSetMediaOutCommand(clip, new_media_out, command); + } } olive::undo_stack.pushIfHasChildren(command); @@ -163,19 +225,91 @@ void SpeedDurationDialog::accept() QDialog::accept(); } +double SpeedDurationDialog::GetUnadjustedLengthTimestamp(ClipBlock *clip) const +{ + double duration = static_cast(olive::time_to_timestamp(clip->length(), timebase_)); + + // Convert duration to non-speed adjusted duration + duration *= clip->speed(); + + return duration; +} + +int64_t SpeedDurationDialog::GetAdjustedDuration(ClipBlock *clip, const double &new_speed) const +{ + double duration = GetUnadjustedLengthTimestamp(clip); + + // Re-adjust by new speed + duration /= new_speed; + + // Return rounded time + return qRound64(duration); +} + +double SpeedDurationDialog::GetAdjustedSpeed(ClipBlock *clip, const int64_t &new_duration) const +{ + double duration = GetUnadjustedLengthTimestamp(clip); + + // Create a fraction of the original duration over the new duration + duration /= static_cast(new_duration); + + return duration; +} + void SpeedDurationDialog::SpeedChanged() { - if (original_length_ > -1) { + if (link_speed_and_duration_->isChecked()) { double new_speed = speed_slider_->GetValue()*0.01; if (qIsNull(new_speed)) { - // 0 speed is considered a still image which could be any length and since we can't divide by zero anyway, we - // assume the length will stay the same - duration_slider_->SetValue(original_length_); + // A speed of 0 is considered a still frame. Since we can't divide by zero and a still frame could be any length, + // we don't bother updating the + return; + } + + bool same_durations = true; + int64_t new_duration = GetAdjustedDuration(clips_.first(), new_speed); + + for (int i=1;iSetValue(new_duration); } else { - // Otherwise, calculate a length that will show the same amount of "content" with a new length - int64_t new_length = qRound(static_cast(original_length_) / new_speed); - duration_slider_->SetValue(new_length); + duration_slider_->SetTristate(); + } + } +} + +void SpeedDurationDialog::DurationChanged() +{ + if (link_speed_and_duration_->isChecked()) { + int64_t new_duration = duration_slider_->GetValue(); + + bool same_speeds = true; + double new_speed = GetAdjustedSpeed(clips_.first(), new_duration); + + for (int i=1;iSetValue(new_speed*100.0); + } else { + speed_slider_->SetTristate(); } } } diff --git a/app/dialog/speedduration/speedduration.h b/app/dialog/speedduration/speedduration.h index d2496a043..bd66f6164 100644 --- a/app/dialog/speedduration/speedduration.h +++ b/app/dialog/speedduration/speedduration.h @@ -19,6 +19,12 @@ public slots: virtual void accept() override; private: + double GetUnadjustedLengthTimestamp(ClipBlock* clip) const; + + int64_t GetAdjustedDuration(ClipBlock* clip, const double& new_speed) const; + + double GetAdjustedSpeed(ClipBlock* clip, const int64_t& new_duration) const; + QList clips_; FloatSlider* speed_slider_; @@ -26,15 +32,15 @@ private: rational timebase_; - double original_speed_; - int64_t original_length_; - + QCheckBox* link_speed_and_duration_; QCheckBox* reverse_speed_checkbox_; QCheckBox* maintain_audio_pitch_checkbox_; QCheckBox* ripple_clips_checkbox_; private slots: void SpeedChanged(); + + void DurationChanged(); }; #endif // SPEEDDURATIONDIALOG_H diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 2d34c4c30..2ead1e203 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -32,7 +32,8 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : has_max_(false), mode_(mode), dragged_(false), - require_valid_input_(true) + require_valid_input_(true), + tristate_(false) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); @@ -79,6 +80,17 @@ void SliderBase::SetAlignment(Qt::Alignment alignment) label_->setAlignment(alignment); } +bool SliderBase::IsTristate() const +{ + return tristate_; +} + +void SliderBase::SetTristate() +{ + tristate_ = true; + UpdateLabel(0); +} + const QVariant &SliderBase::Value() { if (dragged_) { @@ -92,6 +104,9 @@ void SliderBase::SetValue(const QVariant &v) { value_ = ClampValue(v); + // Disable tristate + tristate_ = false; + UpdateLabel(value_); } @@ -140,7 +155,11 @@ const QVariant &SliderBase::ClampValue(const QVariant &v) void SliderBase::UpdateLabel(const QVariant &v) { - label_->setText(ValueToString(v)); + if (tristate_) { + label_->setText("---"); + } else { + label_->setText(ValueToString(v)); + } } QString SliderBase::ValueToString(const QVariant &v) diff --git a/app/widget/slider/sliderbase.h b/app/widget/slider/sliderbase.h index 25ec8570a..70c12c310 100644 --- a/app/widget/slider/sliderbase.h +++ b/app/widget/slider/sliderbase.h @@ -44,6 +44,9 @@ public: void SetAlignment(Qt::Alignment alignment); + bool IsTristate() const; + void SetTristate(); + signals: void ValueChanged(QVariant v); @@ -93,6 +96,8 @@ private: bool require_valid_input_; + bool tristate_; + private slots: void LabelPressed();