diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp
index ddd2100d4..5cf2866b0 100644
--- a/app/codec/ffmpeg/ffmpegdecoder.cpp
+++ b/app/codec/ffmpeg/ffmpegdecoder.cpp
@@ -677,6 +677,8 @@ FootageDescription FFmpegDecoder::probe(const QString &filename,
stream.set_color_range(info.color_range == fb_color_range_jpeg ?
VideoParams::k_color_range_full :
VideoParams::k_color_range_limited);
+ stream.set_color_primaries(info.color_primaries);
+ stream.set_color_transfer(info.color_trc);
stream.set_premultiplied_alpha(false);
desc.add_video_stream(stream);
diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp
index 54de69731..34d5e4146 100644
--- a/app/codec/ffmpeg/ffmpegencoder.cpp
+++ b/app/codec/ffmpeg/ffmpegencoder.cpp
@@ -38,6 +38,73 @@ FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms)
{
}
+bool FFmpegEncoder::get_color_tags_for_colorspace(const QString &colorspace,
+ int *primaries, int *trc,
+ int *matrix)
+{
+ const QString name = colorspace.toLower();
+
+ if (name.contains(QStringLiteral("pq")) ||
+ name.contains(QStringLiteral("2084"))) {
+ *primaries = fb_color_primaries_bt2020;
+ *trc = fb_color_trc_pq;
+ *matrix = fb_col_spc_b_t2020_ncl;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("hlg"))) {
+ *primaries = fb_color_primaries_bt2020;
+ *trc = fb_color_trc_hlg;
+ *matrix = fb_col_spc_b_t2020_ncl;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("2020"))) {
+ *primaries = fb_color_primaries_bt2020;
+ *trc = fb_color_trc_bt709;
+ *matrix = fb_col_spc_b_t2020_ncl;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("p3"))) {
+ *primaries = fb_color_primaries_smpte432;
+ *trc = fb_color_trc_srgb;
+ *matrix = fb_col_spc_b_t709;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("srgb"))) {
+ *primaries = fb_color_primaries_bt709;
+ *trc = fb_color_trc_srgb;
+ *matrix = fb_col_spc_b_t709;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("pal"))) {
+ *primaries = fb_color_primaries_bt470bg;
+ *trc = fb_color_trc_gamma28;
+ *matrix = fb_col_spc_b_t470_bg;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("ntsc"))) {
+ *primaries = fb_color_primaries_smpte170m;
+ *trc = fb_color_trc_smpte170m;
+ *matrix = fb_col_spc_smpt_e170_m;
+ return true;
+ }
+
+ if (name.contains(QStringLiteral("1886")) ||
+ name.contains(QStringLiteral("709"))) {
+ *primaries = fb_color_primaries_bt709;
+ *trc = fb_color_trc_bt709;
+ *matrix = fb_col_spc_b_t709;
+ return true;
+ }
+
+ return false;
+}
+
QStringList FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
{
QStringList pix_fmts;
@@ -187,6 +254,19 @@ bool FFmpegEncoder::open()
1 :
0;
+ // Derive explicit nclc tags (HDR etc.) from the export colorspace;
+ // the bridge falls back to the legacy sRGB/Rec.709 logic when these
+ // are unspecified
+ int color_primaries = fb_color_primaries_unspec;
+ int color_trc = fb_color_trc_unspec;
+ int color_matrix = fb_col_spc_unspec;
+ get_color_tags_for_colorspace(params().color_transform().output(),
+ &color_primaries, &color_trc,
+ &color_matrix);
+ config.video_color_primaries = color_primaries;
+ config.video_color_trc = color_trc;
+ config.video_colorspace = color_matrix;
+
// Custom options (skip Olive-internal keys)
for (auto i = params().video_opts().begin();
i != params().video_opts().end(); i++) {
diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h
index 28120008f..f46a7a046 100644
--- a/app/codec/ffmpeg/ffmpegencoder.h
+++ b/app/codec/ffmpeg/ffmpegencoder.h
@@ -66,6 +66,18 @@ public:
return video_conversion_fmt_;
}
+ /**
+ * @brief Derives nclc color tags from an output colorspace name
+ *
+ * Extracted for testability. Returns true when the name maps to
+ * explicit tags (PQ/HLG/BT.2020, sRGB, P3, Rec.601, Rec.709); returns
+ * false for unknown names, in which case the bridge's legacy
+ * Rec.709/sRGB inference applies.
+ */
+ static bool get_color_tags_for_colorspace(const QString &colorspace,
+ int *primaries, int *trc,
+ int *matrix);
+
private:
/**
* @brief Copy the last error message from the bridge into the encoder error state
diff --git a/app/config/config.cpp b/app/config/config.cpp
index e29a1d5ee..5848aaafa 100644
--- a/app/config/config.cpp
+++ b/app/config/config.cpp
@@ -78,6 +78,8 @@ void Config::set_defaults()
set_entry_internal(QStringLiteral("HoverFocus"), NodeValue::k_boolean, false);
set_entry_internal(QStringLiteral("AudioScrubbing"), NodeValue::k_boolean,
true);
+ set_entry_internal(QStringLiteral("WaveformRgbParade"), NodeValue::k_boolean,
+ false);
set_entry_internal(QStringLiteral("AutorecoveryEnabled"), NodeValue::k_boolean,
true);
set_entry_internal(QStringLiteral("AutorecoveryInterval"), NodeValue::k_int,
diff --git a/app/node/color/CMakeLists.txt b/app/node/color/CMakeLists.txt
index fd4eb2c85..1740dcc90 100644
--- a/app/node/color/CMakeLists.txt
+++ b/app/node/color/CMakeLists.txt
@@ -18,8 +18,10 @@ add_subdirectory(colormanager)
add_subdirectory(displaytransform)
add_subdirectory(ociobase)
add_subdirectory(ociogradingtransformlinear)
+add_subdirectory(ociogradingtransformlog)
add_subdirectory(ociolut)
add_subdirectory(threewaycolor)
+add_subdirectory(whitebalance)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
diff --git a/app/node/color/colormanager/colormanager.cpp b/app/node/color/colormanager/colormanager.cpp
index fc162c57c..59dcce7c3 100644
--- a/app/node/color/colormanager/colormanager.cpp
+++ b/app/node/color/colormanager/colormanager.cpp
@@ -175,6 +175,53 @@ void ColorManager::set_default_input_color_space(const QString &s)
project()->set_default_input_color_space(s);
}
+QString ColorManager::get_colorspace_for_ffmpeg_tags(int primaries,
+ int trc) const
+{
+ // FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values mapped to
+ // candidate colorspace names, in order of preference
+ struct TagMapping {
+ int primaries;
+ int trc;
+ const char *candidates[3];
+ };
+
+ static const TagMapping k_tag_mappings[] = {
+ { 1, 1, { "Rec.709 OETF", "Rec.709", "BT.709" } },
+ { 1, 13, { "sRGB OETF", "sRGB", nullptr } },
+ { 6, 6, { "Rec.601 OETF (NTSC)", "Rec.601 NTSC", nullptr } },
+ { 5, 5, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } },
+ { 5, 6, { "Rec.601 OETF (PAL)", "Rec.601 PAL", nullptr } },
+ { 9, 16, { "Rec.2020 PQ", "BT.2020 PQ", "ST 2084 PQ" } },
+ { 9, 18, { "Rec.2020 HLG", "BT.2020 HLG", "HLG" } },
+ { 9, 1, { "Rec.2020", "BT.2020", nullptr } },
+ { 9, 14, { "Rec.2020", "BT.2020", nullptr } },
+ { 9, 15, { "Rec.2020", "BT.2020", nullptr } },
+ };
+
+ // 0 = unset, 2 = AVCOL_PRI/TRC_UNSPECIFIED
+ if (primaries == 0 || primaries == 2 || trc == 0 || trc == 2) {
+ return QString();
+ }
+
+ const QStringList available = list_available_colorspaces();
+
+ for (const TagMapping &mapping : k_tag_mappings) {
+ if (mapping.primaries == primaries && mapping.trc == trc) {
+ for (const char *candidate : mapping.candidates) {
+ if (candidate &&
+ available.contains(QString::fromUtf8(candidate))) {
+ return QString::fromUtf8(candidate);
+ }
+ }
+ // Known tag pair, but the config has no matching colorspace
+ return QString();
+ }
+ }
+
+ return QString();
+}
+
QString ColorManager::get_reference_color_space() const
{
return project()->get_color_reference_space();
diff --git a/app/node/color/colormanager/colormanager.h b/app/node/color/colormanager/colormanager.h
index e2c82d24b..e1670b39f 100644
--- a/app/node/color/colormanager/colormanager.h
+++ b/app/node/color/colormanager/colormanager.h
@@ -65,6 +65,16 @@ public:
QString get_default_input_color_space() const;
+ /**
+ * @brief Auto-detects an input colorspace from media color tags
+ *
+ * Maps raw FFmpeg color primaries/transfer values (as exposed on
+ * VideoParams) to a colorspace of the active OCIO config. Returns an
+ * empty string when the tags are unknown or the config has no matching
+ * colorspace, in which case the default input colorspace applies.
+ */
+ QString get_colorspace_for_ffmpeg_tags(int primaries, int trc) const;
+
void set_default_input_color_space(const QString &s);
QString get_reference_color_space() const;
diff --git a/app/node/color/ociogradingtransformlog/CMakeLists.txt b/app/node/color/ociogradingtransformlog/CMakeLists.txt
new file mode 100644
index 000000000..57ace5e47
--- /dev/null
+++ b/app/node/color/ociogradingtransformlog/CMakeLists.txt
@@ -0,0 +1,23 @@
+# Olive - Non-Linear Video Editor
+# Copyright (C) 2022 Olive Team
+# Modifications Copyright (C) 2026 Oak 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/color/ociogradingtransformlog/ociogradingtransformlog.cpp
+ node/color/ociogradingtransformlog/ociogradingtransformlog.h
+ PARENT_SCOPE
+)
diff --git a/app/node/color/ociogradingtransformlog/ociogradingtransformlog.cpp b/app/node/color/ociogradingtransformlog/ociogradingtransformlog.cpp
new file mode 100644
index 000000000..432a9c3e3
--- /dev/null
+++ b/app/node/color/ociogradingtransformlog/ociogradingtransformlog.cpp
@@ -0,0 +1,302 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2026 Oak 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 "ociogradingtransformlog.h"
+
+#include
+
+#include "common/ocioutils.h"
+#include "node/project.h"
+#include "render/colorprocessor.h"
+#include "widget/slider/floatslider.h"
+
+namespace olive
+{
+
+// These ids double as the OCIO GPU uniform names for the dynamic
+// GradingPrimaryTransform; do not rename them. OCIO's log style maps the
+// classic wheels as: brightness = lift, contrast = gain, gamma = gamma.
+const QString OCIOGradingTransformLogNode::k_lift_input =
+ QStringLiteral("ocio_grading_primary_brightness");
+const QString OCIOGradingTransformLogNode::k_gain_input =
+ QStringLiteral("ocio_grading_primary_contrast");
+const QString OCIOGradingTransformLogNode::k_gamma_input =
+ QStringLiteral("ocio_grading_primary_gamma");
+const QString OCIOGradingTransformLogNode::k_saturation_input =
+ QStringLiteral("ocio_grading_primary_saturation");
+const QString OCIOGradingTransformLogNode::k_pivot_input =
+ QStringLiteral("ocio_grading_primary_pivot");
+const QString OCIOGradingTransformLogNode::k_clamp_black_enable_input =
+ QStringLiteral("clamp_black_enable_in");
+const QString OCIOGradingTransformLogNode::k_clamp_black_input =
+ QStringLiteral("ocio_grading_primary_clampBlack");
+const QString OCIOGradingTransformLogNode::k_clamp_white_enable_input =
+ QStringLiteral("clamp_white_enable_in");
+const QString OCIOGradingTransformLogNode::k_clamp_white_input =
+ QStringLiteral("ocio_grading_primary_clampWhite");
+
+#define super OCIOBaseNode
+
+OCIOGradingTransformLogNode::OCIOGradingTransformLogNode()
+{
+ add_input(k_lift_input, NodeValue::k_vec4, QVector4D{ 0.0, 0.0, 0.0, 0.0 });
+ set_input_property(k_lift_input, QStringLiteral("base"), 0.01);
+ set_vec4_input_colors(k_lift_input);
+
+ add_input(k_gain_input, NodeValue::k_vec4, QVector4D{ 1.0, 1.0, 1.0, 1.0 });
+ set_input_property(k_gain_input, QStringLiteral("base"), 0.01);
+ set_vec4_input_colors(k_gain_input);
+
+ add_input(k_gamma_input, NodeValue::k_vec4, QVector4D{ 1.0, 1.0, 1.0, 1.0 });
+ set_input_property(k_gamma_input, QStringLiteral("base"), 0.01);
+ set_vec4_input_colors(k_gamma_input);
+
+ add_input(k_saturation_input, NodeValue::k_float, 1.0);
+ set_input_property(k_saturation_input, QStringLiteral("view"),
+ FloatSlider::k_percentage);
+ set_input_property(k_saturation_input, QStringLiteral("min"), 0.0);
+
+ add_input(k_pivot_input, NodeValue::k_float,
+ -0.2); // Default for GRADING_LOG listed in ocio::GradingPrimary
+ set_input_property(k_pivot_input, QStringLiteral("base"), 0.01);
+
+ add_input(k_clamp_black_enable_input, NodeValue::k_boolean, false);
+
+ add_input(k_clamp_black_input, NodeValue::k_float, 0.0);
+ set_input_property(k_clamp_black_input, QStringLiteral("enabled"),
+ get_standard_value(k_clamp_black_enable_input).toBool());
+ set_input_property(k_clamp_black_input, QStringLiteral("base"), 0.01);
+
+ add_input(k_clamp_white_enable_input, NodeValue::k_boolean, false);
+
+ add_input(k_clamp_white_input, NodeValue::k_float, 1.0);
+ set_input_property(k_clamp_white_input, QStringLiteral("enabled"),
+ get_standard_value(k_clamp_white_enable_input).toBool());
+ set_input_property(k_clamp_white_input, QStringLiteral("base"), 0.01);
+
+ // Constrain the white clamp minimum to just above the (static) black clamp
+ // as per ocio::GradingPrimary::validate. When the black clamp is keyframed
+ // or connected, Value() enforces the invariant per frame instead.
+ update_clamp_white_minimum();
+}
+
+QString OCIOGradingTransformLogNode::name() const
+{
+ return tr("OCIO Color Grading (Log)");
+}
+
+QString OCIOGradingTransformLogNode::id() const
+{
+ return QStringLiteral("org.olivevideoeditor.Olive.ociogradingtransformlog");
+}
+
+QVector OCIOGradingTransformLogNode::category() const
+{
+ return { k_category_color };
+}
+
+QString OCIOGradingTransformLogNode::description() const
+{
+ return tr("Lift/gamma/gain color grading using OpenColorIO.");
+}
+
+void OCIOGradingTransformLogNode::retranslate()
+{
+ super::retranslate();
+
+ set_input_name(k_texture_input, tr("Input"));
+ set_input_name(k_lift_input, tr("Lift"));
+ set_input_name(k_gain_input, tr("Gain"));
+ set_input_name(k_gamma_input, tr("Gamma"));
+ set_input_name(k_saturation_input, tr("Saturation"));
+ set_input_name(k_pivot_input, tr("Pivot"));
+ set_input_name(k_clamp_black_enable_input, tr("Enable Black Clamp"));
+ set_input_name(k_clamp_black_input, tr("Black Clamp"));
+ set_input_name(k_clamp_white_enable_input, tr("Enable White Clamp"));
+ set_input_name(k_clamp_white_input, tr("White Clamp"));
+}
+
+void OCIOGradingTransformLogNode::InputValueChangedEvent(const QString &input,
+ int element)
+{
+ Q_UNUSED(element);
+
+ if (input == k_clamp_white_enable_input) {
+ set_input_property(k_clamp_white_input, QStringLiteral("enabled"),
+ get_standard_value(k_clamp_white_enable_input).toBool());
+ } else if (input == k_clamp_black_enable_input) {
+ set_input_property(k_clamp_black_input, QStringLiteral("enabled"),
+ get_standard_value(k_clamp_black_enable_input).toBool());
+ } else if (input == k_clamp_black_input) {
+ // Ensure the white clamp is always greater than the black clamp as per
+ // ocio::GradingPrimary::validate
+ update_clamp_white_minimum();
+ }
+
+ generate_processor();
+}
+
+void OCIOGradingTransformLogNode::InputConnectedEvent(const QString &input,
+ int element, Node *output)
+{
+ super::InputConnectedEvent(input, element, output);
+
+ if (input == k_clamp_black_input) {
+ update_clamp_white_minimum();
+ }
+}
+
+void OCIOGradingTransformLogNode::InputDisconnectedEvent(const QString &input,
+ int element,
+ Node *output)
+{
+ super::InputDisconnectedEvent(input, element, output);
+
+ if (input == k_clamp_black_input) {
+ update_clamp_white_minimum();
+ }
+}
+
+void OCIOGradingTransformLogNode::update_clamp_white_minimum()
+{
+ // A static UI minimum cannot follow an animated black clamp; for keyframed
+ // or connected values the white>black invariant is enforced per frame in
+ // Value() instead
+ if (is_input_keyframing(k_clamp_black_input) ||
+ is_input_connected(k_clamp_black_input)) {
+ return;
+ }
+
+ set_input_property(k_clamp_white_input, QStringLiteral("min"),
+ get_standard_value(k_clamp_black_input).toDouble() + 0.000001);
+}
+
+void OCIOGradingTransformLogNode::generate_processor()
+{
+ if (manager()) {
+ ocio::GradingPrimaryTransformRcPtr gp =
+ ocio::GradingPrimaryTransform::Create(ocio::GRADING_LOG);
+ gp->makeDynamic();
+ gp->setDirection(ocio::TransformDirection::TRANSFORM_DIR_FORWARD);
+
+ try {
+ set_processor(ColorProcessor::create(
+ manager()->get_config()->getProcessor(gp)));
+ } catch (const ocio::Exception &e) {
+ std::cerr << std::endl << e.what() << std::endl;
+ }
+ }
+}
+
+void OCIOGradingTransformLogNode::value(const NodeValueRow &value,
+ const NodeGlobals &globals,
+ NodeValueTable *table) const
+{
+ if (TexturePtr tex = value[k_texture_input].to_texture()) {
+ if (processor()) {
+ ColorTransformJob job(value);
+
+ job.set_color_processor(processor());
+ job.set_input_texture(value[k_texture_input]);
+
+ const int master_channel = 0;
+ const int red_channel = 1;
+ const int green_channel = 2;
+ const int blue_channel = 3;
+
+ // OCIO expects vec3s on the GPU but RGBMs (master + RGB) on the
+ // CPU; the per-style master combination below mirrors
+ // ocio::GradingPrimary. Lift is additive, gain/gamma multiply.
+ QVector4D lift = value[k_lift_input].to_vec4();
+ lift[red_channel] += lift[master_channel];
+ lift[green_channel] += lift[master_channel];
+ lift[blue_channel] += lift[master_channel];
+ job.insert(k_lift_input,
+ NodeValue(NodeValue::k_vec3,
+ QVector3D(lift[red_channel], lift[green_channel],
+ lift[blue_channel])));
+
+ QVector4D gain = value[k_gain_input].to_vec4();
+ gain[red_channel] *= gain[master_channel];
+ gain[green_channel] *= gain[master_channel];
+ gain[blue_channel] *= gain[master_channel];
+ job.insert(k_gain_input,
+ NodeValue(NodeValue::k_vec3,
+ QVector3D(gain[red_channel], gain[green_channel],
+ gain[blue_channel])));
+
+ QVector4D gamma = value[k_gamma_input].to_vec4();
+ gamma[red_channel] *= gamma[master_channel];
+ gamma[green_channel] *= gamma[master_channel];
+ gamma[blue_channel] *= gamma[master_channel];
+ job.insert(k_gamma_input,
+ NodeValue(NodeValue::k_vec3,
+ QVector3D(gamma[red_channel],
+ gamma[green_channel],
+ gamma[blue_channel])));
+
+ if (!value[k_clamp_black_enable_input].to_bool()) {
+ job.insert(k_clamp_black_input,
+ NodeValue(NodeValue::k_float,
+ ocio::GradingPrimary::NoClampBlack()));
+ }
+
+ if (!value[k_clamp_white_enable_input].to_bool()) {
+ job.insert(k_clamp_white_input,
+ NodeValue(NodeValue::k_float,
+ ocio::GradingPrimary::NoClampWhite()));
+ }
+
+ if (value[k_clamp_black_enable_input].to_bool() &&
+ value[k_clamp_white_enable_input].to_bool()) {
+ // ocio::GradingPrimary::validate requires the white clamp to be
+ // greater than the black clamp. Keyframed or connected values
+ // can violate this at arbitrary times, so enforce the invariant
+ // per frame here.
+ const double clamp_black = value[k_clamp_black_input].to_double();
+ const double clamp_white = value[k_clamp_white_input].to_double();
+ if (clamp_white <= clamp_black) {
+ job.insert(k_clamp_white_input,
+ NodeValue(NodeValue::k_float,
+ clamp_black + 0.000001));
+ }
+ }
+
+ table->push(NodeValue::k_texture, tex->to_job(job), this);
+ }
+ }
+}
+
+void OCIOGradingTransformLogNode::config_changed()
+{
+ generate_processor();
+}
+
+void OCIOGradingTransformLogNode::set_vec4_input_colors(const QString &input)
+{
+ set_input_property(input, QStringLiteral("color0"),
+ QColor(192, 192, 192).name());
+ set_input_property(input, QStringLiteral("color1"), QColor(255, 0, 0).name());
+ set_input_property(input, QStringLiteral("color2"), QColor(0, 255, 0).name());
+ set_input_property(input, QStringLiteral("color3"), QColor(0, 0, 255).name());
+}
+
+}
diff --git a/app/node/color/ociogradingtransformlog/ociogradingtransformlog.h b/app/node/color/ociogradingtransformlog/ociogradingtransformlog.h
new file mode 100644
index 000000000..d18ce2fa5
--- /dev/null
+++ b/app/node/color/ociogradingtransformlog/ociogradingtransformlog.h
@@ -0,0 +1,91 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2026 Oak 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 OAK_OCIOGRADINGTRANSFORMLOGNODE_H
+#define OAK_OCIOGRADINGTRANSFORMLOGNODE_H
+
+#include "node/color/ociobase/ociobase.h"
+#include "render/colorprocessor.h"
+
+namespace olive
+{
+
+/**
+ * @brief Lift/gamma/gain grading node built on ocio::GRADING_LOG
+ *
+ * Mirrors OCIOGradingTransformLinearNode for the log grading style. OCIO's
+ * log-style GPU uniforms map to the classic wheels as: brightness = lift,
+ * contrast = gain, gamma = gamma.
+ */
+class OCIOGradingTransformLogNode : public OCIOBaseNode {
+ Q_OBJECT
+public:
+ OCIOGradingTransformLogNode();
+
+ NODE_DEFAULT_FUNCTIONS(OCIOGradingTransformLogNode)
+
+ virtual QString name() const override;
+ virtual QString id() const override;
+ virtual QVector category() const override;
+ virtual QString description() const override;
+
+ virtual void retranslate() override;
+ virtual void InputValueChangedEvent(const QString &input,
+ int element) override;
+ virtual void InputConnectedEvent(const QString &input, int element,
+ Node *output) override;
+ virtual void InputDisconnectedEvent(const QString &input, int element,
+ Node *output) override;
+ void generate_processor();
+
+ virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
+ NodeValueTable *table) const override;
+
+ static const QString k_lift_input;
+ static const QString k_gain_input;
+ static const QString k_gamma_input;
+ static const QString k_saturation_input;
+ static const QString k_pivot_input;
+ static const QString k_clamp_black_enable_input;
+ static const QString k_clamp_black_input;
+ static const QString k_clamp_white_enable_input;
+ static const QString k_clamp_white_input;
+
+protected slots:
+ virtual void config_changed() override;
+
+private:
+ void set_vec4_input_colors(const QString &input);
+
+ /**
+ * @brief Constrains the white clamp UI minimum to just above the black
+ * clamp, as required by ocio::GradingPrimary::validate
+ *
+ * Only applies while the black clamp is a static value; when it is
+ * keyframed or connected the invariant is enforced per frame in Value()
+ * instead.
+ */
+ void update_clamp_white_minimum();
+};
+
+} // olive
+
+#endif
diff --git a/app/node/color/ociolut/ociolut.cpp b/app/node/color/ociolut/ociolut.cpp
index a2b14d74a..8bcf35347 100644
--- a/app/node/color/ociolut/ociolut.cpp
+++ b/app/node/color/ociolut/ociolut.cpp
@@ -77,11 +77,13 @@ OCIOLutNode::OCIOLutNode()
{
add_input(k_file_input, NodeValue::k_file, QString(),
InputFlags(k_input_flag_not_keyframable | k_input_flag_not_connectable));
- set_input_property(
- k_file_input, QStringLiteral("filter"),
- tr("LUT Files (*.cube *.3dl);;Cube LUT (*.cube);;3DL LUT (*.3dl);;All Files (*)"));
+ const QString all_luts =
+ QStringLiteral("*.") +
+ LUTLibrary::supported_extensions().join(QStringLiteral(" *."));
+ set_input_property(k_file_input, QStringLiteral("filter"),
+ tr("LUT Files (%1);;All Files (*)").arg(all_luts));
set_input_property(k_file_input, QStringLiteral("placeholder"),
- tr("Select a .cube or .3dl LUT file"));
+ tr("Select a LUT file"));
// Allow the UI to offer the global LUT library for this input
set_input_property(k_file_input, QStringLiteral("lut_library"), true);
@@ -257,10 +259,8 @@ bool OCIOLutNode::create_processor_from_inputs() const
last_path_.clear();
last_direction_ = -1;
processor_dirty_ = false;
- set_last_error(
- tr("OCIO LUT: unsupported LUT file extension (expected .cube or "
- ".3dl): %1")
- .arg(path));
+ set_last_error(tr("OCIO LUT: unsupported LUT file extension: %1")
+ .arg(path));
return false;
}
diff --git a/app/node/color/whitebalance/CMakeLists.txt b/app/node/color/whitebalance/CMakeLists.txt
new file mode 100644
index 000000000..006e8df15
--- /dev/null
+++ b/app/node/color/whitebalance/CMakeLists.txt
@@ -0,0 +1,22 @@
+# Oak - Non-Linear Video Editor
+# Copyright (C) 2026 Oak 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/color/whitebalance/whitebalance.cpp
+ node/color/whitebalance/whitebalance.h
+ PARENT_SCOPE
+)
diff --git a/app/node/color/whitebalance/whitebalance.cpp b/app/node/color/whitebalance/whitebalance.cpp
new file mode 100644
index 000000000..90e3e7e10
--- /dev/null
+++ b/app/node/color/whitebalance/whitebalance.cpp
@@ -0,0 +1,156 @@
+/***
+
+ Oak - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 "whitebalance.h"
+
+#include
+
+#include "common/filefunctions.h"
+#include "render/job/shaderjob.h"
+#include "render/texture.h"
+#include "widget/slider/floatslider.h"
+
+namespace olive
+{
+
+#define super Node
+
+const QString WhiteBalanceNode::k_texture_input = QStringLiteral("tex_in");
+const QString WhiteBalanceNode::k_temperature_input =
+ QStringLiteral("temperature_in");
+const QString WhiteBalanceNode::k_tint_input = QStringLiteral("tint_in");
+const QString WhiteBalanceNode::k_gain_input = QStringLiteral("wb_gain_in");
+
+WhiteBalanceNode::WhiteBalanceNode()
+{
+ add_input(k_texture_input, NodeValue::k_texture,
+ InputFlags(k_input_flag_not_keyframable));
+
+ add_input(k_temperature_input, NodeValue::k_float, 6500.0);
+ set_input_property(k_temperature_input, QStringLiteral("min"), 1000.0);
+ set_input_property(k_temperature_input, QStringLiteral("max"), 40000.0);
+ set_input_property(k_temperature_input, QStringLiteral("view"),
+ FloatSlider::k_normal);
+
+ add_input(k_tint_input, NodeValue::k_float, 0.0);
+ set_input_property(k_tint_input, QStringLiteral("min"), -1.0);
+ set_input_property(k_tint_input, QStringLiteral("max"), 1.0);
+ set_input_property(k_tint_input, QStringLiteral("base"), 0.01);
+
+ set_effect_input(k_texture_input);
+ set_flag(k_video_effect);
+}
+
+QString WhiteBalanceNode::name() const
+{
+ return tr("White Balance");
+}
+
+QString WhiteBalanceNode::id() const
+{
+ return QStringLiteral("org.olivevideoeditor.Olive.whitebalance");
+}
+
+QVector WhiteBalanceNode::category() const
+{
+ return { k_category_color };
+}
+
+QString WhiteBalanceNode::description() const
+{
+ return tr("Adjust white balance by color temperature and tint.");
+}
+
+void WhiteBalanceNode::retranslate()
+{
+ super::retranslate();
+
+ set_input_name(k_texture_input, tr("Input"));
+ set_input_name(k_temperature_input, tr("Temperature (K)"));
+ set_input_name(k_tint_input, tr("Tint"));
+}
+
+ShaderCode WhiteBalanceNode::get_shader_code(const ShaderRequest &request) const
+{
+ Q_UNUSED(request)
+ return ShaderCode(
+ FileFunctions::read_file_as_string(":/shaders/whitebalance.frag"));
+}
+
+void WhiteBalanceNode::value(const NodeValueRow &value,
+ const NodeGlobals &globals,
+ NodeValueTable *table) const
+{
+ Q_UNUSED(globals)
+
+ if (TexturePtr tex = value[k_texture_input].to_texture()) {
+ ShaderJob job(value);
+
+ const QVector3D gain = get_gain_for_temperature(
+ value[k_temperature_input].to_double(),
+ value[k_tint_input].to_double());
+ job.insert(k_gain_input, NodeValue(NodeValue::k_vec3, gain));
+
+ table->push(NodeValue::k_texture, tex->to_job(job), this);
+ }
+}
+
+QVector3D WhiteBalanceNode::get_gain_for_temperature(double kelvin, double tint)
+{
+ // Tanner Helland blackbody approximation (1000K-40000K), returning
+ // 0-255 per channel
+ kelvin = qBound(1000.0, kelvin, 40000.0);
+ const double t = kelvin / 100.0;
+
+ double red;
+ if (t <= 66.0) {
+ red = 255.0;
+ } else {
+ red = 329.698727446 * qPow(t - 60.0, -0.1332047592);
+ }
+
+ double green;
+ if (t <= 66.0) {
+ green = 99.4708025861 * qLn(t) - 161.1195681661;
+ } else {
+ green = 288.1221695283 * qPow(t - 60.0, -0.0755148492);
+ }
+
+ double blue;
+ if (t >= 66.0) {
+ blue = 255.0;
+ } else if (t <= 19.0) {
+ blue = 0.0;
+ } else {
+ blue = 138.5177312231 * qLn(t - 10.0) - 305.0447927307;
+ }
+
+ // Normalize to the green channel so temperature shifts do not change
+ // exposure, then let tint move along the green-magenta axis
+ red /= green;
+ blue /= green;
+ green = 1.0;
+
+ const double tint_gain = qBound(0.0, 1.0 + tint, 2.0);
+
+ return QVector3D(float(red), float(green * tint_gain), float(blue));
+}
+
+}
diff --git a/app/node/color/whitebalance/whitebalance.h b/app/node/color/whitebalance/whitebalance.h
new file mode 100644
index 000000000..23fd11a19
--- /dev/null
+++ b/app/node/color/whitebalance/whitebalance.h
@@ -0,0 +1,74 @@
+/***
+
+ Oak - Non-Linear Video Editor
+ Copyright (C) 2026 Oak 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 OAK_WHITEBALANCENODE_H
+#define OAK_WHITEBALANCENODE_H
+
+#include
+
+#include "node/node.h"
+
+namespace olive
+{
+
+/**
+ * @brief White balance correction by color temperature and tint
+ *
+ * Converts a scene illuminant temperature (in Kelvin) into per-channel RGB
+ * gains using the Tanner Helland blackbody approximation, normalized so the
+ * green channel is preserved (no exposure shift). Tint shifts the image
+ * along the green-magenta axis.
+ */
+class WhiteBalanceNode : public Node {
+ Q_OBJECT
+public:
+ WhiteBalanceNode();
+
+ NODE_DEFAULT_FUNCTIONS(WhiteBalanceNode)
+
+ virtual QString name() const override;
+ virtual QString id() const override;
+ virtual QVector category() const override;
+ virtual QString description() const override;
+
+ virtual void retranslate() override;
+
+ virtual ShaderCode get_shader_code(const ShaderRequest &request) const override;
+
+ virtual void value(const NodeValueRow &value, const NodeGlobals &globals,
+ NodeValueTable *table) const override;
+
+ /**
+ * @brief RGB gains for a given illuminant temperature and tint
+ *
+ * Extracted for testability. Kelvin is clamped to [1000, 40000]; the
+ * result is normalized so the green channel gain is 1.0 at tint 0.
+ */
+ static QVector3D get_gain_for_temperature(double kelvin, double tint);
+
+ static const QString k_texture_input;
+ static const QString k_temperature_input;
+ static const QString k_tint_input;
+ static const QString k_gain_input;
+};
+
+} // olive
+
+#endif
diff --git a/app/node/factory.cpp b/app/node/factory.cpp
index 28e672162..1590ad220 100644
--- a/app/node/factory.cpp
+++ b/app/node/factory.cpp
@@ -33,8 +33,10 @@
#include "block/transition/diptocolor/diptocolortransition.h"
#include "color/displaytransform/displaytransform.h"
#include "color/ociogradingtransformlinear/ociogradingtransformlinear.h"
+#include "color/ociogradingtransformlog/ociogradingtransformlog.h"
#include "color/ociolut/ociolut.h"
#include "color/threewaycolor/threewaycolor.h"
+#include "color/whitebalance/whitebalance.h"
#include "common/current.h"
#include "distort/cornerpin/cornerpindistortnode.h"
#include "distort/crop/cropdistortnode.h"
@@ -375,6 +377,10 @@ Node *NodeFactory::create_from_factory_index(const NodeFactory::InternalID &id)
return new DisplayTransformNode();
case k_ocio_grading_transform_linear:
return new OCIOGradingTransformLinearNode();
+ case k_ocio_grading_transform_log:
+ return new OCIOGradingTransformLogNode();
+ case k_white_balance:
+ return new WhiteBalanceNode();
case k_ocio_lut:
return new OCIOLutNode();
case k_three_way_color:
diff --git a/app/node/factory.h b/app/node/factory.h
index 50fb72bce..a7b6de0d4 100644
--- a/app/node/factory.h
+++ b/app/node/factory.h
@@ -84,6 +84,8 @@ public:
k_tile_distort,
k_swirl_distort,
k_multicam_node,
+ k_ocio_grading_transform_log,
+ k_white_balance,
// Count value
k_internal_node_count
diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp
index 0dd0c04ce..7e7b9348f 100644
--- a/app/node/project/footage/footage.cpp
+++ b/app/node/project/footage/footage.cpp
@@ -126,11 +126,20 @@ Rational Footage::verify_length_internal(Track::Type type) const
QString Footage::get_colorspace_to_use(const VideoParams ¶ms) const
{
- if (params.colorspace().isEmpty()) {
- return project()->color_manager()->get_default_input_color_space();
- } else {
+ if (!params.colorspace().isEmpty()) {
+ // The user explicitly set this stream's colorspace
return params.colorspace();
}
+
+ // No override: try auto-detecting from the media's color tags
+ const QString detected =
+ project()->color_manager()->get_colorspace_for_ffmpeg_tags(
+ params.color_primaries(), params.color_transfer());
+ if (!detected.isEmpty()) {
+ return detected;
+ }
+
+ return project()->color_manager()->get_default_input_color_space();
}
void Footage::clear()
diff --git a/app/render/lutlibrary.cpp b/app/render/lutlibrary.cpp
index 54e96b1da..b8e53e83a 100644
--- a/app/render/lutlibrary.cpp
+++ b/app/render/lutlibrary.cpp
@@ -29,6 +29,17 @@
namespace olive
{
+const QStringList &LUTLibrary::supported_extensions()
+{
+ // LUT formats OCIO FileTransform can load
+ static const QStringList extensions = {
+ QStringLiteral("cube"), QStringLiteral("3dl"), QStringLiteral("spi1d"),
+ QStringLiteral("spi3d"), QStringLiteral("spimtx"), QStringLiteral("csp"),
+ QStringLiteral("clf"), QStringLiteral("ctf"), QStringLiteral("cub"),
+ };
+ return extensions;
+}
+
bool LUTLibrary::is_supported_extension(const QString &suffix)
{
QString s = suffix;
@@ -36,8 +47,7 @@ bool LUTLibrary::is_supported_extension(const QString &suffix)
s.remove(0, 1);
}
- const QString lower = s.toLower();
- return lower == QStringLiteral("cube") || lower == QStringLiteral("3dl");
+ return supported_extensions().contains(s.toLower());
}
QStringList LUTLibrary::get_directories()
diff --git a/app/render/lutlibrary.h b/app/render/lutlibrary.h
index f171b8fb4..fc339dc16 100644
--- a/app/render/lutlibrary.h
+++ b/app/render/lutlibrary.h
@@ -37,9 +37,16 @@ namespace olive
*/
class LUTLibrary {
public:
+ /**
+ * @brief All LUT file extensions supported by the library
+ *
+ * Extensions OCIO FileTransform can load, lowercase, without the dot.
+ */
+ static const QStringList &supported_extensions();
+
/**
* @brief Returns true if the given file suffix is a supported LUT
- * extension (.cube or .3dl, case-insensitive, leading dot tolerated)
+ * extension (case-insensitive, leading dot tolerated)
*/
static bool is_supported_extension(const QString &suffix);
diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp
index 1e5553522..080ba49be 100644
--- a/app/render/videoparams.cpp
+++ b/app/render/videoparams.cpp
@@ -419,6 +419,10 @@ void VideoParams::load(QXmlStreamReader *reader)
} else if (reader->name() == QStringLiteral("colorrange")) {
set_color_range(
static_cast(reader->readElementText().toInt()));
+ } else if (reader->name() == QStringLiteral("colorprimaries")) {
+ set_color_primaries(reader->readElementText().toInt());
+ } else if (reader->name() == QStringLiteral("colortransfer")) {
+ set_color_transfer(reader->readElementText().toInt());
} else {
reader->skipCurrentElement();
}
@@ -463,6 +467,10 @@ void VideoParams::save(QXmlStreamWriter *writer) const
writer->writeTextElement(QStringLiteral("colorspace"), colorspace_);
writer->writeTextElement(QStringLiteral("colorrange"),
QString::number(color_range_));
+ writer->writeTextElement(QStringLiteral("colorprimaries"),
+ QString::number(color_primaries_));
+ writer->writeTextElement(QStringLiteral("colortransfer"),
+ QString::number(color_transfer_));
}
}
diff --git a/app/render/videoparams.h b/app/render/videoparams.h
index 277cf2032..82b963970 100644
--- a/app/render/videoparams.h
+++ b/app/render/videoparams.h
@@ -378,6 +378,33 @@ public:
color_range_ = color_range;
}
+ /**
+ * @brief Color primaries/transfer as reported by the media
+ *
+ * Raw FFmpeg AVColorPrimaries/AVColorTransferCharacteristic values
+ * (0 = unset, 2 = unspecified). Used to auto-detect the input
+ * colorspace when no explicit colorspace has been set.
+ */
+ int color_primaries() const
+ {
+ return color_primaries_;
+ }
+
+ void set_color_primaries(int p)
+ {
+ color_primaries_ = p;
+ }
+
+ int color_transfer() const
+ {
+ return color_transfer_;
+ }
+
+ void set_color_transfer(int t)
+ {
+ color_transfer_ = t;
+ }
+
int64_t get_time_in_timebase_units(const Rational &time) const;
void load(QXmlStreamReader *reader);
@@ -425,6 +452,8 @@ private:
float x_;
float y_;
ColorRange color_range_;
+ int color_primaries_ = 0;
+ int color_transfer_ = 0;
};
}
diff --git a/app/shaders/rgbwaveform.frag b/app/shaders/rgbwaveform.frag
index cb3a00830..e895e930b 100644
--- a/app/shaders/rgbwaveform.frag
+++ b/app/shaders/rgbwaveform.frag
@@ -4,6 +4,7 @@ uniform vec2 viewport;
uniform vec3 luma_coeffs;
uniform float waveform_scale;
+uniform float parade_mode;
in vec2 ove_texcoord;
out vec4 frag_color;
@@ -16,23 +17,51 @@ void main(void) {
vec4 cur_col = vec4(0.0);
float ratio = 0.0;
- for (int i = 0; float(i) < waveform_height; i++) {
- ratio = float(i) / float(waveform_height - 1.0);
- cur_col.rgb = texture(
- ove_maintex,
- vec2(ove_texcoord.x, ratio)
- ).rgb;
+ if (parade_mode > 0.5) {
+ // RGB parade: three zones side by side, one channel per zone
+ float zone = floor(ove_texcoord.x * 3.0);
+ float zone_x = fract(ove_texcoord.x * 3.0);
+ vec3 zone_color = zone < 0.5 ? vec3(1.0, 0.0, 0.0) :
+ (zone < 1.5 ? vec3(0.0, 1.0, 0.0) :
+ vec3(0.0, 0.0, 1.0));
- cur_col.w = dot(cur_col.rgb, luma_coeffs);
+ for (int i = 0; float(i) < waveform_height; i++) {
+ ratio = float(i) / float(waveform_height - 1.0);
+ vec3 sample_rgb = texture(
+ ove_maintex,
+ vec2(zone_x, ratio)
+ ).rgb;
- col += (
- step(vec4(ove_texcoord.y - quantisation), cur_col) *
- step(cur_col, vec4(ove_texcoord.y + quantisation)) *
- intensity) +
- (step(1.0 - quantisation, ove_texcoord.y) *
- step(vec4(1.0 - quantisation), cur_col) * intensity);
+ float channel = zone < 0.5 ? sample_rgb.r :
+ (zone < 1.5 ? sample_rgb.g : sample_rgb.b);
+
+ float hit = step(ove_texcoord.y - quantisation, channel) *
+ step(channel, ove_texcoord.y + quantisation) * intensity;
+ hit += step(1.0 - quantisation, ove_texcoord.y) *
+ step(1.0 - quantisation, channel) * intensity;
+
+ col.rgb += hit * zone_color;
+ }
+ } else {
+ for (int i = 0; float(i) < waveform_height; i++) {
+ ratio = float(i) / float(waveform_height - 1.0);
+ cur_col.rgb = texture(
+ ove_maintex,
+ vec2(ove_texcoord.x, ratio)
+ ).rgb;
+
+ cur_col.w = dot(cur_col.rgb, luma_coeffs);
+
+ col += (
+ step(vec4(ove_texcoord.y - quantisation), cur_col) *
+ step(cur_col, vec4(ove_texcoord.y + quantisation)) *
+ intensity) +
+ (step(1.0 - quantisation, ove_texcoord.y) *
+ step(vec4(1.0 - quantisation), cur_col) * intensity);
+ }
+
+ col.rgb += vec3(col.w);
}
- col.rgb += vec3(col.w);
frag_color = vec4(col.rgb, 1.0);
}
diff --git a/app/shaders/whitebalance.frag b/app/shaders/whitebalance.frag
new file mode 100644
index 000000000..b8d02d205
--- /dev/null
+++ b/app/shaders/whitebalance.frag
@@ -0,0 +1,15 @@
+uniform sampler2D tex_in;
+
+uniform vec3 wb_gain_in;
+
+in vec2 ove_texcoord;
+out vec4 frag_color;
+
+void main(void)
+{
+ vec4 source = texture(tex_in, ove_texcoord);
+
+ // Deliberately not clamped: white balance must also work on HDR/linear
+ // footage with values above 1.0
+ frag_color = vec4(source.rgb * wb_gain_in, source.a);
+}
diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp
index 82d672cd6..c136facbe 100644
--- a/app/widget/scope/waveform/waveform.cpp
+++ b/app/widget/scope/waveform/waveform.cpp
@@ -22,6 +22,8 @@
#include "waveform.h"
+#include
+#include
#include
#include
#include
@@ -39,9 +41,27 @@ namespace olive
WaveformScope::WaveformScope(QWidget *parent)
: super(parent)
+ , parade_mode_(OAK_CONFIG("WaveformRgbParade").toBool())
{
}
+void WaveformScope::set_parade_mode(bool enabled)
+{
+ parade_mode_ = enabled;
+ OAK_CONFIG("WaveformRgbParade") = enabled;
+ update();
+}
+
+void WaveformScope::contextMenuEvent(QContextMenuEvent *event)
+{
+ QMenu menu(this);
+ QAction *parade = menu.addAction(tr("RGB Parade"));
+ parade->setCheckable(true);
+ parade->setChecked(parade_mode_);
+ connect(parade, &QAction::triggered, this, &WaveformScope::set_parade_mode);
+ menu.exec(event->globalPos());
+}
+
ShaderCode WaveformScope::generate_shader_code()
{
return ShaderCode(
@@ -72,6 +92,10 @@ void WaveformScope::draw_scope(TexturePtr managed_tex, QVariant pipeline)
job.insert(QStringLiteral("waveform_scale"),
NodeValue(NodeValue::k_float, waveform_scale));
+ // Overlay vs. RGB parade display
+ job.insert(QStringLiteral("parade_mode"),
+ NodeValue(NodeValue::k_float, parade_mode_ ? 1.0f : 0.0f));
+
// Insert source texture
job.insert(QStringLiteral("ove_maintex"),
NodeValue(NodeValue::k_texture,
@@ -156,26 +180,40 @@ void WaveformScope::draw_scope_software(QPainter &p, const QImage &image)
continue;
}
- auto mark = [&](float value, int add_r, int add_g, int add_b) {
+ auto mark = [&](int x, float value, int add_r, int add_g, int add_b) {
int scope_y =
waveform_start_dim_y + int((1.0f - value) * waveform_dim_y);
- if (scope_y < waveform_start_dim_y ||
+ if (x < waveform_start_dim_x || x >= waveform_end_dim_x ||
+ scope_y < waveform_start_dim_y ||
scope_y >= waveform_start_dim_y + waveform_dim_y) {
return;
}
- QRgb *dst_line =
- reinterpret_cast(buf.scanLine(scope_y));
- QRgb cur = dst_line[scope_x];
+ QRgb *dst_line = reinterpret_cast(buf.scanLine(scope_y));
+ QRgb cur = dst_line[x];
int nr = qMin(255, qRed(cur) + add_r);
int ng = qMin(255, qGreen(cur) + add_g);
int nb = qMin(255, qBlue(cur) + add_b);
int na = qMax(qMax(nr, ng), nb);
- dst_line[scope_x] = qRgba(nr, ng, nb, na);
+ dst_line[x] = qRgba(nr, ng, nb, na);
};
- mark(r, 30, 0, 0);
- mark(g, 0, 30, 0);
- mark(b, 0, 0, 30);
+ if (parade_mode_) {
+ // RGB parade: each channel gets one third of the scope width
+ const float zone_x = float(sx) / float(src_w) / 3.0f;
+ mark(waveform_start_dim_x +
+ int((0.0f + zone_x) * waveform_dim_x),
+ r, 30, 0, 0);
+ mark(waveform_start_dim_x +
+ int((1.0f / 3.0f + zone_x) * waveform_dim_x),
+ g, 0, 30, 0);
+ mark(waveform_start_dim_x +
+ int((2.0f / 3.0f + zone_x) * waveform_dim_x),
+ b, 0, 0, 30);
+ } else {
+ mark(scope_x, r, 30, 0, 0);
+ mark(scope_x, g, 0, 30, 0);
+ mark(scope_x, b, 0, 0, 30);
+ }
}
}
diff --git a/app/widget/scope/waveform/waveform.h b/app/widget/scope/waveform/waveform.h
index 2b07132ca..6b2db09b6 100644
--- a/app/widget/scope/waveform/waveform.h
+++ b/app/widget/scope/waveform/waveform.h
@@ -34,12 +34,24 @@ public:
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR(WaveformScope)
+ bool parade_mode() const
+ {
+ return parade_mode_;
+ }
+
+ void set_parade_mode(bool enabled);
+
protected:
virtual ShaderCode generate_shader_code() override;
virtual void draw_scope(TexturePtr managed_tex, QVariant pipeline) override;
virtual void draw_scope_software(QPainter &p, const QImage &image) override;
+
+ virtual void contextMenuEvent(QContextMenuEvent *event) override;
+
+private:
+ bool parade_mode_;
};
}
diff --git a/docs/project-file-reference.md b/docs/project-file-reference.md
index b4c858eb9..5fd3ea7b7 100644
--- a/docs/project-file-reference.md
+++ b/docs/project-file-reference.md
@@ -517,6 +517,8 @@ Serialized inside `