diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e86e50aba..40381ae35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,12 +285,14 @@ jobs: os-arch: x86_64 os: macos-10.15 cmake-gen: Ninja + min-deploy: 10.13 - build-type: RelWithDebInfo compiler-name: Clang LLVM os-name: macOS os-arch: arm64 os: macos-11.0 cmake-gen: Ninja + min-deploy: 11.0 env: DEP_LOCATION: /opt/olive-editor name: | @@ -349,7 +351,7 @@ jobs: brew install ninja PATH=$DEP_LOCATION:$DEP_LOCATION/bin:$DEP_LOCATION/include:$DEP_LOCATION/lib:$DEP_LOCATION/crashpad:$PATH \ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 -G "${{ matrix.cmake-gen }}" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ matrix.min-deploy }} -G "${{ matrix.cmake-gen }}" \ -DCMAKE_OSX_ARCHITECTURES="${{ matrix.os-arch }}" - name: Build @@ -367,10 +369,9 @@ jobs: - name: Create Package working-directory: ${{ runner.workspace }}/build - env: - BUNDLE_NAME: "Olive.app" shell: bash run: | + BUNDLE_NAME="Olive.app" brew install dylibbundler if [ "${{ matrix.os-arch }}" == "x86_64" ] @@ -383,12 +384,12 @@ jobs: dylibbundler -b -ns -x "$BUNDLE_NAME/Contents/MacOS/Olive" -s "$DEP_LOCATION/lib" -d "$BUNDLE_NAME/Contents/Frameworks" -p "@executable_path/../Frameworks" $DYLIBBUNDLER_EXTRA_ARGS # Copy Qt frameworks and plugins - cp -R $DEP_LOCATION/lib/Qt*.framework $BUNDLE_NAME/Contents/Frameworks - cp -R $DEP_LOCATION/plugins $BUNDLE_NAME/Contents + cp -Ra $DEP_LOCATION/lib/Qt*.framework $BUNDLE_NAME/Contents/Frameworks + cp -Ra $DEP_LOCATION/plugins $BUNDLE_NAME/Contents # HACK: On x86_64, dylibbundler doesn't resolve this symlink. Weirdly it does on ARM64, # but perhaps I'll bring it up with them soon. - cp $BUNDLE_NAME/Contents/Frameworks/libpng16.16.37.0.dylib $BUNDLE_NAME/Contents/Frameworks/libpng16.16.dylib + cp -a $BUNDLE_NAME/Contents/Frameworks/libpng16.16.37.0.dylib $BUNDLE_NAME/Contents/Frameworks/libpng16.16.dylib if [ "${{ matrix.os-arch }}" == "x86_64" ] then @@ -400,6 +401,42 @@ jobs: mv Olive.sym "$SYM_DIR" fi + - name: Sign Application + working-directory: ${{ runner.workspace }}/build + shell: bash + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + BUNDLE_NAME="Olive.app" + + # Install certificate + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # HACK: Remove unsignable frameworks + rm -r $BUNDLE_NAME/Contents/Frameworks/QtUiPlugin.framework + if [ "${{ matrix.os-arch }}" == "arm64" ] + then + rm -r $BUNDLE_NAME/Contents/Frameworks/QtZlib.framework + fi + + # Sign application + codesign --deep --sign "Developer ID Application: Olive Studios LLC" $BUNDLE_NAME + - name: Deploy Packages working-directory: ${{ runner.workspace }}/build shell: bash diff --git a/app/audio/CMakeLists.txt b/app/audio/CMakeLists.txt index 6efe27768..5d190af1f 100644 --- a/app/audio/CMakeLists.txt +++ b/app/audio/CMakeLists.txt @@ -22,6 +22,8 @@ set(OLIVE_SOURCES audio/audiovisualwaveform.h audio/packedprocessor.cpp audio/packedprocessor.h + audio/planarprocessor.cpp + audio/planarprocessor.h audio/tempoprocessor.cpp audio/tempoprocessor.h PARENT_SCOPE diff --git a/app/audio/packedprocessor.cpp b/app/audio/packedprocessor.cpp index 10eb9ea18..f748da5c2 100644 --- a/app/audio/packedprocessor.cpp +++ b/app/audio/packedprocessor.cpp @@ -76,17 +76,12 @@ QByteArray PackedProcessor::Convert(SampleBufferPtr planar) return QByteArray(); } - int nb_channels = planar->audio_params().channel_count(); - QByteArray output(planar->audio_params().samples_to_bytes(nb_samples), Qt::Uninitialized); uint8_t *output_data = reinterpret_cast(output.data()); - QVector input_arrays(nb_channels); - for (int i=0; i(planar->data(i)); - } - - int ret = swr_convert(swr_ctx_, &output_data, nb_samples, input_arrays.data(), nb_samples); + int ret = swr_convert(swr_ctx_, &output_data, nb_samples, + const_cast(reinterpret_cast(planar->to_raw_ptrs())), + nb_samples); if (ret < 0) { char buf[200]; av_strerror(ret, buf, 200); diff --git a/app/audio/planarprocessor.cpp b/app/audio/planarprocessor.cpp new file mode 100644 index 000000000..abd2be85b --- /dev/null +++ b/app/audio/planarprocessor.cpp @@ -0,0 +1,104 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "planarprocessor.h" + +#include "common/ffmpegutils.h" + +namespace olive { + +PlanarProcessor::PlanarProcessor() : + swr_ctx_(nullptr) +{ +} + +PlanarProcessor::~PlanarProcessor() +{ + Close(); +} + +bool PlanarProcessor::Open(const AudioParams ¶ms) +{ + if (IsOpen()) { + return true; + } + + swr_ctx_ = swr_alloc_set_opts(nullptr, + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), true), + params.sample_rate(), + params.channel_layout(), + FFmpegUtils::GetFFmpegSampleFormat(params.format(), false), + params.sample_rate(), + 0, + nullptr); + + if (!swr_ctx_) { + qCritical() << "Failed to allocate resample context"; + return false; + } + + if (swr_init(swr_ctx_) < 0) { + qCritical() << "Failed to init resample context"; + swr_free(&swr_ctx_); + return false; + } + + params_ = params; + + return true; +} + +SampleBufferPtr PlanarProcessor::Convert(const QByteArray &packed) +{ + if (!IsOpen()) { + qCritical() << "Tried to convert while closed"; + return nullptr; + } + + if (packed.isEmpty()) { + return nullptr; + } + + int nb_samples_per_channel = params_.bytes_to_samples(packed.size()); + + SampleBufferPtr output = SampleBuffer::CreateAllocated(params_, nb_samples_per_channel); + + const uint8_t *input = reinterpret_cast(packed.constData()); + int ret = swr_convert(swr_ctx_, + reinterpret_cast(output->to_raw_ptrs()), nb_samples_per_channel, + &input, nb_samples_per_channel); + if (ret < 0) { + char buf[200]; + av_strerror(ret, buf, 200); + qDebug() << "Planar processor failed with error:" << buf << ret; + } + + return output; +} + +void PlanarProcessor::Close() +{ + if (swr_ctx_) { + swr_free(&swr_ctx_); + } +} + +} diff --git a/app/audio/planarprocessor.h b/app/audio/planarprocessor.h new file mode 100644 index 000000000..ad5167a95 --- /dev/null +++ b/app/audio/planarprocessor.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 PLANARPROCESSOR_H +#define PLANARPROCESSOR_H + +extern "C" { +#include +} + +#include "codec/samplebuffer.h" +#include "render/audioparams.h" + +namespace olive { + +class PlanarProcessor +{ +public: + PlanarProcessor(); + + ~PlanarProcessor(); + + DISABLE_COPY_MOVE(PlanarProcessor) + + bool Open(const AudioParams ¶ms); + + SampleBufferPtr Convert(const QByteArray &packed); + + void Close(); + + bool IsOpen() const + { + return swr_ctx_; + } + +private: + SwrContext *swr_ctx_; + + AudioParams params_; + +}; + +} + +#endif // PLANARPROCESSOR_H diff --git a/app/audio/tempoprocessor.cpp b/app/audio/tempoprocessor.cpp index 0a0aa777e..f22946227 100644 --- a/app/audio/tempoprocessor.cpp +++ b/app/audio/tempoprocessor.cpp @@ -36,7 +36,6 @@ TempoProcessor::TempoProcessor() : filter_graph_(nullptr), buffersrc_ctx_(nullptr), buffersink_ctx_(nullptr), - processed_frame_(nullptr), open_(false) { } @@ -150,47 +149,42 @@ bool TempoProcessor::Open(const AudioParams ¶ms, const double& speed) return true; } -void TempoProcessor::Push(const char *data, int length) +void TempoProcessor::Push(const QByteArray &packed) { - if (flushed_) { - if (length > 0) { - qCritical() << "Tried to push" << length << "bytes after TempoProcessor was closed"; - } + if (!IsOpen()) { + qWarning() << "Tried to push to closed TempoProcessor"; return; } - AVFrame* src_frame; - - if (length == 0) { - // No audio data, flush the last out of the filter graph - src_frame = nullptr; - flushed_ = true; - } else { - src_frame = av_frame_alloc(); - - if (!src_frame) { - qCritical() << "Failed to allocate source frame"; - return; - } - - // Allocate a buffer for the number of samples we got - src_frame->sample_rate = params_.sample_rate(); - src_frame->format = FFmpegUtils::GetFFmpegSampleFormat(params_.format()); - src_frame->channel_layout = params_.channel_layout(); - src_frame->nb_samples = params_.bytes_to_samples(length); - src_frame->pts = timestamp_; - timestamp_ += src_frame->nb_samples; - - if (av_frame_get_buffer(src_frame, 0) < 0) { - qCritical() << "Failed to allocate buffer for source frame"; - av_frame_free(&src_frame); - return; - } - - // Copy buffer from data array to frame - memcpy(src_frame->data[0], data, static_cast(length)); + if (flushed_) { + qWarning() << "Tried to push to flushed TempoProcessor"; + return; } + AVFrame* src_frame = av_frame_alloc(); + + if (!src_frame) { + qCritical() << "Failed to allocate source frame"; + return; + } + + // Allocate a buffer for the number of samples we got + src_frame->sample_rate = params_.sample_rate(); + src_frame->format = FFmpegUtils::GetFFmpegSampleFormat(params_.format()); + src_frame->channel_layout = params_.channel_layout(); + src_frame->nb_samples = params_.bytes_to_samples(packed.size()); + src_frame->pts = timestamp_; + timestamp_ += src_frame->nb_samples; + + if (av_frame_get_buffer(src_frame, 0) < 0) { + qCritical() << "Failed to allocate buffer for source frame"; + av_frame_free(&src_frame); + return; + } + + // Copy buffer from data array to frame + memcpy(src_frame->data[0], packed, packed.size()); + int ret = av_buffersrc_add_frame_flags(buffersrc_ctx_, src_frame, AV_BUFFERSRC_FLAG_KEEP_REF); if (ret < 0) { @@ -202,46 +196,45 @@ void TempoProcessor::Push(const char *data, int length) } } -int TempoProcessor::Pull(char *data, int max_length) +void TempoProcessor::Flush() { - if (!processed_frame_) { - processed_frame_ = av_frame_alloc(); - - // Try to pull samples from the buffersink - int ret = av_buffersink_get_frame(buffersink_ctx_, processed_frame_); - + if (!flushed_) { + int ret = av_buffersrc_add_frame_flags(buffersrc_ctx_, nullptr, AV_BUFFERSRC_FLAG_KEEP_REF); if (ret < 0) { - // We couldn't pull for some reason, if the error was EAGAIN, we just need to send more samples. Otherwise the - // error might be fatal... - if (ret != AVERROR(EAGAIN)) { - qCritical() << "Failed to pull from buffersink" << ret; - } + qCritical() << "Failed to feed buffer source" << ret; + } + flushed_ = true; + } +} - av_frame_free(&processed_frame_); +QByteArray TempoProcessor::Pull() +{ + QByteArray b; + AVFrame *processed_frame = av_frame_alloc(); - return 0; + // Try to pull samples from the buffersink + int ret = av_buffersink_get_frame(buffersink_ctx_, processed_frame); + + if (ret < 0) { + // We couldn't pull for some reason, if the error was EAGAIN, we just need to send more samples. Otherwise the + // error might be fatal... + if (ret != AVERROR(EAGAIN)) { + qCritical() << "Failed to pull from buffersink" << ret; } - processed_frame_byte_index_ = 0; - processed_frame_max_bytes_ = params_.samples_to_bytes(processed_frame_->nb_samples); + av_frame_free(&processed_frame); + return b; } - // Determine how many bytes we should copy into the data array - int copy_length = qMin(max_length, processed_frame_max_bytes_ - processed_frame_byte_index_); + b.resize(params_.samples_to_bytes(processed_frame->nb_samples)); // Copy the bytes - memcpy(data, processed_frame_->data[0] + processed_frame_byte_index_, static_cast(copy_length)); - - // Add the copied amount to the current index - processed_frame_byte_index_ += copy_length; + memcpy(b.data(), processed_frame->data[0], b.size()); // If the index has reached the limit of this processed frame, we can dispose of the frame now - if (processed_frame_byte_index_ == processed_frame_max_bytes_) { - av_frame_free(&processed_frame_); - processed_frame_ = nullptr; - } + av_frame_free(&processed_frame); - return copy_length; + return b; } void TempoProcessor::Close() @@ -253,11 +246,6 @@ void TempoProcessor::Close() filter_graph_ = nullptr; } - if (processed_frame_) { - av_frame_free(&processed_frame_); - processed_frame_ = nullptr; - } - buffersrc_ctx_ = nullptr; buffersink_ctx_ = nullptr; } diff --git a/app/audio/tempoprocessor.h b/app/audio/tempoprocessor.h index bac242751..c56eafc5d 100644 --- a/app/audio/tempoprocessor.h +++ b/app/audio/tempoprocessor.h @@ -52,9 +52,11 @@ public: bool Open(const AudioParams& params, const double &speed); - void Push(const char *data, int length); + void Push(const QByteArray &packed); - int Pull(char* data, int max_length); + void Flush(); + + QByteArray Pull(); void Close(); @@ -67,10 +69,6 @@ private: AVFilterContext* buffersink_ctx_; - AVFrame* processed_frame_; - int processed_frame_byte_index_; - int processed_frame_max_bytes_; - AudioParams params_; int64_t timestamp_; diff --git a/app/codec/exportcodec.cpp b/app/codec/exportcodec.cpp index 947f9da63..254a9989c 100644 --- a/app/codec/exportcodec.cpp +++ b/app/codec/exportcodec.cpp @@ -44,6 +44,8 @@ QString ExportCodec::GetCodecName(ExportCodec::Codec c) return tr("PNG"); case kCodecProRes: return tr("ProRes"); + case kCodecCineform: + return tr("Cineform"); case kCodecTIFF: return tr("TIFF"); case kCodecMP2: @@ -79,6 +81,7 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) case kCodecH264rgb: case kCodecH265: case kCodecProRes: + case kCodecCineform: case kCodecMP2: case kCodecMP3: case kCodecAAC: diff --git a/app/codec/exportcodec.h b/app/codec/exportcodec.h index 1b5d1df47..f4d17dfa0 100644 --- a/app/codec/exportcodec.h +++ b/app/codec/exportcodec.h @@ -42,6 +42,7 @@ public: kCodecOpenEXR, kCodecPNG, kCodecProRes, + kCodecCineform, kCodecTIFF, kCodecVP9, diff --git a/app/codec/exportformat.cpp b/app/codec/exportformat.cpp index e89110142..59561d50b 100644 --- a/app/codec/exportformat.cpp +++ b/app/codec/exportformat.cpp @@ -117,7 +117,7 @@ QList ExportFormat::GetVideoCodecs(ExportFormat::Format f) case kFormatTIFF: return {ExportCodec::kCodecTIFF}; case kFormatQuickTime: - return {ExportCodec::kCodecH264, ExportCodec::kCodecH264rgb, ExportCodec::kCodecH265, ExportCodec::kCodecProRes}; + return {ExportCodec::kCodecH264, ExportCodec::kCodecH264rgb, ExportCodec::kCodecH265, ExportCodec::kCodecProRes, ExportCodec::kCodecCineform}; case kFormatWebM: return {ExportCodec::kCodecVP9}; case kFormatOgg: diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index fba94088d..9b2a0da1b 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -268,7 +268,7 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn AVStream* avstream = fmt_ctx->streams[i]; // Find decoder for this stream, if it exists we can proceed - AVCodec* decoder = avcodec_find_decoder(avstream->codecpar->codec_id); + const AVCodec* decoder = avcodec_find_decoder(avstream->codecpar->codec_id); if (decoder && (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO @@ -1010,7 +1010,7 @@ bool FFmpegDecoder::Instance::Open(const char *filename, int stream_index) avstream_ = fmt_ctx_->streams[stream_index]; // Find decoder - AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id); + const AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id); // Handle failure to find decoder if (codec == nullptr) { diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index a6ee5c68c..c1ea0927a 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -50,7 +50,7 @@ QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const { QStringList pix_fmts; - AVCodec* codec_info = GetEncoder(c); + const AVCodec* codec_info = GetEncoder(c); if (codec_info) { for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { @@ -570,7 +570,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV } // Find encoder - AVCodec* encoder = GetEncoder(codec); + const AVCodec* encoder = GetEncoder(codec); if (!encoder) { SetError(tr("Failed to find codec for 0x%1").arg(codec, 16)); return false; @@ -669,7 +669,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV return true; } -bool FFmpegEncoder::InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx, AVCodec* codec) +bool FFmpegEncoder::InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx, const AVCodec* codec) { *stream = avformat_new_stream(fmt_ctx_, nullptr); if (!(*stream)) { @@ -687,7 +687,7 @@ bool FFmpegEncoder::InitializeCodecContext(AVStream **stream, AVCodecContext **c return true; } -bool FFmpegEncoder::SetupCodecContext(AVStream* stream, AVCodecContext* codec_ctx, AVCodec* codec) +bool FFmpegEncoder::SetupCodecContext(AVStream* stream, AVCodecContext* codec_ctx, const AVCodec* codec) { int error_code; @@ -833,7 +833,7 @@ bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) return true; } -AVCodec *FFmpegEncoder::GetEncoder(ExportCodec::Codec c) +const AVCodec *FFmpegEncoder::GetEncoder(ExportCodec::Codec c) { switch (c) { case ExportCodec::kCodecH264: @@ -844,6 +844,8 @@ AVCodec *FFmpegEncoder::GetEncoder(ExportCodec::Codec c) return avcodec_find_encoder(AV_CODEC_ID_DNXHD); case ExportCodec::kCodecProRes: return avcodec_find_encoder(AV_CODEC_ID_PRORES); + case ExportCodec::kCodecCineform: + return avcodec_find_encoder(AV_CODEC_ID_CFHD); case ExportCodec::kCodecH265: return avcodec_find_encoder(AV_CODEC_ID_HEVC); case ExportCodec::kCodecVP9: diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 1d643f4d3..d75d3f4ea 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -22,6 +22,7 @@ #define FFMPEGENCODER_H extern "C" { +#include #include #include #include @@ -69,15 +70,15 @@ private: bool WriteAVFrame(AVFrame* frame, AVCodecContext *codec_ctx, AVStream *stream); bool InitializeStream(enum AVMediaType type, AVStream** stream, AVCodecContext** codec_ctx, const ExportCodec::Codec &codec); - bool InitializeCodecContext(AVStream** stream, AVCodecContext** codec_ctx, AVCodec* codec); - bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx, AVCodec *codec); + bool InitializeCodecContext(AVStream** stream, AVCodecContext** codec_ctx, const AVCodec* codec); + bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx, const AVCodec *codec); void FlushEncoders(); void FlushCodecCtx(AVCodecContext* codec_ctx, AVStream *stream); bool InitializeResampleContext(SampleBufferPtr audio); - static AVCodec *GetEncoder(ExportCodec::Codec c); + static const AVCodec *GetEncoder(ExportCodec::Codec c); AVFormatContext* fmt_ctx_; diff --git a/app/common/ffmpegutils.h b/app/common/ffmpegutils.h index 3e5ff105d..2262dc299 100644 --- a/app/common/ffmpegutils.h +++ b/app/common/ffmpegutils.h @@ -22,6 +22,7 @@ #define FFMPEGABSTRACTION_H extern "C" { +#include #include } diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index eedf4d0f4..b80f8b237 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -157,10 +157,12 @@ int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational // Convert values to integers QList timecode_numbers; + bool negative = timecode.trimmed().startsWith('-'); + foreach (const QString& element, timecode_split) { valid = true; - timecode_numbers.append((element.isEmpty()) ? 0 : element.toLong(&valid)); + timecode_numbers.append((element.isEmpty()) ? 0 : qAbs(element.toLong(&valid))); // If element cannot be converted to a number, if (!valid) { @@ -203,6 +205,9 @@ int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational } if (ok) *ok = true; + + if (negative) timestamp = -timestamp; + return timestamp; } case kMilliseconds: diff --git a/app/dialog/export/codec/CMakeLists.txt b/app/dialog/export/codec/CMakeLists.txt index 7f4e2b5c8..1669edd70 100644 --- a/app/dialog/export/codec/CMakeLists.txt +++ b/app/dialog/export/codec/CMakeLists.txt @@ -16,11 +16,15 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - dialog/export/codec/codecsection.h + dialog/export/codec/cineformsection.cpp + dialog/export/codec/cineformsection.h dialog/export/codec/codecsection.cpp - dialog/export/codec/h264section.h + dialog/export/codec/codecsection.h + dialog/export/codec/codecstack.cpp + dialog/export/codec/codecstack.h dialog/export/codec/h264section.cpp - dialog/export/codec/imagesection.h + dialog/export/codec/h264section.h dialog/export/codec/imagesection.cpp + dialog/export/codec/imagesection.h PARENT_SCOPE ) diff --git a/app/dialog/export/codec/cineformsection.cpp b/app/dialog/export/codec/cineformsection.cpp new file mode 100644 index 000000000..dedecb32e --- /dev/null +++ b/app/dialog/export/codec/cineformsection.cpp @@ -0,0 +1,85 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "cineformsection.h" + +#include +#include + +namespace olive { + +CineformSection::CineformSection(QWidget *parent) : + CodecSection(parent) +{ + QGridLayout *layout = new QGridLayout(this); + + layout->setMargin(0); + + int row = 0; + + layout->addWidget(new QLabel(tr("Quality:")), row, 0); + + quality_combobox_ = new QComboBox(); + + /* Correspond to the following indexes for FFmpeg + * + * -quality E..V....... set quality (from 0 to 12) (default film3+) + * film3+ 0 E..V....... + * film3 1 E..V....... + * film2+ 2 E..V....... + * film2 3 E..V....... + * film1.5 4 E..V....... + * film1+ 5 E..V....... + * film1 6 E..V....... + * high+ 7 E..V....... + * high 8 E..V....... + * medium+ 9 E..V....... + * medium 10 E..V....... + * low+ 11 E..V....... + * low 12 E..V....... + * + */ + + quality_combobox_->addItem(tr("Film Scan 3+")); + quality_combobox_->addItem(tr("Film Scan 3")); + quality_combobox_->addItem(tr("Film Scan 2+")); + quality_combobox_->addItem(tr("Film Scan 2")); + quality_combobox_->addItem(tr("Film Scan 1.5")); + quality_combobox_->addItem(tr("Film Scan 1+")); + quality_combobox_->addItem(tr("Film Scan 1")); + quality_combobox_->addItem(tr("High+")); + quality_combobox_->addItem(tr("High")); + quality_combobox_->addItem(tr("Medium+")); + quality_combobox_->addItem(tr("Medium")); + quality_combobox_->addItem(tr("Low+")); + quality_combobox_->addItem(tr("Low")); + + // Default to "medium" + quality_combobox_->setCurrentIndex(10); + + layout->addWidget(quality_combobox_, row, 1); +} + +void CineformSection::AddOpts(EncodingParams *params) +{ + params->set_video_option(QStringLiteral("quality"), QString::number(quality_combobox_->currentIndex())); +} + +} diff --git a/app/dialog/export/codec/cineformsection.h b/app/dialog/export/codec/cineformsection.h new file mode 100644 index 000000000..6dc09f2d0 --- /dev/null +++ b/app/dialog/export/codec/cineformsection.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 CINEFORMSECTION_H +#define CINEFORMSECTION_H + +#include + +#include "codecsection.h" + +namespace olive { + +class CineformSection : public CodecSection +{ + Q_OBJECT +public: + CineformSection(QWidget *parent = nullptr); + + virtual void AddOpts(EncodingParams* params) override; + +private: + QComboBox *quality_combobox_; + +}; + +} + +#endif // CINEFORMSECTION_H diff --git a/app/dialog/export/codec/codecstack.cpp b/app/dialog/export/codec/codecstack.cpp new file mode 100644 index 000000000..cdffb12cd --- /dev/null +++ b/app/dialog/export/codec/codecstack.cpp @@ -0,0 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "codecstack.h" + +namespace olive { + +#define super QStackedWidget + +CodecStack::CodecStack(QWidget *parent) + : super{parent} +{ + connect(this, &CodecStack::currentChanged, this, &CodecStack::OnChange); +} + +void CodecStack::addWidget(QWidget *widget) +{ + super::addWidget(widget); + + OnChange(currentIndex()); +} + +void CodecStack::OnChange(int index) +{ + for (int i=0; isetSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } else { + widget(i)->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + } + widget(i)->adjustSize(); + } + adjustSize(); +} + +} diff --git a/app/dialog/export/codec/codecstack.h b/app/dialog/export/codec/codecstack.h new file mode 100644 index 000000000..116f8754c --- /dev/null +++ b/app/dialog/export/codec/codecstack.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 CODECSTACK_H +#define CODECSTACK_H + +#include + +namespace olive { + +class CodecStack : public QStackedWidget +{ + Q_OBJECT +public: + explicit CodecStack(QWidget *parent = nullptr); + + void addWidget(QWidget *widget); + +signals: + +private slots: + void OnChange(int index); + +}; + +} + +#endif // CODECSTACK_H diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp index 101bf388f..6b15be526 100644 --- a/app/dialog/export/codec/h264section.cpp +++ b/app/dialog/export/codec/h264section.cpp @@ -42,6 +42,31 @@ H264Section::H264Section(int default_crf, QWidget *parent) : layout->setMargin(0); int row = 0; + layout->addWidget(new QLabel(tr("Encode Speed:")), row, 0); + + + preset_combobox_ = new QComboBox(); + preset_combobox_->setToolTip(tr("This setting allows you to tweak the ratio of export speed to compression quality. \n\n" + "If using Constant Rate Factor, slower speeds will result in smaller file sizes for the same quality. \n\n" + "If using Target Bit Rate or Target File Size, slower speeds will result in higher quality for the same bitrate/filesize. \n\n" + "This setting is equivalent to the `preset` setting in libx264.")); + + preset_combobox_->addItem(tr("Ultra Fast")); + preset_combobox_->addItem(tr("Super Fast")); + preset_combobox_->addItem(tr("Very Fast")); + preset_combobox_->addItem(tr("Faster")); + preset_combobox_->addItem(tr("Fast")); + preset_combobox_->addItem(tr("Medium")); + preset_combobox_->addItem(tr("Slow")); + preset_combobox_->addItem(tr("Slower")); + preset_combobox_->addItem(tr("Very Slow")); + + //Default to "medium" + preset_combobox_->setCurrentIndex(5); + + layout->addWidget(preset_combobox_, row, 1); + + row++; layout->addWidget(new QLabel(tr("Compression Method:")), row, 0); @@ -110,6 +135,8 @@ void H264Section::AddOpts(EncodingParams *params) params->set_video_buffer_size(2000000); } + + params->set_video_option(QStringLiteral("preset"), QString::number(preset_combobox_->currentIndex())); } H264CRFSection::H264CRFSection(int default_crf, QWidget *parent) : diff --git a/app/dialog/export/codec/h264section.h b/app/dialog/export/codec/h264section.h index 95f7fa84a..78fbef19a 100644 --- a/app/dialog/export/codec/h264section.h +++ b/app/dialog/export/codec/h264section.h @@ -23,6 +23,7 @@ #include #include +#include #include "codecsection.h" #include "widget/slider/floatslider.h" @@ -111,6 +112,7 @@ private: H264FileSizeSection* filesize_section_; + QComboBox *preset_combobox_; }; class H265Section : public H264Section diff --git a/app/dialog/export/exportadvancedvideodialog.cpp b/app/dialog/export/exportadvancedvideodialog.cpp index 9e38969fb..71143b50b 100644 --- a/app/dialog/export/exportadvancedvideodialog.cpp +++ b/app/dialog/export/exportadvancedvideodialog.cpp @@ -48,6 +48,7 @@ ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(const QList &pix_f thread_slider_ = new IntegerSlider(); thread_slider_->SetMinimum(0); thread_slider_->SetDefaultValue(0); + thread_slider_->InsertLabelSubstitution(0, tr("Auto")); performance_layout->addWidget(thread_slider_, row, 1); row++; diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index f8908cea3..d99cc59b0 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -173,7 +173,7 @@ QWidget *ExportVideoTab::SetupCodecSection() row++; - codec_stack_ = new QStackedWidget(); + codec_stack_ = new CodecStack(); codec_layout->addWidget(codec_stack_, row, 0, 1, 2); image_section_ = new ImageSection(); @@ -185,6 +185,9 @@ QWidget *ExportVideoTab::SetupCodecSection() h265_section_ = new H265Section(); codec_stack_->addWidget(h265_section_); + cineform_section_ = new CineformSection(); + codec_stack_->addWidget(cineform_section_); + row++; QPushButton* advanced_btn = new QPushButton(tr("Advanced")); @@ -240,6 +243,9 @@ void ExportVideoTab::VideoCodecChanged() case ExportCodec::kCodecH265: SetCodecSection(h265_section_); break; + case ExportCodec::kCodecCineform: + SetCodecSection(cineform_section_); + break; default: SetCodecSection(ExportCodec::IsCodecAStillImage(codec) ? image_section_ : nullptr); } @@ -251,7 +257,6 @@ void ExportVideoTab::VideoCodecChanged() } else { pix_fmt_.clear(); } - qDebug() << "Set default pix fmt" << pix_fmt_; } void ExportVideoTab::SetTime(const rational &time) diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index ce74d5a0a..4a952fb15 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -26,6 +26,8 @@ #include #include "common/rational.h" +#include "dialog/export/codec/cineformsection.h" +#include "dialog/export/codec/codecstack.h" #include "dialog/export/codec/h264section.h" #include "dialog/export/codec/imagesection.h" #include "node/color/colormanager/colormanager.h" @@ -155,10 +157,11 @@ private: QCheckBox* maintain_aspect_checkbox_; QComboBox* scaling_method_combobox_; - QStackedWidget* codec_stack_; + CodecStack* codec_stack_; ImageSection* image_section_; H264Section* h264_section_; H264Section* h265_section_; + CineformSection *cineform_section_; ColorSpaceChooser* color_space_chooser_; diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index a52258fa0..c3e471b9c 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -31,6 +31,7 @@ #include "common/filefunctions.h" #include "config/config.h" +#include "render/audioparams.h" #include "render/videoparams.h" #include "ui/icons/icons.h" #include "widget/menu/menu.h" diff --git a/app/dialog/speedduration/speeddurationdialog.cpp b/app/dialog/speedduration/speeddurationdialog.cpp index f5308e8c3..cf6af022a 100644 --- a/app/dialog/speedduration/speeddurationdialog.cpp +++ b/app/dialog/speedduration/speeddurationdialog.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include "core.h" @@ -39,58 +40,82 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons { setWindowTitle(tr("Speed/Duration")); - QGridLayout *layout = new QGridLayout(this); + QVBoxLayout *layout = new QVBoxLayout(this); - int row = 0; + { + QGroupBox *speed_group = new QGroupBox(); + layout->addWidget(speed_group); - layout->addWidget(new QLabel(tr("Speed:")), row, 0); + QGridLayout *speed_layout = new QGridLayout(speed_group); - speed_slider_ = new FloatSlider(); - speed_slider_->SetDisplayType(FloatSlider::kPercentage); - connect(speed_slider_, &FloatSlider::ValueChanged, this, &SpeedDurationDialog::SpeedChanged); - layout->addWidget(speed_slider_, row, 1); + int row = 0; - row++; + speed_layout->addWidget(new QLabel(tr("Speed:")), row, 0); - layout->addWidget(new QLabel(tr("Duration:")), row, 0); + speed_slider_ = new FloatSlider(); + speed_slider_->SetDisplayType(FloatSlider::kPercentage); + connect(speed_slider_, &FloatSlider::ValueChanged, this, &SpeedDurationDialog::SpeedChanged); + speed_layout->addWidget(speed_slider_, row, 1); - dur_slider_ = new RationalSlider(); - dur_slider_->SetTimebase(timebase); - dur_slider_->SetDisplayType(RationalSlider::kTime); - connect(dur_slider_, &RationalSlider::ValueChanged, this, &SpeedDurationDialog::DurationChanged); - layout->addWidget(dur_slider_, row, 1); + row++; - row++; + speed_layout->addWidget(new QLabel(tr("Duration:")), row, 0); - link_box_ = new QCheckBox(tr("Link Speed and Duration")); - link_box_->setChecked(true); - layout->addWidget(link_box_, row, 0, 1, 2); + dur_slider_ = new RationalSlider(); + dur_slider_->SetTimebase(timebase); + dur_slider_->SetDisplayType(RationalSlider::kTime); + connect(dur_slider_, &RationalSlider::ValueChanged, this, &SpeedDurationDialog::DurationChanged); + speed_layout->addWidget(dur_slider_, row, 1); - row++; + row++; + + link_box_ = new QCheckBox(tr("Link Speed and Duration")); + link_box_->setChecked(true); + speed_layout->addWidget(link_box_, row, 0, 1, 2); + } + + reverse_box_ = new QCheckBox(tr("Reverse")); + layout->addWidget(reverse_box_); + + maintain_audio_pitch_box_ = new QCheckBox(tr("Maintain Audio Pitch")); + layout->addWidget(maintain_audio_pitch_box_); ripple_box_ = new QCheckBox(tr("Ripple Trailing Clips")); - layout->addWidget(ripple_box_, row, 0, 1, 2); - - row++; + layout->addWidget(ripple_box_); QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); btns->setCenterButtons(true); connect(btns, &QDialogButtonBox::accepted, this, &SpeedDurationDialog::accept); connect(btns, &QDialogButtonBox::rejected, this, &SpeedDurationDialog::reject); - layout->addWidget(btns, row, 0, 1, 2); + layout->addWidget(btns); // Determine which speed value to use start_speed_ = clips.first()->speed(); start_duration_ = clips.first()->length(); + start_reverse_ = clips.first()->reverse(); + start_maintain_audio_pitch_ = clips.first()->maintain_audio_pitch(); for (int i=1; ispeed())) { + ClipBlock *c = clips.at(i); + + if (!qIsNaN(start_speed_) && !qFuzzyCompare(start_speed_, c->speed())) { // Speed differs per clip start_speed_ = qSNaN(); } - if (start_duration_ != -1 && clips.at(i)->length() != start_duration_) { + if (start_duration_ != -1 && c->length() != start_duration_) { start_duration_ = -1; } + + // Yes, in theory a bool should only ever be 0 or 1 anyway, but MSVC complained and it is + // *possible* that a bool could be something else, so this code is safer + int clip_reverse = c->reverse() ? 1 : 0; + int clip_maintain_pitch = c->maintain_audio_pitch() ? 1 : 0; + if (start_reverse_ != -1 && clip_reverse != start_reverse_) { + start_reverse_ = -1; + } + if (start_maintain_audio_pitch_ != -1 && clip_maintain_pitch != start_maintain_audio_pitch_) { + start_maintain_audio_pitch_ = -1; + } } if (qIsNaN(start_speed_)) { @@ -104,6 +129,18 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons } else { dur_slider_->SetValue(start_duration_); } + + if (start_reverse_ == -1) { + reverse_box_->setTristate(); + } else { + reverse_box_->setChecked(start_reverse_); + } + + if (start_maintain_audio_pitch_ == -1) { + maintain_audio_pitch_box_->setTristate(); + } else { + maintain_audio_pitch_box_->setChecked(start_maintain_audio_pitch_); + } } void SpeedDurationDialog::accept() @@ -133,6 +170,20 @@ void SpeedDurationDialog::accept() } } + // Set reverse values + if (!reverse_box_->isTristate()) { + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kReverseInput)), reverse_box_->isChecked())); + } + } + + // Set reverse values + if (!maintain_audio_pitch_box_->isTristate()) { + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kMaintainAudioPitchInput)), maintain_audio_pitch_box_->isChecked())); + } + } + // Set duration values foreach (ClipBlock *c, clips_) { rational proposed_length = c->length(); diff --git a/app/dialog/speedduration/speeddurationdialog.h b/app/dialog/speedduration/speeddurationdialog.h index 135f77434..be24ec9d6 100644 --- a/app/dialog/speedduration/speeddurationdialog.h +++ b/app/dialog/speedduration/speeddurationdialog.h @@ -56,8 +56,16 @@ private: QCheckBox *link_box_; + QCheckBox *reverse_box_; + + QCheckBox *maintain_audio_pitch_box_; + QCheckBox *ripple_box_; + int start_reverse_; + + int start_maintain_audio_pitch_; + double start_speed_; rational start_duration_; diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index d966098a0..efd46e316 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -33,6 +33,7 @@ const QString ClipBlock::kBufferIn = QStringLiteral("buffer_in"); 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"); ClipBlock::ClipBlock() : in_transition_(nullptr), @@ -52,6 +53,8 @@ ClipBlock::ClipBlock() : AddInput(kReverseInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); IgnoreHashingFrom(kReverseInput); + AddInput(kMaintainAudioPitchInput, NodeValue::kBoolean, false, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + PrependInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); SetValueHintForInput(kBufferIn, ValueHint(NodeValue::kBuffer)); } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index ee8eef2e8..82350d89f 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -71,6 +71,21 @@ public: return GetStandardValue(kReverseInput).toBool(); } + void set_reverse(bool e) + { + SetStandardValue(kReverseInput, e); + } + + bool maintain_audio_pitch() const + { + return GetStandardValue(kMaintainAudioPitchInput).toBool(); + } + + void set_maintain_audio_pitch(bool e) + { + SetStandardValue(kMaintainAudioPitchInput, e); + } + TransitionBlock* in_transition() { return in_transition_; @@ -110,6 +125,7 @@ public: static const QString kMediaInInput; static const QString kSpeedInput; static const QString kReverseInput; + static const QString kMaintainAudioPitchInput; protected: virtual void LinkChangeEvent() override; diff --git a/app/node/distort/CMakeLists.txt b/app/node/distort/CMakeLists.txt index 2962e21ca..375232ccc 100644 --- a/app/node/distort/CMakeLists.txt +++ b/app/node/distort/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(cornerpin) add_subdirectory(crop) add_subdirectory(flip) add_subdirectory(transform) diff --git a/app/node/distort/cornerpin/CMakeLists.txt b/app/node/distort/cornerpin/CMakeLists.txt new file mode 100644 index 000000000..e89b0f94f --- /dev/null +++ b/app/node/distort/cornerpin/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/distort/cornerpin/cornerpindistortnode.cpp + node/distort/cornerpin/cornerpindistortnode.h + PARENT_SCOPE +) diff --git a/app/node/distort/cornerpin/cornerpindistortnode.cpp b/app/node/distort/cornerpin/cornerpindistortnode.cpp new file mode 100644 index 000000000..b14909fe7 --- /dev/null +++ b/app/node/distort/cornerpin/cornerpindistortnode.cpp @@ -0,0 +1,226 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "cornerpindistortnode.h" + +#include "common/lerp.h" +#include "core.h" +#include "widget/slider/floatslider.h" + +namespace olive { + +const QString CornerPinDistortNode::kTextureInput = QStringLiteral("tex_in"); +const QString CornerPinDistortNode::kTopLeftInput = QStringLiteral("top_left_in"); +const QString CornerPinDistortNode::kTopRightInput = QStringLiteral("top_right_in"); +const QString CornerPinDistortNode::kBottomRightInput = QStringLiteral("bottom_right_in"); +const QString CornerPinDistortNode::kBottomLeftInput = QStringLiteral("bottom_left_in"); +const QString CornerPinDistortNode::kPerspectiveInput = QStringLiteral("perspective_in"); + +CornerPinDistortNode::CornerPinDistortNode() +{ + AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); + AddInput(kPerspectiveInput, NodeValue::kBoolean, true); + AddInput(kTopLeftInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kTopRightInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kBottomRightInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); + AddInput(kBottomLeftInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); +} + +void CornerPinDistortNode::Retranslate() +{ + SetInputName(kTextureInput, tr("Texture")); + SetInputName(kPerspectiveInput, tr("Perspective")); + SetInputName(kTopLeftInput, tr("Top Left")); + SetInputName(kTopRightInput, tr("Top Right")); + SetInputName(kBottomRightInput, tr("Bottom Right")); + SetInputName(kBottomLeftInput, tr("Bottom Left")); +} + +void CornerPinDistortNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + ShaderJob job; + job.InsertValue(value); + job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); + job.InsertValue(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, globals.resolution(), this)); + + // Convert slider values to their pixel values and then convert to clip space (-1.0 ... 1.0) for overriding the + // vertex coordinates. + const QVector2D &resolution = globals.resolution(); + QVector2D half_resolution = resolution * 0.5; + QVector2D top_left = QVector2D(ValueToPixel(0, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D top_right = QVector2D(ValueToPixel(1, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D bottom_right = QVector2D(ValueToPixel(2, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + QVector2D bottom_left = QVector2D(ValueToPixel(3, value, resolution)) / half_resolution - QVector2D(1.0, 1.0); + + // Override default vertex coordinates. + QVector adjusted_vertices = {top_left.x(), top_left.y(), 0.0f, + top_right.x(), top_right.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f, + + top_left.x(), top_left.y(), 0.0f, + bottom_left.x(), bottom_left.y(), 0.0f, + bottom_right.x(), bottom_right.y(), 0.0f}; + job.SetVertexCoordinates(adjusted_vertices); + + // If no texture do nothing + if (!job.GetValue(kTextureInput).data().isNull()) { + // In the special case that all sliders are in their default position just + // push the texture. + if (!(job.GetValue(kTopLeftInput).data().value().isNull() + && job.GetValue(kTopRightInput).data().value().isNull() && + job.GetValue(kBottomRightInput).data().value().isNull() && + job.GetValue(kBottomLeftInput).data().value().isNull())) { + table->Push(NodeValue::kShaderJob, QVariant::fromValue(job), this); + } else { + table->Push(NodeValue::kTexture, job.GetValue(kTextureInput).data(), this); + } + } +} + +ShaderCode CornerPinDistortNode::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + QString frag = FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/cornerpin.frag")); + QString vert = FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/cornerpin.vert")); + + // HACK: No good, very bad hack +#ifndef Q_OS_MAC + frag.prepend(QStringLiteral("#version 130\n\n")); + vert.prepend(QStringLiteral("#version 130\n\n")); +#else + vert.prepend(QStringLiteral("#extension GL_EXT_gpu_shader4 : require\n\n")); +#endif + + return ShaderCode(frag, vert); +} + +QPointF CornerPinDistortNode::ValueToPixel(int value, const NodeValueRow& row, const QVector2D &resolution) const +{ + Q_ASSERT(value >= 0 && value <= 3); + switch (value) { + case 0: // Top left + return QPointF(row[kTopLeftInput].data().value().x(), + row[kTopLeftInput].data().value().y()); + break; + case 1: // Top right + return QPointF(resolution.x() + row[kTopRightInput].data().value().x(), + row[kTopRightInput].data().value().y()); + break; + case 2: // Bottom right + return QPointF(resolution.x() + row[kBottomRightInput].data().value().x(), + resolution.y() + row[kBottomRightInput].data().value().y()); + break; + case 3: //Bottom left + return QPointF(row[kBottomLeftInput].data().value().x(), + row[kBottomLeftInput].data().value().y() + resolution.y()); + break; + default: // We should never get here + return QPointF(); + } +} + +void CornerPinDistortNode::DrawGizmos(const NodeValueRow &row, const NodeGlobals &globals, QPainter *p) +{ + const QVector2D &resolution = globals.resolution(); + + const double handle_radius = GetGizmoHandleRadius(p->transform()); + + p->setPen(QPen(Qt::white, 0)); + + QPointF top_left = ValueToPixel(0, row, resolution); + QPointF top_right = ValueToPixel(1, row, resolution); + QPointF bottom_right = ValueToPixel(2, row, resolution); + QPointF bottom_left = ValueToPixel(3, row, resolution); + + // Add the correct offset to each slider + SetInputProperty(kTopLeftInput, QStringLiteral("offset"), QVector2D(0.0, 0.0)); + SetInputProperty(kTopRightInput, QStringLiteral("offset"), QVector2D(resolution.x() , 0.0)); + SetInputProperty(kBottomRightInput, QStringLiteral("offset"), resolution); + SetInputProperty(kBottomLeftInput, QStringLiteral("offset"), QVector2D(0.0, resolution.y())); + + // Draw bounding box + p->drawLine(QLineF(top_left, top_right)); + p->drawLine(QLineF(top_right, bottom_right)); + p->drawLine(QLineF(bottom_right, bottom_left)); + p->drawLine(QLineF(bottom_left, top_left)); + + // Create handles + gizmo_resize_handle_[0] = CreateGizmoHandleRect(top_left, handle_radius); + gizmo_resize_handle_[1] = CreateGizmoHandleRect(top_right, handle_radius); + gizmo_resize_handle_[2] = CreateGizmoHandleRect(bottom_right, handle_radius); + gizmo_resize_handle_[3] = CreateGizmoHandleRect(bottom_left, handle_radius); + + // Draw handles + DrawAndExpandGizmoHandles(p, handle_radius, gizmo_resize_handle_, kGizmoCornerCount); +} + +bool CornerPinDistortNode::GizmoPress(const NodeValueRow &row, const NodeGlobals &globals, const QPointF &p) +{ + bool gizmo_active[kGizmoCornerCount] = {false}; + + for (int i = 0; i < kGizmoCornerCount; i++) { + gizmo_active[i] = gizmo_resize_handle_[i].contains(p); + + if (gizmo_active[i]) { + gizmo_drag_start_ = p; + gizmo_res_ = globals.resolution(); + gizmo_drag_ = i; + return true; + } + } + + return false; +} + +void CornerPinDistortNode::GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) +{ + if (gizmo_dragger_.isEmpty()) { + gizmo_dragger_.resize(2); + if (gizmo_drag_ == 0) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kTopLeftInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kTopLeftInput), 1), time); + } + if (gizmo_drag_ == 1) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 1), time); + } + if (gizmo_drag_ == 2) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 1), time); + } + if (gizmo_drag_ == 3) { + gizmo_dragger_[0].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 0), time); + gizmo_dragger_[1].Start(NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 1), time); + } + } + + QPointF diff = p - gizmo_drag_start_; + gizmo_dragger_[0].Drag(gizmo_dragger_[0].GetStartValue().toDouble() + diff.x()); + gizmo_dragger_[1].Drag(gizmo_dragger_[1].GetStartValue().toDouble() + diff.y()); +} + +void CornerPinDistortNode::GizmoRelease(MultiUndoCommand *command) { + for (NodeInputDragger &i : gizmo_dragger_) { + i.End(command); + } + gizmo_dragger_.clear(); +} + +} diff --git a/app/node/distort/cornerpin/cornerpindistortnode.h b/app/node/distort/cornerpin/cornerpindistortnode.h new file mode 100644 index 000000000..6261d7e1a --- /dev/null +++ b/app/node/distort/cornerpin/cornerpindistortnode.h @@ -0,0 +1,109 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 CORNERPINDISTORTNODE_H +#define CORNERPINDISTORTNODE_H + +#include + +#include "node/inputdragger.h" +#include "node/node.h" + +namespace olive { +class CornerPinDistortNode : public Node +{ + Q_OBJECT +public: + CornerPinDistortNode(); + + NODE_DEFAULT_DESTRUCTOR(CornerPinDistortNode) + + virtual Node* copy() const override + { + return new CornerPinDistortNode(); + } + + virtual QString Name() const override + { + return tr("Corner Pin"); + } + + virtual QString id() const override + { + return QStringLiteral("org.olivevideoeditor.Olive.cornerpin"); + } + + virtual QVector Category() const override + { + return {kCategoryDistort}; + } + + virtual QString Description() const override + { + return tr("Distort the image by dragging the corners."); + } + + virtual void Retranslate() override; + + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + + virtual ShaderCode GetShaderCode(const QString &shader_id) const override; + + virtual bool HasGizmos() const override + { + return true; + } + + virtual void DrawGizmos(const NodeValueRow& row, const NodeGlobals &globals, QPainter *p) override; + + virtual bool GizmoPress(const NodeValueRow& row, const NodeGlobals &globals, const QPointF &p) override; + virtual void GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) override; + virtual void GizmoRelease(MultiUndoCommand *command) override; + + /** + * @brief Convenience function - converts the 2D slider values from being + * an offset to the actual pixel value. + */ + QPointF ValueToPixel(int value, const NodeValueRow &row, const QVector2D &resolution) const; + + static const QString kTextureInput; + static const QString kPerspectiveInput; + static const QString kTopLeftInput; + static const QString kTopRightInput; + static const QString kBottomRightInput; + static const QString kBottomLeftInput; + +private: + // Gizmo variables + static const int kGizmoCornerCount = 4; + QRectF gizmo_resize_handle_[kGizmoCornerCount]; + QRectF gizmo_whole_rect_; + + int gizmo_drag_; + QVector gizmo_dragger_; + QPointF gizmo_drag_start_; + QVector2D gizmo_res_; + +}; + +} + + +#endif // CORNERPINDISTORTNODE_H diff --git a/app/node/factory.cpp b/app/node/factory.cpp index ef6d262f0..f2dd7934c 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -30,6 +30,7 @@ #include "block/transition/crossdissolve/crossdissolvetransition.h" #include "block/transition/diptocolor/diptocolortransition.h" #include "color/displaytransform/displaytransform.h" +#include "distort/cornerpin/cornerpindistortnode.h" #include "distort/crop/cropdistortnode.h" #include "distort/flip/flipdistortnode.h" #include "distort/transform/transformdistortnode.h" @@ -54,6 +55,7 @@ #include "project/folder/folder.h" #include "project/footage/footage.h" #include "project/sequence/sequence.h" +#include "time/timeoffset/timeoffsetnode.h" #include "time/timeremap/timeremap.h" namespace olive { @@ -262,6 +264,10 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new FlipDistortNode(); case kNoiseGenerator: return new NoiseGeneratorNode(); + case kTimeOffsetNode: + return new TimeOffsetNode(); + case kCornerPinDistort: + return new CornerPinDistortNode(); case kDisplayTransform: return new DisplayTransformNode(); diff --git a/app/node/factory.h b/app/node/factory.h index e7cca6a1e..58e2673a9 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -65,6 +65,8 @@ public: kOpacityEffect, kFlipDistort, kNoiseGenerator, + kTimeOffsetNode, + kCornerPinDistort, kDisplayTransform, // Count value diff --git a/app/node/time/CMakeLists.txt b/app/node/time/CMakeLists.txt index bd780b985..a038672fb 100644 --- a/app/node/time/CMakeLists.txt +++ b/app/node/time/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(timeoffset) add_subdirectory(timeremap) set(OLIVE_SOURCES diff --git a/app/node/time/timeoffset/CMakeLists.txt b/app/node/time/timeoffset/CMakeLists.txt new file mode 100644 index 000000000..b3976d54b --- /dev/null +++ b/app/node/time/timeoffset/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/time/timeoffset/timeoffsetnode.cpp + node/time/timeoffset/timeoffsetnode.h + PARENT_SCOPE +) diff --git a/app/node/time/timeoffset/timeoffsetnode.cpp b/app/node/time/timeoffset/timeoffsetnode.cpp new file mode 100644 index 000000000..d137847ee --- /dev/null +++ b/app/node/time/timeoffset/timeoffsetnode.cpp @@ -0,0 +1,91 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "timeoffsetnode.h" + +#include "widget/slider/rationalslider.h" + +namespace olive { + +const QString TimeOffsetNode::kTimeInput = QStringLiteral("time_in"); +const QString TimeOffsetNode::kInputInput = QStringLiteral("input_in"); + +#define super Node + +TimeOffsetNode::TimeOffsetNode() +{ + AddInput(kTimeInput, NodeValue::kRational, QVariant::fromValue(rational(0)), InputFlags(kInputFlagNotConnectable)); + SetInputProperty(kTimeInput, QStringLiteral("view"), RationalSlider::kTime); + SetInputProperty(kTimeInput, QStringLiteral("viewlock"), true); + IgnoreHashingFrom(kTimeInput); + + AddInput(kInputInput, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); +} + +void TimeOffsetNode::Retranslate() +{ + SetInputName(kTimeInput, QStringLiteral("Time")); + SetInputName(kInputInput, QStringLiteral("Input")); +} + +TimeRange TimeOffsetNode::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const +{ + if (input == kInputInput) { + return TimeRange(GetRemappedTime(input_time.in()), GetRemappedTime(input_time.out())); + } else { + return super::InputTimeAdjustment(input, element, input_time); + } +} + +TimeRange TimeOffsetNode::OutputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const +{ + /*if (input == kInputInput) { + rational target_time = GetValueAtTime(kTimeInput, input_time.in()).value(); + + return TimeRange(target_time, target_time + input_time.length()); + } else { + return super::OutputTimeAdjustment(input, element, input_time); + }*/ + return super::OutputTimeAdjustment(input, element, input_time); +} + +void TimeOffsetNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + table->Push(value[kInputInput]); +} + +void TimeOffsetNode::Hash(QCryptographicHash &hash, const NodeGlobals &globals, const VideoParams &video_params) const +{ + // Don't hash anything of our own, just pass-through to the connected node at the remapped tmie + if (IsInputConnected(kInputInput)) { + Node *out = GetConnectedOutput(kInputInput); + + NodeGlobals new_globals = globals; + new_globals.set_time(TimeRange(GetRemappedTime(globals.time().in()), GetRemappedTime(globals.time().out()))); + Node::Hash(out, GetValueHintForInput(kInputInput), hash, new_globals, video_params); + } +} + +rational TimeOffsetNode::GetRemappedTime(const rational &input) const +{ + return input + GetValueAtTime(kTimeInput, input).value(); +} + +} diff --git a/app/node/time/timeoffset/timeoffsetnode.h b/app/node/time/timeoffset/timeoffsetnode.h new file mode 100644 index 000000000..aa712891f --- /dev/null +++ b/app/node/time/timeoffset/timeoffsetnode.h @@ -0,0 +1,76 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 TIMEOFFSETNODE_H +#define TIMEOFFSETNODE_H + +#include "node/node.h" + +namespace olive { + +class TimeOffsetNode : public Node +{ +public: + TimeOffsetNode(); + + NODE_DEFAULT_DESTRUCTOR(TimeOffsetNode) + NODE_COPY_FUNCTION(TimeOffsetNode) + + virtual QString Name() const override + { + return tr("Time Offset"); + } + + virtual QString id() const override + { + return QStringLiteral("org.olivevideoeditor.Olive.timeoffset"); + } + + virtual QVector Category() const override + { + return {kCategoryGeneral}; + } + + virtual QString Description() const override + { + return tr("Offset time passing through the graph."); + } + + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + + virtual void Retranslate() override; + + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + + static const QString kTimeInput; + static const QString kInputInput; + +protected: + virtual void Hash(QCryptographicHash &hash, const NodeGlobals &globals, const VideoParams& video_params) const override; + +private: + rational GetRemappedTime(const rational& input) const; + +}; + +} + +#endif // TIMEOFFSETNODE_H diff --git a/app/render/audioparams.h b/app/render/audioparams.h index bb9e86b3b..bf2e6f08b 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -21,6 +21,10 @@ #ifndef AUDIOPARAMS_H #define AUDIOPARAMS_H +extern "C" { +#include +} + #include #include #include diff --git a/app/render/job/shaderjob.h b/app/render/job/shaderjob.h index ba5ad1427..335d442b5 100644 --- a/app/render/job/shaderjob.h +++ b/app/render/job/shaderjob.h @@ -22,6 +22,7 @@ #define SHADERJOB_H #include +#include #include "generatejob.h" #include "render/colorprocessor.h" @@ -119,7 +120,15 @@ public: shader_desc_ = shader_desc; } + void SetVertexCoordinates(const QVector &vertex_coords) + { + vertex_overrides_ = vertex_coords; + } + const QVector& GetVertexCoordinates() + { + return vertex_overrides_; + } private: QString shader_id_; @@ -133,7 +142,10 @@ private: bool use_ocio_; ColorProcessorPtr color_processor_; + OCIO::GpuShaderDescRcPtr shader_desc_; + + QVector vertex_overrides_; }; diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 37e43e88d..7aa0955cb 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -569,7 +569,13 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video QOpenGLBuffer vert_vbo_; vert_vbo_.create(); vert_vbo_.bind(); - vert_vbo_.allocate(blit_vertices.constData(), blit_vertices.size() * sizeof(GLfloat)); + // If the job has vertex coordinate overrides use them instead of the defaults. + if (!job.GetVertexCoordinates().isEmpty()) { + Q_ASSERT(job.GetVertexCoordinates().size() == 18); + vert_vbo_.allocate(job.GetVertexCoordinates().constData(), job.GetVertexCoordinates().size() * sizeof(float)); + } else { + vert_vbo_.allocate(blit_vertices.constData(), blit_vertices.size() * sizeof(GLfloat)); + } vert_vbo_.release(); QOpenGLBuffer frag_vbo_; @@ -886,7 +892,11 @@ GLuint OpenGLRenderer::CompileShader(GLenum type, const QString &code) QStringLiteral("#version 120\n\n"); #endif - QString complete_code = shader_preamble; + QString complete_code; + + if (!code.startsWith(QStringLiteral("#version"))) { + complete_code = shader_preamble; + } if (code.isEmpty()) { // Use default code diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index fc6c04456..1769393c2 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -25,6 +25,9 @@ #include #include +#include "audio/packedprocessor.h" +#include "audio/planarprocessor.h" +#include "audio/tempoprocessor.h" #include "node/block/clip/clip.h" #include "node/block/transition/transition.h" #include "node/project/project.h" @@ -288,8 +291,35 @@ NodeValueTable RenderProcessor::GenerateBlockTable(const Track *track, const Tim // Just silence, don't think there's any other practical application of 0 speed audio samples_from_this_block->silence(); } else if (!qFuzzyCompare(speed_value, 1.0)) { - // Multiply time - samples_from_this_block->speed(speed_value); + if (clip_cast->maintain_audio_pitch()) { + PackedProcessor packer; + packer.Open(samples_from_this_block->audio_params()); + + QByteArray packed = packer.Convert(samples_from_this_block); + + if (!packed.isEmpty()) { + TempoProcessor tp; + tp.Open(samples_from_this_block->audio_params(), speed_value); + + // FIXME: This is not the best way to do this, the TempoProcessor works best + // when it's given a continuous stream of audio, which is challenging + // in our current "modular" audio system. This should still work reasonably + // well on export (assuming audio is all generated at once on export), but + // users may hear clicks and pops in the audio during preview due to this + // approach. + tp.Push(packed); + tp.Flush(); + packed = tp.Pull(); + tp.Close(); + + PlanarProcessor planar; + planar.Open(samples_from_this_block->audio_params()); + samples_from_this_block = planar.Convert(packed); + } + } else { + // Multiply time + samples_from_this_block->speed(speed_value); + } } if (reversed) { diff --git a/app/shaders/cornerpin.frag b/app/shaders/cornerpin.frag new file mode 100644 index 000000000..7dd7e3306 --- /dev/null +++ b/app/shaders/cornerpin.frag @@ -0,0 +1,48 @@ +// Input texture +uniform sampler2D ove_maintex; +uniform sampler2D tex_in; +uniform bool perspective_in; + +// Input texture coordinate +varying vec2 ove_texcoord; + +varying vec2 q; +varying vec2 b1; +varying vec2 b2; +varying vec2 b3; + +float Wedge2D(vec2 v, vec2 w) { + return (v.x*w.y) - (v.y*w.x); +} + +void main() { + if(perspective_in){ + gl_FragColor = texture2D(tex_in, ove_texcoord); + } else { + float A = Wedge2D(b2, b3); + float B = Wedge2D(b3, q) - Wedge2D(b1, b2); + float C = Wedge2D(b1, q); + + vec2 uv; + + // solve for v + if (abs(A) < 0.001) { + uv.y = -C/B; + } else { + float discrim = B*B - 4.0*A*C; + uv.y = 0.5 * (-B + sqrt(discrim)) / A; + } + + // solve for u + vec2 denom = b1 + uv.y * b3; + if (abs(denom.x) > abs(denom.y)) { + uv.x = (q.x - b2.x * uv.y) / denom.x; + } else { + uv.x = (q.y - b2.y * uv.y) / denom.y; + } + + uv.y = 1.0 - uv.y; + + gl_FragColor = texture2D(tex_in, uv); + } +} diff --git a/app/shaders/cornerpin.vert b/app/shaders/cornerpin.vert new file mode 100644 index 000000000..1eadf95fe --- /dev/null +++ b/app/shaders/cornerpin.vert @@ -0,0 +1,99 @@ +uniform bool perspective_in; +uniform vec2 top_left_in; +uniform vec2 top_right_in; +uniform vec2 bottom_left_in; +uniform vec2 bottom_right_in; + +uniform vec2 resolution_in; + +uniform mat4 ove_mvpmat; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 ove_texcoord; + +varying vec2 q; +varying vec2 b1; +varying vec2 b2; +varying vec2 b3; + +void main() { + // The slider inputs only contain the amount they have changed rather than + // their pixel locations so we adjust them here. + vec2 t_l = top_left_in; + vec2 t_r = top_right_in + vec2(resolution_in.x, 0.0); + vec2 b_r = bottom_right_in + resolution_in; + vec2 b_l = bottom_left_in + vec2(0.0, resolution_in.y); + + gl_Position = ove_mvpmat * a_position; + + if (perspective_in){ + // Find the center of the quadrilateral by finding where the two diagonals intersect. + // https://www.reedbeta.com/blog/quadrilateral-interpolation-part-1/ + + // Here we calculate the gradient and constant (y = mx + c) for each diagonal. + float m1 = (t_r.y - b_l.y)/(t_r.x - b_l.x); + float c1 = b_l.y - m1 * b_l.x; + float m2 = (b_r.y - t_l.y)/(b_r.x - t_l.x); + float c2 = t_l.y - m2 * t_l.x; + + // Find the intersection by setting the two line equations equal and rearrange. + float mid_x = (c2 - c1) / (m1 - m2); + float mid_y = m1 * mid_x + c1; + + // Find the distance from each corner to our center point + float d0 = length(vec2(mid_x - b_l.x, mid_y - b_l.y)); + float d1 = length(vec2(b_r.x - mid_x, mid_y - b_r.y)); + float d2 = length(vec2(t_r.x - mid_x, t_r.y - mid_y)); + float d3 = length(vec2(mid_x - t_l.x, t_l.y - mid_y)); + + float q = 1.0; + + /* + Vertex IDs (aspect ratio irrelevant): + 0_____1 + 3|\ | + | \ | + | \ | + | \ | + |____\|2 + 4 5 + */ + + if (gl_VertexID == 0 || gl_VertexID == 3) { + q = (d1+d3)/d3; + } else if (gl_VertexID == 1) { + q = (d0+d2)/d2; + } else if (gl_VertexID == 2 || gl_VertexID == 5) { + q = (d3+d1)/d1; + } else { + q = (d2+d0)/d0; + } + + gl_Position[0] *= q; + gl_Position[1] *= q; + gl_Position[3] = q; + } else{ + // https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/ + vec2 pos; + + if (gl_VertexID == 0 || gl_VertexID == 3) { // top left + pos = t_l; + } else if (gl_VertexID == 1) { // top right + pos = t_r; + } else if (gl_VertexID == 2 || gl_VertexID == 5) { // bottom right + pos = b_r; + } else if (gl_VertexID == 4) { // bottom left + pos = b_l; + } + + q = pos - b_l; + b1 = b_r - b_l; + b2 = t_l - b_l; + b3 = b_l - b_r - t_l + t_r; + } + + + ove_texcoord = a_texcoord; +} diff --git a/app/ts/es_ES.ts b/app/ts/es_ES.ts index c28761a8e..71bce4706 100644 --- a/app/ts/es_ES.ts +++ b/app/ts/es_ES.ts @@ -4883,7 +4883,7 @@ y salida. On - Avtivado + Activado diff --git a/app/ts/zh_CN.ts b/app/ts/zh_CN.ts old mode 100755 new mode 100644 diff --git a/app/ts/zh_TW.ts b/app/ts/zh_TW.ts old mode 100755 new mode 100644 diff --git a/app/widget/slider/base/sliderbase.cpp b/app/widget/slider/base/sliderbase.cpp index 116a61c71..7ca14d1f0 100644 --- a/app/widget/slider/base/sliderbase.cpp +++ b/app/widget/slider/base/sliderbase.cpp @@ -104,7 +104,17 @@ void SliderBase::changeEvent(QEvent *e) void SliderBase::UpdateLabel() { - label_->setText(tristate_ ? tr("---") : GetFormattedValueToString()); + QString s; + + if (tristate_) { + s = tr("---"); + } else if (label_substitutions_.contains(GetValueInternal())) { + s = label_substitutions_.value(GetValueInternal()); + } else { + s = GetFormattedValueToString(); + } + + label_->setText(s); } QVariant SliderBase::AdjustValue(const QVariant &value) const diff --git a/app/widget/slider/base/sliderbase.h b/app/widget/slider/base/sliderbase.h index 2643ca674..64d4ed2f4 100644 --- a/app/widget/slider/base/sliderbase.h +++ b/app/widget/slider/base/sliderbase.h @@ -49,6 +49,12 @@ public: QString GetFormattedValueToString(const QVariant& v) const; + void InsertLabelSubstitution(const QVariant &value, const QString &label) + { + label_substitutions_.insert(value, label); + UpdateLabel(); + } + public slots: void ShowEditor(); @@ -92,6 +98,8 @@ private: bool format_plural_; + QMap label_substitutions_; + private slots: void LineEditConfirmed(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index d8827a7c0..206fe9719 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -466,11 +466,8 @@ void ViewerWidget::ReceivedAudioBufferForPlayback() // If the tempo must be adjusted, adjust now if (tempo_processor_.IsOpen()) { - tempo_processor_.Push(pack.data(), pack.size()); - int actual = tempo_processor_.Pull(pack.data(), pack.size()); - if (actual != pack.size()) { - pack.resize(actual); - } + tempo_processor_.Push(pack); + pack = tempo_processor_.Pull(); } // TempoProcessor may have emptied the array