diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 8cf1d4243..cf54907a5 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -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 diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 6b6d5e075..e4e47c327 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -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_; diff --git a/app/codec/waveinput.cpp b/app/codec/waveinput.cpp index 6f278f7c7..b7342a88c 100644 --- a/app/codec/waveinput.cpp +++ b/app/codec/waveinput.cpp @@ -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(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(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(length))); + return file_.read(buffer, qMin(calculate_max_read(), length)); } bool WaveInput::seek(qint64 pos) { - return file_.seek(data_position_ + qMin(pos, static_cast(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(data_size_)); + return params_.bytes_to_samples(data_size_); } bool WaveInput::find_str(QFile *f, const char *str) diff --git a/app/codec/waveinput.h b/app/codec/waveinput.h index 9046f536a..d8b863da1 100644 --- a/app/codec/waveinput.h +++ b/app/codec/waveinput.h @@ -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); diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 15db443b3..373fa0fdd 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -25,6 +25,7 @@ #include #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 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(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(&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(&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(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_) { diff --git a/app/node/project/footage/footage.h b/app/node/project/footage/footage.h index c195f14e1..1d45dbc04 100644 --- a/app/node/project/footage/footage.h +++ b/app/node/project/footage/footage.h @@ -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: /** diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 29d603ece..0f9f6f88c 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -285,10 +285,14 @@ void NodeTraverser::PostProcessTable(const Node *node, const QString& output, co FootageJob job = v.data().value(); 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); + } } } } diff --git a/app/render/job/footagejob.h b/app/render/job/footagejob.h index 1bbadf910..7d674b3d5 100644 --- a/app/render/job/footagejob.h +++ b/app/render/job/footagejob.h @@ -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_; + }; } diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index c1f403047..c6785fdd9 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -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) {