diff --git a/CMakeLists.txt b/CMakeLists.txt index 778bda302..6bdd354e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,6 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(olive-editor VERSION 0.2.0 LANGUAGES CXX) -option(UPDATE_TS "Update translations" OFF) option(BUILD_DOXYGEN "Build Doxygen documentation" OFF) set(CMAKE_CXX_STANDARD 11) diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 3e159d255..d6fbae232 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -27,6 +27,7 @@ #include #include +#include "audiovisualwaveform.h" #include "common/define.h" #include "outputmanager.h" #include "render/audioparams.h" @@ -94,6 +95,8 @@ signals: void OutputDeviceStarted(AudioPlaybackCache* cache, qint64 offset, int playback_speed); + void OutputWaveformStarted(const AudioVisualWaveform* waveform, const rational &start, int playback_speed); + void AudioParamsChanged(const AudioParams& params); void OutputPushed(const QByteArray& data); diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 4063d413a..408a03f10 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -23,14 +23,69 @@ #include #include "config/config.h" +#include "common/functiontimer.h" namespace olive { -const int AudioVisualWaveform::kSumSampleRate = 200; - -void AudioVisualWaveform::AddSum(const float *samples, int nb_samples, int nb_channels) +AudioVisualWaveform::AudioVisualWaveform() : + channels_(0) { - data_.append(SumSamples(samples, nb_samples, nb_channels)); + // Must be a power of 2 + static const rational kMinimumSampleRate = rational(1, 8); + static const rational kMaximumSampleRate = 8192; + + for (rational i=kMinimumSampleRate; i<=kMaximumSampleRate; i*=2) { + mipmapped_data_.insert({i, Sample()}); + } +} + +void AudioVisualWaveform::OverwriteSamplesFromBuffer(SampleBufferPtr samples, int sample_rate, const rational &start, double target_rate, Sample& data, int &start_index, int &samples_length) +{ + start_index = time_to_samples(start, target_rate); + samples_length = time_to_samples(static_cast(samples->sample_count()) / static_cast(sample_rate), target_rate); + + int end_index = start_index + samples_length; + if (data.size() < end_index) { + data.resize(end_index); + } + + int chunk_size = sample_rate / target_rate; + + for (int i=0; isample_count() - src_index)); + + memcpy(&data.data()[i + start_index], + summary.constData(), + summary.size() * sizeof(SamplePerChannel)); + } +} + +void AudioVisualWaveform::OverwriteSamplesFromMipmap(const AudioVisualWaveform::Sample &input, double input_sample_rate, int &input_start, int &input_length, const rational &start, double output_rate, AudioVisualWaveform::Sample &output_data) +{ + int start_index = time_to_samples(start, output_rate); + int samples_length = time_to_samples(static_cast(input_length / channels_) / input_sample_rate, output_rate); + + int end_index = start_index + samples_length; + if (output_data.size() < end_index) { + output_data.resize(end_index); + } + + int chunk_size = input_sample_rate / output_rate; + + for (int i=0; i(samples->sample_count()) / static_cast(sample_rate)); + // Old less optimized code. Keeping this around as a reference, but the below code is at least + // 10x faster so this shouldn't be used in production. + // + // int input_start, input_length; + // for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + // OverwriteSamplesFromBuffer(samples, sample_rate, start, it->first.toDouble(), it->second, input_start, input_length); + // } - int end_index = start_index + samples_length; - if (data_.size() < end_index) { - data_.resize(end_index); - } + // Process the largest mipmap directly for the samples + auto current_mipmap = mipmapped_data_.rbegin(); + int input_start, input_length; + OverwriteSamplesFromBuffer(samples, sample_rate, start, current_mipmap->first.toDouble(), current_mipmap->second, input_start, input_length); - int chunk_size = sample_rate / kSumSampleRate; + while (true) { + // For each smaller mipmap, we just process from the mipmap before it, making each one + // exponentially faster to create + auto previous_mipmap = current_mipmap; + current_mipmap++; + if (current_mipmap == mipmapped_data_.rend()) { + break; + } - for (int i=0; i summary = SumSamples(samples, - src_index, - qMin(chunk_size, samples->sample_count() - src_index)); - - memcpy(&data_.data()[i + start_index], - summary.constData(), - summary.size() * sizeof(SamplePerChannel)); + OverwriteSamplesFromMipmap(previous_mipmap->second, previous_mipmap->first.toDouble(), + input_start, input_length, start, current_mipmap->first.toDouble(), + current_mipmap->second); } } void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const rational &dest, const rational& offset, const rational& length) { - if (sums.data_.isEmpty()) { - return; + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + + Sample& our_arr = it->second; + const Sample& their_arr = sums.mipmapped_data_.at(rate); + + double rate_dbl = rate.toDouble(); + + // Get our destination sample + int our_start_index = time_to_samples(dest, rate_dbl); + + // Get our source sample + int their_start_index = time_to_samples(offset, rate_dbl); + + // Determine how much we're copying + int copy_len = their_arr.size() - their_start_index; + if (!length.isNull()) { + copy_len = qMin(copy_len, time_to_samples(length, rate_dbl)); + } + + // Determine end index of our array + int end_index = our_start_index + copy_len; + if (our_arr.size() < end_index) { + our_arr.resize(end_index); + } + + memcpy(reinterpret_cast(our_arr.data()) + our_start_index * sizeof(SamplePerChannel), + reinterpret_cast(their_arr.constData()) + their_start_index * sizeof(SamplePerChannel), + copy_len * sizeof(SamplePerChannel)); } - - int start_index = time_to_samples(dest); - int sample_start = time_to_samples(offset); - - int copy_len = sums.data_.size() - sample_start; - if (!length.isNull()) { - copy_len = qMin(copy_len, time_to_samples(length)); - } - - int end_index = start_index + copy_len; - - if (data_.size() < end_index) { - data_.resize(end_index); - } - - memcpy(reinterpret_cast(data_.data()) + start_index * sizeof(SamplePerChannel), - reinterpret_cast(sums.data_.constData()) + time_to_samples(offset) * sizeof(SamplePerChannel), - copy_len * sizeof(SamplePerChannel)); -} - -AudioVisualWaveform AudioVisualWaveform::Mid(const rational &time) const -{ - int sample_index = time_to_samples(time); - - // Create a copy of this waveform chop the early section off - AudioVisualWaveform copy = *this; - copy.data_ = data_.mid(sample_index); - - return copy; -} - -void AudioVisualWaveform::Append(const AudioVisualWaveform &waveform) -{ - data_.append(waveform.data_); -} - -void AudioVisualWaveform::TrimIn(const rational &time) -{ - data_ = data_.mid(time_to_samples(time)); -} - -void AudioVisualWaveform::TrimOut(const rational &time) -{ - data_.resize(data_.size() - time_to_samples(time)); -} - -void AudioVisualWaveform::PrependSilence(const rational &time) -{ - int added_samples = time_to_samples(time); - - // Resize buffer for extra space - data_.resize(data_.size() + added_samples); - - // Shift all data forward - for (int i=data_.size()-1; i>=added_samples; i--) { - data_[i] = data_[i - added_samples]; - } - - // Fill remainder with silence - memset(reinterpret_cast(data_.data()), 0, added_samples * sizeof(SamplePerChannel)); -} - -void AudioVisualWaveform::AppendSilence(const rational &time) -{ - int added_samples = time_to_samples(time); - - // Resize buffer for extra space - int old_size = data_.size(); - data_.resize(old_size + added_samples); - - // Fill remainder with silence - memset(reinterpret_cast(&data_[old_size]), 0, (data_.size() - old_size) * sizeof(SamplePerChannel)); } void AudioVisualWaveform::Shift(const rational &from, const rational &to) { - int from_index = time_to_samples(from); - int to_index = time_to_samples(to); + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + double rate_dbl = rate.toDouble(); + Sample& data = it->second; - if (from_index == to_index) { - return; - } + int from_index = time_to_samples(from, rate_dbl); + int to_index = time_to_samples(to, rate_dbl); - if (from_index > data_.size()) { - return; - } - - if (from_index > to_index) { - // Shifting backwards <- - int copy_sz = data_.size() - from_index; - - for (int i=0; i - int old_sz = data_.size(); - - int distance = (to_index - from_index); - - data_.resize(data_.size() + distance); - - int copy_sz = old_sz - from_index; - - for (int i=0; i data.size()) { + return; } - memset(reinterpret_cast(&data_[from_index]), 0, distance * sizeof(SamplePerChannel)); + if (from_index > to_index) { + // Shifting backwards <- + int copy_sz = data.size() - from_index; + + for (int i=0; i + int old_sz = data.size(); + + int distance = (to_index - from_index); + + data.resize(data.size() + distance); + + int copy_sz = old_sz - from_index; + + for (int i=0; i(&data[from_index]), 0, distance * sizeof(SamplePerChannel)); + } } } -QVector AudioVisualWaveform::SumSamples(const float *samples, int nb_samples, int nb_channels) +AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const rational &start, const rational &length) const { - return SumSamplesInternal(samples, nb_samples, nb_channels); + // Find mipmap that requries + auto using_mipmap = GetMipmapForScale(length.flipped().toDouble()); + + double rate_dbl = using_mipmap->first.toDouble(); + + int start_sample = time_to_samples(start, rate_dbl); + int sample_length = time_to_samples(length, rate_dbl); + + return ReSumSamples(&using_mipmap->second.constData()[start_sample], sample_length, channels_); } -QVector AudioVisualWaveform::SumSamples(const qfloat16 *samples, int nb_samples, int nb_channels) +AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const float *samples, int nb_samples, int nb_channels) { - return SumSamplesInternal(samples, nb_samples, nb_channels); + AudioVisualWaveform::Sample summed_samples(nb_channels); + + for (int i=0;i AudioVisualWaveform::SumSamples(SampleBufferPtr samples, int start_index, int length) +AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(SampleBufferPtr samples, int start_index, int length) { - QVector summed_samples(samples->audio_params().channel_count()); + AudioVisualWaveform::Sample summed_samples(samples->audio_params().channel_count()); int end_index = start_index + length; for (int i=start_index; iaudio_params().channel_count(); channel++) { - ExpandMinMax(summed_samples[channel], samples->data(channel)[i]); + ExpandMinMax(summed_samples[channel], samples->data(channel)[i]); } } return summed_samples; } -QVector AudioVisualWaveform::ReSumSamples(const SamplePerChannel* samples, +AudioVisualWaveform::Sample AudioVisualWaveform::ReSumSamples(const SamplePerChannel* samples, int nb_samples, int nb_channels) { - QVector summed_samples(nb_channels); + AudioVisualWaveform::Sample summed_samples(nb_channels); for (int i=0;i AudioVisualWaveform::ReSumSamples return summed_samples; } -void AudioVisualWaveform::DrawSample(QPainter *painter, const QVector& sample, int x, int y, int height) +void AudioVisualWaveform::DrawSample(QPainter *painter, const Sample& sample, int x, int y, int height, bool rectified) { + if (sample.isEmpty()) { + return; + } + int channel_height = height / sample.size(); int channel_half_height = channel_height / 2; for (int i=0;i(1.0f)); - qfloat16 min = qMax(sample.at(i).min, static_cast(-1.0)); + float max = qMin(sample.at(i).max, 1.0f); + float min = qMax(sample.at(i).min, -1.0f); - if (Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool()) { + if (rectified) { int channel_bottom = y + channel_height * (i + 1); int diff = qRound((max - min) * channel_half_height); @@ -261,16 +300,26 @@ void AudioVisualWaveform::DrawSample(QPainter *painter, const QVector= samples.nb_samples()) { + auto using_mipmap = samples.GetMipmapForScale(scale); + + rational rate = using_mipmap->first; + double rate_dbl = rate.toDouble(); + const Sample& arr = using_mipmap->second; + + int start_sample_index = samples.time_to_samples(start_time, rate_dbl); + + if (start_sample_index >= arr.size()) { return; } int next_sample_index = start_sample_index; int sample_index; - QVector summary; + Sample summary; int summary_index = -1; const QRect& viewport = painter->viewport(); @@ -279,51 +328,54 @@ void AudioVisualWaveform::DrawWaveform(QPainter *painter, const QRect& rect, con int start = qMax(rect.x(), -top_left.x()); int end = qMin(rect.right(), -top_left.x() + viewport.width()); + bool rectified = Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool(); + for (int i=start;i(kSumSampleRate) * static_cast(i - rect.x() + 1) / scale) * samples.channel_count()); + next_sample_index = qMin(arr.size(), + start_sample_index + qFloor(rate_dbl * static_cast(i - rect.x() + 1) / scale) * samples.channel_count()); if (summary_index != sample_index) { - summary = AudioVisualWaveform::ReSumSamples(&samples.data_.at(sample_index), + summary = AudioVisualWaveform::ReSumSamples(&arr.at(sample_index), qMax(samples.channel_count(), next_sample_index - sample_index), samples.channel_count()); summary_index = sample_index; } - DrawSample(painter, summary, i, rect.y(), rect.height()); + DrawSample(painter, summary, i, rect.y(), rect.height(), rectified); } } -int AudioVisualWaveform::time_to_samples(const rational &time) const +int AudioVisualWaveform::time_to_samples(const rational &time, double sample_rate) const { - return time_to_samples(time.toDouble()); + return time_to_samples(time.toDouble(), sample_rate); } -int AudioVisualWaveform::time_to_samples(const double &time) const +int AudioVisualWaveform::time_to_samples(const double &time, double sample_rate) const { - return qFloor(time * kSumSampleRate) * channels_; + return qFloor(time * sample_rate) * channels_; } -template -QVector AudioVisualWaveform::SumSamplesInternal(const T *samples, int nb_samples, int nb_channels) +std::map::const_iterator AudioVisualWaveform::GetMipmapForScale(double scale) const { - QVector summed_samples(nb_channels); - - for (int i=0;i(summed_samples[i%nb_channels], samples[i]); + // Find largest mipmap for this scale (or the largest if we don't find one sufficient) + auto using_mipmap = mipmapped_data_.cend(); + using_mipmap--; + for (auto it=mipmapped_data_.cbegin(); it!=mipmapped_data_.cend(); it++) { + if (it->first.toDouble() >= scale) { + using_mipmap = it; + break; + } } - - return summed_samples; + return using_mipmap; } -template -void AudioVisualWaveform::ExpandMinMax(AudioVisualWaveform::SamplePerChannel &sum, T value) +void AudioVisualWaveform::ExpandMinMax(AudioVisualWaveform::SamplePerChannel &sum, float value) { if (value < sum.min) { sum.min = value; diff --git a/app/audio/audiovisualwaveform.h b/app/audio/audiovisualwaveform.h index 13e68b664..be488169f 100644 --- a/app/audio/audiovisualwaveform.h +++ b/app/audio/audiovisualwaveform.h @@ -21,7 +21,6 @@ #ifndef SUMSAMPLES_H #define SUMSAMPLES_H -#include #include #include @@ -37,11 +36,11 @@ namespace olive { */ class AudioVisualWaveform { public: - AudioVisualWaveform() = default; + AudioVisualWaveform(); struct SamplePerChannel { - qfloat16 min; - qfloat16 max; + float min; + float max; }; using Sample = QVector; @@ -56,18 +55,11 @@ public: channels_ = channels; } - int nb_samples() const - { - return data_.size(); - } - - const SamplePerChannel* const_data() const - { - return data_.constData(); - } - - void AddSum(const float* samples, int nb_samples, int nb_channels); - + /** + * @brief Writes samples into the visual waveform buffer + * + * Starting at `start`, writes samples over anything in the buffer, expanding it if necessary. + */ void OverwriteSamples(SampleBufferPtr samples, int sample_rate, const rational& start = rational()); /** @@ -91,40 +83,34 @@ public: */ void OverwriteSums(const AudioVisualWaveform& sums, const rational& dest, const rational& offset = rational(), const rational &length = rational()); - AudioVisualWaveform Mid(const rational& time) const; - void Append(const AudioVisualWaveform& waveform); - void TrimIn(const rational& time); - void TrimOut(const rational& time); - void PrependSilence(const rational& time); - void AppendSilence(const rational& time); void Shift(const rational& from, const rational& to); - // FIXME: Move to dynamic - static const int kSumSampleRate; + Sample GetSummaryFromTime(const rational& start, const rational& length) const; - static QVector SumSamples(const float* samples, int nb_samples, int nb_channels); - static QVector SumSamples(const qfloat16* samples, int nb_samples, int nb_channels); - static QVector SumSamples(SampleBufferPtr samples, int start_index, int length); + static Sample SumSamples(const float* samples, int nb_samples, int nb_channels); + static Sample SumSamples(SampleBufferPtr samples, int start_index, int length); - static QVector ReSumSamples(const SamplePerChannel *samples, int nb_samples, int nb_channels); + static Sample ReSumSamples(const SamplePerChannel *samples, int nb_samples, int nb_channels); - static void DrawSample(QPainter* painter, const QVector &sample, int x, int y, int height); + static void DrawSample(QPainter* painter, const Sample &sample, int x, int y, int height, bool rectified); static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const AudioVisualWaveform& samples, const rational &start_time); private: - template - static QVector SumSamplesInternal(const T* samples, int nb_samples, int nb_channels); + static void ExpandMinMax(SamplePerChannel &sum, float value); - template - static void ExpandMinMax(SamplePerChannel &sum, T value); + void OverwriteSamplesFromBuffer(SampleBufferPtr samples, int sample_rate, const rational& start, double target_rate, Sample &data, int &start_index, int &samples_length); - int time_to_samples(const rational& time) const; - int time_to_samples(const double& time) const; + void OverwriteSamplesFromMipmap(const Sample& input, double input_sample_rate, int &input_start, int &input_length, const rational& start, double output_rate, Sample &output_data); - int channels_ = 0; + int time_to_samples(const rational& time, double sample_rate) const; + int time_to_samples(const double& time, double sample_rate) const; - QVector data_; + std::map::const_iterator GetMipmapForScale(double scale) const; + + int channels_; + + std::map mipmapped_data_; }; diff --git a/app/core.cpp b/app/core.cpp index 6b0cebd5c..e29811114 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -396,9 +396,6 @@ void Core::CreateNewSequence() // Create new sequence Sequence* new_sequence = CreateNewSequenceForProject(active_project); - // Set all defaults for the sequence - new_sequence->set_default_parameters(); - SequenceDialog sd(new_sequence, SequenceDialog::kNew, main_window_); // Make sure SequenceDialog doesn't make an undo command for editing the sequence, since we make an undo command for @@ -1091,6 +1088,11 @@ void Core::OpenRecoveryProject(const QString &filename) OpenProjectInternal(filename, true); } +void Core::OpenNodeInViewer(ViewerOutput *viewer) +{ + main_window_->OpenNodeInViewer(viewer); +} + void Core::CheckForAutoRecoveries() { QFile autorecovery_index(GetAutoRecoveryIndexFilename()); diff --git a/app/core.h b/app/core.h index 4824a275d..38051c56b 100644 --- a/app/core.h +++ b/app/core.h @@ -300,6 +300,8 @@ public: void OpenRecoveryProject(const QString& filename); + void OpenNodeInViewer(ViewerOutput* viewer); + static const uint kProjectVersion; public slots: diff --git a/app/dialog/preferences/tabs/preferencesdisktab.cpp b/app/dialog/preferences/tabs/preferencesdisktab.cpp index 83e2250d6..950eaa8ca 100644 --- a/app/dialog/preferences/tabs/preferencesdisktab.cpp +++ b/app/dialog/preferences/tabs/preferencesdisktab.cpp @@ -69,7 +69,7 @@ PreferencesDiskTab::PreferencesDiskTab() cache_behavior_layout->addWidget(new QLabel(tr("Cache Ahead:")), row, 0); cache_ahead_slider_ = new FloatSlider(); - cache_ahead_slider_->SetFormat(tr("%1 second(s)")); + cache_ahead_slider_->SetFormat(tr("%1 seconds")); cache_ahead_slider_->SetMinimum(0); cache_ahead_slider_->SetValue(Config::Current()["DiskCacheAhead"].value().toDouble()); cache_behavior_layout->addWidget(cache_ahead_slider_, row, 1); @@ -78,7 +78,7 @@ PreferencesDiskTab::PreferencesDiskTab() cache_behind_slider_ = new FloatSlider(); cache_behind_slider_->SetMinimum(0); - cache_behind_slider_->SetFormat(tr("%1 second(s)")); + cache_behind_slider_->SetFormat(tr("%1 seconds")); cache_behind_slider_->SetValue(Config::Current()["DiskCacheBehind"].value().toDouble()); cache_behavior_layout->addWidget(cache_behind_slider_, row, 3); diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index dc874debe..646d20642 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -77,7 +77,9 @@ PreferencesGeneralTab::PreferencesGeneralTab() int row = 0; - timeline_layout->addWidget(new QLabel(tr("Auto-Scroll Method:")), row, 0); + QLabel* autoscroll_lbl = new QLabel(tr("Auto-Scroll Method:")); + autoscroll_lbl->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + timeline_layout->addWidget(autoscroll_lbl, row, 0); // ComboBox indices match enum indices autoscroll_method_ = new QComboBox(); @@ -101,9 +103,17 @@ PreferencesGeneralTab::PreferencesGeneralTab() default_still_length_ = new FloatSlider(); default_still_length_->SetMinimum(0.1); - default_still_length_->SetFormat(tr("%1 second(s)")); + default_still_length_->SetFormat(tr("%1 seconds")); default_still_length_->SetValue(Config::Current()["DefaultStillLength"].value().toDouble()); timeline_layout->addWidget(default_still_length_); + + row++; + + timeline_layout->addWidget(new QLabel(tr("Default Sequence Parameters:")), row, 0); + + QPushButton* default_sequence_params_btn = new QPushButton(tr("Edit")); + connect(default_sequence_params_btn, &QPushButton::clicked, this, &PreferencesGeneralTab::EditDefaultSequenceSettings); + timeline_layout->addWidget(default_sequence_params_btn, row, 1); } { @@ -126,7 +136,7 @@ PreferencesGeneralTab::PreferencesGeneralTab() autorecovery_interval_ = new IntegerSlider(); autorecovery_interval_->SetMinimum(1); autorecovery_interval_->SetMaximum(60); - autorecovery_interval_->SetFormat(tr("%1 minute(s)")); + autorecovery_interval_->SetFormat(QT_TRANSLATE_N_NOOP("olive::SliderBase", "%n minute(s)"), true); autorecovery_interval_->SetValue(Config::Current()[QStringLiteral("AutorecoveryInterval")].toLongLong()); autorecovery_layout->addWidget(autorecovery_interval_, row, 1); @@ -176,6 +186,17 @@ void PreferencesGeneralTab::Accept(MultiUndoCommand *command) Config::Current()[QStringLiteral("AutorecoveryInterval")] = QVariant::fromValue(autorecovery_interval_->GetValue()); Config::Current()[QStringLiteral("AutorecoveryMaximum")] = QVariant::fromValue(autorecovery_maximum_->GetValue()); Core::instance()->SetAutorecoveryInterval(autorecovery_interval_->GetValue()); + + // Default sequence parameters + VideoParams dsvp = default_sequence_.GetVideoParams(); + AudioParams dsap = default_sequence_.GetAudioParams(); + Config::Current()[QStringLiteral("DefaultSequenceWidth")] = dsvp.width(); + Config::Current()[QStringLiteral("DefaultSequenceHeight")] = dsvp.height(); + Config::Current()[QStringLiteral("DefaultSequencePixelAspect")] = QVariant::fromValue(dsvp.pixel_aspect_ratio()); + Config::Current()[QStringLiteral("DefaultSequenceFrameRate")] = QVariant::fromValue(dsvp.frame_rate().flipped()); + Config::Current()[QStringLiteral("DefaultSequenceInterlacing")] = dsvp.interlacing(); + Config::Current()[QStringLiteral("DefaultSequenceAudioFrequency")] = dsap.sample_rate(); + Config::Current()[QStringLiteral("DefaultSequenceAudioLayout")] = QVariant::fromValue(dsap.channel_layout()); } void PreferencesGeneralTab::AddLanguage(const QString &locale_name) @@ -185,4 +206,11 @@ void PreferencesGeneralTab::AddLanguage(const QString &locale_name) language_combobox_->setItemData(language_combobox_->count() - 1, locale_name); } +void PreferencesGeneralTab::EditDefaultSequenceSettings() +{ + SequenceDialog sd(&default_sequence_, SequenceDialog::kExisting, this); + sd.SetNameIsEditable(false); + sd.exec(); +} + } diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index e78b2466a..f4f8ab8c8 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -57,6 +57,11 @@ private: IntegerSlider* autorecovery_maximum_; + Sequence default_sequence_; + +private slots: + void EditDefaultSequenceSettings(); + }; } diff --git a/app/node/color/colormanager/colormanager.cpp b/app/node/color/colormanager/colormanager.cpp index 521975d71..a8cba4557 100644 --- a/app/node/color/colormanager/colormanager.cpp +++ b/app/node/color/colormanager/colormanager.cpp @@ -21,7 +21,6 @@ #include "colormanager.h" #include -#include #include #include "common/define.h" diff --git a/app/node/graph.cpp b/app/node/graph.cpp index f76f390b4..6e5055903 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -61,6 +61,7 @@ void NodeGraph::childEvent(QChildEvent *event) connect(node, &Node::ValueChanged, this, &NodeGraph::ValueChanged); emit NodeAdded(node); + emit node->AddedToGraph(this); } else if (event->type() == QEvent::ChildRemoved) { @@ -72,6 +73,7 @@ void NodeGraph::childEvent(QChildEvent *event) disconnect(node, &Node::ValueChanged, this, &NodeGraph::ValueChanged); emit NodeRemoved(node); + emit node->RemovedFromGraph(this); } } diff --git a/app/node/node.cpp b/app/node/node.cpp index c0678db5a..f087111d1 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -1245,7 +1245,7 @@ bool Node::AreLinked(Node *a, Node *b) return a->links_.contains(b); } -void Node::AddInput(const QString &id, NodeValue::Type type, const QVariant &default_value, Node::InputFlags flags) +void Node::InsertInput(const QString &id, NodeValue::Type type, const QVariant &default_value, Node::InputFlags flags, int index) { if (id.isEmpty()) { qWarning() << "Rejected adding input with an empty ID on node" << this->id(); @@ -1264,8 +1264,8 @@ void Node::AddInput(const QString &id, NodeValue::Type type, const QVariant &def i.flags = flags; i.array_size = 0; - input_ids_.append(id); - input_data_.append(i); + input_ids_.insert(index, id); + input_data_.insert(index, i); if (!standard_immediates_.value(id, nullptr)) { standard_immediates_.insert(id, CreateImmediate(id)); @@ -2294,7 +2294,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() // Start moving other nodes foreach (Node* surrounding, node_->parent()->nodes()) { - if (bounding_rect.contains(surrounding->GetPosition()) && surrounding != node_) { + if (bounding_rect.contains(surrounding->GetPosition()) && surrounding != node_ && surrounding != ignore_node_) { QPointF new_pos = surrounding->GetPosition(); qreal move_rate = 0.50; @@ -2306,6 +2306,7 @@ void NodeSetPositionAndShiftSurroundingsCommand::redo() new_pos.setY(new_pos.y() + move_rate); auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, new_pos, true); + sur_command->SetIgnoreNode(node_); sur_command->redo(); commands_.append(sur_command); } diff --git a/app/node/node.h b/app/node/node.h index 86e4d2f54..e7fdc1361 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -799,7 +799,23 @@ protected: }; - void AddInput(const QString& id, NodeValue::Type type, const QVariant& default_value, InputFlags flags = InputFlags(kInputFlagNormal)); + void InsertInput(const QString& id, NodeValue::Type type, const QVariant& default_value, InputFlags flags, int index); + + void PrependInput(const QString& id, NodeValue::Type type, const QVariant& default_value, InputFlags flags = InputFlags(kInputFlagNormal)) + { + InsertInput(id, type, default_value, flags, 0); + } + + void PrependInput(const QString& id, NodeValue::Type type, InputFlags flags = InputFlags(kInputFlagNormal)) + { + PrependInput(id, type, QVariant(), flags); + } + + void AddInput(const QString& id, NodeValue::Type type, const QVariant& default_value, InputFlags flags = InputFlags(kInputFlagNormal)) + { + InsertInput(id, type, default_value, flags, input_ids_.size()); + } + void AddInput(const QString& id, NodeValue::Type type, InputFlags flags = InputFlags(kInputFlagNormal)) { AddInput(id, type, QVariant(), flags); @@ -926,6 +942,10 @@ signals: void InputDataTypeChanged(const QString& id, NodeValue::Type type); + void AddedToGraph(NodeGraph* graph); + + void RemovedFromGraph(NodeGraph* graph); + private: class ArrayInsertCommand : public UndoCommand { @@ -1331,7 +1351,8 @@ public: NodeSetPositionAndShiftSurroundingsCommand(Node* node, const QPointF& pos, bool move_dependencies_relatively) : node_(node), position_(pos), - move_dependencies_(move_dependencies_relatively) + move_dependencies_(move_dependencies_relatively), + ignore_node_(nullptr) {} virtual ~NodeSetPositionAndShiftSurroundingsCommand() override @@ -1353,6 +1374,11 @@ public: } } + void SetIgnoreNode(Node* n) + { + ignore_node_ = n; + } + private: Node* node_; @@ -1362,6 +1388,8 @@ private: QVector commands_; + Node* ignore_node_; + }; class NodeSetPositionAsChildCommand : public UndoCommand diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 4dd6eebd7..271fbf36d 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -160,13 +160,15 @@ public: static Type TypeFromString(const QString& s) { - if (s.at(1) == ':') { - if (s.at(0) == 'v') { - // Video stream - return Track::kVideo; - } else if (s.at(0) == 'a') { - // Audio stream - return Track::kAudio; + if (s.size() >= 3) { + if (s.at(1) == ':') { + if (s.at(0) == 'v') { + // Video stream + return Track::kVideo; + } else if (s.at(0) == 'a') { + // Audio stream + return Track::kAudio; + } } } diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 81c89b383..e8c16848e 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -39,7 +39,8 @@ const uint64_t ViewerOutput::kVideoParamEditMask = VideoParamEdit::kWidthHeight ViewerOutput::ViewerOutput(bool create_default_streams) : video_frame_cache_(this), audio_playback_cache_(this), - cache_enabled_(true) + video_cache_enabled_(true), + audio_cache_enabled_(true) { AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray)); SetInputProperty(kVideoParamsInput, QStringLiteral("mask"), QVariant::fromValue(kVideoParamEditMask)); @@ -54,6 +55,7 @@ ViewerOutput::ViewerOutput(bool create_default_streams) : if (create_default_streams) { AddStream(Track::kVideo, QVariant()); AddStream(Track::kAudio, QVariant()); + set_default_parameters(); } } @@ -196,7 +198,7 @@ void ViewerOutput::set_default_parameters() void ViewerOutput::ShiftVideoCache(const rational &from, const rational &to) { - if (cache_enabled_) { + if (video_cache_enabled_) { video_frame_cache_.Shift(from, to); } @@ -205,7 +207,7 @@ void ViewerOutput::ShiftVideoCache(const rational &from, const rational &to) void ViewerOutput::ShiftAudioCache(const rational &from, const rational &to) { - if (cache_enabled_) { + if (audio_cache_enabled_) { audio_playback_cache_.Shift(from, to); } @@ -222,18 +224,16 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, { Q_UNUSED(element) - if (cache_enabled_) { - if (from == kTextureInput || from == kSamplesInput - || from == kVideoParamsInput || from == kAudioParamsInput) { - TimeRange invalidated_range(qMax(rational(), range.in()), - qMin(GetLength(), range.out())); + if ((video_cache_enabled_ && (from == kTextureInput || from == kVideoParamsInput)) + || (audio_cache_enabled_ && (from == kSamplesInput || from == kAudioParamsInput))) { + TimeRange invalidated_range(qMax(rational(), range.in()), + qMin(GetLength(), range.out())); - if (invalidated_range.in() != invalidated_range.out()) { - if (from == kTextureInput || from == kVideoParamsInput) { - video_frame_cache_.Invalidate(invalidated_range, job_time); - } else { - audio_playback_cache_.Invalidate(invalidated_range, job_time); - } + if (invalidated_range.in() != invalidated_range.out()) { + if (from == kTextureInput || from == kVideoParamsInput) { + video_frame_cache_.Invalidate(invalidated_range, job_time); + } else { + audio_playback_cache_.Invalidate(invalidated_range, job_time); } } } @@ -257,11 +257,6 @@ QVector ViewerOutput::inputs_for_output(const QString &output) const return inputs; } -const rational& ViewerOutput::GetLength() const -{ - return last_length_; -} - QVector ViewerOutput::GetEnabledStreamsAsReferences() const { QVector refs; @@ -302,41 +297,21 @@ void ViewerOutput::Retranslate() void ViewerOutput::VerifyLength() { - NodeTraverser traverser; + rational subtitle_length; - rational video_length, audio_length, subtitle_length; - - { - video_length = GetCustomLength(Track::kVideo); - - if (video_length.isNull() && IsInputConnected(kTextureInput)) { - NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kTextureInput), TimeRange(0, 0)); - video_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value(); - } - - if (cache_enabled_) { - video_frame_cache_.SetLength(video_length); - } + video_length_ = VerifyLengthInternal(Track::kVideo); + if (video_cache_enabled_) { + video_frame_cache_.SetLength(video_length_); } - { - audio_length = GetCustomLength(Track::kAudio); - - if (audio_length.isNull() && IsInputConnected(kSamplesInput)) { - NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kSamplesInput), TimeRange(0, 0)); - audio_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value(); - } - - if (cache_enabled_) { - audio_playback_cache_.SetLength(audio_length); - } + audio_length_ = VerifyLengthInternal(Track::kAudio); + if (audio_cache_enabled_) { + audio_playback_cache_.SetLength(audio_length_); } - { - subtitle_length = GetCustomLength(Track::kSubtitle); - } + subtitle_length = VerifyLengthInternal(Track::kSubtitle); - rational real_length = qMax(subtitle_length, qMax(video_length, audio_length)); + rational real_length = qMax(subtitle_length, qMax(video_length_, audio_length_)); if (real_length != last_length_) { last_length_ = real_length; @@ -362,9 +337,30 @@ void ViewerOutput::InputDisconnectedEvent(const QString &input, int element, con super::InputDisconnectedEvent(input, element, output); } -rational ViewerOutput::GetCustomLength(Track::Type type) const +rational ViewerOutput::VerifyLengthInternal(Track::Type type) const { - Q_UNUSED(type) + NodeTraverser traverser; + + switch (type) { + case Track::kVideo: + if (IsInputConnected(kTextureInput)) { + NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kTextureInput), TimeRange(0, 0)); + qDebug() << "Got video length:" << t.Get(NodeValue::kRational, QStringLiteral("length")).value(); + return t.Get(NodeValue::kRational, QStringLiteral("length")).value(); + } + break; + case Track::kAudio: + if (IsInputConnected(kSamplesInput)) { + NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kSamplesInput), TimeRange(0, 0)); + return t.Get(NodeValue::kRational, QStringLiteral("length")).value(); + } + break; + case Track::kNone: + case Track::kSubtitle: + case Track::kCount: + break; + } + return rational(); } @@ -403,7 +399,7 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) } if (frame_rate_changed) { - if (cache_enabled_) { + if (video_cache_enabled_) { video_frame_cache_.SetTimebase(new_video_params.frame_rate_as_time_base()); } emit FrameRateChanged(new_video_params.frame_rate()); @@ -425,7 +421,7 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) emit AudioParamsChanged(); - if (cache_enabled_) { + if (audio_cache_enabled_) { audio_playback_cache_.SetParameters(GetAudioParams()); } @@ -529,11 +525,6 @@ int ViewerOutput::AddStream(Track::Type type, const QVariant& value) return index; } -void ViewerOutput::SetViewerCacheEnabled(bool e) -{ - cache_enabled_ = e; -} - void ViewerOutput::InputResized(const QString &input, int old_size, int new_size) { if (input == kVideoParamsInput || input == kAudioParamsInput) { diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index a56602b49..3e25c5e55 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -72,12 +72,24 @@ public: VideoParams GetVideoParams(int index = 0) const { - return GetStandardValue(kVideoParamsInput, index).value(); + // This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway), + // but it does suppress a warning message that we don't need + if (index < InputArraySize(kVideoParamsInput)) { + return GetStandardValue(kVideoParamsInput, index).value(); + } else { + return VideoParams(); + } } AudioParams GetAudioParams(int index = 0) const { - return GetStandardValue(kAudioParamsInput, index).value(); + // This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway), + // but it does suppress a warning message that we don't need + if (index < InputArraySize(kAudioParamsInput)) { + return GetStandardValue(kAudioParamsInput, index).value(); + } else { + return AudioParams(); + } } void SetVideoParams(const VideoParams &video, int index = 0) @@ -111,7 +123,9 @@ public: VideoParams GetFirstEnabledVideoStream() const; AudioParams GetFirstEnabledAudioStream() const; - const rational &GetLength() const; + const rational &GetLength() const { return last_length_; } + const rational &GetVideoLength() const { return video_length_; } + const rational &GetAudioLength() const { return audio_length_; } FrameHashCache* video_frame_cache() { @@ -174,7 +188,7 @@ protected: virtual void InputDisconnectedEvent(const QString &input, int element, const NodeOutput &output) override; - virtual rational GetCustomLength(Track::Type type) const; + virtual rational VerifyLengthInternal(Track::Type type) const; virtual void ShiftVideoEvent(const rational &from, const rational &to); @@ -188,10 +202,13 @@ protected: int AddStream(Track::Type type, const QVariant &value); - void SetViewerCacheEnabled(bool e); + void SetViewerVideoCacheEnabled(bool e) { video_cache_enabled_ = e; } + void SetViewerAudioCacheEnabled(bool e) { audio_cache_enabled_ = e; } private: rational last_length_; + rational video_length_; + rational audio_length_; FrameHashCache video_frame_cache_; @@ -205,7 +222,8 @@ private: TimelinePoints timeline_points_; - bool cache_enabled_; + bool video_cache_enabled_; + bool audio_cache_enabled_; private slots: void InputResized(const QString& input, int old_size, int new_size); diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 8344cca0d..dc7e112c8 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -43,14 +43,14 @@ Footage::Footage(const QString &filename) : ViewerOutput(false), cancelled_(nullptr) { - AddInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + SetCacheTextures(true); + SetViewerVideoCacheEnabled(false); + + PrependInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); Clear(); set_filename(filename); - - SetCacheTextures(true); - SetViewerCacheEnabled(false); } void Footage::Retranslate() @@ -180,7 +180,7 @@ void Footage::InputValueChangedEvent(const QString &input, int element) } } -rational Footage::GetCustomLength(Track::Type type) const +rational Footage::VerifyLengthInternal(Track::Type type) const { if (type == Track::kVideo) { VideoParams first_stream = GetFirstEnabledVideoStream(); @@ -196,7 +196,7 @@ rational Footage::GetCustomLength(Track::Type type) const } } - return super::GetCustomLength(type); + return super::VerifyLengthInternal(type); } QString Footage::GetColorspaceToUse(const VideoParams ¶ms) const @@ -295,10 +295,9 @@ QString Footage::DescribeVideoStream(const VideoParams ¶ms) QString Footage::DescribeAudioStream(const AudioParams ¶ms) { - return tr("%1: Audio - %2 Channel(s), %3Hz") - .arg(QString::number(params.stream_index()), - QString::number(params.channel_count()), - QString::number(params.sample_rate())); + return tr("%1: Audio - %n Channel(s), %2Hz", nullptr, params.channel_count()) + .arg(QString::number(params.stream_index()), + QString::number(params.sample_rate())); } void Footage::Hash(const QString& output, QCryptographicHash &hash, const rational &time) const diff --git a/app/node/project/footage/footage.h b/app/node/project/footage/footage.h index 153880987..bed9a1d63 100644 --- a/app/node/project/footage/footage.h +++ b/app/node/project/footage/footage.h @@ -58,7 +58,7 @@ public: virtual QString Name() const override { - return tr("Footage"); + return tr("Media"); } virtual QString id() const override @@ -195,7 +195,7 @@ protected: virtual void InputValueChangedEvent(const QString &input, int element) override; - virtual rational GetCustomLength(Track::Type type) const override; + virtual rational VerifyLengthInternal(Track::Type type) const override; private: QString GetColorspaceToUse(const VideoParams& params) const; diff --git a/app/node/project/projectviewmodel.cpp b/app/node/project/projectviewmodel.cpp index 08c0b773f..9804b74a9 100644 --- a/app/node/project/projectviewmodel.cpp +++ b/app/node/project/projectviewmodel.cpp @@ -430,9 +430,6 @@ void ProjectViewModel::ConnectItem(Node *n) connect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::FolderBeginRemoveItem); connect(f, &Folder::EndRemoveItem, this, &ProjectViewModel::FolderEndRemoveItem); - connect(f, &Folder::BeginInsertItem, this, &ProjectViewModel::ItemAdded); - connect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::ItemRemoved); - foreach (Node* c, f->children()) { ConnectItem(c); } @@ -450,9 +447,6 @@ void ProjectViewModel::DisconnectItem(Node *n) disconnect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::FolderBeginRemoveItem); disconnect(f, &Folder::EndRemoveItem, this, &ProjectViewModel::FolderEndRemoveItem); - disconnect(f, &Folder::BeginInsertItem, this, &ProjectViewModel::ItemAdded); - disconnect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::ItemRemoved); - foreach (Node* c, f->children()) { DisconnectItem(c); } diff --git a/app/node/project/projectviewmodel.h b/app/node/project/projectviewmodel.h index 1e396746a..9a57c80da 100644 --- a/app/node/project/projectviewmodel.h +++ b/app/node/project/projectviewmodel.h @@ -104,11 +104,6 @@ public: */ QModelIndex CreateIndexFromItem(Node *item, int column = 0); -signals: - void ItemAdded(Node* node); - - void ItemRemoved(Node* node); - private: /** * @brief Retrieve the index of `item` in its parent diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index b6c2f1653..3e7659dd7 100644 --- a/app/node/project/sequence/sequence.cpp +++ b/app/node/project/sequence/sequence.cpp @@ -107,7 +107,7 @@ void Sequence::Retranslate() } } -rational Sequence::GetCustomLength(Track::Type type) const +rational Sequence::VerifyLengthInternal(Track::Type type) const { if (!track_lists_.isEmpty()) { switch (type) { diff --git a/app/node/project/sequence/sequence.h b/app/node/project/sequence/sequence.h index c644bdc3f..6b2c1d0ac 100644 --- a/app/node/project/sequence/sequence.h +++ b/app/node/project/sequence/sequence.h @@ -103,7 +103,7 @@ protected: virtual void InputDisconnectedEvent(const QString &input, int element, const NodeOutput &output) override; - virtual rational GetCustomLength(Track::Type type) const override; + virtual rational VerifyLengthInternal(Track::Type type) const override; signals: void TrackAdded(Track* track); diff --git a/app/node/value.cpp b/app/node/value.cpp index 6ab6cd5cf..f3ec8655c 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -382,14 +382,26 @@ NodeValueTable NodeValueTable::Merge(QList tables) NodeValueTable merged_table; // Slipstreams all tables together - foreach (const NodeValueTable& t, tables) { - if (row >= t.Count()) { - continue; + while (true) { + bool all_merged = true; + + foreach (const NodeValueTable& t, tables) { + if (row < t.Count()) { + all_merged = false; + } else { + continue; + } + + int row_index = t.Count() - 1 - row; + + merged_table.Prepend(t.at(row_index)); } - int row_index = t.Count() - 1 - row; + row++; - merged_table.Prepend(t.at(row_index)); + if (all_merged) { + break; + } } return merged_table; diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index c580d4f14..a56e2ea82 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -57,7 +57,6 @@ ProjectPanel::ProjectPanel(QWidget *parent) : explorer_ = new ProjectExplorer(this); layout->addWidget(explorer_); connect(explorer_, &ProjectExplorer::DoubleClickedItem, this, &ProjectPanel::ItemDoubleClickSlot); - connect(explorer_, &ProjectExplorer::ItemRemoved, this, &ProjectPanel::ItemRemoved); // Set toolbar's view to the explorer's view toolbar->SetView(explorer_->view_type()); @@ -233,16 +232,6 @@ void ProjectPanel::SaveConnectedProject() Core::instance()->SaveProject(this->project()); } -void ProjectPanel::ItemRemoved(Node *item) -{ - // Open this footage in a FootageViewer - FootageViewerPanel* panel = PanelManager::instance()->MostRecentlyFocused(); - - if (panel->GetConnectedViewer() == item) { - panel->DisconnectViewerNode(); - } -} - QVector ProjectPanel::GetSelectedFootage() const { QVector items = SelectedItems(); diff --git a/app/panel/project/project.h b/app/panel/project/project.h index c16c36bd7..c8a8c2aef 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -80,8 +80,6 @@ private slots: void SaveConnectedProject(); - void ItemRemoved(Node* item); - }; } diff --git a/app/panel/viewer/viewer.cpp b/app/panel/viewer/viewer.cpp index a79f09b52..f5faf418c 100644 --- a/app/panel/viewer/viewer.cpp +++ b/app/panel/viewer/viewer.cpp @@ -25,13 +25,13 @@ namespace olive { ViewerPanel::ViewerPanel(const QString &object_name, QWidget *parent) : ViewerPanelBase(object_name, parent) { - // Set ViewerWidget as the central widget - ViewerWidget* vw = new ViewerWidget(); - connect(vw, &ViewerWidget::RequestScopePanel, this, &ViewerPanel::CreateScopePanel); - SetTimeBasedWidget(vw); + Init(); +} - // Set strings - Retranslate(); +ViewerPanel::ViewerPanel(QWidget *parent) : + ViewerPanelBase(QStringLiteral("ViewerPanel"), parent) +{ + Init(); } void ViewerPanel::Retranslate() @@ -41,4 +41,15 @@ void ViewerPanel::Retranslate() SetTitle(tr("Viewer")); } +void ViewerPanel::Init() +{ + // Set ViewerWidget as the central widget + ViewerWidget* vw = new ViewerWidget(); + connect(vw, &ViewerWidget::RequestScopePanel, this, &ViewerPanel::CreateScopePanel); + SetTimeBasedWidget(vw); + + // Set strings + Retranslate(); +} + } diff --git a/app/panel/viewer/viewer.h b/app/panel/viewer/viewer.h index 9d83e0c97..3de6172d3 100644 --- a/app/panel/viewer/viewer.h +++ b/app/panel/viewer/viewer.h @@ -34,10 +34,14 @@ class ViewerPanel : public ViewerPanelBase { Q_OBJECT public: ViewerPanel(const QString& object_name, QWidget* parent); + ViewerPanel(QWidget* parent); protected: virtual void Retranslate() override; +private: + void Init(); + }; } diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 57505219b..a01f52734 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -479,6 +479,7 @@ qint64 AudioPlaybackCache::PlaybackDevice::readData(char *data, qint64 maxSize) } } else { qWarning() << "Failed to read data from segment"; + break; } } diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index a7644d636..ab2e65f3c 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -197,6 +197,11 @@ bool FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) cons bool FrameHashCache::SaveCacheFrame(const QString &cache_path, const QByteArray &hash, char *data, const VideoParams &vparam, int linesize_bytes) { + if (cache_path.isEmpty()) { + qWarning() << "Failed to save cache frame with empty path"; + return false; + } + QString fn = CachePathName(cache_path, hash); if (SaveCacheFrame(fn, data, vparam, linesize_bytes)) { @@ -226,6 +231,11 @@ bool FrameHashCache::SaveCacheFrame(const QString &cache_path, const QByteArray FramePtr FrameHashCache::LoadCacheFrame(const QString &cache_path, const QByteArray &hash) { + if (cache_path.isEmpty()) { + qWarning() << "Failed to save cache frame with empty path"; + return nullptr; + } + return LoadCacheFrame(CachePathName(cache_path, hash)); } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 12eabb035..373d3b278 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -678,10 +678,6 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) copied_viewer_node_ = static_cast(copy_map_.value(viewer_node_)); copied_color_manager_ = static_cast(copy_map_.value(viewer_node_->project()->color_manager())); - // Copy parameters - copied_viewer_node_->SetVideoParams(viewer_node_->GetVideoParams()); - copied_viewer_node_->SetAudioParams(viewer_node_->GetAudioParams()); - // Add all connections foreach (Node* node, graph->nodes()) { for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index 938a242e2..0766eecd5 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -20,8 +20,6 @@ #include "renderer.h" -#include - #include "common/ocioutils.h" namespace olive { diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 79c6a5d43..adfba9ced 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -540,13 +540,17 @@ QVariant RenderProcessor::ProcessFrameGeneration(const Node *node, const Generat bool RenderProcessor::CanCacheFrames() { - return true; + return ticket_->property("type").value() == RenderManager::kTypeVideo; } QVariant RenderProcessor::GetCachedTexture(const QByteArray& hash) { - VideoParams video_params = GetCacheVideoParams(); QString cache_dir = ticket_->property("cache").toString(); + if (cache_dir.isEmpty()) { + return QVariant(); + } + + VideoParams video_params = GetCacheVideoParams(); FramePtr f = FrameHashCache::LoadCacheFrame(cache_dir, hash); diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index 3c317773b..662342daa 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -41,7 +41,7 @@ ProjectImportTask::ProjectImportTask(ProjectViewModel *model, Folder *folder, co file_count_ = Core::CountFilesInFileList(filenames_); - SetTitle(tr("Importing %1 file(s)").arg(file_count_)); + SetTitle(tr("Importing %n file(s)", nullptr, file_count_)); } const int &ProjectImportTask::GetFileCount() const @@ -139,7 +139,8 @@ void ProjectImportTask::ValidateImageSequence(Footage *footage, QFileInfoList& i // By this point we've established that video contains a single still image stream. Now we'll // see if it ends with numbers. if (Decoder::GetImageSequenceDigitCount(footage->filename()) > 0 - && !image_sequence_ignore_files_.contains(footage->filename())) { + && !image_sequence_ignore_files_.contains(footage->filename()) + && footage->InputArraySize(Footage::kVideoParamsInput)) { VideoParams video_stream = footage->GetVideoParams(0); QSize dim(video_stream.width(), video_stream.height()); diff --git a/app/ts/en_US.ts b/app/ts/en_US.ts index 6e8cb8d60..645d9a47f 100644 --- a/app/ts/en_US.ts +++ b/app/ts/en_US.ts @@ -1,4767 +1,54 @@ - + - AudioParams - - - %1 Hz - - - - - Mono - - - - - Stereo - - - - - 2.1 - - - - - 5.1 - - - - - 7.1 - - - - - Unknown (0x%1) - - - - - Config - - - Error loading settings - - - - - Failed to load application settings. This session will use defaults. - -%1 - - - - - Error saving settings - - - - - Failed to save application settings. The application may lack write permissions to this location. - - - - - Footage - - - %1 FPS - - - - - %1 Hz - - - - - Filename: %1 - - - - - This footage is not valid for use - - - - - ImportTool - - - Don't ask me again - - - - - No Active Sequence - - - - - No sequence is currently open. Would you like to create one? - - - - - Automatically Detect Parameters From Footage - - - - - Set Parameters Manually - - - - - MoveItemCommand - - - Move Item - - - - - NodeCopyPasteWidget - - - Error pasting nodes - - - - - Failed to paste nodes: %1 - - - - - NodeFactory - - - None - - - - - NodeViewItem - - - %1... - - - - - PresetManager - - - Save Preset - - - - - Set preset name: - - - - - Invalid preset name - - - - - You must enter a preset name - - - - - Preset exists - - - - - A preset with this name already exists. Would you like to replace it? - - - - - RatioDialog - - - Enter custom ratio (e.g. "4:3", "16/9", etc.): - - - - - Invalid custom ratio - - - - - Failed to parse "%1" into an aspect ratio. Please format a rational fraction with a ':' or a '/' separator. - - - - - RenameItemCommand - - - Rename Item - - - - - Sequence - - - %1 FPS - - - - - Stream - - - %1: Audio - %2 Channels, %3Hz - - - - - %1: Unknown - - - - - %1: Image - %2x%3 - - - - - %1: Video - %2x%3 - - - - - TimelineViewBlockItem - - - %1 - -In: %2 -Out: %3 -Length: %4 - - - - - Tool - - - Empty - - - - - Bars - - - - - Solid - - - - - Title - - - - - Tone - - - - - Unknown - - - - - VideoParams - - - 8-bit - - - - - 16-bit Integer - - - - - Half-Float (16-bit) - - - - - Full-Float (32-bit) - - - - - Unknown (0x%1) - - - - - %1 FPS - - - - - Square Pixels (%1) - - - - - NTSC Standard (%1) - - - - - NTSC Widescreen (%1) - - - - - PAL Standard (%1) - - - - - PAL Widescreen (%1) - - - - - HD Anamorphic 1080 (%1) - - - - - main - - - Show this help text - - - - - Show application version - - - - - Start in full-screen mode - - - - - Export only (No GUI) - - - - - Override language with file - - - - - qm-file - - - - - Project to open on startup - - - - - olive::AboutDialog - - - About %1 - - - - - Olive is a non-linear video editor. This software is free and protected by the GNU GPL. - - - - - Olive Team is obliged to inform users that Olive source code is available for download from its website. - - - - - olive::ActionSearch - - - Search for action... - - - - - olive::AudioInput - - - Audio Input - - - - - Audio - - - - - Import an audio footage stream. - - - - - olive::AudioMonitorPanel - - - Audio Monitor - - - - - olive::Block - - - Length - - - - - Media In - - - - - Enabled - - - - - Speed - - - - - olive::BlurFilterNode - - - Blur - - - - - Blurs an image. - - - - - Input - - - - - Method - - - - - Box - - - - - Gaussian - - - - - Radius - - - - - Horizontal - - - - - Vertical - - - - - Repeat Edge Pixels - - - - - olive::ClipBlock - - - Clip - - - - - A time-based node that represents a media source. - - - - - Buffer - - - - - olive::ColorDialog - - - Select Color - - - - - olive::ColorSpaceChooser - - - Color Management - - - - - Input: - - - - - Color Space: - - - - - Display: - - - - - View: - - - - - Look: - - - - - (None) - - - - - olive::ColorValuesTab - - - Red - - - - - Green - - - - - Blue - - - - - olive::ColorValuesWidget - - - Preview - - - - - Input - - - - - Reference - - - - - Display - - - - - olive::ConformTask - - - Conforming Audio %1:%2 - - - - - olive::Core - - - Import error - - - - - Nothing to import - - - - - Importing... - - - - - Import footage... - - - - - Failed to import footage - - - - - Failed to find active Project panel - - - - - No Active Project - - - - - No project is currently open to set the properties for - - - - - Failed to create new folder - - - - - - Failed to find active project - - - - - New Folder - - - - - Failed to create new sequence - - - - - Possible image sequence detected - - - - - The file '%1' looks like it might be part of an image sequence. Would you like to import it as such? - - - - - You must specify a project file to export - - - - - Specified project does not exist - - - - - Project contains no sequences, nothing to export - - - - - This project has multiple sequences. Which do you wish to export? - - - - - Enter number (or %1 to cancel): - - - - - Invalid sequence number - - - - - Export succeeded - - - - - Export failed: %1 - - - - - Project failed to load: %1 - - - - - Failed to open startup file - - - - - The project "%1" doesn't exist. A new project will be started instead. - - - - - - Missing OpenTimelineIO Libraries - - - - - - This build was compiled without OpenTimelineIO and therefore cannot open OpenTimelineIO files. - - - - - Save Project - - - - - - Error - - - - - This Sequence is empty. There is nothing to export. - - - - - No valid sequence detected. - -Make sure a sequence is loaded and it has a connected Viewer node. - - - - - Olive Project - - - - - OpenTimelineIO - - - - - Save Project As - - - - - Load Project - - - - - Label Node - - - - - Set node label - - - - - Sequence %1 - - - - - Cannot open recent project - - - - - The project "%1" doesn't exist. Would you like to remove this file from the recent list? - - - - - Unsaved Changes - - - - - The project '%1' has unsaved changes. Would you like to save them? - - - - - Save - - - - - Save All - - - - - Don't Save - - - - - Don't Save All - - - - - Failed to cache sequence - - - - - No active viewer found with this sequence. - - - - - Open Project - - - - - olive::CrashHandlerDialog - - - Olive - - - - - We're sorry, Olive has crashed. Please help us fix it by sending an error report. - - - - - Describe what you were doing in as much detail as possible. If you can, provide steps to reproduce this crash. - - - - - Crash Report: - - - - - Send Error Report - - - - - Don't Send - - - - - Waiting for crash report to be generated... - - - - - Upload Failed - - - - - Failed to send error report. Please try again later. - - - - - No Crash Summary - - - - - Are you sure you want to send an error report with no crash summary? - - - - - olive::CrossDissolveTransition - - - Cross Dissolve - - - - - Smoothly transition between two clips. - - - - - olive::CurvePanel - - - Curve Editor - - - - - olive::CurveView - - - Zoom to Fit - - - - - olive::CurveWidget - - - Linear - - - - - Bezier - - - - - Hold - - - - - olive::DipToColorTransition - - - Dip To Color - - - - - Transition between clips by dipping to a color. - - - - - olive::DiskCacheDialog - - - Disk Cache: %1 - - - - - Disk Cache Settings - - - - - Maximum Disk Cache: - - - - - %1 GB - - - - - - - Clear Disk Cache - - - - - Automatically clear disk cache on close - - - - - Are you sure you want to clear the disk cache in '%1'? - - - - - Disk Cache Cleared - - - - - Disk cache failed to fully clear. You may have to delete the cache files manually. - - - - - Disk Cache Partially Cleared - - - - - olive::DiskManager - - - - Disk Cache Error - - - - - Unable to set custom application disk cache. Using default instead. - - - - - Disk Cache - - - - - You've chosen to change the default disk cache location. This will invalidate your current cache. Would you like to continue? - - - - - Failed to open disk cache at "%1". Try a different folder. - - - - - olive::ElapsedCounterWidget - - - Elapsed: %1 - - - - - Remaining: %1 - - - - - olive::ExportAdvancedVideoDialog - - - Advanced - - - - - Pixel - - - - - Pixel Format: - - - - - Performance - - - - - Threads: - - - - - olive::ExportAudioTab - - - Codec: - - - - - Sample Rate: - - - - - Channel Layout: - - - - - Format: - - - - - olive::ExportCodec - - - DNxHD - - - - - H.264 - - - - - H.265 - - - - - OpenEXR - - - - - PNG - - - - - ProRes - - - - - TIFF - - - - - MP2 - - - - - MP3 - - - - - AAC - - - - - PCM (Uncompressed) - - - - - Unknown - - - - - olive::ExportDialog - - - Filename: - - - - - Browse for exported file filename - - - - - Preset: - - - - - Same As Source - High Quality - - - - - Same As Source - Medium Quality - - - - - Same As Source - Low Quality - - - - - Range: - - - - - Entire Sequence - - - - - In to Out - - - - - Format: - - - - - Export Video - - - - - Export Audio - - - - - Video - - - - - Audio - - - - - - Export - - - - - Preview - - - - - Invalid parameters - - - - - Both video and audio are disabled. There's nothing to export. - - - - - Invalid filename - - - - - The filename must contain the extension "%1". Would you like to append it automatically? - - - - - Failed to create output directory - - - - - The intended output directory doesn't exist and Olive couldn't create it. Please choose a different filename. - - - - - Confirm Overwrite - - - - - The file "%1" already exists. Do you want to overwrite it? - - - - - Invalid Parameters - - - - - Width and height must be multiples of 2. - - - - - olive::ExportFormat - - - DNxHD - - - - - Matroska Video - - - - - MPEG-4 Video - - - - - OpenEXR - - - - - PNG - - - - - TIFF - - - - - QuickTime - - - - - Unknown - - - - - olive::ExportTask - - - Exporting "%1" - - - - - Failed to create encoder - - - - - Failed to open file - - - - - Failed to overwrite "%1". Export has been saved as "%2" instead. - - - - - olive::ExportVideoTab - - - Basic - - - - - Width: - - - - - Height: - - - - - Maintain Aspect Ratio: - - - - - Scaling Method: - - - - - Fit - - - - - Stretch - - - - - Crop - - - - - Frame Rate: - - - - - Pixel Aspect Ratio: - - - - - Interlacing: - - - - - Quality: - - - - - Codec - - - - - Codec: - - - - - Advanced - - - - - olive::FloatSlider - - - %1 dB - - - - - %1% - - - - - olive::FootagePropertiesDialog - - - "%1" Properties - - - - - Name: - - - - - Tracks: - - - - - olive::FootageRelinkDialog - - - Footage - - - - - Filename - - - - - Actions - - - - - Browse - - - - - Relink Footage - - - - - Relink "%1" - - - - - All Files - - - - - olive::FootageViewerPanel - - - Footage Viewer - - - - - olive::GapBlock - - - Gap - - - - - A time-based node that represents an empty space. - - - - - olive::H264BitRateSection - - - Target Bit Rate (Mbps): - - - - - Maximum Bit Rate (Mbps): - - - - - Two-Pass - - - - - olive::H264FileSizeSection - - - Target File Size (MB): - - - - - Two-Pass - - - - - olive::H264Section - - - Compression Method: - - - - - Constant Rate Factor - - - - - Target Bit Rate - - - - - Target File Size - - - - - olive::ImageSection - - - Image Sequence: - - - - - olive::InterlacedComboBox - - - None (Progressive) - - - - - Top-Field First - - - - - Bottom-Field First - - - - - olive::KeyframePropertiesDialog - - - Keyframe Properties - - - - - In: - - - - - Out: - - - - - Linear - - - - - Hold - - - - - Bezier - - - - - olive::KeyframeViewBase - - - Linear - - - - - Bezier - - - - - Hold - - - - - P&roperties - - - - - olive::LoadOTIOTask - - - Failed to load OpenTimelineIO from file "%1" - - - - - Unknown OpenTimelineIO root element - - - - - Failed to load clip - - - - - olive::MainMenu - - - &Save '%1' - - - - - Save '%1' &As - - - - - Close '%1' - - - - - Close All Except '%1' - - - - - &Save Project - - - - - Save Project &As - - - - - Close Project - - - - - Close All Except Current Project - - - - - (None) - - - - - &File - - - - - &New - - - - - &Open Project - - - - - Open &Recent - - - - - &Clear Recent List - - - - - Sa&ve All Projects - - - - - &Import... - - - - - &Export - - - - - &Media... - - - - - &Project Properties... - - - - - Close All Projects - - - - - E&xit - - - - - &Edit - - - - - Insert - - - - - Overwrite - - - - - Select &All - - - - - Deselect All - - - - - Ripple to In Point - - - - - Ripple to Out Point - - - - - Edit to In Point - - - - - Edit to Out Point - - - - - Delete In/Out Point - - - - - Ripple Delete In/Out Point - - - - - Set/Edit Marker - - - - - &View - - - - - Zoom In - - - - - Zoom Out - - - - - Increase Track Height - - - - - Decrease Track Height - - - - - Toggle Show All - - - - - Full Screen - - - - - Full Screen Viewer - - - - - &Playback - - - - - Go to Start - - - - - Previous Frame - - - - - Play/Pause - - - - - Play In to Out - - - - - Next Frame - - - - - Go to End - - - - - Go to Previous Cut - - - - - Go to Next Cut - - - - - Go to In Point - - - - - Go to Out Point - - - - - Shuttle Left - - - - - Shuttle Stop - - - - - Shuttle Right - - - - - Loop - - - - - &Sequence - - - - - Cache Entire Sequence - - - - - Cache Sequence In/Out - - - - - Maximize Panel - - - - - Lock Panels - - - - - Reset to Default Layout - - - - - &Tools - - - - - Pointer Tool - - - - - Edit Tool - - - - - Ripple Tool - - - - - Rolling Tool - - - - - Razor Tool - - - - - Slip Tool - - - - - Slide Tool - - - - - Hand Tool - - - - - Zoom Tool - - - - - Transition Tool - - - - - Enable Snapping - - - - - Preferences - - - - - &Help - - - - - A&ction Search - - - - - Send &Feedback... - - - - - &About... - + olive::Footage + + %1: Audio - %n Channel(s), %2Hz + + %1: Audio - %n Channel, %2Hz + %1: Audio - %n Channels, %2Hz + olive::MainStatusBar - - - Welcome to %1 %2 - - - - - Running %1 background tasks - - - - - olive::MainWindow - - - Driver Warning - - - - - Olive has detected your system is using the Nouveau graphics driver. - -This driver is known to have stability and performance issues with Olive. It is highly recommended you install the proprietary NVIDIA driver before continuing to use Olive. - - - - - olive::ManagedDisplayWidget - - - Color Space - - - - - No color manager connected - - - - - Display - - - - - View - - - - - Look - - - - - (None) - - - - - OpenColorIO Error - - - - - Failed to set color configuration: %1 - - - - - olive::ManagedPixelSamplerWidget - - - Display - - - - - Reference - - - - - olive::MathNode - - - Math - - - - - Perform a mathematical operation between two values. - - - - - Method - - - - - - Value - - - - - Add - - - - - Subtract - - - - - Multiply - - - - - Divide - - - - - Power - - - - - olive::MatrixGenerator - - - Orthographic Matrix - - - - - Ortho - - - - - Generate an orthographic matrix using position, rotation, and scale. - - - - - Position - - - - - Rotation - - - - - Scale - - - - - Uniform Scale - - - - - Anchor Point - - - - - olive::MediaInput - - - Footage - - - - - olive::MenuShared - - - &Project - - - - - &Sequence - - - - - &Folder - - - - - Cu&t - - - - - Cop&y - - - - - &Paste - - - - - Paste Insert - - - - - Duplicate - - - - - Delete - - - - - Ripple Delete - - - - - Split - - - - - Set In Point - - - - - Set Out Point - - - - - Reset In Point - - - - - Reset Out Point - - - - - Clear In/Out Point - - - - - Add Default Transition - - - - - Link/Unlink - - - - - Enable/Disable - - - - - Nest - - - - - Frames - - - - - Drop Frame - - - - - Non-Drop Frame - - - - - Milliseconds - - - - - Seconds - - - - - olive::MergeNode - - - Merge - - - - - Merge two textures together. - - - - - Base - - - - - Blend - - - - - olive::Node - - - Input - - - - - Output - - - - - General - - - - - Math - - - - - Color - - - - - Filter - - - - - Timeline - - - - - Generator - - - - - Channel - - - - - Transition - - - - - Uncategorized - - - - - olive::NodeInput - - - Input - - - - - olive::NodeOutput - - - Output - - - - - olive::NodePanel - - - Node Editor - - - - - olive::NodeParam - - - Value - - - - - None - - - - - Integer - - - - - Float - - - - - Rational - - - - - Boolean - - - - - Color - - - - - Matrix - - - - - Text - - - - - Font - - - - - File - - - - - Texture - - - - - Samples - - - - - Footage - - - - - Vector 2D - - - - - Vector 3D - - - - - Vector 4D - - - - - Unknown - + + Running %n background task(s) + + Running %n background task + Running %n background tasks + olive::NodeParamViewArrayWidget - - - + - - - - - %1 elements - - - - - olive::NodeParamViewConnectedLabel - - - Connected to - - - - - Nothing - - - - - Disconnect - - - - - olive::NodeParamViewItem - - - %1 (%2) - - - - - olive::NodeParamViewItemBody - - - %1: - - - - - olive::NodeParamViewKeyframeControl - - - Warning - - - - - Are you sure you want to disable keyframing on this value? This will clear all existing keyframes. - - - - - olive::NodeTablePanel - - - Table View - - - - - olive::NodeTableView - - - Type - - - - - Source - - - - - R/X - - - - - G/Y - - - - - B/Z - - - - - A/W - - - - - (unknown) - - - - - olive::NodeTreeView - - - Nodes - - - - - olive::NodeView - - - Label - - - - - Auto-Position - - - - - Smooth Edges - - - - - Filter - - - - - Show All - - - - - Show Selected Blocks Only - - - - - Direction - - - - - Top to Bottom - - - - - Bottom to Top - - - - - Left to Right - - - - - Right to Left - - - - - Add - - - - - olive::PanNode - - - - Pan - - - - - Adjust the stereo panning of an audio source. - - - - - Samples - - - - - olive::PanelWidget - - - %1: %2 - - - - - olive::ParamPanel - - - Parameter Editor - - - - - (none) - - - - - (multiple) - - - - - olive::PathWidget - - - Browse - - - - - Browse for path - - - - - olive::PixelAspectRatioComboBox - - - Set Custom Pixel Aspect Ratio - - - - - Custom... - - - - - Custom (%1) - - - - - olive::PixelSamplerPanel - - - Pixel Sampler - - - - - olive::PixelSamplerWidget - - - Color - - - - - <html><font color='#FF8080'>R: %1</font><br><font color='#80FF80'>G: %2</font><br><font color='#8080FF'>B: %3</font><br>A: %4</html> - - - - - olive::PolygonGenerator - - - Polygon - - - - - Generate a 2D polygon of any amount of points. - - - - - Points - - - - - Color - - - - - olive::PreCacheTask - - - Pre-caching %1:%2 - - - - - olive::PreferencesAppearanceTab - - - Theme - - - - - Node Color Scheme - - - - - olive::PreferencesAudioTab - - - Output Device: - - - - - Input Device: - - - - - Sample Rate: - - - - - Audio Recording: - - - - - Mono - - - - - Stereo - - - - - Refresh Devices - - - - - Please wait... - - - - - Default - - - - - olive::PreferencesBehaviorTab - - - Behavior - - - - - General - - - - - Enable hover focus - - - - - Panels will be considered focused when the mouse cursor is over them without having to click them. - - - - - Scroll wheel zooms by default instead of scrolling - - - - - Holding CTRL while using Olive toggles this setting - - - - - Audio - - - - - Enable audio scrubbing - - - - - Timeline - - - - - Auto-Seek to Imported Clips - - - - - Edit Tool Also Seeks - - - - - Edit Tool Selects Links - - - - - Enable Drag Files to Timeline - - - - - Invert Timeline Scroll Axes - - - - - Hold ALT on any UI element to switch scrolling axes - - - - - Seek Also Selects - - - - - Seek to the End of Pastes - - - - - Selecting Also Seeks - - - - - Playback - - - - - Ask For Name When Setting Marker - - - - - Automatically rewind at the end of a sequence - - - - - Project - - - - - Drop Files on Media to Replace - - - - - Nodes - - - - - Add Default Effects to New Clips - - - - - Auto-Scale By Default - - - - - Splitting Clips Copies Dependencies - - - - - Multiple clips can share the same nodes. Disable this to automatically share node dependencies among clips when copying or splitting them. - - - - - olive::PreferencesDialog - - - Preferences - - - - - General - - - - - Appearance - - - - - Behavior - - - - - Disk - - - - - Audio - - - - - Keyboard - - - - - olive::PreferencesDiskTab - - - Disk Management - - - - - Disk Cache Location: - - - - - Disk Cache Settings - - - - - Cache Behavior - - - - - Cache Ahead: - - - - - - %1 seconds - - - - - Cache Behind: - - - - - Disk Cache - - - - - Failed to set disk cache location. Access was denied. - - - - - olive::PreferencesGeneralTab - - - Language: - - - - - Auto-Scroll Method: - - - - - None - - - - - Page Scrolling - - - - - Smooth Scrolling - - - - - Rectified Waveforms: - - - - - Default Still Image Length: - - - - - %1 seconds - - - - - %1 (%2) - - - - - olive::PreferencesKeyboardTab - - - Search for action or shortcut - - - - - Action - - - - - Shortcut - - - - - Import - - - - - Export - - - - - Reset Selected - - - - - Reset All - - - - - Confirm Reset All Shortcuts - - - - - Are you sure you wish to reset all keyboard shortcuts to their defaults? - - - - - Import Keyboard Shortcuts - - - - - - Error saving shortcuts - - - - - Failed to open file for reading - - - - - Export Keyboard Shortcuts - - - - - Export Shortcuts - - - - - Shortcuts exported successfully - - - - - Failed to open file for writing - - - - - olive::ProgressDialog - - - Cancel - - - - - olive::Project - - - - (untitled) - - - - - olive::ProjectExplorer - - - &New - - - - - &Import... - - - - - &Project Properties... - - - - - Open in New Tab - - - - - Open in New Window - - - - - Reveal in Explorer - - - - - Reveal in Finder - - - - - Reveal in File Manager - - - - - Pre-Cache - - - - - No sequences exist in project - - - - - For "%1" - - - - - P&roperties - - - - - Confirm Footage Deletion - - - - - The footage "%1" is currently used in the following sequence(s): - -%2 -What would you like to do with these clips? - - - - - Offline Footage - - - - - Delete Clips - - - - - olive::ProjectExplorerNavigation - - - Go to parent folder - - - - - olive::ProjectImportErrorDialog - - - Import Error - - - - - The following files failed to import. Olive likely does not support their formats. - + + %n element(s) + + %n element + %n elements + olive::ProjectImportTask - - - Importing %1 files - - - - - olive::ProjectLoadBaseTask - - - Loading '%1' - - - - - olive::ProjectLoadTask - - - This project is newer than this version of Olive and cannot be opened. - - - - - - This project is from a version of Olive that is no longer supported in this version. - - - - - Failed to read file "%1" for reading. - - - - - olive::ProjectPanel - - - Folder - - - - - Project - - - - - (none) - - - - - olive::ProjectPropertiesDialog - - - Project Properties for '%1' - - - - - OpenColorIO Configuration: - - - - - (default) - - - - - Default Input Color Space: - - - - - Browse - - - - - Color Management - - - - - Use Default Location - - - - - Store Alongside Project - - - - - Use Custom Location: - - - - - Disk Cache Settings - - - - - - "Store alignside project" functionality not implemented yet - - - - - Disk Cache - - - - - OpenColorIO Config Error - - - - - Failed to set OpenColorIO configuration: %1 - - - - - Invalid path - - - - - The cache path is invalid. Please check it and try again. - - - - - Browse for OpenColorIO configuration - - - - - olive::ProjectSaveTask - - - Saving '%1' - - - - - Failed to write XML data - - - - - Failed to overwrite "%1". Project has been saved as "%2" instead. - - - - - Failed to open temporary file "%1" for writing. - - - - - olive::ProjectToolbar - - - New... - - - - - Open Project - - - - - Save Project - - - - - Undo - - - - - Redo - - - - - Search media, markers, etc. - - - - - Switch to Tree View - - - - - Switch to List View - - - - - Switch to Icon View - - - - - olive::ProjectViewModel - - - Name - - - - - Duration - - - - - Rate - - - - - Move Items - - - - - olive::RenderCancelDialog - - - Waiting for workers to finish... - - - - - Renderer - - - - - olive::RichTextDialog - - - B - - - - - Bold - - - - - I - - - - - Italic - - - - - U - - - - - Underline - - - - - S - - - - - Strikethrough - - - - - Font Family - - - - - Font Size - - - - - L - - - - - Left Align - - - - - C - - - - - Center Align - - - - - R - - - - - Right Align - - - - - J - - - - - Justify Align - - - - - olive::SaveOTIOTask - - - Exporting project to OpenTimelineIO - - - - - Project contains no sequences to export. - - - - - Failed to serialize sequence "%1" - - - - - olive::ScopePanel - - - Waveform - - - - - Histogram - - - - - Scope - - - - - olive::SequenceDialog - - - Name: - - - - - New Sequence - - - - - Editing "%1" - - - - - Error editing Sequence - - - - - Please enter a name for this Sequence. - - - - - olive::SequenceDialogParameterTab - - - Video - - - - - Width: - - - - - Height: - - - - - Frame Rate: - - - - - Pixel Aspect Ratio: - - - - - Interlacing: - - - - - Audio - - - - - Sample Rate: - - - - - Channels: - - - - - Preview - - - - - Resolution: - - - - - Quality: - - - - - Save Preset - - - - - (%1x%2) - - - - - olive::SequenceDialogPresetTab - - - Preset - - - - - My Presets - - - - - 4K UHD - - - - - 1080p - - - - - 720p - - - - - NTSC - - - - - PAL - - - - - %1 23.976 FPS - - - - - %1 25 FPS - - - - - %1 29.97 FPS - - - - - %1 50 FPS - - - - - %1 59.94 FPS - - - - - %1 Standard - - - - - %1 Widescreen - - - - - Delete Preset - - - - - olive::SequenceViewerPanel - - - Sequence Viewer - + + Importing %n file(s) + + Importing %n file + Importing %n files + olive::SliderBase - - - Invalid Value - - - - - The entered value is not valid for this field. - - - - - olive::SolidGenerator - - - Solid - - - - - Generate a solid color. - - - - - Color - - - - - olive::StringSlider - - - (none) - - - - - olive::StrokeFilterNode - - - Stroke - - - - - Creates a stroke outline around an image. - - - - - Input - - - - - Color - - - - - Radius - - - - - Opacity - - - - - Inner - - - - - olive::Task - - - Task - - - - - Unknown error - - - - - olive::TaskDialog - - - Task Failed - - - - - olive::TaskManagerPanel - - - Task Manager - - - - - olive::TaskViewItem - - - Error: %1 - - - - - olive::TextGenerator - - - Sample Text - - - - - - Text - - - - - Generate rich text. - - - - - Font - - - - - Font Size - - - - - Color - - - - - Vertical Align - - - - - Top - - - - - Center - - - - - Bottom - - - - - olive::TimeBasedPanel - - - (none) - - - - - olive::TimeBasedWidget - - - Set Marker - - - - - Marker name: - - - - - olive::TimeInput - - - Time - - - - - Generates the time (in seconds) at this frame - - - - - olive::TimelinePanel - - - Timeline - - - - - olive::TimelineWidget - - - - Properties - - - - - Use Audio Time Units - - - - - olive::ToolPanel - - - Tools - - - - - olive::Toolbar - - - Pointer Tool - - - - - Edit Tool - - - - - Ripple Tool - - - - - Rolling Tool - - - - - Razor Tool - - - - - Slip Tool - - - - - Slide Tool - - - - - Hand Tool - - - - - Zoom Tool - - - - - Transition Tool - - - - - Record Tool - - - - - Add Tool - - - - - Toggle Snapping - - - - - olive::TrackOutput - - - Track - - - - - Node for representing and processing a single array of Blocks sorted by time. Also represents the end of a Sequence. - - - - - Blocks - - - - - Muted - - - - - Video %1 - - - - - Audio %1 - - - - - Subtitle %1 - - - - - Track %1 - - - - - olive::TrackViewItem - - - M - - - - - L - - - - - olive::TransitionBlock - - - From - - - - - To - - - - - Curve - - - - - Linear - - - - - Exponential - - - - - Logarithmic - - - - - olive::TrigonometryNode - - - Trigonometry - - - - - Perform a trigonometry operation on a value. - - - - - Sine - - - - - Cosine - - - - - Tangent - - - - - Inverse Sine - - - - - Inverse Cosine - - - - - Inverse Tangent - - - - - Hyperbolic Sine - - - - - Hyperbolic Cosine - - - - - Hyperbolic Tangent - - - - - Method - - - - - olive::VideoDividerComboBox - - - Full - - - - - 1/%1 - - - - - olive::VideoInput - - - Video Input - - - - - Video - - - - - Import a video footage stream. - - - - - olive::VideoStreamProperties - - - Pixel Aspect: - - - - - Interlacing: - - - - - Color Space: - - - - - Default (%1) - - - - - Premultiplied Alpha - - - - - Image Sequence - - - - - Start Index: - - - - - End Index: - - - - - Frame Rate: - - - - - Invalid Configuration - - - - - Image sequence end index must be a value higher than the start index. - - - - - olive::ViewerOutput - - - Viewer - - - - - Interface between a Viewer panel and the node system. - - - - - Texture - - - - - Samples - - - - - Video Tracks - - - - - Audio Tracks - - - - - Subtitle Tracks - - - - - olive::ViewerPanel - - - Viewer - - - - - olive::ViewerWidget - - - Error - - - - - No in or out points are set to cache. - - - - - - Safe Margins - - - - - Zoom - - - - - Fit - - - - - %1% - - - - - Full Screen - - - - - Screen %1: %2x%3 - - - - - Deinterlace - - - - - Scopes - - - - - Cache - - - - - Auto-Cache - - - - - Pause Auto-Cache During Playback - - - - - Cache Entire Sequence - - - - - Cache Sequence In/Out - - - - - Off - - - - - On - - - - - Custom Aspect - - - - - Show Audio Waveform - - - - - olive::VolumeNode - - - - Volume - - - - - Adjusts the volume of an audio source. - - - - - Samples - + + %n minute(s) + + %n minute + %n minutes + diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index 22ba0eb58..0ba3f2475 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -36,11 +36,12 @@ const int kMaximumSmoothness = 8; AudioMonitor::AudioMonitor(QWidget *parent) : QOpenGLWidget(parent), file_(nullptr), + waveform_(nullptr), cached_channels_(0) { values_.resize(kMaximumSmoothness); - connect(AudioManager::instance(), &AudioManager::OutputDeviceStarted, this, &AudioMonitor::OutputDeviceSet); + connect(AudioManager::instance(), &AudioManager::OutputWaveformStarted, this, &AudioMonitor::OutputAudioVisualWaveformSet); connect(AudioManager::instance(), &AudioManager::OutputPushed, this, &AudioMonitor::OutputPushed); connect(AudioManager::instance(), &AudioManager::AudioParamsChanged, this, &AudioMonitor::SetParams); connect(AudioManager::instance(), &AudioManager::Stopped, this, &AudioMonitor::Stop); @@ -84,6 +85,7 @@ void AudioMonitor::Stop() { delete file_; file_ = nullptr; + waveform_ = nullptr; } void AudioMonitor::OutputPushed(const QByteArray &d) @@ -97,6 +99,20 @@ void AudioMonitor::OutputPushed(const QByteArray &d) SetUpdateLoop(true); } +void AudioMonitor::OutputAudioVisualWaveformSet(const AudioVisualWaveform *waveform, const rational &start, int playback_speed) +{ + Stop(); + + waveform_ = waveform; + waveform_time_ = start; + + playback_speed_ = playback_speed; + + last_time_ = QDateTime::currentMSecsSinceEpoch(); + + SetUpdateLoop(true); +} + void AudioMonitor::SetUpdateLoop(bool e) { if (e) { @@ -211,8 +227,24 @@ void AudioMonitor::paintGL() QVector v(params_.channel_count(), 0); - if (file_) { - UpdateValuesFromFile(v); + if (file_ || waveform_) { + // Determines how many milliseconds have passed since last update + qint64 current_time = QDateTime::currentMSecsSinceEpoch(); + qint64 delta_time = current_time - last_time_; + int abs_speed = qAbs(playback_speed_); + + // Multiply by speed if the speed is not 1 + if (abs_speed != 1) { + delta_time *= abs_speed; + } + + if (file_) { + UpdateValuesFromFile(v, delta_time); + } else if (waveform_) { + UpdateValuesFromWaveform(v, delta_time); + } + + last_time_ = current_time; } PushValue(v); @@ -254,7 +286,7 @@ void AudioMonitor::paintGL() } } - if (all_zeroes && !file_) { + if (all_zeroes && !file_ && !waveform_) { // Optimize by disabling the update loop SetUpdateLoop(false); } @@ -266,20 +298,10 @@ void AudioMonitor::mousePressEvent(QMouseEvent *) update(); } -void AudioMonitor::UpdateValuesFromFile(QVector& v) +void AudioMonitor::UpdateValuesFromFile(QVector& v, qint64 delta_time) { - // Determines how many milliseconds have passed since last update - qint64 current_time = QDateTime::currentMSecsSinceEpoch(); - qint64 time_passed = current_time - last_time_; - int abs_speed = qAbs(playback_speed_); - - // Multiply by speed if the speed is not 1 - if (abs_speed != 1) { - time_passed *= abs_speed; - } - // Convert ms to float seconds and determine how many bytes that is - qint64 bytes_to_read = params_.time_to_bytes(static_cast(time_passed) * 0.001); + qint64 bytes_to_read = params_.time_to_bytes(static_cast(delta_time) * 0.001); if (playback_speed_ < 0) { // If reversing, jump back by the amount of bytes we're going to read @@ -297,31 +319,35 @@ void AudioMonitor::UpdateValuesFromFile(QVector& v) file_->seek(file_->pos() - bytes_to_read); } - // If speed is not 1, transform it here - if (abs_speed != 1) { - int sample_sz = params_.samples_to_bytes(1); - int in_nb_samples = params_.bytes_to_samples(b.size()); - int out_nb_samples = in_nb_samples / abs_speed; - QByteArray speed_adjusted(out_nb_samples * sample_sz, Qt::Uninitialized); + BytesToSampleSummary(b, v); +} - for (int i=0;i &v, qint64 delta_time) +{ + // Delta time is provided in milliseconds, so we convert to seconds in rational + rational length(delta_time, 1000); + + AudioVisualWaveform::Sample sum = waveform_->GetSummaryFromTime(waveform_time_, length); + + for (int i=0; i v.at(output_index)) { + v[output_index] = max; } - - b = speed_adjusted; } - BytesToSampleSummary(b, v); - - last_time_ = current_time; + waveform_time_ += length; } void AudioMonitor::PushValue(const QVector &v) { - values_.removeFirst(); - values_.append(v); + int lim = values_.size()-1; + for (int i=0; i &v) diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index e9189b3d8..daeb5cec2 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -25,6 +25,7 @@ #include #include +#include "audio/audiovisualwaveform.h" #include "common/define.h" #include "render/audioparams.h" #include "render/audioplaybackcache.h" @@ -46,8 +47,9 @@ public slots: void OutputPushed(const QByteArray& d); + void OutputAudioVisualWaveformSet(const AudioVisualWaveform *waveform, const rational& start, int playback_speed); + protected: - //virtual void paintEvent(QPaintEvent* event) override; virtual void paintGL() override; virtual void mousePressEvent(QMouseEvent* event) override; @@ -55,7 +57,9 @@ protected: private: void SetUpdateLoop(bool e); - void UpdateValuesFromFile(QVector &v); + void UpdateValuesFromFile(QVector &v, qint64 delta_time); + + void UpdateValuesFromWaveform(QVector &v, qint64 delta_time); void PushValue(const QVector& v); @@ -68,6 +72,9 @@ private: QIODevice* file_; qint64 last_time_; + const AudioVisualWaveform* waveform_; + rational waveform_time_; + int playback_speed_; QVector< QVector > values_; diff --git a/app/widget/nodeparamview/nodeparamviewarraywidget.cpp b/app/widget/nodeparamview/nodeparamviewarraywidget.cpp index 45514ed91..8203131a3 100644 --- a/app/widget/nodeparamview/nodeparamviewarraywidget.cpp +++ b/app/widget/nodeparamview/nodeparamviewarraywidget.cpp @@ -53,7 +53,7 @@ void NodeParamViewArrayWidget::UpdateCounter(const QString& input, int old_size, { Q_UNUSED(old_size) if (input == input_) { - count_lbl_->setText(tr("%1 element(s)").arg(new_size)); + count_lbl_->setText(tr("%n element(s)", nullptr, new_size)); } } diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index b1c3e3dbf..aa8508ffd 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -381,7 +381,7 @@ void NodeParamViewItemBody::Retranslate() if (ic.IsArray() && ic.element() >= 0) { // Make the label the array index - i.value().main_label->setText(tr("%n:", nullptr, ic.element())); + i.value().main_label->setText(tr("%1:").arg(ic.element())); } else { // Set to the input's name i.value().main_label->setText(tr("%1:").arg(ic.name())); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 50af8ca33..e49096321 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -557,7 +557,7 @@ void NodeParamViewWidgetBridge::InputValueChanged(const NodeInput &input, const void NodeParamViewWidgetBridge::PropertyChanged(const QString& input, const QString &key, const QVariant &value) { - if (input != input_.input()) { + if (input != input_.input() || (input_.IsArray() && input_.element() == -1)) { return; } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 94028868b..7582ac5d2 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -668,6 +668,13 @@ void NodeView::ShowContextMenu(const QPoint &pos) QAction* autopos = m.addAction(tr("Auto-Position")); connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); + ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode()); + if (viewer) { + m.addSeparator(); + QAction* open_in_viewer_action = m.addAction(tr("Open in Viewer")); + connect(open_in_viewer_action, &QAction::triggered, this, &NodeView::OpenSelectedNodeInViewer); + } + } else { QAction* curved_action = m.addAction(tr("Smooth Edges")); @@ -756,6 +763,16 @@ void NodeView::ContextMenuFilterChanged(QAction *action) Q_UNUSED(action) } +void NodeView::OpenSelectedNodeInViewer() +{ + QVector selected = scene_.GetSelectedNodes(); + ViewerOutput* viewer = selected.isEmpty() ? nullptr : dynamic_cast(selected.first()); + + if (viewer) { + Core::instance()->OpenNodeInViewer(viewer); + } +} + void NodeView::AttachNodesToCursor(const QVector &nodes) { QVector items(nodes.size()); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 792d697b2..e01376419 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -194,6 +194,11 @@ private slots: */ void ContextMenuFilterChanged(QAction* action); + /** + * @brief Opens the selected node in a Viewer + */ + void OpenSelectedNodeInViewer(); + }; } diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 3c870caba..dddf8abff 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -95,8 +95,6 @@ ProjectExplorer::ProjectExplorer(QWidget *parent) : connect(tree_view_, &ProjectExplorerTreeView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); connect(list_view_, &ProjectExplorerListView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); connect(icon_view_, &ProjectExplorerIconView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); - - connect(&model_, &ProjectViewModel::ItemRemoved, this, &ProjectExplorer::ItemRemoved); } const ProjectToolbar::ViewType &ProjectExplorer::view_type() const diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 190faa510..5602a7e23 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -100,8 +100,6 @@ signals: */ void DoubleClickedItem(Node* item); - void ItemRemoved(Node* node); - private: /** * @brief Get all the blocks that solely rely on an input node diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 06853c5a9..b1490ed12 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -39,6 +39,7 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : dragged_diff_(0), require_valid_input_(true), tristate_(false), + format_plural_(false), drag_ladder_(nullptr), ladder_element_count_(0), dragged_(false) @@ -102,9 +103,10 @@ bool SliderBase::IsDragging() const return drag_ladder_; } -void SliderBase::SetFormat(const QString &s) +void SliderBase::SetFormat(const QString &s, const bool plural) { custom_format_ = s; + format_plural_ = plural; ForceLabelUpdate(); } @@ -114,6 +116,11 @@ void SliderBase::ClearFormat() ForceLabelUpdate(); } +bool SliderBase::IsFormatPlural() const +{ + return format_plural_; +} + void SliderBase::ForceLabelUpdate() { UpdateLabel(Value()); @@ -228,10 +235,17 @@ QString SliderBase::GetFormat() const } } +bool SliderBase::UsingLadders() const +{ + return ladder_element_count_ > 0 && Config::Current()[QStringLiteral("UseSliderLadders")].toBool(); +} + void SliderBase::UpdateLabel(const QVariant &v) { if (tristate_) { label_->setText("---"); + } else if (format_plural_) { + label_->setText(tr(GetFormat().toUtf8().constData(), nullptr, v.toInt())); } else { label_->setText(GetFormat().arg(ValueToString(v))); } @@ -322,7 +336,7 @@ void SliderBase::LadderDragged(int value, double multiplier) drag_ladder_->SetValue(ValueToString(clamped_temp_dragged_value_)); - if (!Config::Current()[QStringLiteral("UseSliderLadders")].toBool()) { + if (!UsingLadders()) { RepositionLadder(); } @@ -437,7 +451,7 @@ void SliderBase::ResetValue() void SliderBase::RepositionLadder() { if (drag_ladder_) { - if (Config::Current()[QStringLiteral("UseSliderLadders")].toBool()) { + if (UsingLadders()) { drag_ladder_->move(QCursor::pos() - QPoint(drag_ladder_->width()/2, drag_ladder_->height()/2)); } else { QPoint label_global_pos = label_->mapToGlobal(label_->pos()); diff --git a/app/widget/slider/sliderbase.h b/app/widget/slider/sliderbase.h index c5ac8dd58..720647721 100644 --- a/app/widget/slider/sliderbase.h +++ b/app/widget/slider/sliderbase.h @@ -61,9 +61,11 @@ public: bool IsDragging() const; - void SetFormat(const QString& s); + void SetFormat(const QString& s, const bool plural=false); void ClearFormat(); + bool IsFormatPlural() const; + void SetLadderElementCount(int b) { ladder_element_count_ = b; @@ -102,6 +104,8 @@ private: QString GetFormat() const; + bool UsingLadders() const; + SliderLabel* label_; FocusableLineEdit* editor_; @@ -130,6 +134,8 @@ private: QString custom_format_; + bool format_plural_; + SliderLadder* drag_ladder_; int ladder_element_count_; diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index 1e1145d36..2d9977be2 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -77,7 +77,7 @@ SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QString drag_timer_.setInterval(10); connect(&drag_timer_, &QTimer::timeout, this, &SliderLadder::TimerUpdate); - if (Config::Current()[QStringLiteral("UseSliderLadders")].toBool()) { + if (UsingLadders()) { drag_start_x_ = -1; } else { #if defined(Q_OS_MAC) @@ -95,7 +95,7 @@ SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QString SliderLadder::~SliderLadder() { - if (Config::Current()[QStringLiteral("UseSliderLadders")].toBool()) { + if (UsingLadders()) { } else { #if defined(Q_OS_MAC) @@ -143,7 +143,7 @@ void SliderLadder::TimerUpdate() int ladder_right = this->x() + this->width() - 1; int now_pos = QCursor::pos().x(); - if (Config::Current()[QStringLiteral("UseSliderLadders")].toBool()) { + if (UsingLadders()) { bool is_under_mouse = (now_pos >= ladder_left && now_pos <= ladder_right); @@ -227,7 +227,12 @@ void SliderLadder::TimerUpdate() } } -SliderLadderElement::SliderLadderElement(const double &multiplier, QString width_hint, QWidget *parent) : +bool SliderLadder::UsingLadders() const +{ + return elements_.size() > 1; +} + + SliderLadderElement::SliderLadderElement(const double &multiplier, QString width_hint, QWidget *parent) : QWidget(parent), multiplier_(multiplier), highlighted_(false), diff --git a/app/widget/slider/sliderladder.h b/app/widget/slider/sliderladder.h index 6587400bd..2f2ac7eff 100644 --- a/app/widget/slider/sliderladder.h +++ b/app/widget/slider/sliderladder.h @@ -83,6 +83,8 @@ signals: void Released(); private: + bool UsingLadders() const; + int drag_start_x_; int drag_start_y_; diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index d65ccacfc..df7d264ff 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -85,6 +85,7 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) // Disconnect length changed signal disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + disconnect(viewer_node_, &ViewerOutput::RemovedFromGraph, this, &TimeBasedWidget::ConnectedNodeRemovedFromGraph); // Disconnect rate change signals if they were connected disconnect(viewer_node_, &ViewerOutput::FrameRateChanged, this, &TimeBasedWidget::AutoUpdateTimebase); @@ -109,6 +110,7 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) if (viewer_node_) { // Connect length changed signal connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + connect(viewer_node_, &ViewerOutput::RemovedFromGraph, this, &TimeBasedWidget::ConnectedNodeRemovedFromGraph); // Connect ruler and scrollbar to timeline points ruler()->ConnectTimelinePoints(viewer_node_->GetTimelinePoints()); @@ -216,6 +218,11 @@ void TimeBasedWidget::AutoUpdateTimebase() } } +void TimeBasedWidget::ConnectedNodeRemovedFromGraph() +{ + ConnectViewerNode(nullptr); +} + TimeRuler *TimeBasedWidget::ruler() const { return ruler_; @@ -281,29 +288,27 @@ void TimeBasedWidget::PassWheelEventsToScrollBar(QObject *object) void TimeBasedWidget::SetTimestamp(int64_t timestamp) { - if (GetTime() != timestamp) { - if (UserIsDraggingPlayhead()) { - // If the user is dragging the playhead, we will simply nudge over and not use autoscroll rules. - QMetaObject::invokeMethod(this, "CatchUpScrollToPlayhead", Qt::QueuedConnection); - } else { - // Otherwise, assume we jumped to this out of nowhere and must now autoscroll - switch (static_cast(Config::Current()["Autoscroll"].toInt())) { - case AutoScroll::kNone: - // Do nothing - break; - case AutoScroll::kPage: - QMetaObject::invokeMethod(this, "PageScrollToPlayhead", Qt::QueuedConnection); - break; - case AutoScroll::kSmooth: - QMetaObject::invokeMethod(this, "CenterScrollOnPlayhead", Qt::QueuedConnection); - break; - } + if (UserIsDraggingPlayhead()) { + // If the user is dragging the playhead, we will simply nudge over and not use autoscroll rules. + QMetaObject::invokeMethod(this, "CatchUpScrollToPlayhead", Qt::QueuedConnection); + } else { + // Otherwise, assume we jumped to this out of nowhere and must now autoscroll + switch (static_cast(Config::Current()["Autoscroll"].toInt())) { + case AutoScroll::kNone: + // Do nothing + break; + case AutoScroll::kPage: + QMetaObject::invokeMethod(this, "PageScrollToPlayhead", Qt::QueuedConnection); + break; + case AutoScroll::kSmooth: + QMetaObject::invokeMethod(this, "CenterScrollOnPlayhead", Qt::QueuedConnection); + break; } - - ruler_->SetTime(timestamp); - - TimeChangedEvent(timestamp); } + + ruler_->SetTime(timestamp); + + TimeChangedEvent(timestamp); } void TimeBasedWidget::SetTimebase(const rational &timebase) diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 562b36a47..9429635c4 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -230,6 +230,8 @@ private slots: void AutoUpdateTimebase(); + void ConnectedNodeRemovedFromGraph(); + }; } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 799b0e21c..520a21c5e 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -50,8 +50,10 @@ namespace olive { +#define super TimeBasedWidget + TimelineWidget::TimelineWidget(QWidget *parent) : - TimeBasedWidget(true, true, parent), + super(true, true, parent), rubberband_(QRubberBand::Rectangle, this), active_tool_(nullptr), use_audio_time_units_(false) @@ -111,8 +113,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : connect(views_.first()->view()->horizontalScrollBar(), &QScrollBar::rangeChanged, scrollbar(), &QScrollBar::setRange); vert_layout->addWidget(scrollbar()); - connect(ruler(), &TimeRuler::TimeChanged, this, &TimelineWidget::SetViewTimestamp); - foreach (TimelineAndTrackView* tview, views_) { TimelineView* view = tview->view(); @@ -187,7 +187,7 @@ void TimelineWidget::Clear() void TimelineWidget::TimebaseChangedEvent(const rational &timebase) { - TimeBasedWidget::TimebaseChangedEvent(timebase); + super::TimebaseChangedEvent(timebase); timecode_label_->SetTimebase(timebase); @@ -198,7 +198,7 @@ void TimelineWidget::TimebaseChangedEvent(const rational &timebase) void TimelineWidget::resizeEvent(QResizeEvent *event) { - TimeBasedWidget::resizeEvent(event); + super::resizeEvent(event); // Update timecode label size UpdateTimecodeWidthFromSplitters(views_.first()->splitter()); @@ -206,6 +206,8 @@ void TimelineWidget::resizeEvent(QResizeEvent *event) void TimelineWidget::TimeChangedEvent(const int64_t& timestamp) { + super::TimeChangedEvent(timestamp); + SetViewTimestamp(timestamp); timecode_label_->SetValue(timestamp); @@ -213,7 +215,7 @@ void TimelineWidget::TimeChangedEvent(const int64_t& timestamp) void TimelineWidget::ScaleChangedEvent(const double &scale) { - TimeBasedWidget::ScaleChangedEvent(scale); + super::ScaleChangedEvent(scale); foreach (TimelineAndTrackView* view, views_) { view->view()->SetScale(scale); diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 83ceba890..719dbd367 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -180,9 +180,11 @@ void SeekableWidget::SeekToScreenPoint(int screen) } } - SetTime(timestamp); + if (timestamp != GetTime()) { + SetTime(timestamp); - emit TimeChanged(timestamp); + emit TimeChanged(timestamp); + } } void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) diff --git a/app/widget/videoparamedit/videoparamedit.cpp b/app/widget/videoparamedit/videoparamedit.cpp index 25ced1aee..63754ed5a 100644 --- a/app/widget/videoparamedit/videoparamedit.cpp +++ b/app/widget/videoparamedit/videoparamedit.cpp @@ -99,6 +99,8 @@ VideoParamEdit::VideoParamEdit(QWidget* parent) : // FIXME: Replace with rational slider frame_rate_slider_ = new FloatSlider(); + frame_rate_slider_->SetMinimum(0); + frame_rate_slider_->SetDecimalPlaces(2); connect(frame_rate_slider_, &FloatSlider::ValueChanged, this, &VideoParamEdit::Changed); layout->addWidget(frame_rate_slider_, row, 1); @@ -211,6 +213,8 @@ VideoParamEdit::VideoParamEdit(QWidget* parent) : void VideoParamEdit::SetParameterMask(uint64_t mask) { + mask_ = mask; + width_lbl_->setVisible(mask & kWidthHeight); width_slider_->setVisible(mask & kWidthHeight); height_lbl_->setVisible(mask & kWidthHeight); @@ -220,7 +224,7 @@ void VideoParamEdit::SetParameterMask(uint64_t mask) depth_slider_->setVisible(mask & kDepth); frame_rate_lbl_->setVisible(mask & kFrameRate); - frame_rate_combobox_->setVisible((mask & kFrameRate) && (mask & ~kFrameRateIsArbitrary)); + frame_rate_combobox_->setVisible((mask & kFrameRate) && !(mask & kFrameRateIsArbitrary)); frame_rate_slider_->setVisible((mask & kFrameRate) && (mask & kFrameRateIsArbitrary)); pixel_aspect_lbl_->setVisible(mask & kPixelAspect); diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 462d6ddf8..20921b94b 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -30,21 +30,38 @@ namespace olive { +#define super SeekableWidget + AudioWaveformView::AudioWaveformView(QWidget *parent) : - SeekableWidget(parent), + super(parent), playback_(nullptr) { setAutoFillBackground(true); setBackgroundRole(QPalette::Base); +} - cached_waveform_.resize(QThread::idealThreadCount()); +AudioVisualWaveform GenerateWaveform(QIODevice* device, AudioParams params, TimeRange range) +{ + device->open(QFile::ReadOnly); + device->seek(params.time_to_bytes(range.in())); + + SampleBufferPtr samples = SampleBuffer::CreateFromPackedData(params, device->read(params.time_to_bytes(range.length()))); + AudioVisualWaveform waveform; + waveform.set_channel_count(params.channel_count()); + waveform.OverwriteSamples(samples, params.sample_rate()); + device->close(); + delete device; + return waveform; } void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) { if (playback_) { - disconnect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::ForceUpdateOfRange); - disconnect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::BackendParamsChanged); + pool_.clear(); + pool_.waitForDone(); + + disconnect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::RenderRange); + //disconnect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::RenderRange); SetTimebase(0); } @@ -52,18 +69,20 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) playback_ = playback; if (playback_) { - connect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::ForceUpdateOfRange); - connect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::BackendParamsChanged); + connect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::RenderRange); + //connect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::RenderRange); SetTimebase(playback_->GetParameters().sample_rate_as_time_base()); - } - ForceUpdate(); + waveform_.set_channel_count(playback_->GetParameters().channel_count()); + + RenderRange(TimeRange(0, playback_->GetLength())); + } } void AudioWaveformView::paintEvent(QPaintEvent *event) { - QWidget::paintEvent(event); + super::paintEvent(event); if (!playback_) { return; @@ -80,40 +99,9 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) // Draw in/out points DrawTimelinePoints(&p); - CachedWaveformInfo wanted_info = {size(), GetScale(), GetScroll(), params}; - - for (int i=0; i(); - connect(cache.watcher, &QFutureWatcher::finished, this, &AudioWaveformView::BackgroundCacheFinished); - cache.watcher->setFuture(QtConcurrent::run(this, - &AudioWaveformView::DrawWaveform, - playback_->CreatePlaybackDevice(), - wanted_info, - slice_start, - slice_end)); - - } - } + // Draw waveform + p.setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color + AudioVisualWaveform::DrawWaveform(&p, rect(), GetScale(), waveform_, SceneToTime(GetScroll())); // Draw playhead p.setPen(PLAYHEAD_COLOR); @@ -122,117 +110,38 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) p.drawLine(playhead_x, 0, playhead_x, height()); } -QPixmap AudioWaveformView::DrawWaveform(QIODevice* fs, CachedWaveformInfo info, int slice_start, int slice_end) const +void AudioWaveformView::RenderRange(const TimeRange &range) { - QPixmap pixmap(slice_end - slice_start, info.size.height()); - pixmap.fill(Qt::transparent); + // Floor to second increments + int64_t start = qFloor(range.in().toDouble()); + int64_t end = qCeil(range.out().toDouble()); - if (fs->open(QFile::ReadOnly)) { + for (; start!=end; start++) { + TimeRange this_range(start, start+1); - QPainter wave_painter(&pixmap); + QFutureWatcher* watcher = new QFutureWatcher(); + connect(watcher, &QFutureWatcher::finished, this, &AudioWaveformView::BackgroundFinished); - // FIXME: Hardcoded color - wave_painter.setPen(QColor(64, 255, 160)); - - int drew = 0; - - fs->seek(info.params.samples_to_bytes(ScreenToUnitRounded(slice_start))); - - for (int x=slice_start; xatEnd(); x++) { - int samples_len = ScreenToUnitRounded(x+1) - ScreenToUnitRounded(x); - int max_read_size = info.params.samples_to_bytes(samples_len); - - QByteArray read_buffer = fs->read(max_read_size); - - // Detect whether we've reached EOF and recalculate sample count if so - if (read_buffer.size() < max_read_size) { - samples_len = info.params.bytes_to_samples(read_buffer.size()); - } - - QVector samples = AudioVisualWaveform::SumSamples(reinterpret_cast(read_buffer.constData()), - samples_len, - info.params.channel_count()); - - for (int i=0;iclose(); + jobs_.insert(this_range, watcher); + watcher->setFuture(QtConcurrent::run(&pool_, GenerateWaveform, playback_->CreatePlaybackDevice(), playback_->GetParameters(), this_range)); } - - delete fs; - - return pixmap; } -void AudioWaveformView::BackendParamsChanged() +void AudioWaveformView::BackgroundFinished() { - SetTimebase(playback_->GetParameters().sample_rate_as_time_base()); -} + QFutureWatcher* watcher = static_cast*>(sender()); -void AudioWaveformView::ForceUpdate() -{ - // Forces the cache to invalidate - for (int i=0; i= width()) { - return; - } - - int start_invalidate = qMax(0, in/cached_waveform_.size()); - int end_invalidate = qMin(cached_waveform_.size()-1, out/cached_waveform_.size()); - - for (int i=start_invalidate; i<=end_invalidate; i++) { - // Invalidate these - cached_waveform_[i].info.size = QSize(); - } - - update(); -} - -void AudioWaveformView::BackgroundCacheFinished() -{ - // Retrieve sender - QFutureWatcher* watcher = static_cast*>(sender()); - - // Determine index - int index = -1; - for (int i=0; iresult(); + waveform_.OverwriteSums(rendered, it.key().in()); + jobs_.erase(it); + update(); break; } } - if (index > -1) { - // Store generated pixmap - cached_waveform_[index].info = cached_waveform_[index].caching_info; - cached_waveform_[index].pixmap = watcher->result(); - cached_waveform_[index].watcher = nullptr; - - // Reset size - cached_waveform_[index].caching_info.size = QSize(); - - // Update with new pixmap - update(); - } - - // Clean up delete watcher; } diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h index b8f1ebd49..18e59eb7f 100644 --- a/app/widget/viewer/audiowaveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -41,51 +41,27 @@ public: void SetViewer(AudioPlaybackCache *playback); + const AudioVisualWaveform* waveform() const + { + return &waveform_; + } + protected: virtual void paintEvent(QPaintEvent* event) override; private: - struct CachedWaveformInfo { - QSize size; - double scale; - int scroll; - AudioParams params; + void RenderRange(const TimeRange& range); - bool operator==(const CachedWaveformInfo& rhs) const - { - return size == rhs.size - && qFuzzyCompare(scale, rhs.scale) - && scroll == rhs.scroll - && params == rhs.params; - } - - bool operator!=(const CachedWaveformInfo& rhs) const - { - return !(*this == rhs); - } - }; - - struct ActiveCache { - QPixmap pixmap; - CachedWaveformInfo info; - CachedWaveformInfo caching_info; - QFutureWatcher* watcher = nullptr; - }; - - QPixmap DrawWaveform(QIODevice *fs, CachedWaveformInfo info, int slice_start, int slice_end) const; + QThreadPool pool_; AudioPlaybackCache *playback_; - QVector cached_waveform_; + AudioVisualWaveform waveform_; + + QHash*> jobs_; private slots: - void BackendParamsChanged(); - - void ForceUpdate(); - - void ForceUpdateOfRange(const TimeRange& range); - - void BackgroundCacheFinished(); + void BackgroundFinished(); }; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 8a8a229d7..2a236c55f 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -72,6 +72,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : display_widget_ = new ViewerDisplayWidget(); display_widget_->setAcceptDrops(true); + display_widget_->SetShowWidgetBackground(true); connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); connect(display_widget_, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor); connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged); @@ -109,6 +110,11 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); layout->addWidget(controls_); + // If audio is invalidated during playback, we wait some time before starting it again + audio_restart_timer_.setInterval(250); + audio_restart_timer_.setSingleShot(true); + connect(&audio_restart_timer_, &QTimer::timeout, this, &ViewerWidget::StartAudioOutput); + // FIXME: Magic number SetScale(48.0); @@ -185,6 +191,8 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); connect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange); + connect(n->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &ViewerWidget::AudioCacheInvalidated); + connect(n->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &ViewerWidget::AudioCacheValidated); connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); VideoParams vp = n->GetVideoParams(); @@ -229,6 +237,8 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); disconnect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange); + disconnect(n->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &ViewerWidget::AudioCacheInvalidated); + disconnect(n->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &ViewerWidget::AudioCacheValidated); disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); ruler()->SetPlaybackCache(nullptr); @@ -390,13 +400,27 @@ void ViewerWidget::DecodeCachedImage(RenderTicketPtr ticket, const QString &fn, bool ViewerWidget::ShouldForceWaveform() const { return GetConnectedNode() - && !GetConnectedNode()->IsInputConnected(ViewerOutput::kTextureInput) - && GetConnectedNode()->IsInputConnected(ViewerOutput::kSamplesInput); + && !GetConnectedNode()->GetConnectedTextureOutput().IsValid() + && GetConnectedNode()->GetConnectedSampleOutput().IsValid(); +} + +void ViewerWidget::StartAudioOutput() +{ + AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache(); + if (audio_cache->GetParameters().is_valid()) { + AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); + AudioManager::instance()->StartOutput(audio_cache, + audio_cache->GetParameters().time_to_bytes(GetTime()), + playback_speed_); + emit AudioManager::instance()->OutputWaveformStarted(waveform_view_->waveform(), + GetTime(), playback_speed_); + } } void ViewerWidget::UpdateTextureFromNode(const rational& time) { bool frame_exists_at_time = FrameExistsAtTime(time); + bool frame_might_be_still = GetConnectedNode() && GetConnectedNode()->GetConnectedTextureOutput().IsValid() && GetConnectedNode()->GetVideoLength().isNull(); // Check playback queue for a frame if (IsPlaying()) { @@ -433,24 +457,24 @@ void ViewerWidget::UpdateTextureFromNode(const rational& time) } // Only show warning if frame actually exists - if (frame_exists_at_time) { + if (frame_exists_at_time && !frame_might_be_still) { qWarning() << "Playback queue failed to keep up"; } } - if (!frame_exists_at_time) { + if (frame_exists_at_time || frame_might_be_still) { + // Frame was not in queue, will require rendering or decoding from cache + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame); + nonqueue_watchers_.append(watcher); + watcher->SetTicket(GetFrame(time, true)); + } else { // There is definitely no frame here, we can immediately flip to showing nothing nonqueue_watchers_.clear(); SetDisplayImage(nullptr, false); return; } - - // Frame was not in queue, will require rendering or decoding from cache - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame); - nonqueue_watchers_.append(watcher); - watcher->SetTicket(GetFrame(time, true)); } void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) @@ -527,6 +551,7 @@ void ViewerWidget::PauseInternal() playback_queue_.clear(); playback_backup_timer_.stop(); + audio_restart_timer_.stop(); } prequeuing_ = false; @@ -593,9 +618,7 @@ QString ViewerWidget::GetCachedFilenameFromTime(const rational &time) bool ViewerWidget::FrameExistsAtTime(const rational &time) { - return GetConnectedNode() - && ((time >= 0 && time < GetConnectedNode()->video_frame_cache()->GetLength()) - || GetConnectedNode()->video_frame_cache()->GetLength().isNull()); + return GetConnectedNode() && time >= 0 && time < GetConnectedNode()->GetVideoLength(); } void ViewerWidget::SetDisplayImage(FramePtr frame, bool main_only) @@ -650,13 +673,7 @@ void ViewerWidget::FinishPlayPreprocess() { int64_t playback_start_time = ruler()->GetTime(); - AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache(); - if (audio_cache->GetParameters().is_valid()) { - AudioManager::instance()->SetOutputParams(audio_cache->GetParameters()); - AudioManager::instance()->StartOutput(audio_cache, - audio_cache->GetParameters().time_to_bytes(GetTime()), - playback_speed_); - } + StartAudioOutput(); playback_timer_.Start(playback_start_time, playback_speed_, timebase_dbl()); display_widget_->ResetFPSTimer(); @@ -1263,4 +1280,21 @@ void ViewerWidget::Dropped(QDropEvent *event) } } +void ViewerWidget::AudioCacheInvalidated() +{ + if (IsPlaying()) { + AudioManager::instance()->StopOutput(); + } +} + +void ViewerWidget::AudioCacheValidated() +{ + if (IsPlaying()) { + // This timer will restart audio + AudioManager::instance()->StopOutput(); + audio_restart_timer_.stop(); + audio_restart_timer_.start(); + } +} + } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index c3f35f505..2213acd79 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -245,6 +245,8 @@ private: PreviewAutoCacher auto_cacher_; + QTimer audio_restart_timer_; + static QVector instances_; private slots: @@ -292,6 +294,11 @@ private slots: void Dropped(QDropEvent* event); + void AudioCacheInvalidated(); + void AudioCacheValidated(); + + void StartAudioOutput(); + }; } diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index d7524c3e3..12a6305c4 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -49,7 +49,8 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : hand_dragging_(false), deinterlace_(false), show_fps_(false), - frames_skipped_(0) + frames_skipped_(0), + show_widget_background_(false) { connect(Core::instance(), &Core::ToolChanged, this, &ViewerDisplayWidget::UpdateCursor); @@ -301,7 +302,7 @@ void ViewerDisplayWidget::dropEvent(QDropEvent *event) void ViewerDisplayWidget::OnPaint() { // Clear background to empty - QColor bg_color = palette().window().color(); + QColor bg_color = show_widget_background_ ? palette().window().color() : Qt::black; renderer()->ClearDestination(bg_color.redF(), bg_color.greenF(), bg_color.blueF()); // We only draw if we have a pipeline diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 811c8a8d7..f7598d8f6 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -71,6 +71,12 @@ public: void SetVideoParams(const VideoParams ¶ms); void SetTime(const rational& time); + void SetShowWidgetBackground(bool e) + { + show_widget_background_ = e; + update(); + } + FramePtr last_loaded_buffer() const; /** @@ -288,6 +294,8 @@ private: QVector frame_rate_averages_; int frame_rate_average_count_; + bool show_widget_background_; + private slots: void EmitColorAtCursor(QMouseEvent* e); diff --git a/app/window/mainwindow/mainstatusbar.cpp b/app/window/mainwindow/mainstatusbar.cpp index c16cbce81..ad4732802 100644 --- a/app/window/mainwindow/mainstatusbar.cpp +++ b/app/window/mainwindow/mainstatusbar.cpp @@ -71,7 +71,7 @@ void MainStatusBar::UpdateStatus() if (manager_->GetTaskCount() == 1) { showMessage(t->GetTitle()); } else { - showMessage(tr("Running %1 background task(s)").arg(manager_->GetTaskCount())); + showMessage(tr("Running %n background task(s)", nullptr, manager_->GetTaskCount())); } bar_->setVisible(true); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 8cc4c3e1e..b25993dd3 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -261,6 +261,27 @@ ScopePanel *MainWindow::AppendScopePanel() return AppendFloatingPanelInternal(scope_panels_); } +void MainWindow::OpenNodeInViewer(ViewerOutput *node) +{ + if (viewer_panels_.contains(node)) { + // This node already has a viewer, raise it + viewer_panels_.value(node)->raise(); + } else { + // Create a viewer for this node + ViewerPanel* viewer = PanelManager::instance()->CreatePanel(this); + + viewer->SetSignalInsteadOfClose(true); + viewer->setFloating(true); + viewer->setVisible(true); + viewer->ConnectViewerNode(node); + + connect(viewer, &ViewerPanel::CloseRequested, this, &MainWindow::ViewerCloseRequested); + connect(node, &ViewerOutput::RemovedFromGraph, this, &MainWindow::ViewerWithPanelRemovedFromGraph); + + viewer_panels_.insert(node, viewer); + } +} + void MainWindow::SetFullscreen(bool fullscreen) { if (fullscreen) { @@ -481,6 +502,22 @@ void MainWindow::ProjectCloseRequested() Core::instance()->CloseProject(p, true); } +void MainWindow::ViewerCloseRequested() +{ + ViewerPanel* panel = static_cast(sender()); + + viewer_panels_.remove(viewer_panels_.key(panel)); + + panel->deleteLater(); +} + +void MainWindow::ViewerWithPanelRemovedFromGraph() +{ + ViewerOutput* vo = static_cast(sender()); + viewer_panels_.take(vo)->deleteLater(); + disconnect(vo, &ViewerOutput::RemovedFromGraph, this, &MainWindow::ViewerWithPanelRemovedFromGraph); +} + void MainWindow::FloatingPanelCloseRequested() { PanelWidget* panel = static_cast(sender()); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 35db59aee..fbbb38a70 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -71,6 +71,8 @@ public: ScopePanel* AppendScopePanel(); + void OpenNodeInViewer(ViewerOutput* node); + enum ProgressStatus { kProgressNone, kProgressShow, @@ -155,6 +157,7 @@ private: PixelSamplerPanel* pixel_sampler_panel_; QList scope_panels_; NodeTablePanel* table_panel_; + QMap viewer_panels_; #ifdef Q_OS_WINDOWS unsigned int taskbar_btn_id_; @@ -173,6 +176,10 @@ private slots: void ProjectCloseRequested(); + void ViewerCloseRequested(); + + void ViewerWithPanelRemovedFromGraph(); + void FloatingPanelCloseRequested(); void StatusBarDoubleClicked();