render: properly defined "out of bounds time" behavior with footage
If a user extends a clip beyond the footage's length, they can choose whether they want a black frame, the last frame continued, or for the footage to loop infinitely (same for extending earlier than the start of footage).
This commit is contained in:
+39
-7
@@ -107,7 +107,7 @@ FramePtr Decoder::RetrieveVideo(const rational &timecode, const int ÷r)
|
||||
return RetrieveVideoInternal(timecode, divider);
|
||||
}
|
||||
|
||||
SampleBufferPtr Decoder::RetrieveAudio(const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, const QAtomicInt *cancelled)
|
||||
SampleBufferPtr Decoder::RetrieveAudio(const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, Footage::LoopMode loop_mode, const QAtomicInt *cancelled)
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
|
||||
@@ -133,7 +133,7 @@ SampleBufferPtr Decoder::RetrieveAudio(const TimeRange &range, const AudioParams
|
||||
}
|
||||
|
||||
// See if we got the conform
|
||||
SampleBufferPtr buffer = RetrieveAudioFromConform(conform_filename, range);
|
||||
SampleBufferPtr buffer = RetrieveAudioFromConform(conform_filename, range, loop_mode);
|
||||
|
||||
if (!buffer) {
|
||||
// We'll need to conform this ourselves
|
||||
@@ -151,7 +151,7 @@ SampleBufferPtr Decoder::RetrieveAudio(const TimeRange &range, const AudioParams
|
||||
QFile::rename(working_fn, conform_filename);
|
||||
|
||||
// Return audio as planned
|
||||
buffer = RetrieveAudioFromConform(conform_filename, range);
|
||||
buffer = RetrieveAudioFromConform(conform_filename, range, loop_mode);
|
||||
} else {
|
||||
// Failed
|
||||
qCritical() << "Failed to conform audio";
|
||||
@@ -302,16 +302,48 @@ bool Decoder::ConformAudioInternal(const QString& filename, const AudioParams &p
|
||||
return false;
|
||||
}
|
||||
|
||||
SampleBufferPtr Decoder::RetrieveAudioFromConform(const QString &conform_filename, const TimeRange& range)
|
||||
SampleBufferPtr Decoder::RetrieveAudioFromConform(const QString &conform_filename, const TimeRange& range, Footage::LoopMode loop_mode)
|
||||
{
|
||||
WaveInput input(conform_filename);
|
||||
|
||||
if (input.open()) {
|
||||
const AudioParams& input_params = input.params();
|
||||
|
||||
// Read bytes from wav
|
||||
QByteArray packed_data = input.read(input_params.time_to_bytes(range.in()),
|
||||
input_params.time_to_bytes(range.length()));
|
||||
QByteArray packed_data(input_params.time_to_bytes(range.length()), Qt::Uninitialized);
|
||||
|
||||
qint64 read_index = input_params.time_to_bytes(range.in());
|
||||
qint64 write_index = 0;
|
||||
|
||||
while (write_index < packed_data.size()) {
|
||||
if (loop_mode == Footage::kLoopModeLoop) {
|
||||
while (read_index >= input.data_length()) {
|
||||
read_index -= input.data_length();
|
||||
}
|
||||
|
||||
while (read_index < 0) {
|
||||
read_index += input.data_length();
|
||||
}
|
||||
}
|
||||
|
||||
qint64 write_count = 0;
|
||||
|
||||
if (read_index < 0) {
|
||||
// Reading before 0, write silence here until audio data would actually start
|
||||
write_count = qMin(-read_index, qint64(packed_data.size()));
|
||||
memset(packed_data.data() + write_index, 0, write_count);
|
||||
} else if (read_index >= input.data_length()) {
|
||||
// Reading after data length, write silence until the end of the buffer
|
||||
write_count = packed_data.size() - write_index;
|
||||
memset(packed_data.data() + write_index, 0, write_count);
|
||||
} else {
|
||||
write_count = qMin(input.data_length() - read_index, packed_data.size() - write_index);
|
||||
input.read(read_index, packed_data.data() + write_index, write_count);
|
||||
}
|
||||
|
||||
read_index += write_count;
|
||||
write_index += write_count;
|
||||
}
|
||||
|
||||
input.close();
|
||||
|
||||
// Create sample buffer
|
||||
|
||||
+2
-2
@@ -163,7 +163,7 @@ public:
|
||||
*
|
||||
* This function is thread safe and can only run while the decoder is open. \see Open()
|
||||
*/
|
||||
SampleBufferPtr RetrieveAudio(const TimeRange& range, const AudioParams& params, const QString &cache_path, const QAtomicInt *cancelled);
|
||||
SampleBufferPtr RetrieveAudio(const TimeRange& range, const AudioParams& params, const QString &cache_path, Footage::LoopMode loop_mode, const QAtomicInt *cancelled);
|
||||
|
||||
/**
|
||||
* @brief Generate a Footage object from a file
|
||||
@@ -276,7 +276,7 @@ signals:
|
||||
void IndexProgress(double);
|
||||
|
||||
private:
|
||||
SampleBufferPtr RetrieveAudioFromConform(const QString& conform_filename, const TimeRange &range);
|
||||
SampleBufferPtr RetrieveAudioFromConform(const QString& conform_filename, const TimeRange &range, Footage::LoopMode loop_mode);
|
||||
|
||||
CodecStream stream_;
|
||||
|
||||
|
||||
+10
-8
@@ -158,38 +158,40 @@ bool WaveInput::is_open() const
|
||||
return file_.isOpen();
|
||||
}
|
||||
|
||||
QByteArray WaveInput::read(int length)
|
||||
QByteArray WaveInput::read(qint64 length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return file_.read(qMin(calculate_max_read(), static_cast<qint64>(length)));
|
||||
return file_.read(qMin(calculate_max_read(), length));
|
||||
}
|
||||
|
||||
QByteArray WaveInput::read(int offset, int length)
|
||||
QByteArray WaveInput::read(qint64 offset, qint64 length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
seek(offset);
|
||||
return file_.read(qMin(calculate_max_read(), static_cast<qint64>(length)));
|
||||
return file_.read(qMin(calculate_max_read(), length));
|
||||
}
|
||||
|
||||
qint64 WaveInput::read(int offset, char *buffer, int length)
|
||||
qint64 WaveInput::read(qint64 offset, char *buffer, qint64 length)
|
||||
{
|
||||
if (!is_open()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Q_ASSERT(length > 0);
|
||||
|
||||
seek(offset);
|
||||
return file_.read(buffer, qMin(calculate_max_read(), static_cast<qint64>(length)));
|
||||
return file_.read(buffer, qMin(calculate_max_read(), length));
|
||||
}
|
||||
|
||||
bool WaveInput::seek(qint64 pos)
|
||||
{
|
||||
return file_.seek(data_position_ + qMin(pos, static_cast<qint64>(data_size_)));
|
||||
return file_.seek(data_position_ + qMin(pos, qint64(data_size_)));
|
||||
}
|
||||
|
||||
bool WaveInput::at_end() const
|
||||
@@ -216,7 +218,7 @@ const quint32 &WaveInput::data_length() const
|
||||
|
||||
int WaveInput::sample_count() const
|
||||
{
|
||||
return params_.bytes_to_samples(static_cast<int>(data_size_));
|
||||
return params_.bytes_to_samples(data_size_);
|
||||
}
|
||||
|
||||
bool WaveInput::find_str(QFile *f, const char *str)
|
||||
|
||||
@@ -40,9 +40,9 @@ public:
|
||||
|
||||
bool is_open() const;
|
||||
|
||||
QByteArray read(int length);
|
||||
QByteArray read(int offset, int length);
|
||||
qint64 read(int offset, char *buffer, int length);
|
||||
QByteArray read(qint64 length);
|
||||
QByteArray read(qint64 offset, qint64 length);
|
||||
qint64 read(qint64 offset, char *buffer, qint64 length);
|
||||
|
||||
bool seek(qint64 pos);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "common/clamp.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "common/xmlutils.h"
|
||||
#include "config/config.h"
|
||||
@@ -36,6 +37,7 @@
|
||||
namespace olive {
|
||||
|
||||
const QString Footage::kFilenameInput = QStringLiteral("file_in");
|
||||
const QString Footage::kLoopModeInput = QStringLiteral("loop_in");
|
||||
|
||||
#define super ViewerOutput
|
||||
|
||||
@@ -46,6 +48,9 @@ Footage::Footage(const QString &filename) :
|
||||
SetCacheTextures(true);
|
||||
SetViewerVideoCacheEnabled(false);
|
||||
|
||||
PrependInput(kLoopModeInput, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
|
||||
IgnoreHashingFrom(kLoopModeInput);
|
||||
|
||||
PrependInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
|
||||
|
||||
Clear();
|
||||
@@ -58,12 +63,14 @@ void Footage::Retranslate()
|
||||
super::Retranslate();
|
||||
|
||||
SetInputName(kFilenameInput, tr("Filename"));
|
||||
SetInputName(kLoopModeInput, tr("Loop Mode"));
|
||||
SetComboBoxStrings(kLoopModeInput, {tr("None"), tr("Loop"), tr("Clamp")});
|
||||
}
|
||||
|
||||
QVector<QString> Footage::inputs_for_output(const QString &output) const
|
||||
{
|
||||
Q_UNUSED(output)
|
||||
return {kFilenameInput};
|
||||
return {kFilenameInput, kLoopModeInput};
|
||||
}
|
||||
|
||||
bool Footage::LoadCustom(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled)
|
||||
@@ -226,6 +233,11 @@ void Footage::SetValid()
|
||||
valid_ = true;
|
||||
}
|
||||
|
||||
Footage::LoopMode Footage::loop_mode() const
|
||||
{
|
||||
return static_cast<LoopMode>(GetStandardValue(kLoopModeInput).toInt());
|
||||
}
|
||||
|
||||
QString Footage::filename() const
|
||||
{
|
||||
return GetStandardValue(kFilenameInput).toString();
|
||||
@@ -333,10 +345,14 @@ void Footage::Hash(const QString& output, QCryptographicHash &hash, const ration
|
||||
|
||||
// Footage timestamp
|
||||
if (params.video_type() != VideoParams::kVideoTypeStill) {
|
||||
int64_t video_ts = Timecode::time_to_timestamp(time, params.time_base());
|
||||
rational adjusted_time = AdjustTimeByLoopMode(time, loop_mode(), GetLength());
|
||||
|
||||
// Add timestamp in units of the video stream's timebase
|
||||
hash.addData(reinterpret_cast<const char*>(&video_ts), sizeof(video_ts));
|
||||
if (!adjusted_time.isNaN()) {
|
||||
int64_t video_ts = Timecode::time_to_timestamp(adjusted_time, params.time_base());
|
||||
|
||||
// Add timestamp in units of the video stream's timebase
|
||||
hash.addData(reinterpret_cast<const char*>(&video_ts), sizeof(video_ts));
|
||||
}
|
||||
|
||||
// Add start time - used for both image sequences and video streams
|
||||
auto start_time = params.start_time();
|
||||
@@ -354,32 +370,29 @@ NodeValueTable Footage::Value(const QString &output, NodeValueDatabase &value) c
|
||||
// Pop filename from table
|
||||
QString file = value[kFilenameInput].Take(NodeValue::kFile).toString();
|
||||
|
||||
LoopMode loop_mode = static_cast<LoopMode>(value[kLoopModeInput].Take(NodeValue::kCombo).toInt());
|
||||
|
||||
// Merge table
|
||||
NodeValueTable table = value.Merge();
|
||||
|
||||
// If the file exists and the reference is valid, push a footage job to the renderer
|
||||
if (QFileInfo(file).exists()) {
|
||||
FootageJob job(decoder_, filename(), ref.type());
|
||||
|
||||
rational length;
|
||||
FootageJob job(decoder_, filename(), ref.type(), GetLength(), loop_mode);
|
||||
|
||||
if (ref.type() == Track::kVideo) {
|
||||
VideoParams vp = GetVideoParams(ref.index());
|
||||
|
||||
// Ensure the colorspace is valid and not empty
|
||||
vp.set_colorspace(GetColorspaceToUse(vp));
|
||||
length = Timecode::timestamp_to_time(vp.duration(), vp.time_base());
|
||||
|
||||
job.set_video_params(vp);
|
||||
} else {
|
||||
AudioParams ap = GetAudioParams(ref.index());
|
||||
job.set_audio_params(ap);
|
||||
job.set_cache_path(project()->cache_path());
|
||||
length = Timecode::timestamp_to_time(ap.duration(), ap.time_base());
|
||||
}
|
||||
|
||||
table.Push(NodeValue::kRational, QVariant::fromValue(length), this, false, QStringLiteral("length"));
|
||||
|
||||
table.Push(NodeValue::kRational, QVariant::fromValue(GetLength()), this, false, QStringLiteral("length"));
|
||||
table.Push(NodeValue::kFootageJob, QVariant::fromValue(job), this);
|
||||
}
|
||||
|
||||
@@ -423,6 +436,38 @@ NodeOutput Footage::GetConnectedSampleOutput()
|
||||
}
|
||||
}
|
||||
|
||||
bool TimeIsOutOfBounds(const rational& time, const rational& length)
|
||||
{
|
||||
return time < 0 || time >= length;
|
||||
}
|
||||
|
||||
rational Footage::AdjustTimeByLoopMode(rational time, Footage::LoopMode loop_mode, const rational &length)
|
||||
{
|
||||
if (TimeIsOutOfBounds(time, length)) {
|
||||
switch (loop_mode) {
|
||||
case kLoopModeOff:
|
||||
// Return no time to indicate no frame should be shown here
|
||||
time = rational::NaN;
|
||||
break;
|
||||
case kLoopModeClamp:
|
||||
// Clamp footage time to length
|
||||
time = clamp(time, rational(0), length);
|
||||
break;
|
||||
case kLoopModeLoop:
|
||||
// Loop footage time around job length
|
||||
do {
|
||||
if (time >= length) {
|
||||
time -= length;
|
||||
} else {
|
||||
time += length;
|
||||
}
|
||||
} while (TimeIsOutOfBounds(time, length));
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
void Footage::UpdateTooltip()
|
||||
{
|
||||
if (valid_) {
|
||||
|
||||
@@ -44,6 +44,12 @@ class Footage : public ViewerOutput
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum LoopMode {
|
||||
kLoopModeOff,
|
||||
kLoopModeLoop,
|
||||
kLoopModeClamp
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Footage Constructor
|
||||
*/
|
||||
@@ -102,6 +108,11 @@ public:
|
||||
*/
|
||||
void SetValid();
|
||||
|
||||
/**
|
||||
* @brief Get currently set loop mode
|
||||
*/
|
||||
LoopMode loop_mode() const;
|
||||
|
||||
/**
|
||||
* @brief Return the current filename of this Footage object
|
||||
*/
|
||||
@@ -180,7 +191,10 @@ public:
|
||||
|
||||
virtual NodeOutput GetConnectedSampleOutput() override;
|
||||
|
||||
static rational AdjustTimeByLoopMode(rational time, LoopMode loop_mode, const rational& length);
|
||||
|
||||
static const QString kFilenameInput;
|
||||
static const QString kLoopModeInput;
|
||||
|
||||
protected:
|
||||
/**
|
||||
|
||||
@@ -285,10 +285,14 @@ void NodeTraverser::PostProcessTable(const Node *node, const QString& output, co
|
||||
FootageJob job = v.data().value<FootageJob>();
|
||||
|
||||
if (job.type() == Track::kVideo) {
|
||||
QVariant value = ProcessVideoFootage(job, range.in());
|
||||
rational footage_time = Footage::AdjustTimeByLoopMode(range.in(), job.loop_mode(), job.length());
|
||||
|
||||
if (!value.isNull()) {
|
||||
output_params.Push(NodeValue::kTexture, value, node);
|
||||
if (!footage_time.isNaN()) {
|
||||
QVariant value = ProcessVideoFootage(job, footage_time);
|
||||
|
||||
if (!value.isNull()) {
|
||||
output_params.Push(NodeValue::kTexture, value, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,17 @@ class FootageJob
|
||||
{
|
||||
public:
|
||||
FootageJob() :
|
||||
type_(Track::kNone)
|
||||
type_(Track::kNone),
|
||||
loop_mode_(Footage::kLoopModeOff)
|
||||
{
|
||||
}
|
||||
|
||||
FootageJob(const QString& decoder, const QString& filename, Track::Type type) :
|
||||
FootageJob(const QString& decoder, const QString& filename, Track::Type type, const rational& length, Footage::LoopMode loop_mode) :
|
||||
decoder_(decoder),
|
||||
filename_(filename),
|
||||
type_(type)
|
||||
type_(type),
|
||||
length_(length),
|
||||
loop_mode_(loop_mode)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -85,6 +88,26 @@ public:
|
||||
cache_path_ = p;
|
||||
}
|
||||
|
||||
const rational& length() const
|
||||
{
|
||||
return length_;
|
||||
}
|
||||
|
||||
void set_length(const rational& length)
|
||||
{
|
||||
length_ = length;
|
||||
}
|
||||
|
||||
Footage::LoopMode loop_mode() const
|
||||
{
|
||||
return loop_mode_;
|
||||
}
|
||||
|
||||
void set_loop_mode(Footage::LoopMode loop_mode)
|
||||
{
|
||||
loop_mode_ = loop_mode;
|
||||
}
|
||||
|
||||
private:
|
||||
QString decoder_;
|
||||
|
||||
@@ -98,6 +121,10 @@ private:
|
||||
|
||||
QString cache_path_;
|
||||
|
||||
rational length_;
|
||||
|
||||
Footage::LoopMode loop_mode_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -424,6 +424,7 @@ QVariant RenderProcessor::ProcessAudioFootage(const FootageJob &stream, const Ti
|
||||
|
||||
SampleBufferPtr frame = decoder->RetrieveAudio(input_time, audio_params,
|
||||
stream.cache_path(),
|
||||
stream.loop_mode(),
|
||||
&IsCancelled());
|
||||
|
||||
if (frame) {
|
||||
|
||||
Reference in New Issue
Block a user