tests: coverage round 7 (audio, distort, filter/keying, math/transition nodes)
- node_audio_test: PanNode/VolumeNode sample math, TimeInput, ValueNode - node_distort_test: transform/crop/flip/cornerpin/mask/ripple/swirl/ tile/wave inputs, shader code, value jobs, resolution autoscale math - node_filter_keying_test: opacity/blur/dropshadow/mosaic/stroke jobs, chroma/color-difference key and despill value composition - node_math_transition_test: TrigonometryNode ops, MergeNode, TransitionBlock offsets/progress/crossfade math, SubtitleBlock, traverser globals and table caching
This commit is contained in:
@@ -71,6 +71,10 @@ add_executable(olive-gtest
|
|||||||
node_generator_test.cpp
|
node_generator_test.cpp
|
||||||
render_misc_test.cpp
|
render_misc_test.cpp
|
||||||
multicam_serializer_test.cpp
|
multicam_serializer_test.cpp
|
||||||
|
node_audio_test.cpp
|
||||||
|
node_distort_test.cpp
|
||||||
|
node_filter_keying_test.cpp
|
||||||
|
node_math_transition_test.cpp
|
||||||
timeline_marker_test.cpp
|
timeline_marker_test.cpp
|
||||||
undo_stack_test.cpp
|
undo_stack_test.cpp
|
||||||
plugin_support_test.cpp
|
plugin_support_test.cpp
|
||||||
|
|||||||
@@ -0,0 +1,802 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVector3D>
|
||||||
|
|
||||||
|
#include "node/audio/pan/pan.h"
|
||||||
|
#include "node/audio/volume/volume.h"
|
||||||
|
#include "node/color/colormanager/colormanager.h"
|
||||||
|
#include "node/globals.h"
|
||||||
|
#include "node/input/time/timeinput.h"
|
||||||
|
#include "node/input/value/valuenode.h"
|
||||||
|
#include "node/keyframe.h"
|
||||||
|
#include "node/project.h"
|
||||||
|
#include "node/traverser.h"
|
||||||
|
#include "olive/core/render/audioparams.h"
|
||||||
|
#include "olive/core/render/samplebuffer.h"
|
||||||
|
#include "olive/core/util/rational.h"
|
||||||
|
#include "render/job/samplejob.h"
|
||||||
|
#include "widget/slider/floatslider.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Minimal node that emits a single configurable value, used to feed the
|
||||||
|
// samples input of the audio nodes without any decoder or audio hardware.
|
||||||
|
class ConstantValueNode : public olive::Node {
|
||||||
|
public:
|
||||||
|
ConstantValueNode() = default;
|
||||||
|
|
||||||
|
NODE_DEFAULT_FUNCTIONS(ConstantValueNode)
|
||||||
|
|
||||||
|
virtual QString Name() const override
|
||||||
|
{
|
||||||
|
return QStringLiteral("Test Constant");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual QString id() const override
|
||||||
|
{
|
||||||
|
return QStringLiteral("org.oak.test.constant");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual QVector<CategoryID> Category() const override
|
||||||
|
{
|
||||||
|
return { kCategoryGenerator };
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetOutput(const olive::NodeValue &value)
|
||||||
|
{
|
||||||
|
output_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Value(const olive::NodeValueRow &value,
|
||||||
|
const olive::NodeGlobals &globals,
|
||||||
|
olive::NodeValueTable *table) const override
|
||||||
|
{
|
||||||
|
Q_UNUSED(value)
|
||||||
|
Q_UNUSED(globals)
|
||||||
|
|
||||||
|
table->Push(output_);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
olive::NodeValue output_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Traverser that resolves SampleJobs on the CPU (no audio hardware) so the
|
||||||
|
// non-static (job) paths of the audio nodes can be verified end to end.
|
||||||
|
class SampleResolvingTraverser : public olive::NodeTraverser {
|
||||||
|
public:
|
||||||
|
void Resolve(olive::NodeValue &value)
|
||||||
|
{
|
||||||
|
ResolveJobs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual olive::core::SampleBuffer
|
||||||
|
CreateSampleBuffer(const olive::core::AudioParams ¶ms,
|
||||||
|
int sample_count) override
|
||||||
|
{
|
||||||
|
return olive::core::SampleBuffer(params, size_t(sample_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ProcessSamples(olive::core::SampleBuffer &destination,
|
||||||
|
const olive::Node *node,
|
||||||
|
const olive::TimeRange &range,
|
||||||
|
const olive::SampleJob &job) override
|
||||||
|
{
|
||||||
|
Q_UNUSED(range)
|
||||||
|
|
||||||
|
for (size_t i = 0; i < destination.sample_count(); i++) {
|
||||||
|
node->ProcessSamples(job.GetValues(), job.samples(), destination,
|
||||||
|
int(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T> T *AddNode(olive::Project *project)
|
||||||
|
{
|
||||||
|
T *node = new T();
|
||||||
|
node->setParent(project);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstantValueNode *AddConstant(olive::Project *project,
|
||||||
|
const olive::NodeValue &value)
|
||||||
|
{
|
||||||
|
auto *node = new ConstantValueNode();
|
||||||
|
node->setParent(project);
|
||||||
|
node->SetOutput(value);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::NodeKeyframe *AddKey(olive::Node *node, const QString &input,
|
||||||
|
const olive::core::rational &time,
|
||||||
|
const QVariant &value)
|
||||||
|
{
|
||||||
|
auto *key = new olive::NodeKeyframe(
|
||||||
|
time, value, olive::NodeKeyframe::kLinear, 0, -1, input);
|
||||||
|
key->setParent(node);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh traverser per call: NodeTraverser caches tables per node+range, so
|
||||||
|
// reusing one would return stale results after the node's parameters change.
|
||||||
|
olive::NodeValueTable GenerateTableAt(const olive::Node *node,
|
||||||
|
const olive::core::rational &time)
|
||||||
|
{
|
||||||
|
olive::NodeTraverser traverser;
|
||||||
|
return traverser.GenerateTable(
|
||||||
|
node, olive::TimeRange(time, time + olive::core::rational(1, 30)));
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::core::AudioParams StereoParams()
|
||||||
|
{
|
||||||
|
return olive::core::AudioParams(48000, olive::core::kChannelLayoutStereo,
|
||||||
|
olive::core::SampleFormat::F32P);
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::core::AudioParams MonoParams()
|
||||||
|
{
|
||||||
|
return olive::core::AudioParams(48000, olive::core::kChannelLayoutMono,
|
||||||
|
olive::core::SampleFormat::F32P);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a stereo buffer with the given per-channel samples. Both channels
|
||||||
|
// must have the same number of samples.
|
||||||
|
olive::core::SampleBuffer MakeStereoBuffer(const std::vector<float> &channel0,
|
||||||
|
const std::vector<float> &channel1)
|
||||||
|
{
|
||||||
|
olive::core::SampleBuffer buffer(StereoParams(), channel0.size());
|
||||||
|
for (size_t i = 0; i < channel0.size(); i++) {
|
||||||
|
buffer.data(0)[i] = channel0[i];
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < channel1.size(); i++) {
|
||||||
|
buffer.data(1)[i] = channel1[i];
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::core::SampleBuffer MakeMonoBuffer(const std::vector<float> &samples)
|
||||||
|
{
|
||||||
|
olive::core::SampleBuffer buffer(MonoParams(), samples.size());
|
||||||
|
for (size_t i = 0; i < samples.size(); i++) {
|
||||||
|
buffer.data(0)[i] = samples[i];
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::NodeValue SampleValue(const olive::core::SampleBuffer &buffer)
|
||||||
|
{
|
||||||
|
return olive::NodeValue(olive::NodeValue::kSamples,
|
||||||
|
QVariant::fromValue(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects a stereo buffer {1,2,3,4}/{5,6,7,8} to the node's samples input.
|
||||||
|
ConstantValueNode *ConnectTestSamples(olive::Project *project,
|
||||||
|
olive::Node *node,
|
||||||
|
const QString &samples_input)
|
||||||
|
{
|
||||||
|
ConstantValueNode *samples = AddConstant(
|
||||||
|
project, SampleValue(MakeStereoBuffer({ 1.0f, 2.0f, 3.0f, 4.0f },
|
||||||
|
{ 5.0f, 6.0f, 7.0f, 8.0f })));
|
||||||
|
olive::Node::ConnectEdge(samples, olive::NodeInput(node, samples_input));
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(PanNode, Metadata)
|
||||||
|
{
|
||||||
|
olive::PanNode pan;
|
||||||
|
|
||||||
|
EXPECT_EQ(pan.Name(), QStringLiteral("Pan"));
|
||||||
|
EXPECT_EQ(pan.id(), QStringLiteral("org.olivevideoeditor.Olive.pan"));
|
||||||
|
EXPECT_FALSE(pan.Description().isEmpty());
|
||||||
|
EXPECT_TRUE(pan.Category().contains(olive::Node::kCategoryFilter));
|
||||||
|
|
||||||
|
// Registered as an audio effect with the samples input as effect input
|
||||||
|
EXPECT_TRUE(pan.GetFlags() & olive::Node::kAudioEffect);
|
||||||
|
EXPECT_EQ(pan.GetEffectInputID(), olive::PanNode::kSamplesInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, InputDefaults)
|
||||||
|
{
|
||||||
|
olive::PanNode pan;
|
||||||
|
|
||||||
|
EXPECT_EQ(pan.GetInputDataType(olive::PanNode::kSamplesInput),
|
||||||
|
olive::NodeValue::kSamples);
|
||||||
|
EXPECT_FALSE(pan.IsInputKeyframable(olive::PanNode::kSamplesInput));
|
||||||
|
EXPECT_TRUE(pan.IsInputConnectable(olive::PanNode::kSamplesInput));
|
||||||
|
|
||||||
|
EXPECT_EQ(pan.GetInputDataType(olive::PanNode::kPanningInput),
|
||||||
|
olive::NodeValue::kFloat);
|
||||||
|
EXPECT_TRUE(pan.IsInputKeyframable(olive::PanNode::kPanningInput));
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
pan.GetStandardValue(olive::PanNode::kPanningInput).toDouble(), 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(pan.GetInputProperty(olive::PanNode::kPanningInput,
|
||||||
|
QStringLiteral("min"))
|
||||||
|
.toDouble(),
|
||||||
|
-1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(pan.GetInputProperty(olive::PanNode::kPanningInput,
|
||||||
|
QStringLiteral("max"))
|
||||||
|
.toDouble(),
|
||||||
|
1.0);
|
||||||
|
EXPECT_EQ(int(pan.GetInputProperty(olive::PanNode::kPanningInput,
|
||||||
|
QStringLiteral("view"))
|
||||||
|
.toInt()),
|
||||||
|
int(olive::FloatSlider::kPercentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, RetranslateSetsInputNames)
|
||||||
|
{
|
||||||
|
olive::PanNode pan;
|
||||||
|
|
||||||
|
pan.Retranslate();
|
||||||
|
|
||||||
|
EXPECT_EQ(pan.GetInputName(olive::PanNode::kSamplesInput),
|
||||||
|
QStringLiteral("Samples"));
|
||||||
|
EXPECT_EQ(pan.GetInputName(olive::PanNode::kPanningInput),
|
||||||
|
QStringLiteral("Pan"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, StaticCenterPanLeavesSamplesUnchanged)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
ConnectTestSamples(&project, pan, olive::PanNode::kSamplesInput);
|
||||||
|
|
||||||
|
// Pan 0 is a no-op, but the (unmodified) buffer is still pushed
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
ASSERT_EQ(out.sample_count(), 4u);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1));
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, StaticRightPanAttenuatesLeftChannel)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
pan->SetStandardValue(olive::PanNode::kPanningInput, 0.5);
|
||||||
|
ConnectTestSamples(&project, pan, olive::PanNode::kSamplesInput);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1) * 0.5f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, StaticLeftPanAttenuatesRightChannel)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
pan->SetStandardValue(olive::PanNode::kPanningInput, -0.5);
|
||||||
|
ConnectTestSamples(&project, pan, olive::PanNode::kSamplesInput);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1));
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5) * 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, StaticFullRightPanSilencesLeftChannel)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
pan->SetStandardValue(olive::PanNode::kPanningInput, 1.0);
|
||||||
|
ConnectTestSamples(&project, pan, olive::PanNode::kSamplesInput);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, NonStereoSamplesPassThroughUnchanged)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
pan->SetStandardValue(olive::PanNode::kPanningInput, 0.5);
|
||||||
|
|
||||||
|
ConstantValueNode *samples = AddConstant(
|
||||||
|
&project, SampleValue(MakeMonoBuffer({ 1.0f, -2.0f, 3.0f })));
|
||||||
|
olive::Node::ConnectEdge(
|
||||||
|
samples, olive::NodeInput(pan, olive::PanNode::kSamplesInput));
|
||||||
|
|
||||||
|
// Pan only supports stereo: a mono buffer is pushed through untouched
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
ASSERT_EQ(out.audio_params().channel_count(), 1);
|
||||||
|
ASSERT_EQ(out.sample_count(), 3u);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[0], 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[1], -2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[2], 3.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, NoSamplesInputProducesNoOutput)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
pan->SetStandardValue(olive::PanNode::kPanningInput, 0.5);
|
||||||
|
|
||||||
|
// Without an allocated buffer on the samples input, Value() pushes nothing
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
EXPECT_EQ(table.Get(olive::NodeValue::kSamples).type(),
|
||||||
|
olive::NodeValue::kNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, KeyframedPanProducesSampleJobButLosesPanValue)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::PanNode *pan = AddNode<olive::PanNode>(&project);
|
||||||
|
ConnectTestSamples(&project, pan, olive::PanNode::kSamplesInput);
|
||||||
|
|
||||||
|
// A non-static (keyframed) pan makes Value() push a SampleJob instead of
|
||||||
|
// processing the buffer immediately
|
||||||
|
pan->SetInputIsKeyframing(olive::PanNode::kPanningInput, true);
|
||||||
|
AddKey(pan, olive::PanNode::kPanningInput, olive::core::rational(0), 1.0);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(pan, olive::core::rational(0));
|
||||||
|
olive::NodeValue result = table.Get(olive::NodeValue::kSamples);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kSamples);
|
||||||
|
ASSERT_TRUE(result.canConvert<olive::SampleJob>());
|
||||||
|
|
||||||
|
// NOTE: Unlike VolumeNode, PanNode::Value() never inserts the panning
|
||||||
|
// value into the SampleJob, so the job's value map is empty and
|
||||||
|
// ProcessSamples() always sees a pan of 0 (a plain copy). The production
|
||||||
|
// RenderProcessor only re-evaluates inputs present in the job, so
|
||||||
|
// keyframed pan is silently ignored (suspected bug, documented here).
|
||||||
|
const olive::SampleJob job = result.value<olive::SampleJob>();
|
||||||
|
EXPECT_FALSE(job.GetValues().contains(olive::PanNode::kPanningInput));
|
||||||
|
|
||||||
|
SampleResolvingTraverser resolver;
|
||||||
|
resolver.Resolve(result);
|
||||||
|
|
||||||
|
const olive::core::SampleBuffer out = result.toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
ASSERT_EQ(out.sample_count(), 4u);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1));
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, ProcessSamplesAppliesPanPerSample)
|
||||||
|
{
|
||||||
|
olive::PanNode pan;
|
||||||
|
|
||||||
|
olive::NodeValueRow row;
|
||||||
|
row.insert(olive::PanNode::kPanningInput,
|
||||||
|
olive::NodeValue(olive::NodeValue::kFloat, 0.5));
|
||||||
|
|
||||||
|
olive::core::SampleBuffer input(StereoParams(), 2);
|
||||||
|
olive::core::SampleBuffer output(StereoParams(), 2);
|
||||||
|
input.data(0)[0] = 1.5f;
|
||||||
|
input.data(0)[1] = -2.0f;
|
||||||
|
input.data(1)[0] = 0.25f;
|
||||||
|
input.data(1)[1] = 8.0f;
|
||||||
|
|
||||||
|
pan.ProcessSamples(row, input, output, 0);
|
||||||
|
pan.ProcessSamples(row, input, output, 1);
|
||||||
|
|
||||||
|
// Panning right attenuates the left channel only
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[0], 0.75f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[1], -1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[0], 0.25f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[1], 8.0f);
|
||||||
|
|
||||||
|
// Panning left attenuates the right channel only
|
||||||
|
row.insert(olive::PanNode::kPanningInput,
|
||||||
|
olive::NodeValue(olive::NodeValue::kFloat, -0.25));
|
||||||
|
pan.ProcessSamples(row, input, output, 0);
|
||||||
|
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[0], 1.5f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[0], 0.1875f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PanNode, ProcessSamplesWithoutPanValueCopiesInput)
|
||||||
|
{
|
||||||
|
olive::PanNode pan;
|
||||||
|
|
||||||
|
// No panning value in the row: samples are copied unchanged
|
||||||
|
olive::NodeValueRow row;
|
||||||
|
|
||||||
|
olive::core::SampleBuffer input(StereoParams(), 1);
|
||||||
|
olive::core::SampleBuffer output(StereoParams(), 1);
|
||||||
|
input.data(0)[0] = 3.0f;
|
||||||
|
input.data(1)[0] = -4.0f;
|
||||||
|
|
||||||
|
pan.ProcessSamples(row, input, output, 0);
|
||||||
|
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[0], 3.0f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[0], -4.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, Metadata)
|
||||||
|
{
|
||||||
|
olive::VolumeNode volume;
|
||||||
|
|
||||||
|
EXPECT_EQ(volume.Name(), QStringLiteral("Volume"));
|
||||||
|
EXPECT_EQ(volume.id(),
|
||||||
|
QStringLiteral("org.olivevideoeditor.Olive.volume"));
|
||||||
|
EXPECT_FALSE(volume.Description().isEmpty());
|
||||||
|
EXPECT_TRUE(volume.Category().contains(olive::Node::kCategoryFilter));
|
||||||
|
|
||||||
|
EXPECT_TRUE(volume.GetFlags() & olive::Node::kAudioEffect);
|
||||||
|
EXPECT_EQ(volume.GetEffectInputID(), olive::VolumeNode::kSamplesInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, InputDefaults)
|
||||||
|
{
|
||||||
|
olive::VolumeNode volume;
|
||||||
|
|
||||||
|
EXPECT_EQ(volume.GetInputDataType(olive::VolumeNode::kSamplesInput),
|
||||||
|
olive::NodeValue::kSamples);
|
||||||
|
EXPECT_FALSE(volume.IsInputKeyframable(olive::VolumeNode::kSamplesInput));
|
||||||
|
|
||||||
|
EXPECT_EQ(volume.GetInputDataType(olive::VolumeNode::kVolumeInput),
|
||||||
|
olive::NodeValue::kFloat);
|
||||||
|
EXPECT_TRUE(volume.IsInputKeyframable(olive::VolumeNode::kVolumeInput));
|
||||||
|
EXPECT_DOUBLE_EQ(
|
||||||
|
volume.GetStandardValue(olive::VolumeNode::kVolumeInput).toDouble(),
|
||||||
|
1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(volume.GetInputProperty(olive::VolumeNode::kVolumeInput,
|
||||||
|
QStringLiteral("min"))
|
||||||
|
.toDouble(),
|
||||||
|
0.0);
|
||||||
|
EXPECT_EQ(int(volume.GetInputProperty(olive::VolumeNode::kVolumeInput,
|
||||||
|
QStringLiteral("view"))
|
||||||
|
.toInt()),
|
||||||
|
int(olive::FloatSlider::kDecibel));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, RetranslateSetsInputNames)
|
||||||
|
{
|
||||||
|
olive::VolumeNode volume;
|
||||||
|
|
||||||
|
volume.Retranslate();
|
||||||
|
|
||||||
|
EXPECT_EQ(volume.GetInputName(olive::VolumeNode::kSamplesInput),
|
||||||
|
QStringLiteral("Samples"));
|
||||||
|
EXPECT_EQ(volume.GetInputName(olive::VolumeNode::kVolumeInput),
|
||||||
|
QStringLiteral("Volume"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, StaticVolumeScalesSamples)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::VolumeNode *volume = AddNode<olive::VolumeNode>(&project);
|
||||||
|
volume->SetStandardValue(olive::VolumeNode::kVolumeInput, 2.0);
|
||||||
|
ConnectTestSamples(&project, volume, olive::VolumeNode::kSamplesInput);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(volume, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
ASSERT_EQ(out.sample_count(), 4u);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1) * 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5) * 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, StaticUnityVolumeLeavesSamplesUnchanged)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::VolumeNode *volume = AddNode<olive::VolumeNode>(&project);
|
||||||
|
volume->SetStandardValue(olive::VolumeNode::kVolumeInput, 1.0);
|
||||||
|
ConnectTestSamples(&project, volume, olive::VolumeNode::kSamplesInput);
|
||||||
|
|
||||||
|
// Volume 1 is a no-op, but the (unmodified) buffer is still pushed
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(volume, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1));
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, StaticZeroVolumeSilencesSamples)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::VolumeNode *volume = AddNode<olive::VolumeNode>(&project);
|
||||||
|
volume->SetStandardValue(olive::VolumeNode::kVolumeInput, 0.0);
|
||||||
|
ConnectTestSamples(&project, volume, olive::VolumeNode::kSamplesInput);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(volume, olive::core::rational(0));
|
||||||
|
const olive::core::SampleBuffer out =
|
||||||
|
table.Get(olive::NodeValue::kSamples).toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, KeyframedVolumeProducesSampleJobWithInterpolatedValue)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::VolumeNode *volume = AddNode<olive::VolumeNode>(&project);
|
||||||
|
ConnectTestSamples(&project, volume, olive::VolumeNode::kSamplesInput);
|
||||||
|
|
||||||
|
// A non-static (keyframed) volume makes Value() push a SampleJob carrying
|
||||||
|
// the volume value instead of processing the buffer immediately
|
||||||
|
volume->SetInputIsKeyframing(olive::VolumeNode::kVolumeInput, true);
|
||||||
|
AddKey(volume, olive::VolumeNode::kVolumeInput, olive::core::rational(0),
|
||||||
|
1.0);
|
||||||
|
AddKey(volume, olive::VolumeNode::kVolumeInput, olive::core::rational(1),
|
||||||
|
3.0);
|
||||||
|
|
||||||
|
// At t=0.5 the linear ramp 1.0 -> 3.0 interpolates to 2.0
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(volume, olive::core::rational(1, 2));
|
||||||
|
olive::NodeValue result = table.Get(olive::NodeValue::kSamples);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kSamples);
|
||||||
|
ASSERT_TRUE(result.canConvert<olive::SampleJob>());
|
||||||
|
|
||||||
|
const olive::SampleJob job = result.value<olive::SampleJob>();
|
||||||
|
ASSERT_TRUE(job.GetValues().contains(olive::VolumeNode::kVolumeInput));
|
||||||
|
EXPECT_DOUBLE_EQ(job.Get(olive::VolumeNode::kVolumeInput).toDouble(), 2.0);
|
||||||
|
|
||||||
|
// Resolve the job on the CPU and verify the scaled samples
|
||||||
|
SampleResolvingTraverser resolver;
|
||||||
|
resolver.Resolve(result);
|
||||||
|
|
||||||
|
const olive::core::SampleBuffer out = result.toSamples();
|
||||||
|
ASSERT_TRUE(out.is_allocated());
|
||||||
|
ASSERT_EQ(out.sample_count(), 4u);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1) * 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5) * 2.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, ProcessSamplesMultipliesPerSample)
|
||||||
|
{
|
||||||
|
olive::VolumeNode volume;
|
||||||
|
|
||||||
|
olive::NodeValueRow row;
|
||||||
|
row.insert(olive::VolumeNode::kVolumeInput,
|
||||||
|
olive::NodeValue(olive::NodeValue::kFloat, 2.0));
|
||||||
|
|
||||||
|
olive::core::SampleBuffer input(StereoParams(), 2);
|
||||||
|
olive::core::SampleBuffer output(StereoParams(), 2);
|
||||||
|
input.data(0)[0] = 1.5f;
|
||||||
|
input.data(0)[1] = -2.0f;
|
||||||
|
input.data(1)[0] = 0.25f;
|
||||||
|
input.data(1)[1] = 8.0f;
|
||||||
|
|
||||||
|
volume.ProcessSamples(row, input, output, 0);
|
||||||
|
volume.ProcessSamples(row, input, output, 1);
|
||||||
|
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[0], 3.0f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[1], -4.0f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[0], 0.5f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[1], 16.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VolumeNode, ProcessSamplesWithoutVolumeLeavesOutputUntouched)
|
||||||
|
{
|
||||||
|
olive::VolumeNode volume;
|
||||||
|
|
||||||
|
// Neither the samples nor the volume input carries a number: the output
|
||||||
|
// must not be written
|
||||||
|
olive::NodeValueRow row;
|
||||||
|
|
||||||
|
olive::core::SampleBuffer input(StereoParams(), 1);
|
||||||
|
olive::core::SampleBuffer output(StereoParams(), 1);
|
||||||
|
input.data(0)[0] = 10.0f;
|
||||||
|
output.data(0)[0] = 123.0f;
|
||||||
|
output.data(1)[0] = 45.0f;
|
||||||
|
|
||||||
|
volume.ProcessSamples(row, input, output, 0);
|
||||||
|
|
||||||
|
EXPECT_FLOAT_EQ(output.data(0)[0], 123.0f);
|
||||||
|
EXPECT_FLOAT_EQ(output.data(1)[0], 45.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TimeInput, Metadata)
|
||||||
|
{
|
||||||
|
olive::TimeInput time;
|
||||||
|
|
||||||
|
EXPECT_EQ(time.Name(), QStringLiteral("Time"));
|
||||||
|
EXPECT_EQ(time.id(), QStringLiteral("org.olivevideoeditor.Olive.time"));
|
||||||
|
EXPECT_FALSE(time.Description().isEmpty());
|
||||||
|
EXPECT_TRUE(time.Category().contains(olive::Node::kCategoryTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TimeInput, ValuePushesCurrentTimeInSeconds)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::TimeInput *time = AddNode<olive::TimeInput>(&project);
|
||||||
|
|
||||||
|
olive::NodeValueTable table = GenerateTableAt(time, olive::core::rational(0));
|
||||||
|
olive::NodeValue result = table.Get(olive::NodeValue::kFloat);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kFloat);
|
||||||
|
EXPECT_DOUBLE_EQ(result.toDouble(), 0.0);
|
||||||
|
|
||||||
|
table = GenerateTableAt(time, olive::core::rational(5, 2));
|
||||||
|
result = table.Get(olive::NodeValue::kFloat);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kFloat);
|
||||||
|
EXPECT_DOUBLE_EQ(result.toDouble(), 2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, Metadata)
|
||||||
|
{
|
||||||
|
olive::ValueNode value;
|
||||||
|
|
||||||
|
EXPECT_EQ(value.Name(), QStringLiteral("Value"));
|
||||||
|
EXPECT_EQ(value.id(), QStringLiteral("org.olivevideoeditor.Olive.value"));
|
||||||
|
EXPECT_FALSE(value.Description().isEmpty());
|
||||||
|
EXPECT_TRUE(value.Category().contains(olive::Node::kCategoryGenerator));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, InputDefaults)
|
||||||
|
{
|
||||||
|
olive::ValueNode value;
|
||||||
|
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kTypeInput),
|
||||||
|
olive::NodeValue::kCombo);
|
||||||
|
EXPECT_FALSE(value.IsInputConnectable(olive::ValueNode::kTypeInput));
|
||||||
|
EXPECT_FALSE(value.IsInputKeyframable(olive::ValueNode::kTypeInput));
|
||||||
|
EXPECT_EQ(value.GetStandardValue(olive::ValueNode::kTypeInput).toInt(), 0);
|
||||||
|
|
||||||
|
// The value input starts out as a float (the first supported type)
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kFloat);
|
||||||
|
EXPECT_FALSE(value.IsInputConnectable(olive::ValueNode::kValueInput));
|
||||||
|
EXPECT_TRUE(value.IsInputKeyframable(olive::ValueNode::kValueInput));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, RetranslateSetsInputNamesAndTypeCombo)
|
||||||
|
{
|
||||||
|
olive::ValueNode value;
|
||||||
|
|
||||||
|
value.Retranslate();
|
||||||
|
|
||||||
|
EXPECT_EQ(value.GetInputName(olive::ValueNode::kTypeInput),
|
||||||
|
QStringLiteral("Type"));
|
||||||
|
EXPECT_EQ(value.GetInputName(olive::ValueNode::kValueInput),
|
||||||
|
QStringLiteral("Value"));
|
||||||
|
|
||||||
|
// The type combo lists the pretty name of every supported type, in order
|
||||||
|
const QStringList types =
|
||||||
|
value.GetInputProperty(olive::ValueNode::kTypeInput,
|
||||||
|
QStringLiteral("combo_str"))
|
||||||
|
.toStringList();
|
||||||
|
ASSERT_EQ(types.size(), 11);
|
||||||
|
EXPECT_EQ(types.at(0), QStringLiteral("Float"));
|
||||||
|
EXPECT_EQ(types.at(1), QStringLiteral("Integer"));
|
||||||
|
EXPECT_EQ(types.at(2), QStringLiteral("Rational"));
|
||||||
|
EXPECT_EQ(types.at(3), QStringLiteral("Vector 2D"));
|
||||||
|
EXPECT_EQ(types.at(4), QStringLiteral("Vector 3D"));
|
||||||
|
EXPECT_EQ(types.at(5), QStringLiteral("Vector 4D"));
|
||||||
|
EXPECT_EQ(types.at(6), QStringLiteral("Color"));
|
||||||
|
EXPECT_EQ(types.at(7), QStringLiteral("Text"));
|
||||||
|
EXPECT_EQ(types.at(8), QStringLiteral("Matrix"));
|
||||||
|
EXPECT_EQ(types.at(9), QStringLiteral("Font"));
|
||||||
|
EXPECT_EQ(types.at(10), QStringLiteral("Boolean"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, ChangingTypeSwitchesValueInputDataType)
|
||||||
|
{
|
||||||
|
olive::ValueNode value;
|
||||||
|
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kFloat);
|
||||||
|
|
||||||
|
value.SetStandardValue(olive::ValueNode::kTypeInput, 1);
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kInt);
|
||||||
|
|
||||||
|
value.SetStandardValue(olive::ValueNode::kTypeInput, 3);
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kVec2);
|
||||||
|
|
||||||
|
value.SetStandardValue(olive::ValueNode::kTypeInput, 6);
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kColor);
|
||||||
|
|
||||||
|
value.SetStandardValue(olive::ValueNode::kTypeInput, 10);
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kBoolean);
|
||||||
|
|
||||||
|
value.SetStandardValue(olive::ValueNode::kTypeInput, 0);
|
||||||
|
EXPECT_EQ(value.GetInputDataType(olive::ValueNode::kValueInput),
|
||||||
|
olive::NodeValue::kFloat);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, ValuePassesThroughFloat)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::ValueNode *value = AddNode<olive::ValueNode>(&project);
|
||||||
|
value->SetStandardValue(olive::ValueNode::kValueInput, 3.5);
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(value, olive::core::rational(0));
|
||||||
|
const olive::NodeValue result = table.Get(olive::NodeValue::kFloat);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kFloat);
|
||||||
|
EXPECT_DOUBLE_EQ(result.toDouble(), 3.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ValueNode, ValuePassesThroughVectorAfterTypeSwitch)
|
||||||
|
{
|
||||||
|
olive::ColorManager::SetUpDefaultConfig();
|
||||||
|
olive::Project project;
|
||||||
|
project.Initialize();
|
||||||
|
|
||||||
|
olive::ValueNode *value = AddNode<olive::ValueNode>(&project);
|
||||||
|
|
||||||
|
// Switch the value input to kVec3 (index 4) and set a vector value
|
||||||
|
value->SetStandardValue(olive::ValueNode::kTypeInput, 4);
|
||||||
|
value->SetStandardValue(olive::ValueNode::kValueInput,
|
||||||
|
QVector3D(1.0f, 2.0f, 3.0f));
|
||||||
|
|
||||||
|
const olive::NodeValueTable table = GenerateTableAt(value, olive::core::rational(0));
|
||||||
|
const olive::NodeValue result = table.Get(olive::NodeValue::kVec3);
|
||||||
|
ASSERT_EQ(result.type(), olive::NodeValue::kVec3);
|
||||||
|
const QVector3D vec = result.toVec3();
|
||||||
|
EXPECT_FLOAT_EQ(vec.x(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(vec.y(), 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(vec.z(), 3.0f);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user