diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index a64b1298f..e13effd5c 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -25,7 +25,6 @@ #include "config/config.h" #include "common/cpuoptimize.h" -#include "common/functiontimer.h" namespace olive { @@ -40,50 +39,50 @@ AudioVisualWaveform::AudioVisualWaveform() : } } -void AudioVisualWaveform::OverwriteSamplesFromBuffer(const SampleBuffer &samples, int sample_rate, const rational &start, double target_rate, Sample& data, int &start_index, int &samples_length) +void AudioVisualWaveform::OverwriteSamplesFromBuffer(const SampleBuffer &samples, int sample_rate, const rational &start, double target_rate, Sample& data, size_t &start_index, size_t &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; + size_t end_index = start_index + samples_length; if (data.size() < end_index) { data.resize(end_index); } double chunk_size = double(sample_rate) / double(target_rate); - for (int i=0; i(input_length / channels_) / input_sample_rate, output_rate); + size_t start_index = time_to_samples(start, output_rate); + size_t samples_length = time_to_samples(static_cast(input_length / channels_) / input_sample_rate, output_rate); - int end_index = start_index + samples_length; + size_t end_index = start_index + samples_length; if (output_data.size() < end_index) { output_data.resize(end_index); } // We guarantee mipmaps are powers of two so integer division should be perfectly accurate here - int chunk_size = input_sample_rate / output_rate; + size_t chunk_size = input_sample_rate / output_rate; - for (int i=0; ifirst.toDouble(), it->second, input_start, input_length); // } // Process the largest mipmap directly for the samples auto current_mipmap = mipmapped_data_.rbegin(); - int input_start, input_length; + size_t input_start, input_length; OverwriteSamplesFromBuffer(samples, sample_rate, start, current_mipmap->first.toDouble(), current_mipmap->second, input_start, input_length); while (true) { @@ -140,16 +139,16 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const r double rate_dbl = rate.toDouble(); // Get our destination sample - int our_start_index = time_to_samples(dest, rate_dbl); + size_t our_start_index = time_to_samples(dest, rate_dbl); // Get our source sample - int their_start_index = time_to_samples(offset, rate_dbl); + size_t their_start_index = time_to_samples(offset, rate_dbl); if (their_start_index >= their_arr.size()) { continue; } // Determine how much we're copying - int copy_len = their_arr.size() - their_start_index; + size_t copy_len = their_arr.size() - their_start_index; if (!length.isNull()) { copy_len = qMin(copy_len, time_to_samples(length, rate_dbl)); if (copy_len == 0) { @@ -158,13 +157,13 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const r } // Determine end index of our array - int end_index = our_start_index + copy_len; + size_t 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), + reinterpret_cast(their_arr.data()) + their_start_index * sizeof(SamplePerChannel), copy_len * sizeof(SamplePerChannel)); } @@ -181,9 +180,9 @@ void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational double rate_dbl = rate.toDouble(); // Get our destination sample - int our_start_index = time_to_samples(start, rate_dbl); - int our_length_index = time_to_samples(length, rate_dbl); - int our_end_index = our_start_index + our_length_index; + size_t our_start_index = time_to_samples(start, rate_dbl); + size_t our_length_index = time_to_samples(length, rate_dbl); + size_t our_end_index = our_start_index + our_length_index; if (our_arr.size() < our_end_index) { our_arr.resize(our_end_index); @@ -193,53 +192,31 @@ void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational } } -void AudioVisualWaveform::Shift(const rational &from, const rational &to) +void AudioVisualWaveform::TrimIn(rational length) { - for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { - rational rate = it->first; - double rate_dbl = rate.toDouble(); - Sample& data = it->second; - - int from_index = time_to_samples(from, rate_dbl); - int to_index = time_to_samples(to, rate_dbl); - - if (from_index == to_index) { - continue; - } - - if (from_index >= data.size()) { - continue; - } - - if (from_index > to_index) { - // Shifting backwards <- - data.remove(to_index, from_index - to_index); - } else { - // Shifting forwards -> - data.insert(from_index, to_index - from_index, {0, 0}); - } + if (length == 0) { + return; } - length_ = qMax(rational(0), length_ + (to-from)); -} + bool negative = (length < 0); + if (negative) { + length = -length; + } -void AudioVisualWaveform::TrimIn(const rational &length) -{ for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { rational rate = it->first; double rate_dbl = rate.toDouble(); Sample& data = it->second; - int chop_length = time_to_samples(length, rate_dbl); - + size_t chop_length = time_to_samples(length, rate_dbl); if (chop_length == 0) { continue; } - if (chop_length > 0) { - data = data.mid(chop_length); + if (!negative) { + data = Sample(data.begin() + chop_length, data.end()); } else { - data.insert(0, -chop_length, SamplePerChannel()); + data.insert(data.begin(), chop_length, SamplePerChannel()); } } @@ -255,6 +232,40 @@ AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset) const return mid; } +AudioVisualWaveform AudioVisualWaveform::Mid(const rational &offset, const rational &length) const +{ + AudioVisualWaveform mid = *this; + + mid.TrimRange(offset, length); + + return mid; +} + +void AudioVisualWaveform::Resize(const rational &length) +{ + if (length_ == length) { + return; + } + + for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) { + rational rate = it->first; + double rate_dbl = rate.toDouble(); + Sample& data = it->second; + + size_t chop_length = time_to_samples(length, rate_dbl); + + data.resize(chop_length); + } + + length_ = length; +} + +void AudioVisualWaveform::TrimRange(const rational &in, const rational &length) +{ + TrimIn(in); + Resize(length); +} + AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const rational &start, const rational &length) const { // Find mipmap that requires @@ -262,10 +273,10 @@ AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const ration 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); + size_t start_sample = time_to_samples(start, rate_dbl); + size_t sample_length = time_to_samples(length, rate_dbl); - const QVector &mipmap_data = using_mipmap->second; + const Sample &mipmap_data = using_mipmap->second; // Determine if the array actually has this sample sample_length = qMin(sample_length, mipmap_data.size() - start_sample); @@ -273,7 +284,7 @@ AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const ration // Based on the above `min`, if sample length <= 0, that means start_sample >= the size of the // array and nothing can be returned. if (sample_length > 0) { - return ReSumSamples(&mipmap_data.constData()[start_sample], sample_length, channels_); + return ReSumSamples(&mipmap_data.data()[start_sample], sample_length, channels_); } // Return null samples @@ -304,7 +315,7 @@ void ExpandMinMaxChannel(const float *a, size_t length, float &min_val, float &m // min and max will contain 4 min and max. To get the absolute min and max // we need to compare the 4 values over themselves by shuffling each time. - for (int i = 0; i < 3; i++) { + for (size_t i = 0; i < 3; i++) { max = _mm_max_ps(max, _mm_shuffle_ps(max, max, 0x93)); min = _mm_min_ps(min, _mm_shuffle_ps(min, min, 0x93)); } @@ -323,7 +334,7 @@ void ExpandMinMaxChannel(const float *a, size_t length, float &min_val, float &m #endif } -AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const SampleBuffer &samples, int start_index, int length) +AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const SampleBuffer &samples, size_t start_index, size_t length) { int channels = samples.audio_params().channel_count(); AudioVisualWaveform::Sample summed_samples(channels); @@ -333,7 +344,7 @@ AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const SampleBuffer & } // for reference: this approximation is n x faster (and less accurate) for a n-tracks clip - // for (int i=start_index; idata(i%channels)[i]); // } @@ -341,12 +352,12 @@ AudioVisualWaveform::Sample AudioVisualWaveform::SumSamples(const SampleBuffer & } AudioVisualWaveform::Sample AudioVisualWaveform::ReSumSamples(const SamplePerChannel* samples, - int nb_samples, + size_t nb_samples, int nb_channels) { AudioVisualWaveform::Sample summed_samples(nb_channels); - for (int i=0;isecond; - int start_sample_index = samples.time_to_samples(start_time, rate_dbl); + size_t 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; + size_t next_sample_index = start_sample_index; + size_t sample_index; Sample summary; - int summary_index = -1; + size_t summary_index = -1; const QRect& viewport = painter->viewport(); QPoint top_left = painter->transform().map(viewport.topLeft()); - int start = qMax(rect.x(), -top_left.x()); - int end = qMin(rect.right(), -top_left.x() + viewport.width()); + size_t start = qMax(rect.x(), -top_left.x()); + size_t end = qMin(rect.right(), -top_left.x() + viewport.width()); bool rectified = OLIVE_CONFIG("RectifiedWaveforms").toBool(); - for (int i=start;i; + using Sample = std::vector; int channel_count() const { @@ -90,17 +90,20 @@ public: void OverwriteSilence(const rational &start, const rational &length); - void Shift(const rational& from, const rational& to); - - void TrimIn(const rational &length); + void TrimIn(rational length); AudioVisualWaveform Mid(const rational &offset) const; + AudioVisualWaveform Mid(const rational &offset, const rational &length) const; + + void Resize(const rational &length); + + void TrimRange(const rational &in, const rational &length); Sample GetSummaryFromTime(const rational& start, const rational& length) const; - static Sample SumSamples(const SampleBuffer &samples, int start_index, int length); + static Sample SumSamples(const SampleBuffer &samples, size_t start_index, size_t length); - static Sample ReSumSamples(const SamplePerChannel *samples, int nb_samples, int nb_channels); + static Sample ReSumSamples(const SamplePerChannel *samples, size_t nb_samples, int nb_channels); static void DrawSample(QPainter* painter, const Sample &sample, int x, int y, int height, bool rectified); @@ -111,12 +114,12 @@ public: static const rational kMaximumSampleRate; private: - void OverwriteSamplesFromBuffer(const SampleBuffer &samples, int sample_rate, const rational& start, double target_rate, Sample &data, int &start_index, int &samples_length); + void OverwriteSamplesFromBuffer(const SampleBuffer &samples, int sample_rate, const rational& start, double target_rate, Sample &data, size_t &start_index, size_t &samples_length); - void OverwriteSamplesFromMipmap(const Sample& input, double input_sample_rate, int &input_start, int &input_length, const rational& start, double output_rate, Sample &output_data); + void OverwriteSamplesFromMipmap(const Sample& input, double input_sample_rate, size_t &input_start, size_t &input_length, const rational& start, double output_rate, Sample &output_data); - int time_to_samples(const rational& time, double sample_rate) const; - int time_to_samples(const double& time, double sample_rate) const; + size_t time_to_samples(const rational& time, double sample_rate) const; + size_t time_to_samples(const double& time, double sample_rate) const; std::map::const_iterator GetMipmapForScale(double scale) const; diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 3cb61d8f1..e4168479d 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -43,7 +43,6 @@ extern "C" { #include "common/define.h" #include "common/ffmpegutils.h" #include "common/filefunctions.h" -#include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "render/framehashcache.h" #include "render/diskmanager.h" @@ -164,125 +163,117 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) p.divider); TexturePtr tex = nullptr; - bool hwscale = true; // Attempt to use GLSL shader for faster YUV to RGB conversion - if (hwscale) { - if (src_fmt == AV_PIX_FMT_YUV420P - || src_fmt == AV_PIX_FMT_YUV422P - || src_fmt == AV_PIX_FMT_YUV444P - || src_fmt == AV_PIX_FMT_YUV420P10LE - || src_fmt == AV_PIX_FMT_YUV422P10LE - || src_fmt == AV_PIX_FMT_YUV444P10LE - || src_fmt == AV_PIX_FMT_YUV420P12LE - || src_fmt == AV_PIX_FMT_YUV422P12LE - || src_fmt == AV_PIX_FMT_YUV444P12LE) { - if (Yuv2RgbShader.isNull()) { - // Compile shader - Yuv2RgbShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); + if (IsPixelFormatGLSLCompatible(static_cast(src_fmt))) { + if (Yuv2RgbShader.isNull()) { + // Compile shader + Yuv2RgbShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); + } + + if (!Yuv2RgbShader.isNull()) { + int px_size; + int bits_per_pixel; + switch (src_fmt) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV444P: + default: + px_size = 1; + bits_per_pixel = 8; + break; + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV422P10LE: + case AV_PIX_FMT_YUV444P10LE: + px_size = 2; + bits_per_pixel = 10; + break; + case AV_PIX_FMT_YUV420P12LE: + case AV_PIX_FMT_YUV422P12LE: + case AV_PIX_FMT_YUV444P12LE: + px_size = 2; + bits_per_pixel = 12; + break; } - if (!Yuv2RgbShader.isNull()) { - int px_size; - int bits_per_pixel; - switch (src_fmt) { - case AV_PIX_FMT_YUV420P: - case AV_PIX_FMT_YUV422P: - case AV_PIX_FMT_YUV444P: - default: - px_size = 1; - bits_per_pixel = 8; - break; - case AV_PIX_FMT_YUV420P10LE: - case AV_PIX_FMT_YUV422P10LE: - case AV_PIX_FMT_YUV444P10LE: - px_size = 2; - bits_per_pixel = 10; - break; - case AV_PIX_FMT_YUV420P12LE: - case AV_PIX_FMT_YUV422P12LE: - case AV_PIX_FMT_YUV444P12LE: - px_size = 2; - bits_per_pixel = 12; - break; - } + AVFrame *hw_in = f.get(); - VideoParams plane_params = vp; - plane_params.set_channel_count(1); + VideoParams plane_params = vp; + plane_params.set_channel_count(1); + plane_params.set_format(native_internal_pix_fmt_); + + if (p.divider != 1) { + ApplyScaler(f.get()); + hw_in = working_frame_; + } else { + // Fallback: shouldn't ever really get here, but just in case plane_params.set_divider(1); - plane_params.set_format(native_internal_pix_fmt_); - TexturePtr y_plane = p.renderer->CreateTexture(plane_params, f->data[0], f->linesize[0] / px_size); - - if (src_fmt == AV_PIX_FMT_YUV420P - || src_fmt == AV_PIX_FMT_YUV422P - || src_fmt == AV_PIX_FMT_YUV420P10LE - || src_fmt == AV_PIX_FMT_YUV422P10LE - || src_fmt == AV_PIX_FMT_YUV420P12LE - || src_fmt == AV_PIX_FMT_YUV422P12LE) { - plane_params.set_width(plane_params.width()/2); - } - - if (src_fmt == AV_PIX_FMT_YUV420P - || src_fmt == AV_PIX_FMT_YUV420P10LE - || src_fmt == AV_PIX_FMT_YUV420P12LE) { - plane_params.set_height(plane_params.height()/2); - } - - TexturePtr u_plane = p.renderer->CreateTexture(plane_params, f->data[1], f->linesize[1] / px_size); - TexturePtr v_plane = p.renderer->CreateTexture(plane_params, f->data[2], f->linesize[2] / px_size); - - ShaderJob job; - job.Insert(QStringLiteral("y_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(y_plane))); - job.Insert(QStringLiteral("u_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(u_plane))); - job.Insert(QStringLiteral("v_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(v_plane))); - job.Insert(QStringLiteral("bits_per_pixel"), NodeValue(NodeValue::kInt, bits_per_pixel)); - job.Insert(QStringLiteral("full_range"), NodeValue(NodeValue::kBoolean, f->color_range == AVCOL_RANGE_JPEG)); - - const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(f.get()->colorspace)); - job.Insert(QStringLiteral("yuv_crv"), NodeValue(NodeValue::kInt, yuv_coeffs[0])); - job.Insert(QStringLiteral("yuv_cgu"), NodeValue(NodeValue::kInt, yuv_coeffs[2])); - job.Insert(QStringLiteral("yuv_cgv"), NodeValue(NodeValue::kInt, yuv_coeffs[3])); - job.Insert(QStringLiteral("yuv_cbu"), NodeValue(NodeValue::kInt, yuv_coeffs[1])); - - int interlacing = 0; - if (p.src_interlacing != VideoParams::kInterlaceNone) { - if (frame_rate_tb_.isNull()) { - frame_rate_tb_ = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), f.get()); - - // Double frame rate for interlaced fields - frame_rate_tb_ *= 2; - - // Flip frame rate so it can be used as a timebase - frame_rate_tb_.flip(); - } - - int64_t req = Timecode::time_to_timestamp(p.time, frame_rate_tb_); - int64_t frm = Timecode::rescale_timestamp(f->pts - instance_.avstream()->start_time, instance_.avstream()->time_base, frame_rate_tb_); - - bool first = (req == frm); - bool top_first = (p.src_interlacing == VideoParams::kInterlacedTopFirst); - - interlacing = (first == top_first) ? 1 : 2; - } - job.Insert(QStringLiteral("interlacing"), NodeValue(NodeValue::kInt, interlacing)); - job.Insert(QStringLiteral("pixel_height"), NodeValue(NodeValue::kInt, f->height)); - - tex = p.renderer->CreateTexture(vp); - p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); } + + TexturePtr y_plane = p.renderer->CreateTexture(plane_params, hw_in->data[0], hw_in->linesize[0] / px_size); + + if (src_fmt == AV_PIX_FMT_YUV420P + || src_fmt == AV_PIX_FMT_YUV422P + || src_fmt == AV_PIX_FMT_YUV420P10LE + || src_fmt == AV_PIX_FMT_YUV422P10LE + || src_fmt == AV_PIX_FMT_YUV420P12LE + || src_fmt == AV_PIX_FMT_YUV422P12LE) { + plane_params.set_width(plane_params.width()/2); + } + + if (src_fmt == AV_PIX_FMT_YUV420P + || src_fmt == AV_PIX_FMT_YUV420P10LE + || src_fmt == AV_PIX_FMT_YUV420P12LE) { + plane_params.set_height(plane_params.height()/2); + } + + TexturePtr u_plane = p.renderer->CreateTexture(plane_params, hw_in->data[1], hw_in->linesize[1] / px_size); + TexturePtr v_plane = p.renderer->CreateTexture(plane_params, hw_in->data[2], hw_in->linesize[2] / px_size); + + ShaderJob job; + job.Insert(QStringLiteral("y_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(y_plane))); + job.Insert(QStringLiteral("u_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(u_plane))); + job.Insert(QStringLiteral("v_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(v_plane))); + job.Insert(QStringLiteral("bits_per_pixel"), NodeValue(NodeValue::kInt, bits_per_pixel)); + job.Insert(QStringLiteral("full_range"), NodeValue(NodeValue::kBoolean, f->color_range == AVCOL_RANGE_JPEG)); + + const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(f.get()->colorspace)); + job.Insert(QStringLiteral("yuv_crv"), NodeValue(NodeValue::kInt, yuv_coeffs[0])); + job.Insert(QStringLiteral("yuv_cgu"), NodeValue(NodeValue::kInt, yuv_coeffs[2])); + job.Insert(QStringLiteral("yuv_cgv"), NodeValue(NodeValue::kInt, yuv_coeffs[3])); + job.Insert(QStringLiteral("yuv_cbu"), NodeValue(NodeValue::kInt, yuv_coeffs[1])); + + int interlacing = 0; + if (p.src_interlacing != VideoParams::kInterlaceNone) { + if (frame_rate_tb_.isNull()) { + frame_rate_tb_ = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), f.get()); + + // Double frame rate for interlaced fields + frame_rate_tb_ *= 2; + + // Flip frame rate so it can be used as a timebase + frame_rate_tb_.flip(); + } + + int64_t req = Timecode::time_to_timestamp(p.time, frame_rate_tb_); + int64_t frm = Timecode::rescale_timestamp(f->pts - instance_.avstream()->start_time, instance_.avstream()->time_base, frame_rate_tb_); + + bool first = (req == frm); + bool top_first = (p.src_interlacing == VideoParams::kInterlacedTopFirst); + + interlacing = (first == top_first) ? 1 : 2; + } + job.Insert(QStringLiteral("interlacing"), NodeValue(NodeValue::kInt, interlacing)); + job.Insert(QStringLiteral("pixel_height"), NodeValue(NodeValue::kInt, f->height)); + + tex = p.renderer->CreateTexture(vp); + p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); } } if (!tex) { // Fallback to software pixel format conversion - int r; - - r = av_buffersrc_add_frame_flags(buffersrc_ctx_, f.get(), AV_BUFFERSRC_FLAG_KEEP_REF); - if (r < 0) { - return nullptr; - } - r = av_buffersink_get_frame(buffersink_ctx_, working_frame_); - if (r < 0) { + if (!ApplyScaler(f.get())) { return nullptr; } @@ -717,6 +708,19 @@ const char *FFmpegDecoder::GetInterlacingModeInFFmpeg(VideoParams::Interlacing i } } +bool FFmpegDecoder::IsPixelFormatGLSLCompatible(AVPixelFormat f) +{ + return f == AV_PIX_FMT_YUV420P + || f == AV_PIX_FMT_YUV422P + || f == AV_PIX_FMT_YUV444P + || f == AV_PIX_FMT_YUV420P10LE + || f == AV_PIX_FMT_YUV422P10LE + || f == AV_PIX_FMT_YUV444P10LE + || f == AV_PIX_FMT_YUV420P12LE + || f == AV_PIX_FMT_YUV422P12LE + || f == AV_PIX_FMT_YUV444P12LE; +} + /* OLD UNUSED CODE: Keeping this around in case the code proves useful void FFmpegDecoder::CacheFrameToDisk(AVFrame *f) @@ -1023,7 +1027,7 @@ bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params } // Add format filter if necessary - if (ideal_pix_fmt != input->format) { + if (ideal_pix_fmt != input->format && !IsPixelFormatGLSLCompatible(static_cast(input->format))) { AVFilterContext* format_filter; snprintf(filter_args, kFilterArgSz, "pix_fmts=%u", ideal_pix_fmt); @@ -1097,6 +1101,22 @@ void FFmpegDecoder::RemoveFirstFrame() cache_at_zero_ = false; } +bool FFmpegDecoder::ApplyScaler(AVFrame *in) +{ + int r; + + r = av_buffersrc_add_frame_flags(buffersrc_ctx_, in, AV_BUFFERSRC_FLAG_KEEP_REF); + if (r < 0) { + return false; + } + r = av_buffersink_get_frame(buffersink_ctx_, working_frame_); + if (r < 0) { + return false; + } + + return true; +} + int FFmpegDecoder::MaximumQueueSize() { // Fairly arbitrary size. This used to need to be the number of current threads to ensure any diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 8df5a67bc..e018cfc75 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -142,6 +142,8 @@ private: static const char* GetInterlacingModeInFFmpeg(VideoParams::Interlacing interlacing); + static bool IsPixelFormatGLSLCompatible(AVPixelFormat f); + AVFramePtr GetFrameFromCache(const int64_t &t) const; void ClearFrameCache(); @@ -150,6 +152,8 @@ private: void RemoveFirstFrame(); + bool ApplyScaler(AVFrame *in); + static int MaximumQueueSize(); RetrieveVideoParams filter_params_; diff --git a/app/codec/samplebuffer.cpp b/app/codec/samplebuffer.cpp index fd4dfa5a7..c5a746387 100644 --- a/app/codec/samplebuffer.cpp +++ b/app/codec/samplebuffer.cpp @@ -36,7 +36,7 @@ SampleBuffer::SampleBuffer(const AudioParams &audio_params, const rational &leng allocate(); } -SampleBuffer::SampleBuffer(const AudioParams &audio_params, int samples_per_channel) : +SampleBuffer::SampleBuffer(const AudioParams &audio_params, size_t samples_per_channel) : audio_params_(audio_params), sample_count_per_channel_(samples_per_channel) { @@ -58,12 +58,7 @@ void SampleBuffer::set_audio_params(const AudioParams ¶ms) audio_params_ = params; } -const int &SampleBuffer::sample_count() const -{ - return sample_count_per_channel_; -} - -void SampleBuffer::set_sample_count(const int &sample_count) +void SampleBuffer::set_sample_count(const size_t &sample_count) { if (is_allocated()) { qWarning() << "Tried to set sample count on allocated sample buffer"; @@ -73,11 +68,6 @@ void SampleBuffer::set_sample_count(const int &sample_count) sample_count_per_channel_ = sample_count; } -bool SampleBuffer::is_allocated() const -{ - return !data_.isEmpty(); -} - void SampleBuffer::allocate() { if (!audio_params_.is_valid()) { @@ -113,10 +103,10 @@ void SampleBuffer::reverse() return; } - int half_nb_sample = sample_count_per_channel_ / 2; + size_t half_nb_sample = sample_count_per_channel_ / 2; - for (int i=0;i(sample_count_per_channel_) / speed); - QVector< QVector > output_data; + std::vector< std::vector > output_data; output_data.resize(audio_params_.channel_count()); for (int i=0; i(i) * speed); + for (size_t i=0;i(i) * speed); for (int j=0;j to_raw_ptrs() + std::vector to_raw_ptrs() { - QVector r(data_.size()); - for (int i=0; i r(data_.size()); + for (size_t i=0; i > data_; + std::vector< std::vector > data_; }; diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 0b7ff983a..e3741723b 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -34,9 +34,6 @@ set(OLIVE_SOURCES common/ffmpegutils.h common/filefunctions.cpp common/filefunctions.h - common/flipmodifiers.cpp - common/flipmodifiers.h - common/functiontimer.h common/html.cpp common/html.h common/jobtime.cpp diff --git a/app/common/flipmodifiers.h b/app/common/flipmodifiers.h deleted file mode 100644 index 12b408e39..000000000 --- a/app/common/flipmodifiers.h +++ /dev/null @@ -1,34 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef FLIPMODIFIERS_H -#define FLIPMODIFIERS_H - -#include - -#include "common/define.h" - -namespace olive { - -Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e); - -} - -#endif // FLIPMODIFIERS_H diff --git a/app/common/functiontimer.h b/app/common/functiontimer.h deleted file mode 100644 index ba1aadbdc..000000000 --- a/app/common/functiontimer.h +++ /dev/null @@ -1,55 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef FUNCTIONTIMER_H -#define FUNCTIONTIMER_H - -#include -#include - -#define TIME_THIS_FUNCTION FunctionTimer __f(__FUNCTION__) -#define START_TIMING {FunctionTimer *__f = new FunctionTimer(__FUNCTION__) -#define STOP_TIMING delete __f;}void() - -class FunctionTimer { -public: - FunctionTimer(const char* s) - { - name_ = s; - time_ = QDateTime::currentMSecsSinceEpoch(); - } - - ~FunctionTimer() - { - qint64 elapsed = (QDateTime::currentMSecsSinceEpoch() - time_); - - if (elapsed > 1) { - qDebug() << name_ << "took" << elapsed; - } - } - -private: - const char* name_; - - qint64 time_; - -}; - -#endif // FUNCTIONTIMER_H diff --git a/app/common/qtutils.cpp b/app/common/qtutils.cpp index 0a750810c..46ee40172 100644 --- a/app/common/qtutils.cpp +++ b/app/common/qtutils.cpp @@ -145,6 +145,23 @@ QStringList QtUtils::WordWrapString(const QString &s, const QFontMetrics &fm, in return list; } +Qt::KeyboardModifiers QtUtils::FlipControlAndShiftModifiers(Qt::KeyboardModifiers e) +{ + if (e & Qt::ControlModifier & Qt::ShiftModifier) { + return e; + } + + if (e & Qt::ShiftModifier) { + e |= Qt::ControlModifier; + e &= ~Qt::ShiftModifier; + } else if (e & Qt::ControlModifier) { + e |= Qt::ShiftModifier; + e &= ~Qt::ControlModifier; + } + + return e; +} + void QtUtils::SetComboBoxData(QComboBox *cb, int data) { for (int i=0; icount(); i++) { diff --git a/app/common/qtutils.h b/app/common/qtutils.h index 74ff4fcf8..60190b072 100644 --- a/app/common/qtutils.h +++ b/app/common/qtutils.h @@ -53,6 +53,8 @@ public: static QStringList WordWrapString(const QString &s, const QFontMetrics &fm, int bounding_width); + static Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e); + static void SetComboBoxData(QComboBox *cb, int data); template diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 7326794ef..b79555937 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -312,6 +312,11 @@ TimeRangeListFrameIterator::TimeRangeListFrameIterator(const TimeRangeList &list UpdateIndexIfNecessary(); } +rational TimeRangeListFrameIterator::Snap(const rational &r) const +{ + return Timecode::snap_time_to_timebase(r, timebase_, Timecode::kFloor); +} + bool TimeRangeListFrameIterator::GetNext(rational *out) { if (!HasNext()) { @@ -368,7 +373,7 @@ void TimeRangeListFrameIterator::UpdateIndexIfNecessary() range_index_++; if (range_index_ < list_.size()) { - current_ = Timecode::snap_time_to_timebase(list_.at(range_index_).in(), timebase_, Timecode::kCeil); + current_ = Snap(list_.at(range_index_).in()); } } } diff --git a/app/common/timerange.h b/app/common/timerange.h index 882b14d36..b6235f9b8 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -207,6 +207,8 @@ public: TimeRangeListFrameIterator(); TimeRangeListFrameIterator(const TimeRangeList &list, const rational &timebase); + rational Snap(const rational &r) const; + bool GetNext(rational *out); bool HasNext() const; diff --git a/app/config/config.cpp b/app/config/config.cpp index 1e0ee70a2..c9e814a16 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -32,6 +32,7 @@ #include "common/filefunctions.h" #include "common/xmlutils.h" #include "core.h" +#include "timeline/timelinecommon.h" #include "ui/colorcoding.h" #include "ui/style/style.h" #include "window/mainwindow/mainwindow.h" @@ -104,6 +105,9 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("ReassocLinToNonLin"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("PreviewNonFloatDontAskAgain"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("TimelineThumbnailMode"), NodeValue::kInt, Timeline::kThumbnailInOut); + SetEntryInternal(QStringLiteral("TimelineWaveformMode"), NodeValue::kInt, Timeline::kWaveformsEnabled); + SetEntryInternal(QStringLiteral("DefaultVideoTransition"), NodeValue::kText, QStringLiteral("org.olivevideoeditor.Olive.crossdissolve")); SetEntryInternal(QStringLiteral("DefaultAudioTransition"), NodeValue::kText, QStringLiteral("org.olivevideoeditor.Olive.crossdissolve")); SetEntryInternal(QStringLiteral("DefaultTransitionLength"), NodeValue::kRational, QVariant::fromValue(rational(1))); diff --git a/app/core.cpp b/app/core.cpp index d6989319a..97a49f8e0 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -394,10 +394,7 @@ void Core::DialogExportShow() rational time; if (GetSequenceToExport(&viewer, &time)) { - ExportDialog* ed = new ExportDialog(viewer, main_window_); - ed->SetTime(time); - connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater); - ed->open(); + OpenExportDialogForViewer(viewer, time, false); } } @@ -1253,6 +1250,15 @@ void Core::OpenNodeInViewer(ViewerOutput *viewer) main_window_->OpenNodeInViewer(viewer); } +void Core::OpenExportDialogForViewer(ViewerOutput *viewer, const rational &time, bool start_still_image) +{ + ExportDialog* ed = new ExportDialog(viewer, start_still_image, main_window_); + ed->SetTime(time); + connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater); + ed->open(); + connect(ed, &ExportDialog::RequestImportFile, this, &Core::ImportSingleFile); +} + void Core::CheckForAutoRecoveries() { QFile autorecovery_index(GetAutoRecoveryIndexFilename()); @@ -1416,6 +1422,13 @@ void Core::OpenProjectInternal(const QString &filename, bool recovery_project) task_dialog->open(); } +void Core::ImportSingleFile(const QString &f) +{ + if (Project *p = GetActiveProject()) { + ImportFiles({f}, p->root()); + } +} + int Core::CountFilesInFileList(const QFileInfoList &filenames) { int file_count = 0; diff --git a/app/core.h b/app/core.h index 1c7a923ef..a2ded04db 100644 --- a/app/core.h +++ b/app/core.h @@ -313,6 +313,8 @@ public: void OpenNodeInViewer(ViewerOutput* viewer); + void OpenExportDialogForViewer(ViewerOutput *viewer, const rational &time, bool start_still_image); + public slots: /** * @brief Starts an open file dialog to load a project from file @@ -654,6 +656,8 @@ private slots: */ void OpenProjectInternal(const QString& filename, bool recovery_project = false); + void ImportSingleFile(const QString &f); + }; } diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 54a1403a8..6210b63f4 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -44,9 +44,10 @@ namespace olive { #define super QDialog -ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : +ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWidget *parent) : super(parent), - viewer_node_(viewer_node) + viewer_node_(viewer_node), + stills_only_mode_(stills_only_mode) { QHBoxLayout* layout = new QHBoxLayout(this); @@ -156,6 +157,30 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : row++; + { + QGroupBox *options_group = new QGroupBox(); + preferences_layout->addWidget(options_group, row, 0, 1, 4); + + QGridLayout *options_layout = new QGridLayout(options_group); + + int opt_row = 0; + + export_bkg_box_ = new QCheckBox(tr("Run In Background")); + export_bkg_box_->setToolTip(tr("Exporting in the background allows you to continue using Olive while " + "exporting, but may result in slower export speeds, and may" + "severely impact editing and playback performance.")); + options_layout->addWidget(export_bkg_box_, opt_row, 0); + + import_file_after_export_ = new QCheckBox(tr("Import Result After Export")); + options_layout->addWidget(import_file_after_export_, opt_row, 1); + + connect(export_bkg_box_, &QCheckBox::toggled, import_file_after_export_, [this](bool e){ + import_file_after_export_->setEnabled(!e); + }); + } + + row++; + QHBoxLayout *btn_layout = new QHBoxLayout(); btn_layout->setMargin(0); preferences_layout->addLayout(btn_layout, row, 0, 1, 4); @@ -170,12 +195,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : btn_layout->addWidget(cancel_btn); connect(cancel_btn, &QPushButton::clicked, this, &ExportDialog::reject); - export_bkg_box_ = new QCheckBox(tr("Run In Background")); - export_bkg_box_->setToolTip(tr("Exporting in the background allows you to continue using Olive while " - "exporting, but may result in slower export speeds, and may" - "severely impact editing and playback performance.")); - btn_layout->addWidget(export_bkg_box_); - btn_layout->addStretch(); splitter->addWidget(preferences_area_); @@ -238,7 +257,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : subtitles_enabled_->setEnabled(has_subtitle_tracks); // If the viewer already has cached params, use them - if (viewer_node_->GetLastUsedEncodingParams().IsValid()) { + if (!stills_only_mode_ && viewer_node_->GetLastUsedEncodingParams().IsValid()) { SetParams(viewer_node_->GetLastUsedEncodingParams()); } else { SetDefaults(); @@ -370,6 +389,11 @@ void ExportDialog::ExportFinished() // If this task was cancelled, we stay open so the user can potentially queue another export } else { // Accept this dialog and close + if (import_file_after_export_) { + QString filename = filename_edit_->text().trimmed(); + emit RequestImportFile(filename); + } + this->accept(); } } @@ -572,7 +596,11 @@ bool ExportDialog::SequenceHasSubtitles() const void ExportDialog::SetDefaults() { - format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); + if (!stills_only_mode_) { + format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); + } else { + format_combobox_->SetFormat(ExportFormat::kFormatPNG); + } FormatChanged(format_combobox_->GetFormat()); VideoParams vp = viewer_node_->GetVideoParams(); @@ -745,7 +773,9 @@ void ExportDialog::done(int r) { preview_viewer_->ConnectViewerNode(nullptr); - viewer_node_->SetLastUsedEncodingParams(GenerateParams()); + if (!stills_only_mode_) { + viewer_node_->SetLastUsedEncodingParams(GenerateParams()); + } super::done(r); } diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index b9cd62b73..307e8b0d4 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -43,7 +43,10 @@ class ExportDialog : public QDialog { Q_OBJECT public: - ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); + ExportDialog(ViewerOutput* viewer_node, bool stills_only_mode, QWidget* parent = nullptr); + ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr) : + ExportDialog(viewer_node, false, parent) + {} rational GetSelectedTimebase() const; void SetSelectedTimebase(const rational &r); @@ -64,6 +67,9 @@ public: public slots: virtual void done(int r) override; +signals: + void RequestImportFile(const QString &s); + private: void AddPreferencesTab(QWidget *inner_widget, const QString &title); @@ -115,6 +121,9 @@ private: QWidget* preferences_area_; QCheckBox *export_bkg_box_; + QCheckBox *import_file_after_export_; + + bool stills_only_mode_; private slots: void BrowseFilename(); diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index 35b8f311e..3f78e570c 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -159,7 +159,7 @@ void SequenceDialog::accept() sequence_->SetVideoParams(video_params); sequence_->SetAudioParams(audio_params); sequence_->SetLabel(name_field_->text()); - sequence_->video_frame_cache()->SetEnabled(parameter_tab_->GetSelectedPreviewAutoCache()); + sequence_->SetVideoAutoCacheEnabled(parameter_tab_->GetSelectedPreviewAutoCache()); } QDialog::accept(); @@ -178,7 +178,6 @@ void SequenceDialog::SetAsDefaultClicked() OLIVE_CONFIG("DefaultSequenceInterlacing") = parameter_tab_->GetSelectedVideoInterlacingMode(); OLIVE_CONFIG("DefaultSequenceAudioFrequency") = parameter_tab_->GetSelectedAudioSampleRate(); OLIVE_CONFIG("DefaultSequenceAudioLayout") = QVariant::fromValue(parameter_tab_->GetSelectedAudioChannelLayout()); - OLIVE_CONFIG("DefaultSequenceAutoCache") = QVariant::fromValue(parameter_tab_->GetSelectedPreviewAutoCache()); } } @@ -194,7 +193,7 @@ SequenceDialog::SequenceParamCommand::SequenceParamCommand(Sequence* s, old_video_params_(s->GetVideoParams()), old_audio_params_(s->GetAudioParams()), old_name_(s->GetLabel()), - old_autocache_(s->video_frame_cache()->IsEnabled()) + old_autocache_(s->IsVideoAutoCacheEnabled()) { } @@ -212,7 +211,7 @@ void SequenceDialog::SequenceParamCommand::redo() sequence_->SetAudioParams(new_audio_params_); } sequence_->SetLabel(new_name_); - sequence_->video_frame_cache()->SetEnabled(new_autocache_); + sequence_->SetVideoAutoCacheEnabled(new_autocache_); } void SequenceDialog::SequenceParamCommand::undo() @@ -224,7 +223,7 @@ void SequenceDialog::SequenceParamCommand::undo() sequence_->SetAudioParams(old_audio_params_); } sequence_->SetLabel(old_name_); - sequence_->video_frame_cache()->SetEnabled(old_autocache_); + sequence_->SetVideoAutoCacheEnabled(old_autocache_); } } diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index 364d34315..a277717ac 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -73,10 +73,13 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg preview_layout->addWidget(new QLabel(tr("Quality:")), row, 0); preview_format_field_ = new PixelFormatComboBox(false); preview_layout->addWidget(preview_format_field_, row, 1, 1, 2); + + /* TEMP: Disable sequence auto-cache, wanna see if clip cache supersedes it. row++; preview_layout->addWidget(new QLabel(tr("Auto-Cache:")), row, 0); + preview_layout->addWidget(preview_autocache_field_, row, 1);*/ preview_autocache_field_ = new QCheckBox(); - preview_layout->addWidget(preview_autocache_field_, row, 1); + layout->addWidget(preview_group); // Set values based on input sequence @@ -89,7 +92,7 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg interlacing_combo_->SetInterlaceMode(vp.interlacing()); preview_resolution_field_->SetDivider(vp.divider()); preview_format_field_->SetPixelFormat(vp.format()); - preview_autocache_field_->setChecked(sequence->video_frame_cache()->IsEnabled()); + preview_autocache_field_->setChecked(sequence->IsVideoAutoCacheEnabled()); audio_sample_rate_field_->SetSampleRate(ap.sample_rate()); audio_channels_field_->SetChannelLayout(ap.channel_layout()); diff --git a/app/dialog/sequence/sequencedialogparametertab.h b/app/dialog/sequence/sequencedialogparametertab.h index e11561b2f..97a5f24a7 100644 --- a/app/dialog/sequence/sequencedialogparametertab.h +++ b/app/dialog/sequence/sequencedialogparametertab.h @@ -66,7 +66,9 @@ public: bool GetSelectedPreviewAutoCache() const { - return preview_autocache_field_->isChecked(); + //return preview_autocache_field_->isChecked(); + // TEMP: Disable sequence auto-cache, wanna see if clip cache supersedes it. + return false; } public slots: diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index 776832906..eb3942068 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -101,7 +101,7 @@ QTreeWidgetItem* SequenceDialogPresetTab::CreateFolder(const QString &name) QTreeWidgetItem *SequenceDialogPresetTab::CreateHDPresetFolder(const QString &name, int width, int height, int divider) { const VideoParams::Format default_format = static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()); - const bool default_autocache = OLIVE_CONFIG("DefaultSequenceAutoCache").toBool(); + const bool default_autocache = false; QTreeWidgetItem* parent = CreateFolder(name); AddStandardItem(parent, std::make_shared(tr("%1 23.976 FPS").arg(name), width, @@ -164,7 +164,7 @@ QTreeWidgetItem *SequenceDialogPresetTab::CreateHDPresetFolder(const QString &na QTreeWidgetItem *SequenceDialogPresetTab::CreateSDPresetFolder(const QString &name, int width, int height, const rational& frame_rate, const rational &standard_par, const rational &wide_par, int divider) { const VideoParams::Format default_format = static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt()); - const bool default_autocache = OLIVE_CONFIG("DefaultSequenceAutoCache").toBool(); + const bool default_autocache = false; QTreeWidgetItem* parent = CreateFolder(name); preset_tree_->addTopLevelItem(parent); AddStandardItem(parent, std::make_shared(tr("%1 Standard").arg(name), diff --git a/app/node/block/block.h b/app/node/block/block.h index 3a65159e2..2ad077ee5 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -96,6 +96,7 @@ public: void set_track(Track* track) { track_ = track; + emit TrackChanged(track_); } bool is_enabled() const; @@ -126,6 +127,8 @@ signals: void PreviewChanged(); + void TrackChanged(Track *track); + protected: virtual void InputValueChangedEvent(const QString& input, int element) override; diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index b94bd0c0e..787cf5442 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -20,6 +20,7 @@ #include "clip.h" +#include "config/config.h" #include "node/output/track/track.h" #include "node/output/viewer/viewer.h" #include "widget/slider/floatslider.h" @@ -34,6 +35,7 @@ const QString ClipBlock::kMediaInInput = QStringLiteral("media_in_in"); const QString ClipBlock::kSpeedInput = QStringLiteral("speed_in"); const QString ClipBlock::kReverseInput = QStringLiteral("reverse_in"); const QString ClipBlock::kMaintainAudioPitchInput = QStringLiteral("maintain_audio_pitch_in"); +const QString ClipBlock::kAutoCacheInput = QStringLiteral("autocache_in"); const QString ClipBlock::kLoopModeInput = QStringLiteral("loop_in"); ClipBlock::ClipBlock() : @@ -53,6 +55,8 @@ ClipBlock::ClipBlock() : AddInput(kMaintainAudioPitchInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + AddInput(kAutoCacheInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + PrependInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); //SetValueHintForInput(kBufferIn, ValueHint(NodeValue::kBuffer)); @@ -105,17 +109,14 @@ void ClipBlock::set_length_and_media_in(const rational &length) return; } - if (!reverse()) { - // Calculate media_in adjustment - waveform_.TrimIn(SequenceToMediaTime(this->length() - length, kSTMIgnoreSpeed | kSTMIgnoreLoop) - media_in()); - - set_media_in(SequenceToMediaTime(this->length() - length, kSTMIgnoreLoop)); - } else { - // Trim waveform out point - waveform_.TrimIn(this->length() - length); - } + rational old_length = this->length(); super::set_length_and_media_in(length); + + if (!reverse()) { + // Calculate media_in adjustment + set_media_in(SequenceToMediaTime(old_length - length, kSTMIgnoreLoop)); + } } rational ClipBlock::media_in() const @@ -126,6 +127,25 @@ rational ClipBlock::media_in() const void ClipBlock::set_media_in(const rational &media_in) { SetStandardValue(kMediaInInput, QVariant::fromValue(media_in)); + + RequestInvalidatedFromConnected(); +} + +void ClipBlock::SetAutocache(bool e) +{ + SetStandardValue(kAutoCacheInput, e); +} + +void ClipBlock::DiscardCache() +{ + if (Node *connected = GetConnectedOutput(kBufferIn)) { + Track::Type type = GetTrackType(); + if (type == Track::kVideo) { + connected->video_frame_cache()->Invalidate(TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } else if (type == Track::kAudio) { + connected->audio_playback_cache()->Invalidate(TimeRange(RATIONAL_MIN, RATIONAL_MAX)); + } + } } rational ClipBlock::SequenceToMediaTime(const rational &sequence_time, uint64_t flags) const @@ -199,12 +219,139 @@ rational ClipBlock::MediaToSequenceTime(const rational &media_time) const return sequence_time; } +void ClipBlock::RequestRangeFromConnected(const TimeRange &range) +{ + Track::Type type = GetTrackType(); + + if (type == Track::kVideo || type == Track::kAudio) { + if (Node *connected = GetConnectedOutput(kBufferIn)) { + TimeRange max_range = media_range(); + if (type == Track::kVideo) { + // Handle thumbnails + RequestRangeForCache(connected->thumbnail_cache(), max_range, range, true, false); + { + TimeRange thumb_range = range.Intersected(max_range); + if (GetAdjustedThumbnailRange(&thumb_range)) { + emit connected->thumbnail_cache()->Request(thumb_range); + } + } + + // Handle video cache + RequestRangeForCache(connected->video_frame_cache(), max_range, range, true, IsAutocaching()); + } else if (type == Track::kAudio) { + // Handle waveforms + RequestRangeForCache(connected->waveform_cache(), max_range, range, true, (OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled)); + + // Handle audio cache + RequestRangeForCache(connected->audio_playback_cache(), max_range, range, true, IsAutocaching()); + } + } + } +} + +void ClipBlock::RequestInvalidatedFromConnected(bool force_all, const TimeRange &intersect) +{ + Track::Type type = GetTrackType(); + + if (type == Track::kVideo || type == Track::kAudio) { + if (Node *connected = GetConnectedOutput(kBufferIn)) { + TimeRange max_range = media_range(); + + if (!intersect.length().isNull()) { + max_range = max_range.Intersected(intersect); + } + + if (type == Track::kVideo) { + // Handle thumbnails + TimeRange thumb_range = max_range; + if (GetAdjustedThumbnailRange(&thumb_range)) { + RequestInvalidatedForCache(connected->thumbnail_cache(), thumb_range); + } + + // Handle video cache + if (IsAutocaching() || force_all) { + RequestInvalidatedForCache(connected->video_frame_cache(), max_range); + } + } else if (type == Track::kAudio) { + // Handle waveforms + if (OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled) { + RequestInvalidatedForCache(connected->waveform_cache(), max_range); + } + + // Handle audio cache + if (IsAutocaching() || force_all) { + RequestInvalidatedForCache(connected->audio_playback_cache(), max_range); + } + } + } + } +} + +void ClipBlock::RequestRangeForCache(PlaybackCache *cache, const TimeRange &max_range, const TimeRange &range, bool invalidate, bool request) +{ + TimeRange r = range.Intersected(max_range); + + if (invalidate) { + cache->Invalidate(r); + } + + if (request) { + emit cache->Request(r); + } +} + +void ClipBlock::RequestInvalidatedForCache(PlaybackCache *cache, const TimeRange &max_range) +{ + TimeRangeList invalid = cache->GetInvalidatedRanges(max_range); + + for (const PlaybackCache::Passthrough &p : cache->GetPassthroughs()) { + invalid.remove(p); + } + + for (const TimeRange &r : invalid) { + RequestRangeForCache(cache, max_range, r, false, true); + } +} + +bool ClipBlock::GetAdjustedThumbnailRange(TimeRange *r) const +{ + switch (static_cast(OLIVE_CONFIG("TimelineThumbnailMode").toInt())) { + case Timeline::kThumbnailOff: + // Don't cache any range + return false; + case Timeline::kThumbnailInOut: + { + // Only cache in point + rational in = this->media_range().in(); + if (r->Contains(in)) { + // Cache only the in point + *r = TimeRange(in, in + thumbnail_cache()->GetTimebase()); + return true; + } else { + // Cache nothing + return false; + } + } + case Timeline::kThumbnailOn: + // Cache entire range + return true; + } + + // Fallback + return true; +} + void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) // If signal is from texture input, transform all times from media time to sequence time if (from == kBufferIn) { + // Render caches where necessary + if (AreCachesEnabled()) { + RequestRangeFromConnected(range); + } + // Adjust range from media time to sequence time TimeRange adj; double speed_value = speed(); @@ -256,6 +403,59 @@ void ClipBlock::LinkChangeEvent() } } +void ClipBlock::InputConnectedEvent(const QString &input, int element, Node *output) +{ + super::InputConnectedEvent(input, element, output); + + if (input == kBufferIn) { + connect(output->thumbnail_cache(), &FrameHashCache::Invalidated, this, &Block::PreviewChanged); + connect(output->waveform_cache(), &AudioPlaybackCache::Invalidated, this, &Block::PreviewChanged); + connect(output->video_frame_cache(), &FrameHashCache::Invalidated, this, &Block::PreviewChanged); + connect(output->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &Block::PreviewChanged); + connect(output->thumbnail_cache(), &FrameHashCache::Validated, this, &Block::PreviewChanged); + connect(output->waveform_cache(), &AudioPlaybackCache::Validated, this, &Block::PreviewChanged); + connect(output->video_frame_cache(), &FrameHashCache::Validated, this, &Block::PreviewChanged); + connect(output->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &Block::PreviewChanged); + } +} + +void ClipBlock::InputDisconnectedEvent(const QString &input, int element, Node *output) +{ + super::InputDisconnectedEvent(input, element, output); + + if (input == kBufferIn) { + disconnect(output->thumbnail_cache(), &FrameHashCache::Invalidated, this, &Block::PreviewChanged); + disconnect(output->waveform_cache(), &AudioPlaybackCache::Invalidated, this, &Block::PreviewChanged); + disconnect(output->video_frame_cache(), &FrameHashCache::Invalidated, this, &Block::PreviewChanged); + disconnect(output->audio_playback_cache(), &AudioPlaybackCache::Invalidated, this, &Block::PreviewChanged); + disconnect(output->thumbnail_cache(), &FrameHashCache::Validated, this, &Block::PreviewChanged); + disconnect(output->waveform_cache(), &AudioPlaybackCache::Validated, this, &Block::PreviewChanged); + disconnect(output->video_frame_cache(), &FrameHashCache::Validated, this, &Block::PreviewChanged); + disconnect(output->audio_playback_cache(), &AudioPlaybackCache::Validated, this, &Block::PreviewChanged); + } +} + +void ClipBlock::InputValueChangedEvent(const QString &input, int element) +{ + super::InputValueChangedEvent(input, element); + + if (input == kAutoCacheInput) { + if (IsAutocaching()) { + RequestInvalidatedFromConnected(); + } else { + Track::Type type = GetTrackType(); + + if (Node *connected = GetConnectedOutput(kBufferIn)) { + if (type == Track::kVideo) { + emit connected->video_frame_cache()->CancelAll(); + } else if (type == Track::kAudio) { + emit connected->audio_playback_cache()->CancelAll(); + } + } + } + } +} + TimeRange ClipBlock::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const { Q_UNUSED(element) @@ -304,6 +504,38 @@ void ClipBlock::Retranslate() SetComboBoxStrings(kLoopModeInput, {tr("None"), tr("Loop"), tr("Clamp")}); } +void ClipBlock::AddCachePassthroughFrom(ClipBlock *other) +{ + if (auto tc = this->video_frame_cache()) { + if (auto oc = other->video_frame_cache()) { + tc->SetPassthrough(oc); + } + } + + if (auto tc = this->audio_playback_cache()) { + if (auto oc = other->audio_playback_cache()) { + tc->SetPassthrough(oc); + } + } + + if (auto tc = this->thumbnails()) { + if (auto oc = other->thumbnails()) { + tc->SetPassthrough(oc); + } + } + + if (auto tc = this->waveform()) { + if (auto oc = other->waveform()) { + tc->SetPassthrough(oc); + } + } +} + +void ClipBlock::ConnectedToPreviewEvent() +{ + RequestInvalidatedFromConnected(); +} + TimeRange ClipBlock::media_range() const { return InputTimeAdjustment(kBufferIn, -1, TimeRange(0, length())); diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index c6571d2b1..5c84a1eca 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -24,6 +24,7 @@ #include "audio/audiovisualwaveform.h" #include "codec/decoder.h" #include "node/block/block.h" +#include "node/output/track/track.h" namespace olive { @@ -47,9 +48,23 @@ public: virtual void set_length_and_media_out(const rational &length) override; virtual void set_length_and_media_in(const rational &length) override; + Track::Type GetTrackType() const + { + if (track()) { + return track()->type(); + } else { + return Track::kNone; + } + } + rational media_in() const; void set_media_in(const rational& media_in); + bool IsAutocaching() const { return GetStandardValue(kAutoCacheInput).toBool(); } + void SetAutocache(bool e); + + void DiscardCache(); + virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; @@ -60,6 +75,8 @@ public: virtual void Retranslate() override; + void RequestInvalidatedFromConnected(bool force_all = false, const TimeRange &intersect = TimeRange()); + double speed() const { return GetStandardValue(kSpeedInput).toDouble(); @@ -110,16 +127,61 @@ public: return block_links_; } - AudioVisualWaveform& waveform() + FrameHashCache *connected_video_cache() const { - return waveform_; + if (Node *n = GetConnectedOutput(kBufferIn)) { + return n->video_frame_cache(); + } else { + return nullptr; + } } + AudioPlaybackCache *connected_audio_cache() const + { + if (Node *n = GetConnectedOutput(kBufferIn)) { + return n->audio_playback_cache(); + } else { + return nullptr; + } + } + + FrameHashCache *thumbnails() + { + if (Node *n = GetConnectedOutput(kBufferIn)) { + return n->thumbnail_cache(); + } else { + return nullptr; + } + } + + AudioWaveformCache *waveform() + { + if (Node *n = GetConnectedOutput(kBufferIn)) { + return n->waveform_cache(); + } else { + return nullptr; + } + } + + void AddCachePassthroughFrom(ClipBlock *other); + ViewerOutput *connected_viewer() const { return connected_viewer_; } + virtual TimeRange GetVideoCacheRange() const override + { + return TimeRange(0, length()); + } + + virtual TimeRange GetAudioCacheRange() const override + { + return TimeRange(0, length()); + } + + virtual void ConnectedToPreviewEvent() override; + TimeRange media_range() const; /** @@ -142,9 +204,17 @@ public: static const QString kMaintainAudioPitchInput; static const QString kLoopModeInput; + static const QString kAutoCacheInput; + protected: virtual void LinkChangeEvent() override; + virtual void InputConnectedEvent(const QString& input, int element, Node *output) override; + + virtual void InputDisconnectedEvent(const QString& input, int element, Node *output) override; + + virtual void InputValueChangedEvent(const QString& input, int element) override; + private: enum SequenceToMediaTimeFlag { @@ -158,6 +228,13 @@ private: rational MediaToSequenceTime(const rational& media_time) const; + void RequestRangeFromConnected(const TimeRange &range); + + void RequestRangeForCache(PlaybackCache *cache, const TimeRange &max_range, const TimeRange &range, bool invalidate, bool request); + void RequestInvalidatedForCache(PlaybackCache *cache, const TimeRange &max_range); + + bool GetAdjustedThumbnailRange(TimeRange *r) const; + QVector block_links_; TransitionBlock* in_transition_; @@ -166,11 +243,8 @@ private: ViewerOutput *connected_viewer_; private: - AudioVisualWaveform waveform_; - rational last_media_in_; - }; } diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp index 9a0622f75..28264a446 100644 --- a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -55,7 +55,7 @@ ShaderCode CrossDissolveTransition::GetShaderCode(const ShaderRequest &request) void CrossDissolveTransition::SampleJobEvent(const SampleBuffer &from_samples, const SampleBuffer &to_samples, SampleBuffer &out_samples, double time_in) const { - for (int i=0; i= 0) { + size_t remain = (out_samples.sample_count() - to_samples.sample_count()); + if (i >= remain) { + qint64 in_index = i - remain; out_samples.data(j)[i] += to_samples.data(j)[in_index] * TransformCurve(progress); } } diff --git a/app/node/color/colormanager/colormanager.cpp b/app/node/color/colormanager/colormanager.cpp index a8a446208..4dfac3e6c 100644 --- a/app/node/color/colormanager/colormanager.cpp +++ b/app/node/color/colormanager/colormanager.cpp @@ -272,7 +272,21 @@ void ColorManager::InputValueChangedEvent(const QString &input, int element) if (input == kConfigFilenameIn) { try { + QString old_default_cs = GetDefaultInputColorSpace(); + SetConfig(OCIO::Config::CreateFromFile(GetConfigFilename().toUtf8())); + + // Set new default colorspace appropriately + int new_default = 0; + QStringList available_cs = ListAvailableColorspaces(); + for (int i=0; i #include "common/cpuoptimize.h" -#include "common/functiontimer.h" namespace olive { diff --git a/app/node/graph.cpp b/app/node/graph.cpp index b4cdf44bd..44165727f 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -39,6 +39,10 @@ void NodeGraph::Clear() { // By deleting the last nodes first, we assume that nodes that are most important are deleted last // (e.g. Project's ColorManager or ProjectSettingsNode. + for (auto it=node_children_.cbegin(); it!=node_children_.cend(); it++) { + (*it)->SetCachesEnabled(false); + } + while (!node_children_.isEmpty()) { delete node_children_.last(); } diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index a248bb41c..14045b054 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -301,21 +301,21 @@ void MathNodeBase::ValueInternal(Operation operation, Pairing pairing, const QSt SampleBuffer samples_a = val_a.toSamples(); SampleBuffer samples_b = val_b.toSamples(); - int max_samples = qMax(samples_a.sample_count(), samples_b.sample_count()); - int min_samples = qMin(samples_a.sample_count(), samples_b.sample_count()); + size_t max_samples = qMax(samples_a.sample_count(), samples_b.sample_count()); + size_t min_samples = qMin(samples_a.sample_count(), samples_b.sample_count()); SampleBuffer mixed_samples = SampleBuffer(samples_a.audio_params(), max_samples); for (int i=0;i(operation, samples_a.data(i)[j], samples_b.data(i)[j]); } } if (max_samples > min_samples) { // Fill in remainder space with 0s - int remainder = max_samples - min_samples; + size_t remainder = max_samples - min_samples; const SampleBuffer &larger_buffer = (max_samples == samples_a.sample_count()) ? samples_a : samples_b; diff --git a/app/node/node.cpp b/app/node/node.cpp index 143f1e9ee..1c2ec5c4e 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -48,13 +48,17 @@ Node::Node() : can_be_deleted_(true), override_color_(-1), folder_(nullptr), - cache_result_(false), - flags_(kNone) + flags_(kNone), + caches_enabled_(true) { AddInput(kEnabledInput, NodeValue::kBoolean, true); video_cache_ = new FrameHashCache(this); + thumbnail_cache_ = new ThumbnailCache(this); audio_cache_ = new AudioPlaybackCache(this); + waveform_cache_ = new AudioWaveformCache(this); + + waveform_cache_->SetSavingEnabled(false); } Node::~Node() @@ -232,6 +236,14 @@ void Node::DisconnectEdge(Node *output, const NodeInput &input) } } +void Node::CopyCacheUuidsFrom(Node *n) +{ + video_cache_->SetUuid(n->video_cache_->GetUuid()); + audio_cache_->SetUuid(n->audio_cache_->GetUuid()); + thumbnail_cache_->SetUuid(n->thumbnail_cache_->GetUuid()); + waveform_cache_->SetUuid(n->waveform_cache_->GetUuid()); +} + QString Node::GetInputName(const QString &id) const { const Input* i = GetInternalInputData(id); @@ -932,12 +944,18 @@ void Node::InvalidateCache(const TimeRange &range, const QString &from, int elem Q_UNUSED(from) Q_UNUSED(element) - if (range.in() != range.out()) { - if (video_cache_->IsEnabled()) { - video_frame_cache()->Invalidate(range); - } - if (audio_cache_->IsEnabled()) { - audio_playback_cache()->Invalidate(range); + if (AreCachesEnabled()) { + if (range.in() != range.out()) { + TimeRange vr = range.Intersected(GetVideoCacheRange()); + if (vr.length() != 0) { + video_frame_cache()->Invalidate(vr); + thumbnail_cache()->Invalidate(vr); + } + TimeRange ar = range.Intersected(GetAudioCacheRange()); + if (ar.length() != 0) { + audio_playback_cache()->Invalidate(ar); + waveform_cache()->Invalidate(ar); + } } } diff --git a/app/node/node.h b/app/node/node.h index f43a4afcc..2ca325d34 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -40,6 +40,7 @@ #include "node/param.h" #include "render/audioparams.h" #include "render/audioplaybackcache.h" +#include "render/audiowaveformcache.h" #include "render/framehashcache.h" #include "render/job/generatejob.h" #include "render/job/samplejob.h" @@ -226,11 +227,24 @@ public: return video_cache_; } + ThumbnailCache* thumbnail_cache() const + { + return thumbnail_cache_; + } + AudioPlaybackCache* audio_playback_cache() const { return audio_cache_; } + AudioWaveformCache* waveform_cache() const + { + return waveform_cache_; + } + + virtual TimeRange GetVideoCacheRange() const { return TimeRange(); } + virtual TimeRange GetAudioCacheRange() const { return TimeRange(); } + struct Position { Position(const QPointF &p = QPointF(0, 0), bool e = false) @@ -339,6 +353,11 @@ public: static void DisconnectEdge(Node *output, const NodeInput& input); + void CopyCacheUuidsFrom(Node *n); + + bool AreCachesEnabled() const { return caches_enabled_; } + void SetCachesEnabled(bool e) { caches_enabled_ = e; } + virtual QString GetInputName(const QString& id) const; void SetInputName(const QString& id, const QString& name); @@ -914,16 +933,6 @@ public: folder_ = folder; } - bool GetCacheTextures() const - { - return cache_result_; - } - - void SetCacheTextures(bool e) - { - cache_result_ = e; - } - class ArrayRemoveCommand : public UndoCommand { public: @@ -983,6 +992,7 @@ public: void SetInputFlags(const QString &input, const InputFlags &f); virtual void LoadFinishedEvent(){} + virtual void ConnectedToPreviewEvent(){} static void SetValueAtTime(const NodeInput &input, const rational &time, const QVariant &value, int track, MultiUndoCommand *command, bool insert_on_all_tracks_if_no_key); @@ -1388,8 +1398,6 @@ private: Folder* folder_; - bool cache_result_; - QMap value_hints_; PositionMap context_positions_; @@ -1401,8 +1409,12 @@ private: QString effect_input_; FrameHashCache *video_cache_; + ThumbnailCache *thumbnail_cache_; AudioPlaybackCache *audio_cache_; + AudioWaveformCache *waveform_cache_; + + bool caches_enabled_; private slots: /** diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 11f6142e2..8c9456626 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -37,7 +37,9 @@ const QString ViewerOutput::kSamplesInput = QStringLiteral("samples_in"); ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_streams) : last_length_(0), video_length_(0), - audio_length_(0) + audio_length_(0), + autocache_input_video_(false), + autocache_input_audio_(false) { AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden)); @@ -214,26 +216,31 @@ void ViewerOutput::set_default_parameters() OLIVE_CONFIG("DefaultSequenceAudioLayout").toULongLong(), AudioParams::kInternalFormat )); - - video_frame_cache()->SetEnabled(OLIVE_CONFIG("DefaultSequenceAutoCache").toBool()); } void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) { Q_UNUSED(element) + if (Node *connected = GetConnectedOutput(from, element)) { + if (from == kTextureInput) { + //emit connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); + if (autocache_input_video_) { + TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetVideoLength())); + emit connected->video_frame_cache()->Request(range.Intersected(max_range)); + } + } else if (from == kSamplesInput) { + TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetAudioLength())); + emit connected->waveform_cache()->Request(range.Intersected(max_range)); + if (autocache_input_audio_) { + emit connected->audio_playback_cache()->Request(range.Intersected(max_range)); + } + } + } + VerifyLength(); super::InvalidateCache(range, from, element, options); - - // TEMP: Just to restore the intended functionality for now. This will be removed later. - if (from == kTextureInput) { - TimeRange r = range.Intersected(TimeRange(0, GetVideoLength())); - if (r.length() != 0) video_frame_cache()->Invalidate(r); - } else if (from == kSamplesInput) { - TimeRange r = range.Intersected(TimeRange(0, GetAudioLength())); - if (r.length() != 0) audio_playback_cache()->Invalidate(r); - } } QVector ViewerOutput::GetEnabledStreamsAsReferences() const @@ -310,6 +317,8 @@ void ViewerOutput::InputConnectedEvent(const QString &input, int element, Node * { if (input == kTextureInput) { emit TextureInputChanged(); + } else if (input == kSamplesInput) { + connect(output->waveform_cache(), &AudioWaveformCache::Validated, this, &ViewerOutput::ConnectedWaveformChanged); } super::InputConnectedEvent(input, element, output); @@ -319,6 +328,8 @@ void ViewerOutput::InputDisconnectedEvent(const QString &input, int element, Nod { if (input == kTextureInput) { emit TextureInputChanged(); + } else if (input == kSamplesInput) { + disconnect(output->waveform_cache(), &AudioWaveformCache::Validated, this, &ViewerOutput::ConnectedWaveformChanged); } super::InputDisconnectedEvent(input, element, output); @@ -376,6 +387,17 @@ Node::ValueHint ViewerOutput::GetConnectedSampleValueHint() return GetValueHintForInput(kSamplesInput); } +void ViewerOutput::ConnectedToPreviewEvent() +{ + if (Node *connected = GetConnectedOutput(kSamplesInput)) { + TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength())); + TimeRangeList invalid = connected->waveform_cache()->GetInvalidatedRanges(max_range); + for (const TimeRange &r : invalid) { + emit connected->waveform_cache()->Request(r); + } + } +} + void ViewerOutput::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { if (HasInputWithID(kTextureInput)) { @@ -415,10 +437,6 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) } if (frame_rate_changed) { - // FIXME: Will need to find a better way to update this soon - //if (video_frame_cache()->IsEnabled()) { - video_frame_cache()->SetTimebase(new_video_params.frame_rate_as_time_base()); - //} emit FrameRateChanged(new_video_params.frame_rate()); } @@ -438,11 +456,6 @@ void ViewerOutput::InputValueChangedEvent(const QString &input, int element) emit AudioParamsChanged(); - // FIXME: Will need to find a better way to update this soon - //if (audio_playback_cache()->IsEnabled()) { - audio_playback_cache()->SetParameters(GetAudioParams()); - //} - cached_audio_params_ = new_audio_params; } diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 81e96f1d2..4a1bf10a2 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -132,6 +132,15 @@ public: return GetVideoStreamCount() + GetAudioStreamCount() + GetSubtitleStreamCount(); } + const AudioWaveformCache *GetConnectedWaveform() + { + if (Node *n = GetConnectedSampleOutput()) { + return n->waveform_cache(); + } else { + return nullptr; + } + } + bool HasEnabledVideoStreams() const; bool HasEnabledAudioStreams() const; bool HasEnabledSubtitleStreams() const; @@ -147,6 +156,16 @@ public: TimelineWorkArea *GetWorkArea() const { return workarea_; } TimelineMarkerList *GetMarkers() const { return markers_; } + virtual TimeRange GetVideoCacheRange() const override + { + return TimeRange(0, GetVideoLength()); + } + + virtual TimeRange GetAudioCacheRange() const override + { + return TimeRange(0, GetAudioLength()); + } + QVector GetEnabledStreamsAsReferences() const; QVector GetEnabledVideoStreams() const; @@ -163,6 +182,11 @@ public: virtual ValueHint GetConnectedSampleValueHint(); + virtual void ConnectedToPreviewEvent() override; + + bool IsVideoAutoCacheEnabled() const { qDebug() << "sequence ac is a stub"; return false; } + void SetVideoAutoCacheEnabled(bool e) { qDebug() << "sequence ac is a stub"; } + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; const EncodingParams &GetLastUsedEncodingParams() const { return last_used_encoding_params_; } @@ -193,6 +217,8 @@ signals: void SampleRateChanged(int sr); + void ConnectedWaveformChanged(); + public slots: void VerifyLength(); @@ -220,6 +246,9 @@ private: TimelineWorkArea *workarea_; TimelineMarkerList *markers_; + bool autocache_input_video_; + bool autocache_input_audio_; + EncodingParams last_used_encoding_params_; }; diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 459e1711f..21b837704 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -47,8 +47,6 @@ Footage::Footage(const QString &filename) : cancelled_(nullptr), total_stream_count_(0) { - SetCacheTextures(true); - PrependInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); Clear(); @@ -61,6 +59,8 @@ Footage::Footage(const QString &filename) : check_timer->setInterval(5000); connect(check_timer, &QTimer::timeout, this, &Footage::CheckFootage); check_timer->start(); + + connect(this->waveform_cache(), &AudioWaveformCache::Validated, this, &ViewerOutput::ConnectedWaveformChanged); } void Footage::Retranslate() diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index fc90aab6f..513990302 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -104,7 +104,11 @@ ProjectSerializer220403::LoadData ProjectSerializer220403::Load(Project *project qWarning() << "Failed to find node with ID" << id; reader->skipCurrentElement(); } else { + // Disable cache while node is being loaded (we'll re-enable it later) + node->SetCachesEnabled(false); + LoadNode(node, xml_node_data, reader); + node->setParent(project); } } @@ -326,6 +330,11 @@ ProjectSerializer220403::LoadData ProjectSerializer220403::Load(Project *project } } + // Re-enable caches + for (Node *n : project->nodes()) { + n->SetCachesEnabled(true); + } + return load_data; } @@ -558,6 +567,20 @@ void ProjectSerializer220403::LoadNode(Node *node, XMLNodeData &xml_node_data, Q reader->skipCurrentElement(); } } + } else if (reader->name() == QStringLiteral("caches")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("audio")) { + node->audio_playback_cache()->SetUuid(reader->readElementText()); + } else if (reader->name() == QStringLiteral("video")) { + node->video_frame_cache()->SetUuid(reader->readElementText()); + } else if (reader->name() == QStringLiteral("thumb")) { + node->thumbnail_cache()->SetUuid(reader->readElementText()); + } else if (reader->name() == QStringLiteral("waveform")) { + node->waveform_cache()->SetUuid(reader->readElementText()); + } else { + reader->skipCurrentElement(); + } + } } else { reader->skipCurrentElement(); } @@ -613,6 +636,15 @@ void ProjectSerializer220403::SaveNode(Node *node, QXmlStreamWriter *writer) con } WriteEndElement(writer); // hints + WriteStartElement(writer, QStringLiteral("caches")); + + writer->writeTextElement(QStringLiteral("audio"), node->audio_playback_cache()->GetUuid().toString()); + writer->writeTextElement(QStringLiteral("video"), node->video_frame_cache()->GetUuid().toString()); + writer->writeTextElement(QStringLiteral("thumb"), node->thumbnail_cache()->GetUuid().toString()); + writer->writeTextElement(QStringLiteral("waveform"), node->waveform_cache()->GetUuid().toString()); + + WriteEndElement(writer); // caches + WriteStartElement(writer, QStringLiteral("custom")); SaveNodeCustom(writer, node); diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index ea21ffa83..766fd1384 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -57,7 +57,7 @@ NodeValueRow NodeTraverser::GenerateRow(NodeValueDatabase *database, const Node NodeValueRow row; for (auto it=database->begin(); it!=database->end(); it++) { // Get hint for which value should be pulled - NodeValue value = GenerateRowValue(node, it.key(), &it.value()); + NodeValue value = GenerateRowValue(node, it.key(), &it.value(), range); row.insert(it.key(), value); } @@ -74,9 +74,9 @@ NodeValueRow NodeTraverser::GenerateRow(const Node *node, const TimeRange &range return GenerateRow(&database, node, range); } -NodeValue NodeTraverser::GenerateRowValue(const Node *node, const QString &input, NodeValueTable *table) +NodeValue NodeTraverser::GenerateRowValue(const Node *node, const QString &input, NodeValueTable *table, const TimeRange &time) { - NodeValue value = GenerateRowValueElement(node, input, -1, table); + NodeValue value = GenerateRowValueElement(node, input, -1, table, time); if (value.array()) { // Resolve each element of array @@ -84,7 +84,7 @@ NodeValue NodeTraverser::GenerateRowValue(const Node *node, const QString &input QVector output(tables.size()); for (int i=0; iGetValueHintForInput(input, element), node->GetInputDataType(input), table); -} - -NodeValue NodeTraverser::GenerateRowValueElement(const Node::ValueHint &hint, NodeValue::Type preferred_type, NodeValueTable *table) -{ - int value_index = GenerateRowValueElementIndex(hint, preferred_type, table); + int value_index = GenerateRowValueElementIndex(node->GetValueHintForInput(input, element), node->GetInputDataType(input), table); if (value_index == -1) { // If value was -1, try getting the last value @@ -112,7 +107,20 @@ NodeValue NodeTraverser::GenerateRowValueElement(const Node::ValueHint &hint, No return NodeValue(); } - return table->TakeAt(value_index); + NodeValue value = table->TakeAt(value_index); + + if (value.type() == NodeValue::kTexture) { + QMutexLocker locker(node->video_frame_cache()->mutex()); + + node->video_frame_cache()->LoadState(); + + QString cache = node->video_frame_cache()->GetValidCacheFilename(time.in()); + if (!cache.isEmpty()) { + value.set_value(CacheJob(cache, value.data())); + } + } + + return value; } int NodeTraverser::GenerateRowValueElementIndex(const Node::ValueHint &hint, NodeValue::Type preferred_type, const NodeValueTable *table) @@ -339,6 +347,10 @@ NodeValueTable NodeTraverser::GenerateBlockTable(const Track *track, const TimeR return table; } +TexturePtr NodeTraverser::ProcessVideoCacheJob(const CacheJob &val) +{ + return nullptr; +} QVector2D NodeTraverser::GenerateResolution() const { @@ -348,6 +360,16 @@ QVector2D NodeTraverser::GenerateResolution() const void NodeTraverser::ResolveJobs(NodeValue &val, const TimeRange &range) { if (val.type() == NodeValue::kTexture || val.type() == NodeValue::kSamples) { + if (val.canConvert()) { + CacheJob job = val.value(); + TexturePtr tex = ProcessVideoCacheJob(job); + if (tex) { + val.set_value(tex); + } else { + val.set_value(job.GetFallback()); + } + } + if (val.canConvert()) { ShaderJob job = val.value(); @@ -426,17 +448,14 @@ void NodeTraverser::ResolveJobs(NodeValue &val, const TimeRange &range) VideoParams render_params = GetCacheVideoParams(); VideoParams job_params = job.video_params(); - // HACK/FIXME: Override old cached probe data that contains an invalid divider. Might be - // good in the future to version the probe data so we can automatically - // ignore older stuff. - job_params.set_divider(render_params.divider()); - - // See if we can make this divider larger (i.e. if the footage is smaller) - while (job_params.divider() > 1 - && VideoParams::GetScaledDimension(job_params.width(), job_params.divider()-1) < render_params.effective_width() - && VideoParams::GetScaledDimension(job_params.height(), job_params.divider()-1) < render_params.effective_height()) { - job_params.set_divider(job_params.divider() - 1); + if (render_params.divider() > 1) { + // Use a divider appropriate for this target resolution + job_params.set_divider(VideoParams::GetDividerForTargetResolution(job_params.width(), job_params.height(), render_params.effective_width(), render_params.effective_height())); + } else { + // Render everything at full res + job_params.set_divider(1); } + job.set_video_params(job_params); if (footage_time.isNaN()) { diff --git a/app/node/traverser.h b/app/node/traverser.h index 84f1b1606..bde1733ae 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -26,9 +26,11 @@ #include "codec/decoder.h" #include "common/cancelableobject.h" #include "node/output/track/track.h" +#include "render/job/cachejob.h" #include "render/cancelatom.h" #include "render/job/footagejob.h" #include "render/job/colortransformjob.h" +#include "render/job/footagejob.h" #include "value.h" namespace olive { @@ -45,9 +47,8 @@ public: NodeValueRow GenerateRow(NodeValueDatabase *database, const Node *node, const TimeRange &range); NodeValueRow GenerateRow(const Node *node, const TimeRange &range); - NodeValue GenerateRowValue(const Node *node, const QString &input, NodeValueTable *table); - NodeValue GenerateRowValueElement(const Node *node, const QString &input, int element, NodeValueTable *table); - NodeValue GenerateRowValueElement(const Node::ValueHint &hint, NodeValue::Type preferred_type, NodeValueTable *table); + NodeValue GenerateRowValue(const Node *node, const QString &input, NodeValueTable *table, const TimeRange &time); + NodeValue GenerateRowValueElement(const Node *node, const QString &input, int element, NodeValueTable *table, const TimeRange &time); int GenerateRowValueElementIndex(const Node::ValueHint &hint, NodeValue::Type preferred_type, const NodeValueTable *table); int GenerateRowValueElementIndex(const Node *node, const QString &input, int element, const NodeValueTable *table); @@ -102,6 +103,8 @@ protected: virtual void ConvertToReferenceSpace(TexturePtr destination, TexturePtr source, const QString &input_cs){} + virtual TexturePtr ProcessVideoCacheJob(const CacheJob &val); + virtual TexturePtr CreateTexture(const VideoParams &p) { return CreateDummyTexture(p); @@ -122,11 +125,6 @@ protected: } } - virtual bool CanCacheFrames() - { - return false; - } - QVector2D GenerateResolution() const; bool IsCancelled() diff --git a/app/node/value.h b/app/node/value.h index 2a0b14620..e4d373ae7 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -231,6 +231,8 @@ public: data_ = QVariant::fromValue(v); } + const QVariant &data() const { return data_; } + template bool canConvert() const { diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index d97eb7bd3..dcbb72fe3 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -58,6 +58,7 @@ ProjectPanel::ProjectPanel(QWidget *parent) : layout->addWidget(explorer_); connect(explorer_, &ProjectExplorer::DoubleClickedItem, this, &ProjectPanel::ItemDoubleClickSlot); connect(explorer_, &ProjectExplorer::SelectionChanged, this, &ProjectPanel::SelectionChanged); + connect(toolbar, &ProjectToolbar::SearchChanged, explorer_, &ProjectExplorer::SetSearchFilter); // Set toolbar's view to the explorer's view toolbar->SetView(explorer_->view_type()); diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index dcde3f3ed..2c779e2e8 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -24,6 +24,8 @@ set(OLIVE_SOURCES render/audioparams.h render/audioplaybackcache.cpp render/audioplaybackcache.h + render/audiowaveformcache.cpp + render/audiowaveformcache.h render/cancelatom.h render/color.cpp render/color.h diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index 39680fce8..d5dd635b7 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -106,7 +106,14 @@ qint64 AudioParams::samples_to_bytes(const qint64 &samples) const { Q_ASSERT(is_valid()); - return samples * channel_count() * bytes_per_sample_per_channel(); + return samples_to_bytes_per_channel(samples) * channel_count(); +} + +qint64 AudioParams::samples_to_bytes_per_channel(const qint64 &samples) const +{ + Q_ASSERT(is_valid()); + + return samples * bytes_per_sample_per_channel(); } rational AudioParams::samples_to_time(const qint64 &samples) const diff --git a/app/render/audioparams.h b/app/render/audioparams.h index 2f195b645..a58767328 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -213,6 +213,7 @@ public: qint64 time_to_samples(const double& time) const; qint64 time_to_samples(const rational& time) const; qint64 samples_to_bytes(const qint64& samples) const; + qint64 samples_to_bytes_per_channel(const qint64& samples) const; rational samples_to_time(const qint64& samples) const; qint64 bytes_to_samples(const qint64 &bytes) const; rational bytes_to_time(const qint64 &bytes) const; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 859ce454e..6113a6fd9 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -39,8 +39,6 @@ AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : AudioPlaybackCache::~AudioPlaybackCache() { - // Segments are volatile, so delete them here - ClearPlaylist(); } void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) @@ -50,115 +48,13 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) } params_ = params; - visual_.set_channel_count(params_.channel_count()); - - // Restart empty file so there's always "something" to play - ClearPlaylist(); - - emit ParametersChanged(); } void AudioPlaybackCache::WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, const SampleBuffer &samples) { - // Ensure if we have enough segments to write this data, creating more if not - qint64 length_diff = params_.time_to_bytes_per_channel(range.out()) - playlist_.GetLength(); - while (length_diff > 0) { - qint64 seg_sz = qMin(kDefaultSegmentSizePerChannel, length_diff); - playlist_.push_back(CreateSegment(seg_sz, playlist_.GetLength())); - length_diff -= seg_sz; - } - - // Keep track of validated ranges so we can signal them all at once at the end - TimeRangeList ranges_we_validated; - - // Calculate buffer size per channel - qint64 buffer_size_per_channel = samples.sample_count() * params_.bytes_per_sample_per_channel(); - - // Write each valid range to the segments - foreach (const TimeRange& r, valid_ranges) { - rational this_segment_in = 0; - - // Write PCM to playlist - for (auto it=playlist_.begin(); it!=playlist_.end(); it++) { - rational this_segment_out = this_segment_in + params_.bytes_per_channel_to_time((*it).size()); - - if (r.in() < this_segment_out) { - // We'll write at least something to this segment - bool succeeded = true; - - // Calculate how much to write - rational this_write_in_point = qMax(r.in(), this_segment_in); - rational this_write_out_point = qMin(r.out(), this_segment_out); - - for (int i=0; i<(*it).channels(); i++) { - QFile seg_file((*it).filename(i)); - - if (seg_file.open(QFile::ReadWrite)) { - // Calculate what the byte offsets are going to be in this segment file - rational in_point_relative = this_write_in_point - this_segment_in; - qint64 dst_offset = params_.time_to_bytes_per_channel(in_point_relative); - - // Calculate where to retrieve data from in the source buffer - qint64 src_offset = params_.time_to_bytes_per_channel(this_write_in_point - range.in()); - - // Determine how many bytes need to be written - qint64 total_write_length = params_.time_to_bytes_per_channel(this_write_out_point - this_write_in_point); - - // Determine how many bytes we actually have in the source buffer - qint64 possible_write_length = qMin(qMax(qint64(0), buffer_size_per_channel - src_offset), total_write_length); - - // Seek to our start offset - seg_file.seek(dst_offset); - - // If we have source bytes to write, write them here - if (possible_write_length > 0) { - // Assume `samples` is valid if we're here, or else `buffer_size_per_channel` and - // therefore `possible_write_length` will be 0. - seg_file.write(reinterpret_cast(samples.data(i)) + src_offset, possible_write_length); - } - - if (possible_write_length < total_write_length) { - // Fill remaining space with silence - QByteArray s(total_write_length - possible_write_length, 0x00); - seg_file.write(s); - } - - seg_file.close(); - } else { - qWarning() << "Failed to write PCM data to" << seg_file.fileName(); - succeeded = false; - } - } - - if (succeeded) { - ranges_we_validated.insert(TimeRange(this_write_in_point, this_write_out_point)); - } - } - - if (r.out() <= this_segment_out) { - // We've reached the end of this range, we can break out of the loop here - break; - } - - // Each segment is contiguous, so this out will be the next segment's in - this_segment_in = this_segment_out; - } - } - - foreach (const TimeRange& v, ranges_we_validated) { - Validate(v); - } -} - -void AudioPlaybackCache::WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform) -{ - // Write each valid range to the segments - foreach (const TimeRange& r, valid_ranges) { - // Write visual - if (waveform) { - visual_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length()); - } else { - visual_.OverwriteSilence(r.in(), r.length()); + for (const TimeRange &r : valid_ranges) { + if (WritePartOfSampleBuffer(samples, r.in(), r.in() - range.in(), r.length())) { + Validate(r); } } } @@ -170,311 +66,70 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range) WritePCM(range, {range}, SampleBuffer()); } -AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const +bool AudioPlaybackCache::WritePartOfSampleBuffer(const SampleBuffer &samples, const rational &write_start, const rational &buffer_start, const rational &length) { - Segment new_seg = s; + qint64 length_in_bytes = params_.time_to_bytes_per_channel(length); - new_seg.set_channels(s.channels()); + qint64 start_cache_offset = params_.time_to_bytes_per_channel(write_start); + qint64 end_cache_offset = start_cache_offset + length_in_bytes; - // Copy data to a new file - for (int i=0; igenerate(); - new_seg_filename = cache_dir.filePath(QStringLiteral("%1.pcm").arg(r)); - } while (QFileInfo::exists(new_seg_filename)); - - return new_seg_filename; -} - -void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, qint64 new_length) -{ - // Read filename - for (int i=0; ichannels(); i++) { - QFile f(s->filename(i)); - if (f.open(QFile::ReadWrite)) { - // Read segment into memory, according to the size we acknowledge - QByteArray data = f.read(s->size()); - - // Trim to new length - data = data.right(new_length); - - // Seek to start and write - f.seek(0); - - // Write trimmed data - f.write(data); - - f.close(); - } - } - - s->set_size(new_length); -} - -void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, qint64 new_length) -{ - // For efficiency, we don't truncate the file, we just truncate our usage of it - s->set_size(new_length); -} - -void AudioPlaybackCache::RemoveSegmentFromArray(int index) -{ - const Segment &s = playlist_.at(index); - for (int i=0; i(this->parent())) { - d->SetDataLimit(params_.time_to_bytes_per_channel(viewer->GetAudioLength())); - } - - return d; -} - -AudioPlaybackCache::Segment::Segment(qint64 size) -{ - size_ = size; -} - -AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, int sample_sz, QObject *parent) : - QIODevice(parent), - playlist_(playlist), - current_segment_(0), - segment_read_index_(0), - sample_size_(sample_sz), - limit_(INT64_MAX) -{ -} - -AudioPlaybackCache::PlaybackDevice::~PlaybackDevice() -{ - close(); -} - -bool AudioPlaybackCache::PlaybackDevice::seek(qint64 pos) -{ - // Default behavior - QIODevice::seek(pos); - - // Find which segment we're in - current_segment_ = playlist_.GetIndexOfPosition(pos); - - // Catch failure to find index - if (current_segment_ == -1) { - return false; - } - - // Find position in segment - segment_read_index_ = pos - playlist_.at(current_segment_).offset(); - - return true; -} - -qint64 AudioPlaybackCache::PlaybackDevice::readData(char *data, qint64 maxSize) -{ - qint64 read_size = 0; - - while (read_size < maxSize - && current_segment_ >= 0 - && current_segment_ < playlist_.size() - && playlist_.at(current_segment_).offset() + segment_read_index_ < limit_) { - const Segment& cs = playlist_.at(current_segment_); - qint64 current_segment_sz = cs.size(); - - if (cs.offset() + current_segment_sz > limit_) { - current_segment_sz = limit_ - cs.offset(); + if (write_len > max_buffer_len) { + zero_len = write_len - max_buffer_len; + write_len = max_buffer_len; } - QVector segment_files(cs.channels()); - segment_files.fill(nullptr); + for (int channel=0; channelopen(QFile::ReadOnly)) { - // Seek to our stored index of this segment - f->seek(segment_read_index_); - } else { - all_files_opened = false; + if (!FileFunctions::DirectoryIsValid(QFileInfo(filename).dir())) { + success = false; break; } - } - // If all file handles opened successfully, time to interleave and send them out - if (all_files_opened) { - // Determine how many bytes to read - qint64 this_read_length = qMin((current_segment_sz - segment_read_index_) * cs.channels(), maxSize - read_size); + QFile f(filename); + if (f.open(QFile::ReadWrite)) { + f.seek(offset_in_segment); + f.write(reinterpret_cast(samples.data(channel)) + current_buffer_offset, write_len); - qint64 target = read_size + this_read_length; - - while (read_size < target) { - for (int i=0; iread(data + read_size, sample_size_); - - // Add to the read size - read_size += sample_size_; + if (zero_len > 0) { + QByteArray b(zero_len, 0); + f.write(b.constData()); } - // Add to the read index - segment_read_index_ += sample_size_; - } - - // If we've reached the end of this segment, tick the counter over to the next segment - if (segment_read_index_ == current_segment_sz) { - // Jump to the next file - segment_read_index_ = 0; - current_segment_++; + f.close(); + } else { + success = false; } } - // Close and delete file handles - for (int i=0; iisOpen()) { - f->close(); - } - delete f; - } - } + current_cache_offset += write_len; + current_buffer_offset += write_len; } - if (read_size < maxSize) { - // Zero out remaining data - memset(data + read_size, 0, maxSize - read_size); - } - - //return read_size; - return maxSize; + return success; } -int AudioPlaybackCache::Playlist::GetIndexOfPosition(qint64 pos) +QString AudioPlaybackCache::GetSegmentFilename(qint64 segment_index, int channel) { - if (this->isEmpty() - || pos < 0 - || pos >= GetLength()) { - return -1; - } - - if (pos < this->first().size()) { - return 0; - } - - if (pos > this->last().offset()) { - return this->size() - 1; - } - - // Use a binary search to find the segment with the right offset - int low = 0; - int high = this->size() - 1; - while (low <= high) { - int mid = low + (high - low) / 2; - - const Segment& mid_segment = this->at(mid); - if (mid_segment.offset() <= pos && mid_segment.offset() + mid_segment.size() > pos) { - return mid; - } else if (mid_segment.offset() < pos) { - low = mid + 1; - } else { - high = mid - 1; - } - } - - return -1; -} - -qint64 AudioPlaybackCache::Playlist::GetLength() const -{ - if (this->isEmpty()) { - return 0; - } - return this->last().offset() + this->last().size(); + return GetThisCacheDirectory().filePath(QStringLiteral("%1.%2").arg(QString::number(segment_index), QString::number(channel))); } } diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 164953418..6a7949482 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -68,171 +68,17 @@ public: void WritePCM(const TimeRange &range, const TimeRangeList &valid_ranges, const SampleBuffer &samples); - void WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform); - void WriteSilence(const TimeRange &range); - class Segment - { - public: - Segment(qint64 size = 0); - - qint64 size() const - { - return size_; - } - - void set_size(qint64 sz) - { - size_ = sz; - } - - qint64 offset() const - { - return offset_; - } - - void set_offset(qint64 o) - { - offset_ = o; - } - - int channels() const - { - return filenames_.size(); - } - - void set_channels(int index) - { - filenames_.resize(index); - } - - const QString& filename(int index) const - { - return filenames_.at(index); - } - - void set_filename(int index, const QString& filename) - { - filenames_[index] = filename; - } - - qint64 end() const - { - return offset_ + size_; - } - - private: - QVector filenames_; - - qint64 size_; - - qint64 offset_; - - }; - - class Playlist : public QVector - { - public: - Playlist() = default; - - int GetIndexOfPosition(qint64 pos); - - qint64 GetLength() const; - - }; - - class PlaybackDevice : public QIODevice - { - public: - PlaybackDevice(const Playlist& playlist, int sample_sz, QObject* parent = nullptr); - - void SetDataLimit(qint64 limit) - { - limit_ = limit; - } - - virtual ~PlaybackDevice() override; - - virtual bool isSequential() const override - { - return false; - } - - virtual bool seek(qint64 pos) override; - - virtual qint64 size() const override - { - return playlist_.GetLength(); - } - - virtual qint64 readData(char *data, qint64 maxSize) override; - - virtual qint64 writeData(const char *data, qint64 maxSize) override - { - Q_UNUSED(data) - Q_UNUSED(maxSize) - - return -1; - } - - private: - Playlist playlist_; - - int current_segment_; - - qint64 segment_read_index_; - - int sample_size_; - - qint64 limit_; - - }; - - /** - * @brief Create a QIODevice that can play whatever's in the cache currently - * - * This device will act very much like a QFile, transparently linking together various segments - * into what will appear to be a single contiguous file. - * - * The caller becomes responsible for ownership of the device, though the parent can be set - * automatically as an optional parameter to this function. - */ - PlaybackDevice* CreatePlaybackDevice(QObject *parent = nullptr) const; - - const AudioVisualWaveform &visual() const - { - return visual_; - } - -signals: - void ParametersChanged(); - private: + bool WritePartOfSampleBuffer(const SampleBuffer &samples, const rational &write_start, const rational &buffer_start, const rational &length); + + QString GetSegmentFilename(qint64 segment_index, int channel); + static const qint64 kDefaultSegmentSizePerChannel; - Segment CloneSegment(const Segment& s) const; - - Segment CreateSegment(const qint64 &size, const qint64 &offset) const; - - QString GenerateSegmentFilename() const; - - void TrimSegmentIn(Segment* s, qint64 new_length); - - void TrimSegmentOut(Segment* s, qint64 new_length); - - void RemoveSegmentFromArray(int index); - - void ClearPlaylist(); - - void UpdateOffsetsFrom(int index); - - Playlist playlist_; - AudioParams params_; - AudioVisualWaveform visual_; - }; } diff --git a/app/render/audiowaveformcache.cpp b/app/render/audiowaveformcache.cpp new file mode 100644 index 000000000..5ed1ee280 --- /dev/null +++ b/app/render/audiowaveformcache.cpp @@ -0,0 +1,134 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "audiowaveformcache.h" + +namespace olive { + +AudioWaveformCache::AudioWaveformCache(QObject *parent) : + PlaybackCache{parent} +{ +} + +void AudioWaveformCache::WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform) +{ + // Write each valid range to the segments + foreach (const TimeRange& r, valid_ranges) { +#ifdef AVW_USE_LIST + // Write visual + TimeRangeList::util_remove(&waveforms_, r); + + if (waveform) { + TimeRangeWithWaveform wv = r; + rational local_start = r.in() - range.in(); + if (local_start != 0) { + wv.waveform = waveform->Mid(local_start, r.length()); + } else { + wv.waveform = *waveform; + } + waveforms_.append(wv); + } +#else + if (waveform) { + waveforms_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length()); + } +#endif + + Validate(r); + } +} + +void AudioWaveformCache::Draw(QPainter *painter, const QRect &rect, const double &scale, const rational &start_time) const +{ + rational end = start_time + rational::fromDouble(rect.width() / scale); + TimeRange draw_range(start_time, end); + +#ifdef AVW_USE_LIST + foreach (const TimeRangeWithWaveform &wv, waveforms_) { + if (wv.OverlapsWith(draw_range)) { + rational substart = std::max(wv.in(), draw_range.in()); + rational subend = std::min(wv.out(), draw_range.out()); + + QRect subrect = rect; + subrect.setLeft(subrect.left() + (substart - draw_range.in()).toDouble()*scale); + subrect.setWidth((subend - substart).toDouble()*scale); + + rational local_start = substart - wv.in(); + AudioVisualWaveform::DrawWaveform(painter, subrect, scale, wv.waveform, local_start); + } + } +#else + AudioVisualWaveform::DrawWaveform(painter, rect, scale, waveforms_, start_time); +#endif +} + +AudioVisualWaveform::Sample AudioWaveformCache::GetSummaryFromTime(const rational &start, const rational &length) const +{ +#ifdef AVW_USE_LIST + QMap sample; + + TimeRange acquire(start, start+length); + foreach (const TimeRangeWithWaveform &wv, waveforms_) { + if (wv.OverlapsWith(acquire)) { + TimeRange this_range = wv.Intersected(acquire); + auto sum = wv.waveform.GetSummaryFromTime(this_range.in() - wv.in(), this_range.length()); + sample.insert(this_range.in(), sum); + } + } + + AudioVisualWaveform::Sample result; + + for (auto it=sample.cbegin(); it!=sample.cend(); it++) { + result.insert(result.end(), it.value().begin(), it.value().end()); + } + + return result; +#else + return waveforms_.GetSummaryFromTime(start, length); +#endif +} + +rational AudioWaveformCache::length() const +{ +#ifdef AVW_USE_LIST + rational len = 0; + + foreach (const TimeRangeWithWaveform &wv, waveforms_) { + len = std::max(len, wv.out()); + } + + return len; +#else + return waveforms_.length(); +#endif +} + +void AudioWaveformCache::SetPassthrough(PlaybackCache *cache) +{ + AudioWaveformCache *c = static_cast(cache); + waveforms_ = c->waveforms_; + for (const TimeRange &r : c->GetValidatedRanges()) { + Validate(r); + } + SetParameters(c->GetParameters()); + SetSavingEnabled(c->IsSavingEnabled()); +} + +} diff --git a/app/render/audiowaveformcache.h b/app/render/audiowaveformcache.h new file mode 100644 index 000000000..a1a6dcfe5 --- /dev/null +++ b/app/render/audiowaveformcache.h @@ -0,0 +1,97 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef AUDIOWAVEFORMCACHE_H +#define AUDIOWAVEFORMCACHE_H + +#include "audio/audiovisualwaveform.h" +#include "playbackcache.h" + +//#define AVW_USE_LIST + +namespace olive { + +class AudioWaveformCache : public PlaybackCache +{ + Q_OBJECT +public: + AudioWaveformCache(QObject *parent = nullptr); + + void WriteWaveform(const TimeRange &range, const TimeRangeList &valid_ranges, const AudioVisualWaveform *waveform); + + const AudioParams &GetParameters() const { return params_; } + void SetParameters(const AudioParams &p) + { + params_ = p; + waveforms_.set_channel_count(p.channel_count()); + } + + void Draw(QPainter* painter, const QRect &rect, const double &scale, const rational &start_time) const; + + AudioVisualWaveform::Sample GetSummaryFromTime(const rational &start, const rational &length) const; + + rational length() const; + + virtual void SetPassthrough(PlaybackCache *cache) override; + +private: +#ifdef AVW_USE_LIST + class TimeRangeWithWaveform : public TimeRange + { + public: + TimeRangeWithWaveform() = default; + TimeRangeWithWaveform(const TimeRange &r) : + TimeRange(r) + { + } + + void set_in(const rational& in) + { + waveform.TrimIn(in - this->in()); + TimeRange::set_in(in); + } + + void set_out(const rational& out) + { + waveform.Resize(out - this->in()); + TimeRange::set_out(out); + } + + void set_range(const rational& in, const rational& out) + { + waveform.TrimRange(in, out-in); + TimeRange::set_range(in, out); + } + + AudioVisualWaveform waveform; + }; + + QVector waveforms_; +#else + AudioVisualWaveform waveforms_; +#endif + + AudioParams params_; + +}; + +} + +#endif // AUDIOWAVEFORMCACHE_H diff --git a/app/render/diskmanager.cpp b/app/render/diskmanager.cpp index 5cd8c0a00..cda301e83 100644 --- a/app/render/diskmanager.cpp +++ b/app/render/diskmanager.cpp @@ -314,9 +314,7 @@ bool DiskCacheFolder::DeleteFileInternal(QMap::iterator hash_ // Remove from disk QFile f(filename); - if (!f.exists()) { - return true; - } else if (f.remove()) { + if (!f.exists() || f.remove()) { // Remove from internal map disk_data_.erase(hash_to_delete); diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 44345f74d..90f5bd858 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -32,12 +32,11 @@ #include "codec/frame.h" #include "common/filefunctions.h" +#include "common/oiioutils.h" #include "render/diskmanager.h" namespace olive { -const QString FrameHashCache::kCacheFormatExtension = QStringLiteral(".exr"); - #define super PlaybackCache FrameHashCache::FrameHashCache(QObject *parent) : @@ -65,6 +64,21 @@ void FrameHashCache::ValidateTime(const rational &time) Validate(TimeRange(time, time + timebase_)); } +QString FrameHashCache::GetValidCacheFilename(const rational &time) const +{ + if (IsFrameCached(time)) { + return CachePathName(time); + } else if (!GetPassthroughs().empty()) { + for (const Passthrough &p : GetPassthroughs()) { + if (p.Contains(time)) { + return CachePathName(GetCacheDirectory(), p.cache, time, timebase_); + } + } + } + + return QString(); +} + bool FrameHashCache::SaveCacheFrame(const int64_t &time, FramePtr frame) const { return SaveCacheFrame(GetCacheDirectory(), GetUuid(), time, frame); @@ -178,15 +192,46 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) } file.setFrameBuffer(framebuffer); + file.readPixels(dw.min.y, dw.max.y); } catch (const std::exception &e) { - qCritical() << "Failed to read cache frame:" << e.what(); + // Not an EXR, maybe it's a JPEG? + QImage img; - // Clear frame to signal that nothing was loaded - frame = nullptr; + if (img.load(fn, "jpg")) { - // Assume this frame is corrupt in some way and delete it - QMetaObject::invokeMethod(DiskManager::instance(), "DeleteSpecificFile", Q_ARG(QString, fn)); + // FIXME: Hardcoded + const int div = 1; + const VideoParams::Format image_format = VideoParams::kFormatUnsigned8; + const int channel_count = 4; + const rational par(1, 1); + + frame = Frame::Create(); + frame->set_video_params(VideoParams(img.width() * div, + img.height() * div, + image_format, + channel_count, + par, + VideoParams::kInterlaceNone, + div)); + + frame->allocate(); + + for (int i=0; idata() + frame->linesize_bytes() * i, + img.bits() + img.bytesPerLine() * i, + frame->width() * frame->video_params().GetBytesPerPixel()); + } + + } else { + qCritical() << "Failed to read cache frame:" << e.what(); + + // Clear frame to signal that nothing was loaded + frame = nullptr; + + // Assume this frame is corrupt in some way and delete it + QMetaObject::invokeMethod(DiskManager::instance(), "DeleteSpecificFile", Q_ARG(QString, fn)); + } } } @@ -194,6 +239,38 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) return frame; } +void FrameHashCache::SetPassthrough(PlaybackCache *cache) +{ + super::SetPassthrough(cache); + SetTimebase(static_cast(cache)->GetTimebase()); +} + +void FrameHashCache::LoadStateEvent(QDataStream &stream) +{ + uint32_t version; + int num, den; + + stream >> version; + + switch (version) { + case 1: + stream >> num; + stream >> den; + timebase_ = rational(num, den); + break; + } +} + +void FrameHashCache::SaveStateEvent(QDataStream &stream) +{ + uint32_t version = 1; + + stream << version; + + stream << timebase_.numerator(); + stream << timebase_.denominator(); +} + rational FrameHashCache::ToTime(const int64_t &ts) const { return Timecode::timestamp_to_time(ts, timebase_); @@ -239,7 +316,7 @@ QString FrameHashCache::CachePathName(const rational &time) const QString FrameHashCache::CachePathName(const QString &cache_path, const QUuid &cache_id, const int64_t &time) { - QString filename = QDir(QDir(cache_path).filePath(cache_id.toString())).filePath(QString::number(time)); + QString filename = GetThisCacheDirectory(cache_path, cache_id).filePath(QString::number(time)); // Register that in some way this hash has been accessed if (DiskManager::instance()) { @@ -256,63 +333,91 @@ QString FrameHashCache::CachePathName(const QString &cache_path, const QUuid &ca bool FrameHashCache::SaveCacheFrame(const QString &filename, const FramePtr frame) { - if (!VideoParams::FormatIsFloat(frame->format())) { - return false; - } - // Ensure directory is created QDir cache_dir = QFileInfo(filename).dir(); if (!FileFunctions::DirectoryIsValid(cache_dir)) { return false; } - // Floating point types are stored in EXR - Imf::PixelType pix_type; + if (VideoParams::FormatIsFloat(frame->format())) { + // Floating point types are stored in EXR + Imf::PixelType pix_type; - if (frame->format() == VideoParams::kFormatFloat16) { - pix_type = Imf::HALF; - } else { - pix_type = Imf::FLOAT; - } - - Imf::Header header(frame->width(), frame->height()); - header.channels().insert("R", Imf::Channel(pix_type)); - header.channels().insert("G", Imf::Channel(pix_type)); - header.channels().insert("B", Imf::Channel(pix_type)); - if (frame->channel_count() == VideoParams::kRGBAChannelCount) { - header.channels().insert("A", Imf::Channel(pix_type)); - } - - header.compression() = Imf::DWAA_COMPRESSION; - header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f)); - header.pixelAspectRatio() = frame->video_params().pixel_aspect_ratio().toDouble(); - - header.insert("oliveDivider", Imf::IntAttribute(frame->video_params().divider())); - - try { - Imf::OutputFile out(filename.toUtf8(), header, 0); - - int bpc = VideoParams::GetBytesPerChannel(frame->format()); - - size_t xs = frame->channel_count() * bpc; - size_t ys = frame->linesize_bytes(); - - Imf::FrameBuffer framebuffer; - framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); - framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); - framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); - if (frame->channel_count() == VideoParams::kRGBAChannelCount) { - framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + if (frame->format() == VideoParams::kFormatFloat16) { + pix_type = Imf::HALF; + } else { + pix_type = Imf::FLOAT; } - out.setFrameBuffer(framebuffer); - out.writePixels(frame->height()); + Imf::Header header(frame->width(), frame->height()); + header.channels().insert("R", Imf::Channel(pix_type)); + header.channels().insert("G", Imf::Channel(pix_type)); + header.channels().insert("B", Imf::Channel(pix_type)); + if (frame->channel_count() == VideoParams::kRGBAChannelCount) { + header.channels().insert("A", Imf::Channel(pix_type)); + } - return true; - } catch (const std::exception &e) { - qCritical() << "Failed to write cache frame:" << e.what(); + header.compression() = Imf::DWAA_COMPRESSION; + header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f)); + header.pixelAspectRatio() = frame->video_params().pixel_aspect_ratio().toDouble(); - return false; + header.insert("oliveDivider", Imf::IntAttribute(frame->video_params().divider())); + + try { + Imf::OutputFile out(filename.toUtf8(), header, 0); + + int bpc = VideoParams::GetBytesPerChannel(frame->format()); + + size_t xs = frame->channel_count() * bpc; + size_t ys = frame->linesize_bytes(); + + Imf::FrameBuffer framebuffer; + framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); + framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); + framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); + if (frame->channel_count() == VideoParams::kRGBAChannelCount) { + framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + } + out.setFrameBuffer(framebuffer); + + out.writePixels(frame->height()); + + return true; + } catch (const std::exception &e) { + qCritical() << "Failed to write cache frame:" << e.what(); + + return false; + } + } else { + QImage::Format fmt = QImage::Format_Invalid; + + switch (frame->format()) { + case VideoParams::kFormatUnsigned8: + if (frame->channel_count() == VideoParams::kRGBAChannelCount){ + fmt = QImage::Format_RGBA8888_Premultiplied; + } else if (frame->channel_count() == VideoParams::kRGBChannelCount){ + fmt = QImage::Format_RGB888; + } + break; + case VideoParams::kFormatUnsigned16: + if (frame->channel_count() == VideoParams::kRGBAChannelCount){ + fmt = QImage::Format_RGBA64_Premultiplied; + } + break; + case VideoParams::kFormatFloat16: + case VideoParams::kFormatFloat32: + case VideoParams::kFormatCount: + case VideoParams::kFormatInvalid: + break; + } + + if (fmt == QImage::Format_Invalid) { + return false; + } + + QImage img(reinterpret_cast(frame->data()), frame->width(), frame->height(), frame->linesize_bytes(), fmt); + + return img.save(filename, "jpg"); } } diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index a98432ac5..378e1a02a 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -48,14 +48,7 @@ public: return GetValidatedRanges().contains(time); } - QString GetValidCacheFilename(const rational &time) const - { - if (IsFrameCached(time)) { - return CachePathName(time); - } else { - return QString(); - } - } + QString GetValidCacheFilename(const rational &time) const; static bool SaveCacheFrame(const QString& filename, FramePtr frame); bool SaveCacheFrame(const int64_t &time, FramePtr frame) const; @@ -65,6 +58,12 @@ public: FramePtr LoadCacheFrame(const int64_t &time) const; static FramePtr LoadCacheFrame(const QString& fn); + virtual void SetPassthrough(PlaybackCache *cache) override; + +protected: + virtual void LoadStateEvent(QDataStream &stream) override; + virtual void SaveStateEvent(QDataStream &stream) override; + private: rational ToTime(const int64_t &ts) const; int64_t ToTimestamp(const rational &ts, Timecode::Rounding rounding = Timecode::kRound) const; @@ -80,8 +79,6 @@ private: rational timebase_; - static const QString kCacheFormatExtension; - private slots: void HashDeleted(const QString &path, const QString &filename); @@ -89,6 +86,17 @@ private slots: }; +class ThumbnailCache : public FrameHashCache +{ + Q_OBJECT +public: + ThumbnailCache(QObject* parent = nullptr) : + FrameHashCache(parent) + { + SetTimebase(rational(1, 10)); + } +}; + } #endif // VIDEORENDERFRAMECACHE_H diff --git a/app/common/flipmodifiers.cpp b/app/render/job/cachejob.h similarity index 55% rename from app/common/flipmodifiers.cpp rename to app/render/job/cachejob.h index ad48704c7..67c6c36b9 100644 --- a/app/common/flipmodifiers.cpp +++ b/app/render/job/cachejob.h @@ -18,24 +18,38 @@ ***/ -#include "flipmodifiers.h" +#ifndef CACHEJOB_H +#define CACHEJOB_H + +#include +#include namespace olive { -Qt::KeyboardModifiers FlipControlAndShiftModifiers(Qt::KeyboardModifiers e) { - if (e & Qt::ControlModifier & Qt::ShiftModifier) { - return e; +class CacheJob +{ +public: + CacheJob() = default; + CacheJob(const QString &filename, const QVariant &fallback = QVariant()) + { + filename_ = filename; } - if (e & Qt::ShiftModifier) { - e |= Qt::ControlModifier; - e &= ~Qt::ShiftModifier; - } else if (e & Qt::ControlModifier) { - e |= Qt::ShiftModifier; - e &= ~Qt::ControlModifier; - } + const QString &GetFilename() const { return filename_; } + void SetFilename(const QString &s) { filename_ = s; } - return e; -} + const QVariant &GetFallback() const { return fallback_; } + void SetFallback(const QVariant &val) { fallback_ = val; } + +private: + QString filename_; + + QVariant fallback_; + +}; } + +Q_DECLARE_METATYPE(olive::CacheJob) + +#endif // CACHEJOB_H diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index d12325c75..0f179dab8 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -27,7 +27,7 @@ namespace olive { -void PlaybackCache::Invalidate(const TimeRange &r, bool signal) +void PlaybackCache::Invalidate(const TimeRange &r) { if (r.in() == r.out()) { qWarning() << "Tried to invalidate zero-length range"; @@ -36,10 +36,16 @@ void PlaybackCache::Invalidate(const TimeRange &r, bool signal) validated_.remove(r); + if (!passthroughs_.empty()) { + TimeRangeList::util_remove(&passthroughs_, r); + } + InvalidateEvent(r); - if (signal) { - emit Invalidated(r); + emit Invalidated(r); + + if (saving_enabled_) { + SaveState(); } } @@ -48,6 +54,159 @@ Node *PlaybackCache::parent() const return dynamic_cast(QObject::parent()); } +QDir PlaybackCache::GetThisCacheDirectory() const +{ + return GetThisCacheDirectory(GetCacheDirectory(), GetUuid()); +} + +QDir PlaybackCache::GetThisCacheDirectory(const QString &cache_path, const QUuid &cache_id) +{ + return QDir(cache_path).filePath(cache_id.toString()); +} + +void PlaybackCache::LoadState() +{ + QDir cache_dir = GetThisCacheDirectory(); + QFile f(cache_dir.filePath(QStringLiteral("state"))); + if (f.open(QFile::ReadOnly)) { + QDataStream s(&f); + + uint32_t version; + s >> version; + + LoadStateEvent(s); + + switch (version) { + case 1: + { + int valid_count, pass_count; + + validated_.clear(); + s >> valid_count; + for (int i=0; i> in_num; + s >> in_den; + s >> out_num; + s >> out_den; + + validated_.insert(TimeRange(rational(in_num, in_den), rational(out_num, out_den))); + } + + passthroughs_.clear(); + s >> pass_count; + for (int i=0; i> in_num; + s >> in_den; + s >> out_num; + s >> out_den; + s >> id; + + Passthrough p = TimeRange(rational(in_num, in_den), rational(out_num, out_den)); + p.cache = id; + passthroughs_.append(p); + } + + break; + } + } + + f.close(); + } +} + +void PlaybackCache::SaveState() +{ + if (!DiskManager::instance()) { + return; + } + + QDir cache_dir = GetThisCacheDirectory(); + QFile f(cache_dir.filePath(QStringLiteral("state"))); + if (validated_.isEmpty() && passthroughs_.isEmpty()) { + if (f.exists()) { + f.remove(); + } + } else { + if (FileFunctions::DirectoryIsValid(cache_dir)) { + if (f.open(QFile::WriteOnly)) { + QDataStream s(&f); + + uint32_t version = 1; + s << version; + + SaveStateEvent(s); + + s << validated_.size(); + + for (const TimeRange &r : validated_) { + s << r.in().numerator(); + s << r.in().denominator(); + s << r.out().numerator(); + s << r.out().denominator(); + } + + s << passthroughs_.size(); + + for (const Passthrough &p : passthroughs_) { + s << p.in().numerator(); + s << p.in().denominator(); + s << p.out().numerator(); + s << p.out().denominator(); + s << p.cache; + } + + f.close(); + } + } + } +} + +void PlaybackCache::Draw(QPainter *p, const rational &start, double scale, const QRect &rect) const +{ + p->fillRect(rect, Qt::red); + + foreach (const TimeRange& range, GetValidatedRanges()) { + int range_left = rect.left() + (range.in() - start).toDouble() * scale; + if (range_left >= rect.right()) { + continue; + } + + int range_right = rect.left() + (range.out() - start).toDouble() * scale; + if (range_right < rect.left()) { + continue; + } + + int adjusted_left = std::max(range_left, rect.left()); + int adjusted_right = std::min(range_right, rect.right()); + + p->fillRect(adjusted_left, + rect.top(), + adjusted_right - adjusted_left, + rect.height(), + Qt::green); + } +} + +void PlaybackCache::SetPassthrough(PlaybackCache *cache) +{ + for (const TimeRange &r : cache->GetValidatedRanges()) { + Passthrough p = r; + p.cache = cache->GetUuid(); + passthroughs_.push_back(p); + } + + passthroughs_.append(cache->GetPassthroughs()); + + if (saving_enabled_) { + SaveState(); + } +} + void PlaybackCache::InvalidateAll() { Invalidate(TimeRange(0, RATIONAL_MAX)); @@ -60,6 +219,10 @@ void PlaybackCache::Validate(const TimeRange &r, bool signal) if (signal) { emit Validated(r); } + + if (saving_enabled_) { + SaveState(); + } } void PlaybackCache::InvalidateEvent(const TimeRange &) @@ -73,12 +236,19 @@ Project *PlaybackCache::GetProject() const PlaybackCache::PlaybackCache(QObject *parent) : QObject(parent), - enabled_(false) + saving_enabled_(true) { uuid_ = QUuid::createUuid(); } -TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting) +void PlaybackCache::SetUuid(const QUuid &u) +{ + uuid_ = u; + + LoadState(); +} + +TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting) const { TimeRangeList invalidated; @@ -93,10 +263,14 @@ TimeRangeList PlaybackCache::GetInvalidatedRanges(TimeRange intersecting) invalidated.remove(range); } + foreach (const TimeRange &range, passthroughs_) { + invalidated.remove(range); + } + return invalidated; } -bool PlaybackCache::HasInvalidatedRanges(const TimeRange &intersecting) +bool PlaybackCache::HasInvalidatedRanges(const TimeRange &intersecting) const { return !validated_.contains(intersecting); } diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index b10b48686..d96709355 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -21,7 +21,10 @@ #ifndef PLAYBACKCACHE_H #define PLAYBACKCACHE_H +#include +#include #include +#include #include #include "common/jobtime.h" @@ -40,37 +43,61 @@ public: PlaybackCache(QObject* parent = nullptr); const QUuid &GetUuid() const { return uuid_; } - void SetUuid(const QUuid &u) { uuid_ = u; } + void SetUuid(const QUuid &u); - bool IsEnabled() const { return enabled_; } - void SetEnabled(bool e) - { - if (enabled_ != e) { - enabled_ = e; - emit EnabledChanged(e); - } - } - - TimeRangeList GetInvalidatedRanges(TimeRange intersecting); - TimeRangeList GetInvalidatedRanges(const rational &length) + TimeRangeList GetInvalidatedRanges(TimeRange intersecting) const; + TimeRangeList GetInvalidatedRanges(const rational &length) const { return GetInvalidatedRanges(TimeRange(0, length)); } - bool HasInvalidatedRanges(const TimeRange &intersecting); - bool HasInvalidatedRanges(const rational &length) + bool HasInvalidatedRanges(const TimeRange &intersecting) const; + bool HasInvalidatedRanges(const rational &length) const { return HasInvalidatedRanges(TimeRange(0, length)); } QString GetCacheDirectory() const; - void Invalidate(const TimeRange& r, bool signal = true); + void Invalidate(const TimeRange& r); + bool HasValidatedRanges() const { return !validated_.isEmpty(); } const TimeRangeList &GetValidatedRanges() const { return validated_; } Node *parent() const; + QDir GetThisCacheDirectory() const; + static QDir GetThisCacheDirectory(const QString &cache_path, const QUuid &cache_id); + + void LoadState(); + void SaveState(); + + void Draw(QPainter *painter, const rational &start, double scale, const QRect &rect) const; + + static int GetCacheIndicatorHeight() + { + return QFontMetrics(QFont()).height()/4; + } + + bool IsSavingEnabled() const { return saving_enabled_; } + void SetSavingEnabled(bool e) { saving_enabled_ = e; } + + virtual void SetPassthrough(PlaybackCache *cache); + + QMutex *mutex() { return &mutex_; } + + class Passthrough : public TimeRange + { + public: + Passthrough(const TimeRange &r) : + TimeRange(r) + {} + + QUuid cache; + }; + + const QVector &GetPassthroughs() const { return passthroughs_; } + public slots: void InvalidateAll(); @@ -79,13 +106,19 @@ signals: void Validated(const olive::TimeRange& r); - void EnabledChanged(bool e); + void Request(const olive::TimeRange& r); + + void CancelAll(); protected: void Validate(const TimeRange& r, bool signal = true); virtual void InvalidateEvent(const TimeRange& range); + virtual void LoadStateEvent(QDataStream &stream){} + + virtual void SaveStateEvent(QDataStream &stream){} + Project* GetProject() const; private: @@ -93,7 +126,11 @@ private: QUuid uuid_; - bool enabled_; + bool saving_enabled_; + + QMutex mutex_; + + QVector passthroughs_; }; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 15cce575a..5a9a5a21e 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -26,22 +26,22 @@ #include "codec/conformmanager.h" #include "node/inputdragger.h" #include "node/project/project.h" +#include "render/diskmanager.h" #include "render/renderprocessor.h" #include "task/customcache/customcachetask.h" #include "task/taskmanager.h" #include "widget/slider/base/numericsliderbase.h" +#include "widget/viewer/viewer.h" namespace olive { -// We may want to make this configurable at some point, so for now this constant is used as a -// placeholder for where that configarable variable would be used. -const bool PreviewAutoCacher::kRealTimeWaveformsEnabled = true; - -PreviewAutoCacher::PreviewAutoCacher() : +PreviewAutoCacher::PreviewAutoCacher(QObject *parent) : + QObject(parent), viewer_node_(nullptr), use_custom_range_(false), - pause_audio_(false), - single_frame_render_(nullptr) + pause_renders_(false), + single_frame_render_(nullptr), + display_color_processor_(nullptr) { // Set defaults SetPlayhead(0); @@ -49,7 +49,7 @@ PreviewAutoCacher::PreviewAutoCacher() : // Wait a certain amount of time before requeuing when we receive an invalidate signal delayed_requeue_timer_.setInterval(OLIVE_CONFIG("AutoCacheDelay").toInt()); delayed_requeue_timer_.setSingleShot(true); - connect(&delayed_requeue_timer_, &QTimer::timeout, this, &PreviewAutoCacher::RequeueFrames); + connect(&delayed_requeue_timer_, &QTimer::timeout, this, &PreviewAutoCacher::TryRender); // Catch when a conform is ready connect(ConformManager::instance(), &ConformManager::ConformReady, this, &PreviewAutoCacher::ConformFinished); @@ -81,30 +81,55 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool dry) RenderTicketPtr PreviewAutoCacher::GetRangeOfAudio(TimeRange range) { - return RenderAudio(range, false); + return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, nullptr); } -void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) +void PreviewAutoCacher::ClearSingleFrameRenders() { - // Stop any current render tasks because a) they might be out of date now anyway, and b) we - // want to dedicate all our rendering power to realtime feedback for the user - CancelVideoTasks(); - - // If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames - if (viewer_node_->video_frame_cache()->IsEnabled() && !NodeInputDragger::IsInputBeingDragged()) { - StartCachingVideoRange(range); + QMap > copy = video_immediate_passthroughs_; + for (auto it=copy.cbegin(); it!=copy.cend(); it++) { + it.key()->Cancel(); + if (!it.key()->IsRunning()) { + RenderManager::instance()->RemoveTicket(it.key()->GetTicket()); + emit it.key()->GetTicket()->Finished(); + } } } -void PreviewAutoCacher::AudioInvalidated(const TimeRange &range) +void PreviewAutoCacher::VideoInvalidatedFromCache(const TimeRange &range) { - // We don't stop rendering audio because currently there's no system of requeuing audio if it's - // cancelled, so some areas may end up unrendered forever - // ClearAudioQueue(); + PlaybackCache *cache = static_cast(sender()); - // If we're auto-caching audio or require realtime waveforms, we'll have to render this - if (viewer_node_->audio_playback_cache()->IsEnabled() || kRealTimeWaveformsEnabled) { - StartCachingAudioRange(range); + VideoInvalidatedFromNode(cache, range); +} + +void PreviewAutoCacher::AudioInvalidatedFromCache(const TimeRange &range) +{ + PlaybackCache *cache = static_cast(sender()); + + AudioInvalidatedFromNode(cache, range); +} + +void PreviewAutoCacher::CancelForCache() +{ + PlaybackCache *cache = static_cast(sender()); + + if (dynamic_cast(cache) || dynamic_cast(cache)) { + for (auto it=pending_video_jobs_.begin(); it!=pending_video_jobs_.end(); ) { + if ((*it).cache == cache) { + it = pending_video_jobs_.erase(it); + } else { + it++; + } + } + } else if (dynamic_cast(cache) || dynamic_cast(cache)) { + for (auto it=pending_audio_jobs_.begin(); it!=pending_audio_jobs_.end(); ) { + if ((*it).cache == cache) { + it = pending_audio_jobs_.erase(it); + } else { + it++; + } + } } } @@ -115,68 +140,45 @@ void PreviewAutoCacher::AudioRendered() // If the task list doesn't contain this watcher, presumably it was cleared as a result of a // viewer switch, so we'll completely ignore this watcher - if (audio_tasks_.contains(watcher)) { + if (running_audio_tasks_.removeOne(watcher)) { // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket - TimeRange range = audio_tasks_.take(watcher); + TimeRange range = watcher->property("time").value(); + Node *node = copy_map_.key(Node::ValueToPtr(watcher->property("node"))); - if (watcher->HasResult()) { - // Remove this task from the list - JobTime watcher_job_time = watcher->property("job").value(); + if (watcher->HasResult() && node) { + if (PlaybackCache *cache = Node::ValueToPtr(watcher->property("cache"))) { + AudioCacheData &d = audio_cache_data_[cache]; - TimeRangeList valid_ranges = audio_job_tracker_.getCurrentSubRanges(range, watcher_job_time); + JobTime watcher_job_time = watcher->property("job").value(); - AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); + TimeRangeList valid_ranges = d.job_tracker.getCurrentSubRanges(range, watcher_job_time); - if (viewer_node_->audio_playback_cache()->IsEnabled()) { - // WritePCM is tolerant to its buffer being null, it will just write silence instead - viewer_node_->audio_playback_cache()->WritePCM(range, - valid_ranges, - watcher->Get().value()); - } + AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value(); - viewer_node_->audio_playback_cache()->WriteWaveform(range, valid_ranges, &waveform); + SampleBuffer buf = watcher->Get().value(); - // Detect if this audio was incomplete because it was waiting on a conform to finish - if (watcher->GetTicket()->property("incomplete").toBool()) { - if (last_conform_task_ > watcher_job_time) { - // Requeue now - viewer_node_->audio_playback_cache()->Invalidate(range); - } else { - // Wait for conform - audio_needing_conform_.insert(range); - } - } else{ - // Retrieve visual waveforms - QVector waveform_list = watcher->GetTicket()->property("waveforms").value< QVector >(); - foreach (const RenderProcessor::RenderedWaveform& waveform_info, waveform_list) { - // Find original track - ClipBlock* block = nullptr; + bool incomplete = watcher->GetTicket()->property("incomplete").toBool(); - for (auto it=copy_map_.cbegin(); it!=copy_map_.cend(); it++) { - if (it.value() == waveform_info.block) { - block = static_cast(it.key()); - break; - } + if (AudioPlaybackCache *pcm = dynamic_cast(cache)) { + // WritePCM is tolerant to its buffer being null, it will just write silence instead + pcm->SetParameters(buf.audio_params()); + pcm->WritePCM(range, + valid_ranges, + watcher->Get().value()); + } else if (AudioWaveformCache *wave = dynamic_cast(cache)) { + wave->SetParameters(buf.audio_params()); + if (!incomplete) { + wave->WriteWaveform(range, valid_ranges, &waveform); } + } - if (block && !valid_ranges.isEmpty()) { - // Generate visual waveform in this background thread - block->waveform().set_channel_count(viewer_node_->GetAudioParams().channel_count()); - - // Determine which of the waveform ranges we got intersects with the valid ranges - TimeRangeList intersections = valid_ranges.Intersects(waveform_info.range + block->in()); - foreach (TimeRange r, intersections) { - // For each range, adjust it relative to the block and write it - r -= block->in(); - - if (waveform_info.silence) { - block->waveform().OverwriteSilence(r.in(), r.length()); - } else { - block->waveform().OverwriteSums(waveform_info.waveform, r.in(), r.in() - waveform_info.range.in(), r.length()); - } - } - - emit block->PreviewChanged(); + if (incomplete) { + if (last_conform_task_ > watcher_job_time) { + // Requeue now + cache->Invalidate(range); + } else { + // Wait for conform + d.needs_conform.insert(range); } } } @@ -193,6 +195,13 @@ void PreviewAutoCacher::VideoRendered() { RenderTicketWatcher* watcher = static_cast(sender()); + const QStringList bad_cache_names = watcher->GetTicket()->property("badcache").toStringList(); + if (!bad_cache_names.empty()) { + for (const QString &fn : bad_cache_names) { + DiskManager::instance()->DeleteSpecificFile(fn); + } + } + // Process passthroughs no matter what, if the viewer was switched, the passthrough map would be // cleared anyway QVector tickets = video_immediate_passthroughs_.take(watcher); @@ -206,21 +215,21 @@ void PreviewAutoCacher::VideoRendered() // If the task list doesn't contain this watcher, presumably it was cleared as a result of a // viewer switch, so we'll completely ignore this watcher - auto it = video_tasks_.find(watcher); - - if (it != video_tasks_.end()) { + if (running_video_tasks_.removeOne(watcher)) { // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket if (watcher->HasResult()) { - // Download frame in another thread if (watcher->GetTicket()->property("cached").toBool()) { if (FrameHashCache *cache = Node::ValueToPtr(watcher->property("cache"))) { - cache->ValidateTime(it.value()); + rational time = watcher->property("time").value(); + JobTime job = watcher->property("job").value(); + + if (video_cache_data_.value(cache).job_tracker.isCurrent(time, job)) { + cache->ValidateTime(time); + } } } } - video_tasks_.erase(it); - // Continue rendering TryRender(); } @@ -273,6 +282,12 @@ void PreviewAutoCacher::AddNode(Node *node) // Add to project copy->setParent(&copied_project_); + // Disable caches for copy + copy->SetCachesEnabled(false); + + // Copy cache UUIDs + copy->CopyCacheUuidsFrom(node); + // Insert into map InsertIntoCopyMap(node, copy); @@ -352,54 +367,68 @@ void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) void PreviewAutoCacher::ConnectToNodeCache(Node *node) { - // TEMP: Retain existing behavior until more work is done - if (node == viewer_node_) { - connect(node->video_frame_cache(), - &PlaybackCache::EnabledChanged, - this, - &PreviewAutoCacher::VideoAutoCacheEnableChanged); + connect(node->video_frame_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::VideoInvalidatedFromCache); - connect(node->audio_playback_cache(), - &PlaybackCache::EnabledChanged, - this, - &PreviewAutoCacher::AudioAutoCacheEnableChanged); + connect(node->thumbnail_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::VideoInvalidatedFromCache); - connect(node->video_frame_cache(), - &PlaybackCache::Invalidated, - this, - &PreviewAutoCacher::VideoInvalidated); + connect(node->audio_playback_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::AudioInvalidatedFromCache); - connect(node->audio_playback_cache(), - &PlaybackCache::Invalidated, - this, - &PreviewAutoCacher::AudioInvalidated); - } + connect(node->waveform_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::AudioInvalidatedFromCache); + + connect(node->video_frame_cache(), + &PlaybackCache::CancelAll, + this, + &PreviewAutoCacher::CancelForCache); + + connect(node->audio_playback_cache(), + &PlaybackCache::CancelAll, + this, + &PreviewAutoCacher::CancelForCache); } void PreviewAutoCacher::DisconnectFromNodeCache(Node *node) { - // TEMP: Retain existing behavior until more work is done - if (node == viewer_node_) { - disconnect(node->video_frame_cache(), - &PlaybackCache::EnabledChanged, - this, - &PreviewAutoCacher::VideoAutoCacheEnableChanged); + disconnect(node->video_frame_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::VideoInvalidatedFromCache); - disconnect(node->audio_playback_cache(), - &PlaybackCache::EnabledChanged, - this, - &PreviewAutoCacher::AudioAutoCacheEnableChanged); + disconnect(node->thumbnail_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::VideoInvalidatedFromCache); - disconnect(node->video_frame_cache(), - &PlaybackCache::Invalidated, - this, - &PreviewAutoCacher::VideoInvalidated); + disconnect(node->audio_playback_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::AudioInvalidatedFromCache); - disconnect(node->audio_playback_cache(), - &PlaybackCache::Invalidated, - this, - &PreviewAutoCacher::AudioInvalidated); - } + disconnect(node->waveform_cache(), + &PlaybackCache::Request, + this, + &PreviewAutoCacher::AudioInvalidatedFromCache); + + disconnect(node->video_frame_cache(), + &PlaybackCache::CancelAll, + this, + &PreviewAutoCacher::CancelForCache); + + disconnect(node->audio_playback_cache(), + &PlaybackCache::CancelAll, + this, + &PreviewAutoCacher::CancelForCache); } void PreviewAutoCacher::UpdateGraphChangeValue() @@ -421,44 +450,64 @@ void PreviewAutoCacher::CancelQueuedSingleFrameRender() } } -void PreviewAutoCacher::VideoInvalidatedList(const TimeRangeList &list) -{ - foreach (const TimeRange &range, list) { - VideoInvalidated(range); - } -} - -void PreviewAutoCacher::AudioInvalidatedList(const TimeRangeList &list) -{ - foreach (const TimeRange &range, list) { - AudioInvalidated(range); - } -} - void PreviewAutoCacher::StartCachingRange(const TimeRange &range, TimeRangeList *range_list, RenderJobTracker *tracker) { range_list->insert(range); tracker->insert(range, graph_changed_time_); } -void PreviewAutoCacher::StartCachingVideoRange(const TimeRange &range) +void PreviewAutoCacher::StartCachingVideoRange(PlaybackCache *cache, const TimeRange &range) { - StartCachingRange(range, &invalidated_video_, &video_job_tracker_); - RequeueFrames(); + Node *node = cache->parent(); + rational using_tb; + if (ThumbnailCache *thumbs = dynamic_cast(cache)) { + using_tb = thumbs->GetTimebase(); + } else { + using_tb = viewer_node_->GetVideoParams().frame_rate_as_time_base(); + } + + TimeRangeListFrameIterator iterator({range}, using_tb); + pending_video_jobs_.push_back({node, cache, range, iterator}); + video_cache_data_[cache].job_tracker.insert(TimeRange(iterator.Snap(range.in()), range.out()), graph_changed_time_); + TryRender(); } -void PreviewAutoCacher::StartCachingAudioRange(const TimeRange &range) +void PreviewAutoCacher::StartCachingAudioRange(PlaybackCache *cache, const TimeRange &range) { - StartCachingRange(range, &invalidated_audio_, &audio_job_tracker_); + Node *node = cache->parent(); + pending_audio_jobs_.push_back({node, cache, range}); + audio_cache_data_[cache].job_tracker.insert(range, graph_changed_time_); TryRender(); } +void PreviewAutoCacher::VideoInvalidatedFromNode(PlaybackCache *cache, const TimeRange &range) +{ + // Stop any current render tasks because a) they might be out of date now anyway, and b) we + // want to dedicate all our rendering power to realtime feedback for the user + //CancelVideoTasks(node); + + // If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames + if (!NodeInputDragger::IsInputBeingDragged()) { + StartCachingVideoRange(cache, range); + } +} + +void PreviewAutoCacher::AudioInvalidatedFromNode(PlaybackCache *cache, const TimeRange &range) +{ + // We don't stop rendering audio because currently there's no system of requeuing audio if it's + // cancelled, so some areas may end up unrendered forever + // ClearAudioQueue(); + + // If we're auto-caching audio or require realtime waveforms, we'll have to render this + StartCachingAudioRange(cache, range); +} + void PreviewAutoCacher::SetPlayhead(const rational &playhead) { cache_range_ = TimeRange(playhead - OLIVE_CONFIG("DiskCacheBehind").value(), playhead + OLIVE_CONFIG("DiskCacheAhead").value()); - RequeueFrames(); + TryRender(); } template @@ -466,30 +515,37 @@ void CancelTasks(const T &task_list, bool and_wait) { for (auto it=task_list.cbegin(); it!=task_list.cend(); it++) { // Signal that the ticket should not be finished - it.key()->Cancel(); + (*it)->Cancel(); } if (and_wait) { // Wait for each ticket to finish for (auto it=task_list.cbegin(); it!=task_list.cend(); it++) { - it.key()->WaitForFinished(); + (*it)->WaitForFinished(); } } } void PreviewAutoCacher::CancelVideoTasks(bool and_wait_for_them_to_finish) { - CancelTasks(video_tasks_, and_wait_for_them_to_finish); + CancelTasks(running_video_tasks_, and_wait_for_them_to_finish); } void PreviewAutoCacher::CancelAudioTasks(bool and_wait_for_them_to_finish) { - CancelTasks(audio_tasks_, and_wait_for_them_to_finish); + CancelTasks(running_audio_tasks_, and_wait_for_them_to_finish); } -void PreviewAutoCacher::SetAudioPaused(bool e) +bool PreviewAutoCacher::IsRenderingCustomRange() const { - pause_audio_ = e; + /*const VideoCacheData &d = video_cache_data_.value(viewer_node_); + return d.iterator.IsCustomRange() && d.iterator.HasNext();*/ + return false; +} + +void PreviewAutoCacher::SetRendersPaused(bool e) +{ + pause_renders_ = e; if (!e) { TryRender(); } @@ -533,12 +589,14 @@ void PreviewAutoCacher::ValueHintChanged(const NodeInput &input) void PreviewAutoCacher::TryRender() { + delayed_requeue_timer_.stop(); + if (!graph_update_queue_.isEmpty()) { // Check if we have jobs running in other threads that shouldn't be interrupted right now // NOTE: We don't check for downloads because, while they run in another thread, they don't // require any access to the graph and therefore don't risk race conditions. - if (!audio_tasks_.isEmpty() - || !video_tasks_.isEmpty()) { + if (!running_audio_tasks_.isEmpty() + || !running_video_tasks_.isEmpty()) { return; } @@ -546,173 +604,164 @@ void PreviewAutoCacher::TryRender() ProcessUpdateQueue(); } - // Check for newly invalidated video and hash it - if (!invalidated_video_.isEmpty()) { - if (!copied_viewer_node_->GetConnectedTextureOutput()) { - queued_frame_iterator_.reset(); - } else if (queued_frame_iterator_.HasNext()) { - queued_frame_iterator_.insert(invalidated_video_); - } else { - queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated_video_, viewer_node_->GetVideoParams().frame_rate_as_time_base()); - } - invalidated_video_.clear(); - } - - if (!invalidated_audio_.isEmpty()) { - // Add newly invalidated audio to iterator - audio_iterator_.insert(invalidated_audio_); - invalidated_audio_.clear(); - } - if (single_frame_render_) { // Make an explicit copy of the render ticket here - it seems that on some systems it can be set // to NULL before we're done with it... RenderTicketPtr t = single_frame_render_; single_frame_render_ = nullptr; - RenderTicketWatcher *watcher = RenderFrame(t->property("time").value(), + // Check if already caching this + RenderTicketWatcher *watcher = RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), + t->property("time").value(), nullptr, t->property("dry").toBool()); video_immediate_passthroughs_[watcher].append(t); } - // Completely arbitrary number. I don't know what's optimal for this yet. - const int max_tasks = 4; + if (!pause_renders_) { + // Completely arbitrary number. I don't know what's optimal for this yet. + const int max_tasks = 4; - // Handle video tasks - rational t; - while (video_tasks_.size() < max_tasks && queued_frame_iterator_.GetNext(&t)) { - RenderTicketWatcher* render_task = video_tasks_.key(t); + // Handle video tasks + while (!pending_video_jobs_.empty()) { + VideoJob &d = pending_video_jobs_.front(); - // We want this hash, if we're not already rendering, start render now - if (!render_task) { - // Don't render any hash more than once - RenderFrame(t, viewer_node_->video_frame_cache(), false); + if (Node *copy = copy_map_.value(d.node)) { + // Queue next frames + rational t; + while (running_video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { + RenderFrame(copy, t, d.cache, false); + + emit SignalCacheProxyTaskProgress(double(d.iterator.frame_index()) / double(d.iterator.size())); + + if (!d.iterator.HasNext()) { + emit StopCacheProxyTasks(); + } + } + } else { + qCritical() << "Failed to find node copy for video job"; + } + + if (d.iterator.HasNext()) { + break; + } else { + pending_video_jobs_.pop_front(); + } } - emit SignalCacheProxyTaskProgress(double(queued_frame_iterator_.frame_index()) / double(queued_frame_iterator_.size())); + // Handle audio tasks + while (!pending_audio_jobs_.empty() && running_audio_tasks_.size() < max_tasks) { + AudioJob &d = pending_audio_jobs_.front(); - if (!queued_frame_iterator_.HasNext()) { - emit StopCacheProxyTasks(); + bool pop = true; + + // Start job + if (Node *copy = copy_map_.value(d.node)) { + TimeRange &queued_range = d.range; + TimeRange use_range = queued_range; + + if (dynamic_cast(d.cache)) { + rational new_out = std::min(use_range.in() + AudioVisualWaveform::kMinimumSampleRate.flipped(), use_range.out()); + + if (new_out != use_range.out()) { + use_range.set_out(new_out); + queued_range.set_in(new_out); + pop = false; + } + } + + RenderAudio(copy, use_range, d.cache); + } else { + qCritical() << "Failed to find node copy for audio job"; + } + + if (pop) { + pending_audio_jobs_.pop_front(); + } } } - - // Handle audio tasks - while (!audio_iterator_.isEmpty() && audio_tasks_.size() < max_tasks && !pause_audio_) { - // Copy first range in list - TimeRange r = audio_iterator_.first(); - - // Limit to the minimum sample rate supported by AudioVisualWaveform - we use this value so that - // whatever chunk we render can be summed down to the smallest mipmap whole - r.set_out(qMin(r.out(), r.in() + AudioVisualWaveform::kMinimumSampleRate.flipped())); - - // Start job - RenderAudio(r, true); - - audio_iterator_.remove(r); - } } -RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, FrameHashCache *cache, bool dry) +RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, PlaybackCache *cache, bool dry) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); watcher->setProperty("cache", Node::PtrToValue(cache)); + watcher->setProperty("time", QVariant::fromValue(time)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); - video_tasks_.insert(watcher, time); - watcher->SetTicket(RenderManager::instance()->RenderFrame(node, - copied_viewer_node_->GetVideoParams(), - copied_viewer_node_->GetAudioParams(), - copied_color_manager_, - time, - RenderMode::kOffline, - cache, - dry ? RenderManager::kNull : RenderManager::kTexture)); + + running_video_tasks_.append(watcher); + + RenderManager::RenderVideoParams rvp(node, + copied_viewer_node_->GetVideoParams(), + copied_viewer_node_->GetAudioParams(), + time, + copied_color_manager_, + RenderMode::kOffline); + + if (FrameHashCache *frame_cache = dynamic_cast(cache)) { + if (ThumbnailCache *wave_cache = dynamic_cast(cache)) { + rvp.video_params.set_divider(VideoParams::GetDividerForTargetResolution(rvp.video_params.width(), rvp.video_params.height(), 160, 120)); + rvp.force_color_output = display_color_processor_; + rvp.force_format = VideoParams::kFormatUnsigned8; + } else { + frame_cache->SetTimebase(viewer_node_->GetVideoParams().frame_rate_as_time_base()); + } + + rvp.AddCache(frame_cache); + } + + rvp.return_type = dry ? RenderManager::kNull : RenderManager::kTexture; + + // Allow using cached images for this render job + rvp.use_cache = true; + + watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); return watcher; } -RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, bool generate_waveforms) +RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, PlaybackCache *cache) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + watcher->setProperty("node", Node::PtrToValue(node)); + watcher->setProperty("cache", Node::PtrToValue(cache)); + watcher->setProperty("time", QVariant::fromValue(r)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); - audio_tasks_.insert(watcher, r); + running_audio_tasks_.append(watcher); - RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(node, r, copied_viewer_node_->GetAudioParams(), RenderMode::kOffline, generate_waveforms); + RenderManager::RenderAudioParams rap(node, + r, + copied_viewer_node_->GetAudioParams(), + RenderMode::kOffline); + + rap.generate_waveforms = dynamic_cast(cache); + rap.clamp = false; + + RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(rap); watcher->SetTicket(ticket); return ticket; } -void PreviewAutoCacher::RequeueFrames() -{ - delayed_requeue_timer_.stop(); - - if (viewer_node_ - && (viewer_node_->video_frame_cache()->IsEnabled() || use_custom_range_) - && viewer_node_->video_frame_cache()->HasInvalidatedRanges(viewer_node_->GetVideoLength()) - && !IsRenderingCustomRange()) { - TimeRange using_range = use_custom_range_ ? custom_autocache_range_ : cache_range_; - - TimeRangeList invalidated = viewer_node_->video_frame_cache()->GetInvalidatedRanges(using_range); - queued_frame_iterator_ = TimeRangeListFrameIterator(invalidated, viewer_node_->video_frame_cache()->GetTimebase()); - - queued_frame_iterator_.SetCustomRange(use_custom_range_); - - emit StopCacheProxyTasks(); - - if (use_custom_range_) { - CustomCacheTask *cct = new CustomCacheTask(viewer_node_->GetLabelOrName()); - connect(this, &PreviewAutoCacher::StopCacheProxyTasks, cct, &CustomCacheTask::Finish); - connect(this, &PreviewAutoCacher::SignalCacheProxyTaskProgress, cct, &CustomCacheTask::ProgressChanged); - connect(cct, &CustomCacheTask::Cancelled, this, &PreviewAutoCacher::CacheProxyTaskCancelled); - TaskManager::instance()->AddTask(cct); - } - - use_custom_range_ = false; - - TryRender(); - } -} - void PreviewAutoCacher::ConformFinished() { // Got an audio conform, requeue all the audio currently needing a conform last_conform_task_.Acquire(); - if (!audio_needing_conform_.isEmpty()) { - // This list should be empty if there was a viewer switch - foreach (const TimeRange &range, audio_needing_conform_) { - viewer_node_->audio_playback_cache()->Invalidate(range); + for (auto it=audio_cache_data_.begin(); it!=audio_cache_data_.end(); it++) { + foreach (const TimeRange &range, it.value().needs_conform) { + it.key()->Request(range); } - audio_needing_conform_.clear(); - } -} - -void PreviewAutoCacher::VideoAutoCacheEnableChanged(bool e) -{ - if (e) { - VideoInvalidatedList(viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength())); - } else { - CancelVideoTasks(); - queued_frame_iterator_.reset(); - } -} - -void PreviewAutoCacher::AudioAutoCacheEnableChanged(bool e) -{ - if (e) { - AudioInvalidatedList(viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength())); - } else { - CancelAudioTasks(); - audio_iterator_.clear(); + it.value().needs_conform.clear(); } } void PreviewAutoCacher::CacheProxyTaskCancelled() { - queued_frame_iterator_.reset(); - RequeueFrames(); + pending_video_jobs_.clear(); + + TryRender(); } void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) @@ -721,7 +770,7 @@ void PreviewAutoCacher::ForceCacheRange(const TimeRange &range) custom_autocache_range_ = range; // Re-hash these frames and start rendering - StartCachingVideoRange(range); + StartCachingVideoRange(viewer_node_->video_frame_cache(), range); } void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) @@ -738,36 +787,25 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) delayed_requeue_timer_.stop(); // Handle video rendering tasks - if (!video_tasks_.isEmpty()) { + if (!running_video_tasks_.isEmpty()) { // Cancel any video tasks and wait for them to finish CancelVideoTasks(true); - video_tasks_.clear(); + running_video_tasks_.clear(); } // Handle audio rendering tasks - if (!audio_tasks_.isEmpty()) { + if (!running_audio_tasks_.isEmpty()) { // Cancel any audio tasks and wait for them to finish CancelAudioTasks(true); - audio_tasks_.clear(); + running_audio_tasks_.clear(); } - // Clear iterators - queued_frame_iterator_.reset(); - audio_iterator_.clear(); - - // Clear any invalidated ranges - invalidated_video_.clear(); - invalidated_audio_.clear(); - // Clear any single frame render that might be queued CancelQueuedSingleFrameRender(); // Not interested in video passthroughs anymore video_immediate_passthroughs_.clear(); - // Not interested in audio conforming anymore - audio_needing_conform_.clear(); - // Disconnect from all node cache's for (auto it=copy_map_.cbegin(); it!=copy_map_.cend(); it++) { DisconnectFromNodeCache(it.key()); @@ -779,8 +817,10 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) copy_map_.clear(); copied_viewer_node_ = nullptr; graph_update_queue_.clear(); - video_job_tracker_.clear(); - audio_job_tracker_.clear(); + + // Ensure all cache data is cleared + video_cache_data_.clear(); + audio_cache_data_.clear(); // Disconnect signals for future node additions/deletions NodeGraph* graph = viewer_node_->parent(); @@ -799,6 +839,8 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) // Copy graph NodeGraph* graph = viewer_node_->parent(); + SetRendersPaused(true); + // Add all nodes for (int i=0; inodes().at(i), copied_project_.nodes().at(i)); @@ -806,6 +848,9 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) for (int i=copied_project_.nodes().size(); inodes().size(); i++) { AddNode(graph->nodes().at(i)); } + for (int i=0; inodes().size(); i++) { + graph->nodes().at(i)->ConnectedToPreviewEvent(); + } // Find copied viewer node copied_viewer_node_ = static_cast(copy_map_.value(viewer_node_)); @@ -830,9 +875,7 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged, Qt::DirectConnection); connect(graph, &NodeGraph::InputValueHintChanged, this, &PreviewAutoCacher::ValueHintChanged, Qt::DirectConnection); - // Copy invalidated ranges and start rendering if necessary - VideoInvalidatedList(viewer_node_->video_frame_cache()->GetInvalidatedRanges(viewer_node_->GetVideoLength())); - AudioInvalidatedList(viewer_node_->audio_playback_cache()->GetInvalidatedRanges(viewer_node_->GetAudioLength())); + SetRendersPaused(false); } } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 0ce429673..05e253851 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -45,7 +45,7 @@ class PreviewAutoCacher : public QObject { Q_OBJECT public: - PreviewAutoCacher(); + PreviewAutoCacher(QObject *parent = nullptr); virtual ~PreviewAutoCacher() override; @@ -53,6 +53,8 @@ public: RenderTicketPtr GetRangeOfAudio(TimeRange range); + void ClearSingleFrameRenders(); + /** * @brief Set the viewer node to auto-cache */ @@ -83,12 +85,15 @@ public: void CancelVideoTasks(bool and_wait_for_them_to_finish = false); void CancelAudioTasks(bool and_wait_for_them_to_finish = false); - bool IsRenderingCustomRange() const - { - return queued_frame_iterator_.IsCustomRange() && queued_frame_iterator_.HasNext(); - } + bool IsRenderingCustomRange() const; - void SetAudioPaused(bool e); + void SetRendersPaused(bool e); + +public slots: + void SetDisplayColorProcessor(ColorProcessorPtr processor) + { + display_color_processor_ = processor; + } signals: void StopCacheProxyTasks(); @@ -98,17 +103,9 @@ signals: private: void TryRender(); - RenderTicketWatcher *RenderFrame(Node *node, const rational &time, FrameHashCache *cache, bool dry); - RenderTicketWatcher *RenderFrame(const rational &time, FrameHashCache *cache, bool dry) - { - return RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), time, cache, dry); - } + RenderTicketWatcher *RenderFrame(Node *node, const rational &time, PlaybackCache *cache, bool dry); - RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, bool generate_waveforms); - RenderTicketPtr RenderAudio(const TimeRange &range, bool generate_waveforms) - { - return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, generate_waveforms); - } + RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, PlaybackCache *cache); /** * @brief Process all changes to internal NodeGraph copy @@ -135,12 +132,12 @@ private: void CancelQueuedSingleFrameRender(); - void VideoInvalidatedList(const TimeRangeList &list); - void AudioInvalidatedList(const TimeRangeList &list); - void StartCachingRange(const TimeRange &range, TimeRangeList *range_list, RenderJobTracker *tracker); - void StartCachingVideoRange(const TimeRange &range); - void StartCachingAudioRange(const TimeRange &range); + void StartCachingVideoRange(PlaybackCache *cache, const TimeRange &range); + void StartCachingAudioRange(PlaybackCache *cache, const TimeRange &range); + + void VideoInvalidatedFromNode(PlaybackCache *cache, const olive::TimeRange &range); + void AudioInvalidatedFromNode(PlaybackCache *cache, const olive::TimeRange &range); class QueuedJob { public: @@ -175,15 +172,9 @@ private: bool use_custom_range_; TimeRange custom_autocache_range_; - TimeRangeList invalidated_video_; - TimeRangeList invalidated_audio_; - - bool pause_audio_; + bool pause_renders_; RenderTicketPtr single_frame_render_; - - QMap audio_tasks_; - QMap video_tasks_; QMap > video_immediate_passthroughs_; JobTime graph_changed_time_; @@ -191,28 +182,53 @@ private: QTimer delayed_requeue_timer_; - TimeRangeList audio_needing_conform_; - JobTime last_conform_task_; - RenderJobTracker video_job_tracker_; - RenderJobTracker audio_job_tracker_; + QVector running_video_tasks_; + QVector running_audio_tasks_; - TimeRangeListFrameIterator queued_frame_iterator_; - TimeRangeList audio_iterator_; + struct VideoJob { + Node *node; + PlaybackCache *cache; + TimeRange range; + TimeRangeListFrameIterator iterator; + }; - static const bool kRealTimeWaveformsEnabled; + struct VideoCacheData { + RenderJobTracker job_tracker; + }; + + struct AudioJob { + Node *node; + PlaybackCache *cache; + TimeRange range; + }; + + struct AudioCacheData { + RenderJobTracker job_tracker; + TimeRangeList needs_conform; + }; + + std::list pending_video_jobs_; + std::list pending_audio_jobs_; + + QHash video_cache_data_; + QHash audio_cache_data_; + + ColorProcessorPtr display_color_processor_; private slots: /** * @brief Handler for when the NodeGraph reports a video change over a certain time range */ - void VideoInvalidated(const olive::TimeRange &range); + void VideoInvalidatedFromCache(const olive::TimeRange &range); /** * @brief Handler for when the NodeGraph reports a audio change over a certain time range */ - void AudioInvalidated(const olive::TimeRange &range); + void AudioInvalidatedFromCache(const olive::TimeRange &range); + + void CancelForCache(); /** * @brief Handler for when the RenderManager has returned rendered audio @@ -239,14 +255,10 @@ private slots: /** * @brief Generic function called whenever the frames to render need to be (re)queued */ - void RequeueFrames(); + //void RequeueFrames(); void ConformFinished(); - void VideoAutoCacheEnableChanged(bool e); - - void AudioAutoCacheEnableChanged(bool e); - void CacheProxyTaskCancelled(); }; diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index e6754e5c7..9aa24dbd1 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -121,6 +121,8 @@ TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom, const V QVariant Renderer::GetDefaultShader() { + QMutexLocker locker(&color_cache_mutex_); + if (default_shader_.isNull()) { default_shader_ = CreateNativeShader(ShaderCode(QString(), QString())); } diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index bf7dc449b..19a0d8efc 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -52,13 +52,10 @@ RenderManager::RenderManager(QObject *parent) : } if (context_) { - video_thread_ = new RenderThread(context_, decoder_cache_, shader_cache_, this); - dry_run_thread_ = new RenderThread(nullptr, decoder_cache_, shader_cache_, this); - audio_thread_ = new RenderThread(nullptr, decoder_cache_, shader_cache_, this); - - video_thread_->start(QThread::IdlePriority); - dry_run_thread_->start(QThread::IdlePriority); - audio_thread_->start(QThread::IdlePriority); + video_thread_ = CreateThread(context_); + dry_run_thread_ = CreateThread(); + audio_thread_ = CreateThread(); + waveform_thread_ = CreateThread(); } decoder_clear_timer_ = new QTimer(this); @@ -73,72 +70,49 @@ RenderManager::~RenderManager() delete shader_cache_; delete decoder_cache_; - video_thread_->quit(); - video_thread_->wait(); - - dry_run_thread_->quit(); - dry_run_thread_->wait(); + for (RenderThread *rt : render_threads_) { + rt->quit(); + rt->wait(); + } context_->PostDestroy(); delete context_; - - audio_thread_->quit(); - audio_thread_->wait(); } } -RenderTicketPtr RenderManager::RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, - ColorManager* color_manager, const rational& time, RenderMode::Mode mode, - FrameHashCache* cache, ReturnType return_type) +RenderThread *RenderManager::CreateThread(Renderer *renderer) { - return RenderFrame(node, - color_manager, - time, - mode, - vparam, - param, - QSize(0, 0), - QMatrix4x4(), - VideoParams::kFormatInvalid, - 0, - nullptr, - cache, - return_type); + auto t = new RenderThread(renderer, decoder_cache_, shader_cache_, this); + render_threads_.push_back(t); + t->start(QThread::IdlePriority); + return t; } -RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - const VideoParams &video_params, const AudioParams &audio_params, - const QSize& force_size, - const QMatrix4x4& force_matrix, VideoParams::Format force_format, - int force_channel_count, - ColorProcessorPtr force_color_output, - FrameHashCache* cache, ReturnType return_type) +RenderTicketPtr RenderManager::RenderFrame(const RenderVideoParams ¶ms) { // Create ticket RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("node", Node::PtrToValue(node)); - ticket->setProperty("time", QVariant::fromValue(time)); - ticket->setProperty("size", force_size); - ticket->setProperty("matrix", force_matrix); - ticket->setProperty("format", force_format); - ticket->setProperty("channelcount", force_channel_count); - ticket->setProperty("mode", mode); + ticket->setProperty("node", Node::PtrToValue(params.node)); + ticket->setProperty("time", QVariant::fromValue(params.time)); + ticket->setProperty("size", params.force_size); + ticket->setProperty("matrix", params.force_matrix); + ticket->setProperty("format", params.force_format); + ticket->setProperty("usecache", params.use_cache); + ticket->setProperty("channelcount", params.force_channel_count); + ticket->setProperty("mode", params.mode); ticket->setProperty("type", kTypeVideo); - ticket->setProperty("colormanager", Node::PtrToValue(color_manager)); - ticket->setProperty("coloroutput", QVariant::fromValue(force_color_output)); - ticket->setProperty("vparam", QVariant::fromValue(video_params)); - ticket->setProperty("aparam", QVariant::fromValue(audio_params)); - ticket->setProperty("return", return_type); + ticket->setProperty("colormanager", Node::PtrToValue(params.color_manager)); + ticket->setProperty("coloroutput", QVariant::fromValue(params.force_color_output)); + Q_ASSERT(params.video_params.is_valid()); + ticket->setProperty("vparam", QVariant::fromValue(params.video_params)); + ticket->setProperty("aparam", QVariant::fromValue(params.audio_params)); + ticket->setProperty("return", params.return_type); + ticket->setProperty("cache", params.cache_dir); + ticket->setProperty("cachetimebase", QVariant::fromValue(params.cache_timebase)); + ticket->setProperty("cacheid", QVariant::fromValue(params.cache_id)); - if (cache) { - ticket->setProperty("cache", cache->GetCacheDirectory()); - ticket->setProperty("cachetimebase", QVariant::fromValue(cache->GetTimebase())); - ticket->setProperty("cacheuuid", QVariant::fromValue(cache->GetUuid())); - } - - if (return_type == ReturnType::kNull) { + if (params.return_type == ReturnType::kNull) { dry_run_thread_->AddTicket(ticket); } else { video_thread_->AddTicket(ticket); @@ -147,34 +121,37 @@ RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manag return ticket; } -RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms) +RenderTicketPtr RenderManager::RenderAudio(const RenderAudioParams ¶ms) { // Create ticket RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("node", Node::PtrToValue(node)); - ticket->setProperty("time", QVariant::fromValue(r)); + ticket->setProperty("node", Node::PtrToValue(params.node)); + ticket->setProperty("time", QVariant::fromValue(params.range)); ticket->setProperty("type", kTypeAudio); - ticket->setProperty("mode", mode); - ticket->setProperty("enablewaveforms", generate_waveforms); - ticket->setProperty("aparam", QVariant::fromValue(params)); + ticket->setProperty("enablewaveforms", params.generate_waveforms); + ticket->setProperty("clamp", params.clamp); + ticket->setProperty("aparam", QVariant::fromValue(params.audio_params)); + ticket->setProperty("mode", params.mode); - audio_thread_->AddTicket(ticket); + if (params.generate_waveforms) { + waveform_thread_->AddTicket(ticket); + } else { + audio_thread_->AddTicket(ticket); + } return ticket; } bool RenderManager::RemoveTicket(RenderTicketPtr ticket) { - if (video_thread_->RemoveTicket(ticket)) { - return true; - } else if (audio_thread_->RemoveTicket(ticket)) { - return true; - } else if (dry_run_thread_->RemoveTicket(ticket)) { - return true; - } else { - return false; + for (RenderThread *rt : render_threads_) { + if (rt->RemoveTicket(ticket)) { + return true; + } } + + return false; } void RenderManager::SetAggressiveGarbageCollection(bool enabled) @@ -222,6 +199,7 @@ RenderThread::RenderThread(Renderer *renderer, DecoderCache *decoder_cache, Shad void RenderThread::AddTicket(RenderTicketPtr ticket) { QMutexLocker locker(&mutex_); + ticket->moveToThread(this); queue_.push_back(ticket); wait_.wakeOne(); } diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 6eed0a5bc..b33818b58 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -101,6 +101,51 @@ public: kNull }; + struct RenderVideoParams { + RenderVideoParams(Node *n, const VideoParams &vparam, const AudioParams &aparam, const rational &t, + ColorManager *colorman, RenderMode::Mode m) + { + node = n; + video_params = vparam; + audio_params = aparam; + time = t; + color_manager = colorman; + use_cache = false; + return_type = kFrame; + force_format = VideoParams::kFormatInvalid; + force_color_output = nullptr; + force_size = QSize(0, 0); + force_channel_count = 0; + mode = m; + } + + void AddCache(FrameHashCache *cache) + { + cache_dir = cache->GetCacheDirectory(); + cache_timebase = cache->GetTimebase(); + cache_id = cache->GetUuid().toString(); + } + + Node *node; + VideoParams video_params; + AudioParams audio_params; + rational time; + ColorManager *color_manager; + bool use_cache; + ReturnType return_type; + RenderMode::Mode mode; + + QString cache_dir; + rational cache_timebase; + QString cache_id; + + QSize force_size; + int force_channel_count; + QMatrix4x4 force_matrix; + VideoParams::Format force_format; + ColorProcessorPtr force_color_output; + }; + static const rational kDryRunInterval; /** @@ -111,17 +156,26 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - FrameHashCache* cache = nullptr, ReturnType return_type = kFrame); - RenderTicketPtr RenderFrame(Node *node, ColorManager* color_manager, - const rational& time, RenderMode::Mode mode, - const VideoParams& video_params, const AudioParams& audio_params, - const QSize& force_size, - const QMatrix4x4& force_matrix, VideoParams::Format force_format, - int force_channel_count, - ColorProcessorPtr force_color_output, - FrameHashCache* cache = nullptr, ReturnType return_type = kFrame); + RenderTicketPtr RenderFrame(const RenderVideoParams ¶ms); + + struct RenderAudioParams { + RenderAudioParams(Node *n, const TimeRange &time, const AudioParams &aparam, RenderMode::Mode m) + { + node = n; + range = time; + audio_params = aparam; + generate_waveforms = false; + clamp = true; + mode = m; + } + + Node *node; + TimeRange range; + AudioParams audio_params; + bool generate_waveforms; + bool clamp; + RenderMode::Mode mode; + }; /** * @brief Asynchronously generate a chunk of audio @@ -130,7 +184,7 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderAudio(Node *viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms); + RenderTicketPtr RenderAudio(const RenderAudioParams ¶ms); bool RemoveTicket(RenderTicketPtr ticket); @@ -154,6 +208,8 @@ private: virtual ~RenderManager() override; + RenderThread *CreateThread(Renderer *renderer = nullptr); + static RenderManager* instance_; Renderer* context_; @@ -174,6 +230,9 @@ private: RenderThread *video_thread_; RenderThread *dry_run_thread_; RenderThread *audio_thread_; + RenderThread *waveform_thread_; + + std::list render_threads_; private slots: void ClearOldDecoders(); diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 94cea35f8..426b0a9ed 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -95,32 +95,32 @@ FramePtr RenderProcessor::GenerateFrame(TexturePtr texture, const rational& time ColorProcessorPtr output_color_transform = ticket_->property("coloroutput").value(); const VideoParams& tex_params = texture->params(); + if (output_color_transform) { + TexturePtr transform_tex = render_ctx_->CreateTexture(tex_params); + ColorTransformJob job; + + job.SetColorProcessor(output_color_transform); + job.SetInputTexture(texture); + job.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); + + render_ctx_->BlitColorManaged(job, transform_tex.get()); + + texture = transform_tex; + } + if (tex_params.effective_width() != frame_params.effective_width() || tex_params.effective_height() != frame_params.effective_height() - || tex_params.format() != frame_params.format() - || output_color_transform) { + || tex_params.format() != frame_params.format()) { TexturePtr blit_tex = render_ctx_->CreateTexture(frame_params); QMatrix4x4 matrix = ticket_->property("matrix").value(); - if (output_color_transform) { - // Yes color transform, blit color managed - ColorTransformJob job; + // No color transform, just blit + ShaderJob job; + job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture))); + job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, matrix)); - job.SetColorProcessor(output_color_transform); - job.SetInputTexture(texture); - job.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); - job.SetTransformMatrix(matrix); - - render_ctx_->BlitColorManaged(job, blit_tex.get()); - } else { - // No color transform, just blit - ShaderJob job; - job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture))); - job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, matrix)); - - render_ctx_->BlitToTexture(render_ctx_->GetDefaultShader(), job, blit_tex.get()); - } + render_ctx_->BlitToTexture(render_ctx_->GetDefaultShader(), job, blit_tex.get()); // Replace texture that we're going to download in the next step texture = blit_tex; @@ -144,6 +144,11 @@ void RenderProcessor::Run() SetCacheVideoParams(ticket_->property("vparam").value()); SetCacheAudioParams(ticket_->property("aparam").value()); + if (IsCancelled()) { + ticket_->Finish(); + return; + } + switch (type) { case RenderManager::kTypeVideo: { @@ -176,10 +181,9 @@ void RenderProcessor::Run() // is actually "complete ticket_->Finish(); } else { - RenderManager::ReturnType return_type = RenderManager::ReturnType(ticket_->property("return").toInt()); - FramePtr frame; QString cache = ticket_->property("cache").toString(); + RenderManager::ReturnType return_type = RenderManager::ReturnType(ticket_->property("return").toInt()); if (return_type == RenderManager::kFrame || !cache.isEmpty()) { // Convert to CPU frame @@ -188,7 +192,7 @@ void RenderProcessor::Run() // Save to cache if requested if (!cache.isEmpty()) { rational timebase = ticket_->property("cachetimebase").value(); - QUuid uuid = ticket_->property("cacheuuid").value(); + QUuid uuid = ticket_->property("cacheid").value(); bool cache_result = FrameHashCache::SaveCacheFrame(cache, uuid, time, timebase, frame); ticket_->setProperty("cached", cache_result); } @@ -226,9 +230,11 @@ void RenderProcessor::Run() SampleBuffer samples = sample_val.toSamples(); if (samples.is_allocated()) { - samples.clamp(); + if (ticket_->property("clamp").toBool() && !IsCancelled()) { + samples.clamp(); + } - if (ticket_->property("enablewaveforms").toBool()) { + if (ticket_->property("enablewaveforms").toBool() && !IsCancelled()) { AudioVisualWaveform vis; vis.set_channel_count(samples.audio_params().channel_count()); vis.OverwriteSamples(samples, samples.audio_params().sample_rate()); @@ -262,14 +268,18 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c qint64 file_last_modified = QFileInfo(stream.filename()).lastModified().toMSecsSinceEpoch(); - if (!decoder.decoder || decoder.last_modified != file_last_modified) { + DecoderPtr dec = nullptr; + + if (decoder.decoder && decoder.last_modified == file_last_modified) { + dec = decoder.decoder; + } else { // No decoder - decoder.decoder = Decoder::CreateFromID(decoder_id); + decoder.decoder = dec = Decoder::CreateFromID(decoder_id); decoder.last_modified = file_last_modified; decoder_cache_->insert(stream, decoder); locker.unlock(); - if (!decoder.decoder->Open(stream)) { + if (!dec->Open(stream)) { qWarning() << "Failed to open decoder for" << stream.filename() << "::" << stream.stream(); return nullptr; @@ -281,7 +291,7 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c } } - return decoder.decoder; + return dec; } void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache *decoder_cache, ShaderCache *shader_cache) @@ -310,8 +320,8 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim TimeRange range_for_block(qMax(b->in(), range.in()), qMin(b->out(), range.out())); - int destination_offset = audio_params.time_to_samples(range_for_block.in() - range.in()); - int max_dest_sz = audio_params.time_to_samples(range_for_block.length()); + qint64 destination_offset = audio_params.time_to_samples(range_for_block.in() - range.in()); + qint64 max_dest_sz = audio_params.time_to_samples(range_for_block.length()); // Destination buffer NodeValueTable table = GenerateTable(b, Track::TransformRangeForBlock(b, range_for_block)); @@ -375,7 +385,7 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim } } - int copy_length = qMin(max_dest_sz, samples_from_this_block.sample_count()); + qint64 copy_length = qMin(max_dest_sz, qint64(samples_from_this_block.sample_count())); // Copy samples into destination buffer for (int i=0; iproperty("enablewaveforms").toBool() && clip_cast) { - // Format information for use in the main thread - RenderedWaveform waveform_info; - waveform_info.block = clip_cast; - waveform_info.range = range_for_block - b->in(); - - if (!(waveform_info.silence = !samples_from_this_block.is_allocated())) { - // Generate a visual waveform from the samples acquired from this block - AudioVisualWaveform visual_waveform; - visual_waveform.set_channel_count(audio_params.channel_count()); - visual_waveform.OverwriteSamples(samples_from_this_block, audio_params.sample_rate()); - waveform_info.waveform = visual_waveform; - } - - QVector waveform_list = ticket_->property("waveforms").value< QVector >(); - waveform_list.append(waveform_info); - ticket_->setProperty("waveforms", QVariant::fromValue(waveform_list)); - } } } @@ -485,7 +475,7 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ unmanaged_texture = decoder->RetrieveVideo(p); - if (unmanaged_texture) { + if (!IsCancelled() && unmanaged_texture) { // We convert to our rendering pixel format, since that will always be float-based which // is necessary for correct color conversion ColorProcessorPtr processor = ColorProcessor::Create(color_manager, @@ -568,7 +558,7 @@ void RenderProcessor::ProcessSamples(SampleBuffer &destination, const Node *node const AudioParams& audio_params = GetCacheAudioParams(); - for (int i=0;i(i) / static_cast(audio_params.sample_rate()); @@ -576,9 +566,10 @@ void RenderProcessor::ProcessSamples(SampleBuffer &destination, const Node *node // Update all non-sample and non-footage inputs for (auto j=job.GetValues().constBegin(); j!=job.GetValues().constEnd(); j++) { - NodeValueTable value = ProcessInput(node, j.key(), TimeRange(this_sample_time, this_sample_time)); + TimeRange r = TimeRange(this_sample_time, this_sample_time); + NodeValueTable value = ProcessInput(node, j.key(), r); - value_db.insert(j.key(), GenerateRowValue(node, j.key(), &value)); + value_db.insert(j.key(), GenerateRowValue(node, j.key(), &value, r)); } node->ProcessSamples(value_db, @@ -613,9 +604,22 @@ void RenderProcessor::ProcessFrameGeneration(TexturePtr destination, const Node destination->Upload(frame->data(), frame->linesize_pixels()); } -bool RenderProcessor::CanCacheFrames() +TexturePtr RenderProcessor::ProcessVideoCacheJob(const CacheJob &val) { - return ticket_->property("type").value() == RenderManager::kTypeVideo; + FramePtr frame = FrameHashCache::LoadCacheFrame(val.GetFilename()); + if (frame) { + TexturePtr tex = CreateTexture(frame->video_params()); + if (tex) { + tex->Upload(frame->data(), frame->linesize_pixels()); + return tex; + } + } else { + QStringList s = ticket_->property("badcache").toStringList(); + s.append(val.GetFilename()); + ticket_->setProperty("badcache", s); + } + + return nullptr; } TexturePtr RenderProcessor::CreateTexture(const VideoParams &p) diff --git a/app/render/renderprocessor.h b/app/render/renderprocessor.h index 9d7942c7a..a537ccf89 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -56,7 +56,7 @@ protected: virtual void ProcessFrameGeneration(TexturePtr destination, const Node *node, const GenerateJob& job) override; - virtual bool CanCacheFrames() override; + virtual TexturePtr ProcessVideoCacheJob(const CacheJob &val) override; virtual TexturePtr CreateTexture(const VideoParams &p) override; diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index 14474d49c..7ac14904e 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -126,7 +126,7 @@ VideoParams::VideoParams(int width, int height, const rational &time_base, Forma int VideoParams::generate_auto_divider(qint64 width, qint64 height) { - const int target_res = 1920*1080; + const int target_res = 1280*720; qint64 megapixels = width * height; @@ -243,6 +243,21 @@ QString VideoParams::GetFormatName(VideoParams::Format format) return QCoreApplication::translate("VideoParams", "Unknown (0x%1)").arg(format, 0, 16); } +int VideoParams::GetDividerForTargetResolution(int src_width, int src_height, int dst_width, int dst_height) +{ + int divider = 0; + int test_width, test_height; + + do { + divider++; + + test_width = VideoParams::GetScaledDimension(src_width, divider); + test_height = VideoParams::GetScaledDimension(src_height, divider); + } while (test_width > dst_width || test_height > dst_height); + + return divider; +} + void VideoParams::calculate_effective_size() { effective_width_ = GetScaledDimension(width(), divider_); diff --git a/app/render/videoparams.h b/app/render/videoparams.h index e65ad7a93..1a75d2bed 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -247,6 +247,8 @@ public: static QString GetFormatName(Format format); + static int GetDividerForTargetResolution(int src_width, int src_height, int dst_width, int dst_height); + static const int kInternalChannelCount; static const rational kPixelAspectSquare; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 1bb764022..b3bfca484 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -60,19 +60,16 @@ bool RenderTask::Render(ColorManager* manager, // 50%, which makes the progress bar look weird to the uninitiated //total_length += r.length().toDouble(); - rational r = range.in(); - while (r != range.out()) { - rational end = qMin(range.out(), r+1); - TimeRange this_range(r, end); + RenderManager::RenderAudioParams rap(viewer_->GetConnectedSampleOutput(), + range, + audio_params_, + RenderMode::kOnline); - RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("range", QVariant::fromValue(this_range)); - PrepareWatcher(watcher, &watcher_thread); - IncrementRunningTickets(); - watcher->SetTicket(RenderManager::instance()->RenderAudio(viewer_->GetConnectedSampleOutput(), this_range, audio_params_, mode, false)); - - r = end; - } + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + watcher->setProperty("range", QVariant::fromValue(range)); + PrepareWatcher(watcher, &watcher_thread); + IncrementRunningTickets(); + watcher->SetTicket(RenderManager::instance()->RenderAudio(rap)); } // Look up hashes @@ -285,15 +282,24 @@ void RenderTask::StartTicket(QThread* watcher_thread, ColorManager* manager, VideoParams::Format force_format, int force_channel_count, ColorProcessorPtr force_color_output) { + RenderManager::RenderVideoParams rvp(viewer_->GetConnectedTextureOutput(), video_params_, audio_params_, + time, manager, mode); + + rvp.force_size = force_size; + rvp.force_matrix = force_matrix; + rvp.force_format = force_format; + rvp.force_color_output = force_color_output; + rvp.force_channel_count = force_channel_count; + + if (cache) { + rvp.AddCache(cache); + } + RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("time", QVariant::fromValue(time)); PrepareWatcher(watcher, watcher_thread); IncrementRunningTickets(); - watcher->SetTicket(RenderManager::instance()->RenderFrame(viewer_->GetConnectedTextureOutput(), manager, time, - mode, video_params_, audio_params_, - force_size, force_matrix, - force_format, force_channel_count, - force_color_output, cache)); + watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); } void RenderTask::TicketDone(RenderTicketWatcher* watcher) diff --git a/app/timeline/timelinecommon.h b/app/timeline/timelinecommon.h index 5c6d96df9..cdc513149 100644 --- a/app/timeline/timelinecommon.h +++ b/app/timeline/timelinecommon.h @@ -38,6 +38,17 @@ public: kTrimOut }; + enum ThumbnailMode { + kThumbnailOff, + kThumbnailInOut, + kThumbnailOn + }; + + enum WaveformMode { + kWaveformsDisabled, + kWaveformsEnabled + }; + static bool IsATrimMode(MovementMode mode) {return mode == kTrimIn || mode == kTrimOut;} struct EditToInfo { diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index 25de1ab9a..a9753b2e5 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -331,7 +331,6 @@ Project* MarkerChangeTimeCommand::GetRelevantProject() const void MarkerChangeTimeCommand::redo() { - old_time_ = marker_->time(); marker_->set_time(new_time_); } diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index 588c39ac5..f298fda10 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -93,11 +93,12 @@ void AudioMonitor::PushSampleBuffer(const SampleBuffer &d) SetUpdateLoop(true); } -void AudioMonitor::StartWaveform(const AudioVisualWaveform *waveform, const rational &start, int playback_speed) +void AudioMonitor::StartWaveform(const AudioWaveformCache *waveform, const rational &start, int playback_speed) { Stop(); - if (start >= waveform->length()) { + waveform_length_ = waveform->length(); + if (start >= waveform_length_) { return; } @@ -242,7 +243,7 @@ void AudioMonitor::paintGL() if (waveform_) { UpdateValuesFromWaveform(v, delta_time); - if (waveform_time_ >= waveform_->length()) { + if (waveform_time_ >= waveform_length_) { Stop(); } } @@ -315,7 +316,7 @@ void AudioMonitor::UpdateValuesFromWaveform(QVector &v, qint64 delta_tim void AudioMonitor::AudioVisualWaveformSampleToInternalValues(const AudioVisualWaveform::Sample &in, QVector &out) { - for (int i=0; iStartWaveform(waveform, start, playback_speed); @@ -73,7 +73,7 @@ public slots: void PushSampleBuffer(const SampleBuffer &samples); - void StartWaveform(const AudioVisualWaveform *waveform, const rational& start, int playback_speed); + void StartWaveform(const AudioWaveformCache *waveform, const rational& start, int playback_speed); protected: virtual void paintGL() override; @@ -97,8 +97,9 @@ private: qint64 last_time_; - const AudioVisualWaveform* waveform_; + const AudioWaveformCache* waveform_; rational waveform_time_; + rational waveform_length_; int playback_speed_; diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 3177da6a5..5a6254bd5 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -26,7 +26,6 @@ #include #include -#include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "node/output/viewer/viewer.h" #include "widget/nodeparamview/nodeparamviewundo.h" diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 7c6a78cee..d752095f6 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -26,7 +26,6 @@ #include #include -#include "common/flipmodifiers.h" #include "common/qtutils.h" #include "config/config.h" #include "core.h" @@ -434,7 +433,7 @@ void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) return; } - event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + event->setModifiers(QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mousePressEvent(event); } @@ -445,7 +444,7 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) return; } - event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + event->setModifiers(QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseMoveEvent(event); } @@ -457,7 +456,7 @@ void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) return; } - event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + event->setModifiers(QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseReleaseEvent(event); } diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 833a83d99..f2ae527e2 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -20,7 +20,6 @@ #include "nodeviewscene.h" -#include "common/functiontimer.h" #include "core.h" #include "node/project/sequence/sequence.h" #include "nodeviewedge.h" diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index bdf853d99..48cc8e3aa 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -67,6 +67,7 @@ ProjectExplorer::ProjectExplorer(QWidget *parent) : // Set up sort filter proxy model sort_model_.setSourceModel(&model_); + sort_model_.setFilterCaseSensitivity(Qt::CaseInsensitive); sort_model_.setSortRole(ProjectViewModel::kInnerTextRole); // Add tree view to stacked widget @@ -301,6 +302,11 @@ void ProjectExplorer::RenameSelectedItem() } } +void ProjectExplorer::SetSearchFilter(const QString &s) +{ + sort_model_.setFilterFixedString(s); +} + void ProjectExplorer::ShowContextMenu() { Menu menu; diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index d69f1b8e3..c03fc2e67 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -94,6 +94,8 @@ public slots: void RenameSelectedItem(); + void SetSearchFilter(const QString &s); + signals: /** * @brief Emitted when an Item is double clicked diff --git a/app/widget/timebased/timescaledobject.cpp b/app/widget/timebased/timescaledobject.cpp index daefa14d2..1102f31ed 100644 --- a/app/widget/timebased/timescaledobject.cpp +++ b/app/widget/timebased/timescaledobject.cpp @@ -64,6 +64,9 @@ rational TimeScaledObject::SceneToTime(const double &x, const double &x_scale, c if (round) { rounded_x_mvmt = qRound64(unscaled_time); + } else if (unscaled_time < 0) { + // "floor" to zero + rounded_x_mvmt = qCeil(unscaled_time); } else { rounded_x_mvmt = qFloor(unscaled_time); } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 62a071730..f54792aef 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -54,6 +54,7 @@ #include "undo/timelineundoworkarea.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" +#include "widget/nodeparamview/nodeparamviewundo.h" #include "widget/nodeparamview/nodeparamview.h" #include "widget/nodeview/nodeviewundo.h" #include "widget/timeruler/timeruler.h" @@ -1109,6 +1110,27 @@ void TimelineWidget::ShowContextMenu() menu.addSeparator(); if (ClipBlock *clip = dynamic_cast(selected.first())) { + { + Menu *cache_menu = new Menu(tr("Cache"), &menu); + menu.addMenu(cache_menu); + + QAction *autocache_action = cache_menu->addAction(tr("Auto-Cache")); + autocache_action->setCheckable(true); + autocache_action->setChecked(clip->IsAutocaching()); + connect(autocache_action, &QAction::triggered, this, &TimelineWidget::SetSelectedClipsAutocaching); + + cache_menu->addSeparator(); + + auto cache_clip = cache_menu->addAction(tr("Cache All")); + connect(cache_clip, &QAction::triggered, this, &TimelineWidget::CacheClips); + + auto cache_inout = cache_menu->addAction(tr("Cache In/Out")); + connect(cache_inout, &QAction::triggered, this, &TimelineWidget::CacheClipsInOut); + + auto cache_discard = cache_menu->addAction(tr("Discard")); + connect(cache_discard, &QAction::triggered, this, &TimelineWidget::CacheDiscard); + } + if (clip->connected_viewer()) { QAction *reveal_in_footage_viewer = menu.addAction(tr("Reveal in Footage Viewer")); reveal_in_footage_viewer->setData(reinterpret_cast(clip->connected_viewer())); @@ -1134,9 +1156,20 @@ void TimelineWidget::ShowContextMenu() toggle_audio_units->setChecked(use_audio_time_units_); connect(toggle_audio_units, &QAction::triggered, this, &TimelineWidget::SetUseAudioTimeUnits); + { + Menu *thumbnail_menu = new Menu(tr("Show Thumbnails"), &menu); + menu.addMenu(thumbnail_menu); + + thumbnail_menu->AddActionWithData(tr("Disabled"), Timeline::kThumbnailOff, OLIVE_CONFIG("TimelineThumbnailMode")); + thumbnail_menu->AddActionWithData(tr("Only At In Points"), Timeline::kThumbnailInOut, OLIVE_CONFIG("TimelineThumbnailMode")); + thumbnail_menu->AddActionWithData(tr("Enabled"), Timeline::kThumbnailOn, OLIVE_CONFIG("TimelineThumbnailMode")); + + connect(thumbnail_menu, &Menu::triggered, this, &TimelineWidget::SetViewThumbnailsEnabled); + } + QAction* show_waveforms = menu.addAction(tr("Show Waveforms")); show_waveforms->setCheckable(true); - show_waveforms->setChecked(views_.first()->view()->GetShowWaveforms()); + show_waveforms->setChecked(OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled); connect(show_waveforms, &QAction::triggered, this, &TimelineWidget::SetViewWaveformsEnabled); menu.addSeparator(); @@ -1200,9 +1233,14 @@ void TimelineWidget::AddableObjectChanged() void TimelineWidget::SetViewWaveformsEnabled(bool e) { - foreach (TimelineAndTrackView* tview, views_) { - tview->view()->SetShowWaveforms(e); - } + OLIVE_CONFIG("TimelineWaveformMode") = e ? Timeline::kWaveformsEnabled : Timeline::kWaveformsDisabled; + UpdateViewports(); +} + +void TimelineWidget::SetViewThumbnailsEnabled(QAction *action) +{ + OLIVE_CONFIG("TimelineThumbnailMode") = action->data(); + UpdateViewports(); } void TimelineWidget::FrameRateChanged() @@ -1276,6 +1314,63 @@ void TimelineWidget::TrackAboutToBeDeleted(Track *track) } } +void TimelineWidget::SetSelectedClipsAutocaching(bool e) +{ + MultiUndoCommand *command = new MultiUndoCommand(); + + for (Block *b : selected_blocks_) { + if (ClipBlock *clip = dynamic_cast(b)) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(clip, ClipBlock::kAutoCacheInput)), e)); + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + +void TimelineWidget::CacheClips() +{ + for (Block *b : selected_blocks_) { + if (ClipBlock *clip = dynamic_cast(b)) { + clip->RequestInvalidatedFromConnected(true); + } + } +} + +void TimelineWidget::CacheClipsInOut() +{ + if (!this->sequence() || !this->sequence()->GetWorkArea()->enabled()) { + return; + } + + TimeTargetObject tto; + tto.SetTimeTarget(this->sequence()); + + const TimeRange &r = this->sequence()->GetWorkArea()->range(); + for (Block *b : qAsConst(selected_blocks_)) { + if (ClipBlock *clip = dynamic_cast(b)) { + if (Node *connected = clip->GetConnectedOutput(clip->kBufferIn)) { + TimeRange adjusted = tto.GetAdjustedTime(this->sequence(), connected, r, true); + clip->RequestInvalidatedFromConnected(true, adjusted); + } + } + } +} + +void TimelineWidget::CacheDiscard() +{ + if (QMessageBox::question(this, tr("Discard Cache"), + tr("This will discard all cache for this clip. " + "If the clip has auto-cache enabled, it will be recached immediately. " + "This cannot be undone.\n\n" + "Do you wish to continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + for (Block *b : selected_blocks_) { + if (ClipBlock *clip = dynamic_cast(b)) { + clip->DiscardCache(); + } + } + } +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { ghost_items_.append(ghost); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 494f19fba..d952c5419 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -423,6 +423,8 @@ private slots: void SetViewWaveformsEnabled(bool e); + void SetViewThumbnailsEnabled(QAction *action); + void FrameRateChanged(); void SampleRateChanged(); @@ -436,6 +438,12 @@ private slots: void TrackAboutToBeDeleted(Track *track); + void SetSelectedClipsAutocaching(bool e); + + void CacheClips(); + void CacheClipsInOut(); + void CacheDiscard(); + }; } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 3914e137f..02dec714d 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -401,6 +401,8 @@ void ImportTool::DropGhosts(bool insert) } } + std::list imported_clips; + if (dst_graph) { QVector block_items(parent()->GetGhostItems().size()); @@ -471,6 +473,8 @@ void ImportTool::DropGhosts(bool insert) Block::Link(block_items.at(j), clip); } } + + imported_clips.push_back(clip); } else if (track_type == Track::kSubtitle) { Subtitle src = ghost->GetData(TimelineViewGhostItem::kAttachedFootage).value(); SubtitleBlock *sub = new SubtitleBlock(); @@ -498,6 +502,11 @@ void ImportTool::DropGhosts(bool insert) Core::instance()->undo_stack()->pushIfHasChildren(command); + while (!imported_clips.empty()) { + imported_clips.front()->RequestInvalidatedFromConnected(); + imported_clips.pop_front(); + } + parent()->ClearGhosts(); dragged_footage_.clear(); } @@ -514,7 +523,6 @@ TimelineViewGhostItem* ImportTool::CreateGhost(const TimeRange &range, const rat snap_points_.push_back(ghost->GetIn()); snap_points_.push_back(ghost->GetOut()); - ghost->SetMode(Timeline::kMove); parent()->AddGhost(ghost); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 3c8f9a7bf..91595afa2 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -24,7 +24,6 @@ #include #include "common/clamp.h" -#include "common/flipmodifiers.h" #include "common/qtutils.h" #include "common/range.h" #include "common/timecodefunctions.h" @@ -410,7 +409,7 @@ void PointerTool::InitiateDragInternal(Block *clicked_item, bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); // Create ghosts for trimming - foreach (Block* clip_item, clips) { + for (Block* clip_item : clips) { if (clip_item != clicked_item && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We @@ -485,7 +484,7 @@ void PointerTool::InitiateDragInternal(Block *clicked_item, // I'm only including it to prevent any potentially unintended behavior. if (clips.size() == 1 && !(modifiers & Qt::AltModifier)) { if (ClipBlock *adjacent_clip = dynamic_cast(adjacent)) { - foreach (Block *adjacent_link, adjacent_clip->block_links()) { + for (Block *adjacent_link : adjacent_clip->block_links()) { adjacent_ghosts.append(AddGhostFromBlock(adjacent_link, flipped_mode)); } } @@ -500,7 +499,7 @@ void PointerTool::InitiateDragInternal(Block *clicked_item, // expected to fill the remaining space (no gap needs to be created) ghost->SetData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast(adjacent)); - foreach (TimelineViewGhostItem *adjacent_ghost, adjacent_ghosts) { + for (TimelineViewGhostItem *adjacent_ghost : adjacent_ghosts) { if (adjacent_ghost) { if (treat_trim_as_slide) { // We're sliding a transition rather than a pure trim/roll @@ -572,6 +571,7 @@ void PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) break; case Timeline::kTrimIn: ghost->SetInAdjustment(time_movement); + ghost->SetMediaInAdjustment(time_movement); break; case Timeline::kTrimOut: ghost->SetOutAdjustment(time_movement); @@ -707,7 +707,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) block = new_block; if (ClipBlock *new_clip = dynamic_cast(block)) { - new_clip->waveform() = static_cast(p.block)->waveform(); + new_clip->AddCachePassthroughFrom(static_cast(p.block)); } } diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp index 9d1ea40ce..da810f321 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.cpp +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -29,24 +29,20 @@ namespace olive { // // BlockSplitCommand // +void BlockSplitCommand::prepare() +{ + reconnect_tree_command_ = new MultiUndoCommand(); + new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_)); +} + void BlockSplitCommand::redo() { old_length_ = block_->length(); Q_ASSERT(point_ > block_->in() && point_ < block_->out()); - if (!reconnect_tree_command_) { - reconnect_tree_command_ = new MultiUndoCommand(); - new_block_ = static_cast(Node::CopyNodeInGraph(block_, reconnect_tree_command_)); - } - reconnect_tree_command_->redo_now(); - if (ClipBlock *new_clip = dynamic_cast(new_block_)) { - ClipBlock *old_clip = static_cast(block_); - new_clip->waveform() = old_clip->waveform(); - } - // Determine our new lengths rational new_length = point_ - block_->in(); rational new_part_length = block_->out() - point_; @@ -61,6 +57,11 @@ void BlockSplitCommand::redo() // Insert new block track->InsertBlockAfter(new_block(), block_); + if (ClipBlock *new_clip = dynamic_cast(new_block_)) { + ClipBlock *old_clip = static_cast(block_); + new_clip->AddCachePassthroughFrom(old_clip); + } + // If the block had an out transition, we move it to the new block moved_transition_ = NodeInput(); diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h index 2cc12ff21..1fe9f9122 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.h +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -54,6 +54,8 @@ public: } protected: + virtual void prepare() override; + virtual void redo() override; virtual void undo() override; diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 2c622fb4d..555d3bc62 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -28,7 +28,6 @@ #include #include "config/config.h" -#include "common/flipmodifiers.h" #include "common/qtutils.h" #include "common/timecodefunctions.h" #include "node/project/footage/footage.h" @@ -47,7 +46,6 @@ TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) : ghosts_(nullptr), show_beam_cursor_(false), connected_track_list_(nullptr), - show_waveforms_(true), transition_overlay_out_(nullptr), transition_overlay_in_(nullptr) { @@ -315,9 +313,9 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) qreal old_opacity = painter->opacity(); painter->setOpacity(0.5); - rational in = ghost->GetAdjustedIn(), out = ghost->GetAdjustedOut(); - DrawBlock(painter, false, attached, track_top, track_height, in, out); - DrawBlock(painter, true, attached, track_top, track_height, in, out); + rational in = ghost->GetAdjustedIn(), out = ghost->GetAdjustedOut(), media_in = ghost->GetAdjustedMediaIn(); + DrawBlock(painter, false, attached, track_top, track_height, in, out, media_in); + DrawBlock(painter, true, attached, track_top, track_height, in, out, media_in); painter->setOpacity(old_opacity); } @@ -466,7 +464,7 @@ void TimelineView::DrawBlocks(QPainter *painter, bool foreground) } } -void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, qreal block_top, qreal block_height, const rational &in, const rational &out) +void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, qreal block_top, qreal block_height, const rational &in, const rational &out, const rational &media_in) { if (dynamic_cast(block) || dynamic_cast(block)) { @@ -486,7 +484,6 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q int text_height = fm.height(); int text_padding = text_height/4; // This ties into the track minimum height being 1.5 int text_total_height = text_height + text_padding + text_padding; - Q_UNUSED(text_total_height) if (foreground) { painter->setBrush(Qt::NoBrush); @@ -521,12 +518,61 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q painter->drawRect(r); if (ClipBlock *clip = dynamic_cast(block)) { + QRect preview_rect = r.toRect(); + + // Draw clip thumbnails + if (clip->GetTrackType() == Track::kVideo + && OLIVE_CONFIG("TimelineThumbnailMode").toInt() != Timeline::kThumbnailOff + && preview_rect.height() > r.height()/3) { + if (const FrameHashCache *thumbs = clip->thumbnails()) { + // Start thumbnails underneath clip name + preview_rect.adjust(0, text_total_height, 0, 0); + + QRect thumb_rect; + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setClipRect(preview_rect); + + if (OLIVE_CONFIG("TimelineThumbnailMode") == Timeline::kThumbnailOn) { + + Sequence *s = clip->track()->sequence(); + int width = s->GetVideoParams().width(); + int height = s->GetVideoParams().height(); + int start; + if (height > 0) { // Prevent divide by zero/invalid params + double scale = double(preview_rect.height())/double(height); + thumb_rect.setWidth(width * scale); + start = (((preview_rect.left() - int(qFloor(block_in))) / thumb_rect.width()) * thumb_rect.width()) + qFloor(block_in); + } else { + start = preview_rect.left(); + } + + for (int i=start; iparent()->GetVideoParams().frame_rate_as_time_base()) + media_in; + DrawThumbnail(painter, thumbs, time_here, i, preview_rect, &thumb_rect); + } + + } else { + + rational time = clip->media_range().in(); + time = Timecode::snap_time_to_timebase(time, thumbs->GetTimebase(), Timecode::kFloor); + DrawThumbnail(painter, thumbs, time, block_left, preview_rect, &thumb_rect); + + } + + painter->setClipping(false); + + } + } + // Draw waveform - if (show_waveforms_) { - QRect waveform_rect = r.toRect(); - painter->setPen(shadow_color); - AudioVisualWaveform::DrawWaveform(painter, waveform_rect, this->GetScale(), clip->waveform(), - SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base())); + if (clip->GetTrackType() == Track::kAudio + && OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled) { + if (const AudioWaveformCache *wave = clip->waveform()) { + rational waveform_start = SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base()) + media_in; + painter->setPen(shadow_color); + + wave->Draw(painter, preview_rect, this->GetScale(), waveform_start); + } } // Draw zebra stripes and markers @@ -594,6 +640,13 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q } } } + + if (const FrameHashCache *cache = clip->connected_video_cache()) { + if (cache->HasValidatedRanges()) { + QRect cache_rect = r.adjusted(0, r.height() - PlaybackCache::GetCacheIndicatorHeight(), 0, 0).toRect(); + cache->Draw(painter, clip->media_in(), GetScale(), cache_rect); + } + } } // For transitions, show lines representing a transition @@ -686,6 +739,20 @@ qreal TimelineView::GetTimelineRightBound() const return GetTimelineLeftBound() + viewport()->width(); } +void TimelineView::DrawThumbnail(QPainter *painter, const FrameHashCache *thumbs, const rational &time, int x, const QRect &preview_rect, QRect *thumb_rect) const +{ + QString thumbnail = thumbs->GetValidCacheFilename(time); + + if (!thumbnail.isEmpty()) { + QImage img; + if (img.load(thumbnail, "jpg")) { + double scale = double(preview_rect.height())/double(img.height()); + *thumb_rect = QRect(x, preview_rect.top(), img.width() * scale, preview_rect.height()); + painter->drawImage(*thumb_rect, img); + } + } +} + int TimelineView::GetTrackY(int track_index) const { if (!connected_track_list_ || !connected_track_list_->GetTrackCount()) { diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 9f9425177..6ce72ca01 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -73,17 +73,6 @@ public: Block* GetItemAtScenePos(const rational& time, int track_index) const; - bool GetShowWaveforms() const - { - return show_waveforms_; - } - - void SetShowWaveforms(bool e) - { - show_waveforms_ = e; - viewport()->update(); - } - signals: void MousePressed(TimelineViewMouseEvent* event); void MouseMoved(TimelineViewMouseEvent* event); @@ -126,10 +115,11 @@ private: void DrawBlocks(QPainter* painter, bool foreground); - void DrawBlock(QPainter *painter, bool foreground, Block *block, qreal top, qreal height, const rational &in, const rational &out); + void DrawBlock(QPainter *painter, bool foreground, Block *block, qreal top, qreal height, const rational &in, const rational &out, const rational &media_in); void DrawBlock(QPainter *painter, bool foreground, Block *block, qreal top, qreal height) { - DrawBlock(painter, foreground, block, top, height, block->in(), block->out()); + ClipBlock *cb = dynamic_cast(block); + DrawBlock(painter, foreground, block, top, height, block->in(), block->out(), cb ? cb->media_in() : 0); } void DrawZebraStripes(QPainter *painter, const QRectF &r); @@ -142,6 +132,8 @@ private: qreal GetTimelineRightBound() const; + void DrawThumbnail(QPainter *painter, const FrameHashCache *thumbs, const rational &time, int x, const QRect &preview_rect, QRect *thumb_rect) const; + QHash* selections_; QVector* ghosts_; @@ -152,8 +144,6 @@ private: TrackList* connected_track_list_; - bool show_waveforms_; - ClipBlock *transition_overlay_out_; ClipBlock *transition_overlay_in_; diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 39d356006..fab83276a 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -46,7 +46,6 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); // Text height is used to calculate widget height - cache_status_height_ = text_height() / 4; // Get the "minimum" space allowed between two line markers on the ruler (in screen pixels) // Mediocre but reliable way of scaling UI objects by font/DPI size @@ -180,7 +179,7 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) int line_bottom = height(); if (show_cache_status_) { - line_bottom -= cache_status_height_; + line_bottom -= PlaybackCache::GetCacheIndicatorHeight(); } int long_height = fm.height(); @@ -252,40 +251,18 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) } // If cache status is enabled - if (show_cache_status_ && playback_cache_) { + if (show_cache_status_ && playback_cache_ && playback_cache_->HasValidatedRanges()) { // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change + int h = PlaybackCache::GetCacheIndicatorHeight(); + QRect cache_rect(0, height() - h, width(), h); + if (ViewerOutput *viewer = dynamic_cast(playback_cache_->parent())) { - rational len = viewer->GetVideoLength(); - int lim_left = GetScroll(); - int lim_right = lim_left + width(); + int right = TimeToScene(viewer->GetVideoLength()); + cache_rect.setWidth(std::max(0, right)); + } - int cache_screen_length = TimeToScene(len); - - if (cache_screen_length > 0) { - int cache_y = height() - cache_status_height_; - - p->fillRect(0, cache_y, cache_screen_length, cache_status_height_, Qt::green); - - foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges(len)) { - int range_left = TimeToScene(range.in()); - if (range_left >= width()) { - continue; - } - - int range_right = TimeToScene(range.out()); - if (range_right < 0) { - continue; - } - - int adjusted_left = qMax(lim_left, range_left); - - p->fillRect(adjusted_left, - cache_y, - qMin(lim_right, range_right) - adjusted_left, - cache_status_height_, - Qt::red); - } - } + if (cache_rect.width() > 0) { + playback_cache_->Draw(p, SceneToTime(GetScroll()), GetScale(), cache_rect); } } @@ -337,7 +314,7 @@ void TimeRuler::UpdateHeight() // Add cache status height if (show_cache_status_) { - height += cache_status_height_; + height += PlaybackCache::GetCacheIndicatorHeight(); } // Add marker height diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index 372e0e602..f23571634 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -53,8 +53,6 @@ private: int CacheStatusHeight() const; - int cache_status_height_; - int minimum_gap_between_lines_; bool text_visible_; diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 073edc0d8..9b5403942 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -47,13 +47,13 @@ AudioWaveformView::AudioWaveformView(QWidget *parent) : setAlignment(Qt::AlignLeft | Qt::AlignTop); } -void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) +void AudioWaveformView::SetViewer(ViewerOutput *playback) { if (playback_) { pool_.clear(); pool_.waitForDone(); - disconnect(playback_, &AudioPlaybackCache::Validated, this, static_cast(&AudioWaveformView::update)); + disconnect(playback_, &ViewerOutput::ConnectedWaveformChanged, this, static_cast(&AudioWaveformView::update)); SetTimebase(0); } @@ -61,9 +61,9 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) playback_ = playback; if (playback_) { - connect(playback_, &AudioPlaybackCache::Validated, this, static_cast(&AudioWaveformView::update)); + connect(playback_, &ViewerOutput::ConnectedWaveformChanged, this, static_cast(&AudioWaveformView::update)); - SetTimebase(playback_->GetParameters().sample_rate_as_time_base()); + SetTimebase(playback_->GetAudioParams().sample_rate_as_time_base()); } } @@ -75,8 +75,12 @@ void AudioWaveformView::drawForeground(QPainter *p, const QRectF &rect) return; } - const AudioParams& params = playback_->GetParameters(); + const AudioWaveformCache *wave = playback_->GetConnectedWaveform(); + if (!wave) { + return; + } + const AudioParams& params = wave->GetParameters(); if (!params.is_valid()) { return; } @@ -87,7 +91,7 @@ void AudioWaveformView::drawForeground(QPainter *p, const QRectF &rect) // Draw waveform p->setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color - AudioVisualWaveform::DrawWaveform(p, rect.toRect(), GetScale(), playback_->visual(), SceneToTime(GetScroll())); + wave->Draw(p, rect.toRect(), GetScale(), SceneToTime(GetScroll())); // Draw playhead p->setPen(PLAYHEAD_COLOR); diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h index 970cb07ad..73162a06a 100644 --- a/app/widget/viewer/audiowaveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -36,7 +36,7 @@ class AudioWaveformView : public SeekableWidget public: AudioWaveformView(QWidget* parent = nullptr); - void SetViewer(AudioPlaybackCache *playback); + void SetViewer(ViewerOutput *playback); protected: virtual void drawForeground(QPainter *painter, const QRectF &rect) override; @@ -44,7 +44,7 @@ protected: private: QThreadPool pool_; - AudioPlaybackCache *playback_; + ViewerOutput *playback_; }; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index fb8fde0da..134874425 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -147,6 +147,9 @@ ViewerWidget::ViewerWidget(QWidget *parent) : instances_.append(this); + auto_cacher_ = new PreviewAutoCacher(this); + connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, auto_cacher_, &PreviewAutoCacher::SetDisplayColorProcessor); + UpdateWaveformViewFromMode(); connect(Core::instance(), &Core::ColorPickerEnabled, this, &ViewerWidget::SetSignalCursorColorEnabled); @@ -193,7 +196,7 @@ void ViewerWidget::TimeChangedEvent(const rational &time) } // Send time to auto-cacher - auto_cacher_.SetPlayhead(time); + auto_cacher_->SetPlayhead(time); last_time_ = time; } @@ -231,7 +234,7 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) UpdateWaveformViewFromMode(); - waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache()); + waveform_view_->SetViewer(GetConnectedNode()); UpdateRendererVideoParameters(); UpdateRendererAudioParameters(); @@ -278,7 +281,7 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n) { - auto_cacher_.SetViewerNode(n); + auto_cacher_->SetViewerNode(n); display_widget_->SetSubtitleTracks(dynamic_cast(n)); } @@ -382,13 +385,13 @@ void ViewerWidget::SetFullScreen(QScreen *screen) void ViewerWidget::CacheEntireSequence() { - auto_cacher_.ForceCacheRange(TimeRange(0, GetConnectedNode()->GetVideoLength())); + auto_cacher_->ForceCacheRange(TimeRange(0, GetConnectedNode()->GetVideoLength())); } void ViewerWidget::CacheSequenceInOut() { if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) { - auto_cacher_.ForceCacheRange(GetConnectedNode()->GetWorkArea()->range()); + auto_cacher_->ForceCacheRange(GetConnectedNode()->GetWorkArea()->range()); } else { QMessageBox::warning(this, tr("Error"), @@ -455,12 +458,7 @@ void ViewerWidget::SetEmptyImage() void ViewerWidget::UpdateAutoCacher() { - auto_cacher_.SetPlayhead(GetTime()); -} - -void ViewerWidget::ClearVideoAutoCacherQueue() -{ - auto_cacher_.CancelVideoTasks(); + auto_cacher_->SetPlayhead(GetTime()); } void ViewerWidget::DecrementPrequeuedAudio() @@ -582,7 +580,7 @@ void ViewerWidget::RequestNextDryRun() } else { RenderTicketWatcher *watcher = new RenderTicketWatcher(this); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::DryRunFinished); - watcher->SetTicket(auto_cacher_.GetSingleFrame(next_time, true)); + watcher->SetTicket(auto_cacher_->GetSingleFrame(next_time, true)); dry_run_next_frame_ += playback_speed_; dry_run_watchers_.append(watcher); } @@ -590,6 +588,11 @@ void ViewerWidget::RequestNextDryRun() } } +void ViewerWidget::SaveFrameAsImage() +{ + Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), GetTime(), true); +} + void ViewerWidget::CloseAudioProcessor() { audio_processor_.Close(); @@ -629,7 +632,7 @@ void ViewerWidget::QueueNextAudioBuffer() RenderTicketWatcher *watcher = new RenderTicketWatcher(this); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForPlayback); audio_playback_queue_.push_back(watcher); - watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end))); + watcher->SetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end))); audio_playback_queue_time_ = queue_end; } @@ -746,7 +749,7 @@ void ViewerWidget::ForceRequeueFromCurrentTime() // Allow half a second for requeue to complete static const rational kRequeueWaitTime(1); - ClearVideoAutoCacherQueue(); + auto_cacher_->ClearSingleFrameRenders(); queue_watchers_.clear(); int queue = DeterminePlaybackQueueSize(); playback_queue_next_frame_ = GetTimestamp() + playback_speed_ * Timecode::time_to_timestamp(kRequeueWaitTime, timebase(), Timecode::kFloor);; @@ -785,9 +788,7 @@ void ViewerWidget::UpdateTextureFromNode() nonqueue_watchers_.append(watcher); // Clear queue because we want this frame more than any others - if (!GetConnectedNode()->video_frame_cache()->IsEnabled() && !auto_cacher_.IsRenderingCustomRange()) { - ClearVideoAutoCacherQueue(); - } + auto_cacher_->ClearSingleFrameRenders(); watcher->SetTicket(GetFrame(time)); } else { @@ -816,10 +817,8 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) foreach (ViewerWidget* viewer, instances_) { if (viewer != this) { viewer->PauseInternal(); - viewer->ClearVideoAutoCacherQueue(); } - - viewer->auto_cacher_.SetAudioPaused(true); + viewer->auto_cacher_->SetRendersPaused(true); } RenderManager::instance()->SetAggressiveGarbageCollection(true); @@ -907,6 +906,7 @@ void ViewerWidget::PauseInternal() qDeleteAll(queue_watchers_); queue_watchers_.clear(); + auto_cacher_->ClearSingleFrameRenders(); playback_backup_timer_.stop(); @@ -920,7 +920,7 @@ void ViewerWidget::PauseInternal() UpdateAudioProcessor(); foreach (ViewerWidget* viewer, instances_) { - viewer->auto_cacher_.SetAudioPaused(false); + viewer->auto_cacher_->SetRendersPaused(false); } UpdateTextureFromNode(); @@ -945,7 +945,7 @@ void ViewerWidget::PushScrubbedAudio() if (ignore_scrub_ == 0) { // Get audio src device from renderer - const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters(); + const AudioParams& params = GetConnectedNode()->GetAudioParams(); if (params.is_valid()) { // NOTE: Hardcoded scrubbing interval (20ms) @@ -954,7 +954,7 @@ void ViewerWidget::PushScrubbedAudio() RenderTicketWatcher *watcher = new RenderTicketWatcher(); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForScrubbing); audio_scrub_watchers_.push_back(watcher); - watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval))); + watcher->SetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval))); } } } @@ -1033,7 +1033,7 @@ RenderTicketPtr ViewerWidget::GetFrame(const rational &t) if (!QFileInfo::exists(cache_fn)) { // Frame hasn't been cached, start render job - return auto_cacher_.GetSingleFrame(t); + return auto_cacher_->GetSingleFrame(t); } else { // Frame has been cached, grab the frame RenderTicketPtr ticket = std::make_shared(); @@ -1061,7 +1061,7 @@ void ViewerWidget::FinishPlayPreprocess() } prequeued_audio_.clear(); - AudioMonitor::StartWaveformOnAll(&GetConnectedNode()->audio_playback_cache()->visual(), + AudioMonitor::StartWaveformOnAll(GetConnectedNode()->GetConnectedWaveform(), GetTime(), playback_speed_); } @@ -1298,6 +1298,7 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) menu.addSeparator(); + /* TEMP: Hide sequence cache options. Want to see if clip caching supersedes it. { Menu* cache_menu = new Menu(tr("Cache"), &menu); menu.addMenu(cache_menu); @@ -1309,7 +1310,7 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) // Cache In/Out Sequence QAction* cache_inout_sequence = cache_menu->addAction(tr("Cache Sequence In/Out")); connect(cache_inout_sequence, &QAction::triggered, this, &ViewerWidget::CacheSequenceInOut); - } + }*/ menu.addSeparator(); @@ -1389,6 +1390,11 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) }); } + menu.addSeparator(); + + auto save_frame_as_image = menu.addAction(tr("Save Frame As Image")); + connect(save_frame_as_image, &QAction::triggered, this, &ViewerWidget::SaveFrameAsImage); + menu.exec(static_cast(sender())->mapToGlobal(pos)); } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index a6c2fec22..4c3a18551 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -226,8 +226,6 @@ private: void UpdateAutoCacher(); - void ClearVideoAutoCacherQueue(); - void DecrementPrequeuedAudio(); void ArmForRecording(); @@ -274,7 +272,7 @@ private: int prequeue_length_; int prequeue_count_; - PreviewAutoCacher auto_cacher_; + PreviewAutoCacher *auto_cacher_; QVector queue_watchers_; @@ -370,6 +368,8 @@ private slots: void RequestNextDryRun(); + void SaveFrameAsImage(); + }; } diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 8999385ae..70bbc51f9 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -34,6 +34,7 @@ #include #include +#include "common/define.h" #include "common/html.h" #include "common/qtutils.h" #include "config/config.h" diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index fd8123371..22ae0e70d 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -183,6 +183,10 @@ MainMenu::MainMenu(MainWindow *parent) : sequence_disk_cache_clear_item_ = sequence_menu_->AddItem("seqcacheclear", this, &MainMenu::SequenceCacheClearTriggered); + // TEMP: Hide sequence cache items for now. Want to see if clip caching will supersede it. + sequence_cache_item_->setVisible(false); + sequence_cache_in_to_out_item_->setVisible(false); + // // WINDOW MENU // diff --git a/tests/general/timerange-tests.cpp b/tests/general/timerange-tests.cpp index 6ddfe14e1..e20b10112 100644 --- a/tests/general/timerange-tests.cpp +++ b/tests/general/timerange-tests.cpp @@ -86,13 +86,13 @@ OLIVE_ADD_TEST(TimeRangeListFrameIteratorSize) ranges.insert(TimeRange(rational(1402, 20), rational(1403, 20))); // 1 ranges.insert(TimeRange(rational(10001, 40), rational(10002, 40))); // 0 ranges.insert(TimeRange(rational(10001, 40), rational(10004, 40))); // 0 - ranges.insert(TimeRange(rational(10001, 40), rational(10005, 40))); // 1 + ranges.insert(TimeRange(rational(10001, 40), rational(10005, 40))); // 2 TimeRangeListFrameIterator iterator(ranges, timebase); QVector vec = iterator.ToVector(); - OLIVE_ASSERT_EQUAL(vec.size(), 253); + OLIVE_ASSERT_EQUAL(vec.size(), 254); OLIVE_ASSERT_EQUAL(iterator.size(), vec.size()); TimeRangeListFrameIterator empty(TimeRangeList(), timebase);