From cc21ed37a7cf1749f6ea04b99e24a1c4dc169915 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 18 May 2021 12:42:47 +1000 Subject: [PATCH] implemented core subtitle support --- app/codec/encoder.cpp | 26 ++- app/codec/encoder.h | 12 ++ app/codec/exportcodec.cpp | 30 +++ app/codec/exportcodec.h | 6 + app/codec/exportformat.cpp | 47 ++++- app/codec/exportformat.h | 2 + app/codec/ffmpeg/ffmpegdecoder.cpp | 9 +- app/codec/ffmpeg/ffmpegencoder.cpp | 99 +++++++++- app/codec/ffmpeg/ffmpegencoder.h | 5 + app/codec/oiio/oiioencoder.cpp | 5 + app/codec/oiio/oiioencoder.h | 1 + app/dialog/export/CMakeLists.txt | 10 +- app/dialog/export/export.cpp | 41 ++-- app/dialog/export/export.h | 5 + app/dialog/export/exportsubtitlestab.cpp | 47 +++++ app/dialog/export/exportsubtitlestab.h | 57 ++++++ app/dialog/export/exportvideotab.cpp | 2 +- app/node/block/CMakeLists.txt | 1 + app/node/block/clip/clip.cpp | 10 +- app/node/block/clip/clip.h | 2 +- app/node/block/subtitle/CMakeLists.txt | 22 +++ app/node/block/subtitle/subtitle.cpp | 62 ++++++ app/node/block/subtitle/subtitle.h | 60 ++++++ app/node/factory.cpp | 3 + app/node/factory.h | 1 + app/render/CMakeLists.txt | 2 + app/render/subtitleparams.cpp | 188 +++++++++++++++++++ app/render/subtitleparams.h | 55 ++++++ app/task/export/export.cpp | 14 +- app/task/export/export.h | 2 + app/task/precache/precachetask.cpp | 1 + app/task/render/render.cpp | 52 ++++- app/task/render/render.h | 6 +- app/tool/tool.h | 5 + app/widget/timelinewidget/timelineundo.h | 13 +- app/widget/timelinewidget/timelinewidget.cpp | 28 ++- app/widget/timelinewidget/tool/add.cpp | 14 +- 37 files changed, 898 insertions(+), 47 deletions(-) create mode 100644 app/dialog/export/exportsubtitlestab.cpp create mode 100644 app/dialog/export/exportsubtitlestab.h create mode 100644 app/node/block/subtitle/CMakeLists.txt create mode 100644 app/node/block/subtitle/subtitle.cpp create mode 100644 app/node/block/subtitle/subtitle.h create mode 100644 app/render/subtitleparams.cpp create mode 100644 app/render/subtitleparams.h diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 5392fff9d..9bedc6b8f 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -91,7 +91,8 @@ EncodingParams::EncodingParams() : video_threads_(0), video_is_image_sequence_(false), audio_enabled_(false), - audio_bit_rate_(0) + audio_bit_rate_(0), + subtitles_enabled_(false) { } @@ -114,6 +115,13 @@ void EncodingParams::EnableAudio(const AudioParams &audio_params, const ExportCo audio_codec_ = acodec; } +void EncodingParams::EnableSubtitles(const SubtitleParams::Encoding &encoding, const ExportCodec::Codec &scodec) +{ + subtitles_enabled_ = true; + subtitles_encoding_ = encoding; + subtitles_codec_ = scodec; +} + void EncodingParams::set_video_option(const QString &key, const QString &value) { video_opts_.insert(key, value); @@ -219,6 +227,21 @@ const AudioParams &EncodingParams::audio_params() const return audio_params_; } +bool EncodingParams::subtitles_enabled() const +{ + return subtitles_enabled_; +} + +SubtitleParams::Encoding EncodingParams::subtitles_encoding() const +{ + return subtitles_encoding_; +} + +ExportCodec::Codec EncodingParams::subtitles_codec() const +{ + return subtitles_codec_; +} + const rational &EncodingParams::GetExportLength() const { return export_length_; @@ -310,6 +333,7 @@ Encoder::Type Encoder::GetTypeFromFormat(ExportFormat::Format f) case ExportFormat::kFormatFLAC: case ExportFormat::kFormatOgg: case ExportFormat::kFormatWebM: + case ExportFormat::kFormatSRT: return kEncoderTypeFFmpeg; case ExportFormat::kFormatOpenEXR: case ExportFormat::kFormatPNG: diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 5bcb8738b..49466c6a5 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -31,7 +31,9 @@ #include "codec/frame.h" #include "codec/samplebuffer.h" #include "common/timerange.h" +#include "node/block/subtitle/subtitle.h" #include "render/audioparams.h" +#include "render/subtitleparams.h" #include "render/videoparams.h" namespace olive { @@ -47,6 +49,7 @@ public: void EnableVideo(const VideoParams& video_params, const ExportCodec::Codec& vcodec); void EnableAudio(const AudioParams& audio_params, const ExportCodec::Codec &acodec); + void EnableSubtitles(const SubtitleParams::Encoding &encoding, const ExportCodec::Codec &scodec); void set_video_option(const QString& key, const QString& value); void set_video_bit_rate(const int64_t& rate); @@ -91,6 +94,10 @@ public: audio_bit_rate_ = b; } + bool subtitles_enabled() const; + SubtitleParams::Encoding subtitles_encoding() const; + ExportCodec::Codec subtitles_codec() const; + const rational& GetExportLength() const; void SetExportLength(const rational& GetExportLength); @@ -116,6 +123,10 @@ private: AudioParams audio_params_; int64_t audio_bit_rate_; + bool subtitles_enabled_; + ExportCodec::Codec subtitles_codec_; + SubtitleParams::Encoding subtitles_encoding_; + rational export_length_; }; @@ -174,6 +185,7 @@ public slots: virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) = 0; virtual bool WriteAudio(olive::SampleBufferPtr audio) = 0; + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) = 0; virtual void Close() = 0; diff --git a/app/codec/exportcodec.cpp b/app/codec/exportcodec.cpp index f4955743c..039537651 100644 --- a/app/codec/exportcodec.cpp +++ b/app/codec/exportcodec.cpp @@ -60,6 +60,8 @@ QString ExportCodec::GetCodecName(ExportCodec::Codec c) return tr("Vorbis"); case kCodecVP9: return tr("VP9"); + case kCodecSRT: + return tr("SubRip SRT"); case kCodecCount: break; } @@ -82,6 +84,7 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) case kCodecOpus: case kCodecFLAC: case kCodecVP9: + case kCodecSRT: return false; case kCodecOpenEXR: case kCodecPNG: @@ -94,4 +97,31 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) return false; } +SubtitleParams::Encoding ExportCodec::GetDefaultSubtitleEncoding(Codec c) +{ + switch (c) { + case kCodecSRT: + return SubtitleParams::kWindows1252; + case kCodecDNxHD: + case kCodecH264: + case kCodecH265: + case kCodecProRes: + case kCodecMP2: + case kCodecMP3: + case kCodecAAC: + case kCodecPCM: + case kCodecVorbis: + case kCodecOpus: + case kCodecFLAC: + case kCodecVP9: + case kCodecOpenEXR: + case kCodecPNG: + case kCodecTIFF: + case kCodecCount: + break; + } + + return SubtitleParams::kEncodingInvalid; +} + } diff --git a/app/codec/exportcodec.h b/app/codec/exportcodec.h index 70e5cb74b..ee1a3a1c0 100644 --- a/app/codec/exportcodec.h +++ b/app/codec/exportcodec.h @@ -25,6 +25,7 @@ #include #include "common/define.h" +#include "render/subtitleparams.h" namespace olive { @@ -52,6 +53,9 @@ public: kCodecVorbis, kCodecFLAC, + // Subtitle codecs + kCodecSRT, + kCodecCount }; @@ -59,6 +63,8 @@ public: static bool IsCodecAStillImage(Codec c); + static SubtitleParams::Encoding GetDefaultSubtitleEncoding(Codec c); + }; } diff --git a/app/codec/exportformat.cpp b/app/codec/exportformat.cpp index 6b5ec1784..3b53371e0 100644 --- a/app/codec/exportformat.cpp +++ b/app/codec/exportformat.cpp @@ -53,6 +53,8 @@ QString ExportFormat::GetName(olive::ExportFormat::Format f) return tr("Ogg"); case kFormatWebM: return tr("WebM"); + case kFormatSRT: + return tr("SubRip SRT"); case kFormatCount: break; @@ -90,6 +92,8 @@ QString ExportFormat::GetExtension(ExportFormat::Format f) return QStringLiteral("ogg"); case kFormatWebM: return QStringLiteral("webm"); + case kFormatSRT: + return QStringLiteral("srt"); case kFormatCount: break; } @@ -121,7 +125,7 @@ QList ExportFormat::GetVideoCodecs(ExportFormat::Format f) case kFormatAIFF: case kFormatMP3: case kFormatFLAC: - return {}; + case kFormatSRT: case kFormatCount: break; } @@ -132,22 +136,19 @@ QList ExportFormat::GetVideoCodecs(ExportFormat::Format f) QList ExportFormat::GetAudioCodecs(ExportFormat::Format f) { switch (f) { + // Video/audio formats case kFormatDNxHD: return {ExportCodec::kCodecPCM}; case kFormatMatroska: return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus}; case kFormatMPEG4: - return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; + return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3}; case kFormatQuickTime: return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM}; case kFormatWebM: return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus}; - case kFormatOpenEXR: - case kFormatPNG: - case kFormatTIFF: - return {}; - + // Audio only formats case kFormatWAV: return {ExportCodec::kCodecPCM}; case kFormatAIFF: @@ -159,9 +160,39 @@ QList ExportFormat::GetAudioCodecs(ExportFormat::Format f) case kFormatOgg: return {ExportCodec::kCodecOpus, ExportCodec::kCodecVorbis, ExportCodec::kCodecPCM}; - + // Video only formats + case kFormatOpenEXR: + case kFormatPNG: + case kFormatTIFF: + case kFormatSRT: case kFormatCount: break; + + } + + return {}; +} + +QList ExportFormat::GetSubtitleCodecs(Format f) +{ + switch (f) { + case kFormatDNxHD: + case kFormatMPEG4: + case kFormatOpenEXR: + case kFormatQuickTime: + case kFormatPNG: + case kFormatTIFF: + case kFormatWAV: + case kFormatAIFF: + case kFormatMP3: + case kFormatFLAC: + case kFormatOgg: + case kFormatWebM: + case kFormatCount: + break; + case kFormatMatroska: + case kFormatSRT: + return {ExportCodec::kCodecSRT}; } return {}; diff --git a/app/codec/exportformat.h b/app/codec/exportformat.h index 50cf73685..36be9db58 100644 --- a/app/codec/exportformat.h +++ b/app/codec/exportformat.h @@ -47,6 +47,7 @@ public: kFormatFLAC, kFormatOgg, kFormatWebM, + kFormatSRT, kFormatCount }; @@ -55,6 +56,7 @@ public: static QString GetExtension(Format f); static QList GetVideoCodecs(ExportFormat::Format f); static QList GetAudioCodecs(ExportFormat::Format f); + static QList GetSubtitleCodecs(ExportFormat::Format f); static QStringList GetPixelFormatsForCodec(Format f, ExportCodec::Codec c); diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 5c49a6ae1..70250122a 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -272,7 +272,8 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn if (decoder && (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO - || avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)) { + || avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO + || avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)) { if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { @@ -367,7 +368,7 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn desc.AddVideoStream(stream); - } else { + } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { // Create an audio stream object uint64_t channel_layout = avstream->codecpar->channel_layout; @@ -412,6 +413,10 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn stream.set_duration(avstream->duration); desc.AddAudioStream(stream); + } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) { + + qDebug() << "Subtitle probing: Stub"; + } } diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index a5590105a..75afb11f0 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -27,6 +27,7 @@ extern "C" { #include #include "common/ffmpegutils.h" +#include "common/timecodefunctions.h" namespace olive { @@ -83,6 +84,7 @@ QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const case ExportCodec::kCodecFLAC: case ExportCodec::kCodecOpus: case ExportCodec::kCodecVorbis: + case ExportCodec::kCodecSRT: case ExportCodec::kCodecCount: // These are audio or invalid codecs and therefore have no pixel formats break; @@ -178,6 +180,13 @@ bool FFmpegEncoder::Open() } } + // Initialize a subtitle stream if it's enabled + if (params().subtitles_enabled()) { + if (!InitializeStream(AVMEDIA_TYPE_SUBTITLE, &subtitle_stream_, &subtitle_codec_ctx_, params().subtitles_codec())) { + return false; + } + } + av_dump_format(fmt_ctx_, 0, filename_c_str, 1); // Open output file for writing @@ -347,6 +356,76 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) return result; } + +QString GetAssTime(const rational &time) +{ + int64_t total_centiseconds = qRound64(time.toDouble() * 100); + + int64_t cs = total_centiseconds % 100; + int64_t ss = (total_centiseconds / 100) % 60; + int64_t mm = (total_centiseconds / 6000) % 60; + int64_t hh = total_centiseconds / 360000; + + return QStringLiteral("%1:%2:%3.%4").arg( + QString::number(hh), + QStringLiteral("%1").arg(mm, 2, 10, QLatin1Char('0')), + QStringLiteral("%1").arg(ss, 2, 10, QLatin1Char('0')), + QStringLiteral("%1").arg(cs, 2, 10, QLatin1Char('0')) + ); +} + +bool FFmpegEncoder::WriteSubtitle(const SubtitleBlock *sub_block) +{ + AVSubtitle subtitle; + memset(&subtitle, 0, sizeof(subtitle)); + + AVSubtitleRect rect; + memset(&rect, 0, sizeof(rect)); + + QString ass_line = QStringLiteral("Dialogue: 0,%1,%2,Default,,0,0,0,,%3").arg( + GetAssTime(sub_block->in()), + GetAssTime(sub_block->out()), + sub_block->GetText() + ); + + QByteArray utf8_sub = sub_block->GetText().toUtf8(); + QByteArray utf8_ass = ass_line.toUtf8(); + + rect.type = SUBTITLE_ASS; + rect.text = utf8_sub.data(); + rect.ass = utf8_ass.data(); + + AVSubtitleRect *rect_array = ▭ + subtitle.num_rects = 1; + subtitle.rects = &rect_array; + + subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), true); + subtitle.end_display_time = qRound64(sub_block->length().toDouble() * 1000); + + QVector out_buf(1024 * 1024); + + int sub_sz = avcodec_encode_subtitle(subtitle_codec_ctx_, out_buf.data(), out_buf.size(), &subtitle); + if (sub_sz < 0) { + return false; + } + + AVPacket *pkt = av_packet_alloc(); + + pkt->stream_index = subtitle_stream_->index; + pkt->data = out_buf.data(); + pkt->size = sub_sz; + pkt->pts = subtitle.pts; + pkt->duration = subtitle.end_display_time; + pkt->dts = pkt->pts; + av_packet_rescale_ts(pkt, av_get_time_base_q(), subtitle_stream_->time_base); + + av_interleaved_write_frame(fmt_ctx_, pkt); + + av_packet_free(&pkt); + + return true; +} + /* void FFmpegEncoder::WriteAudio(AudioParams pcm_info, QIODevice* file) { @@ -464,7 +543,7 @@ void FFmpegEncoder::FFmpegError(const QString& context, int error_code) char err[1024]; av_strerror(error_code, err, 1024); - QString formatted_err = tr("%1: %2 %3").arg(context, formatted_err, QString::number(error_code)); + QString formatted_err = tr("%1: %2 %3").arg(context, err, QString::number(error_code)); qDebug() << formatted_err; SetError(formatted_err); } @@ -516,8 +595,8 @@ fail: bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AVCodecContext** codec_ctx_ptr, const ExportCodec::Codec& codec) { - if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO) { - SetError(tr("Cannot initialize a stream that is not a video or audio type")); + if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO && type != AVMEDIA_TYPE_SUBTITLE) { + SetError(tr("Cannot initialize a stream that is not a video, audio, or subtitle type")); return false; } @@ -570,6 +649,9 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV case ExportCodec::kCodecFLAC: codec_id = AV_CODEC_ID_FLAC; break; + case ExportCodec::kCodecSRT: + codec_id = AV_CODEC_ID_SUBRIP; + break; case ExportCodec::kCodecCount: break; } @@ -648,7 +730,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV } } - } else { + } else if (type == AVMEDIA_TYPE_AUDIO) { // Assume audio stream codec_ctx->sample_rate = params().audio_params().sample_rate(); @@ -661,6 +743,15 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV codec_ctx->bit_rate = params().audio_bit_rate(); } + } else if (type == AVMEDIA_TYPE_SUBTITLE) { + + codec_ctx->time_base = av_get_time_base_q(); + + QByteArray ass_header = SubtitleParams::GenerateASSHeader().toUtf8(); + codec_ctx->subtitle_header = new uint8_t[ass_header.size()]; + memcpy(codec_ctx->subtitle_header, ass_header.constData(), ass_header.size()); + codec_ctx->subtitle_header_size = ass_header.size(); + } if (!SetupCodecContext(stream, codec_ctx, encoder)) { diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 24d50e7fc..cae844fc1 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -46,6 +46,8 @@ public: virtual bool WriteAudio(olive::SampleBufferPtr audio) override; + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; + virtual void Close() override; virtual VideoParams::Format GetDesiredPixelFormat() const override @@ -90,6 +92,9 @@ private: int audio_frame_offset_; int audio_write_count_; + AVStream* subtitle_stream_; + AVCodecContext* subtitle_codec_ctx_; + bool open_; }; diff --git a/app/codec/oiio/oiioencoder.cpp b/app/codec/oiio/oiioencoder.cpp index ff9d9191f..f872a758b 100644 --- a/app/codec/oiio/oiioencoder.cpp +++ b/app/codec/oiio/oiioencoder.cpp @@ -68,6 +68,11 @@ bool OIIOEncoder::WriteAudio(SampleBufferPtr audio) return false; } +bool OIIOEncoder::WriteSubtitle(const SubtitleBlock *sub_block) +{ + return false; +} + void OIIOEncoder::Close() { // Do nothing diff --git a/app/codec/oiio/oiioencoder.h b/app/codec/oiio/oiioencoder.h index 555efe067..7325cdf49 100644 --- a/app/codec/oiio/oiioencoder.h +++ b/app/codec/oiio/oiioencoder.h @@ -36,6 +36,7 @@ public slots: virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override; virtual bool WriteAudio(SampleBufferPtr audio) override; + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; virtual void Close() override; diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 0b2c76203..845d4ab91 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -18,13 +18,15 @@ add_subdirectory(codec) set(OLIVE_SOURCES ${OLIVE_SOURCES} - dialog/export/export.h dialog/export/export.cpp - dialog/export/exportadvancedvideodialog.h + dialog/export/export.h dialog/export/exportadvancedvideodialog.cpp - dialog/export/exportaudiotab.h + dialog/export/exportadvancedvideodialog.h dialog/export/exportaudiotab.cpp - dialog/export/exportvideotab.h + dialog/export/exportaudiotab.h + dialog/export/exportsubtitlestab.cpp + dialog/export/exportsubtitlestab.h dialog/export/exportvideotab.cpp + dialog/export/exportvideotab.h PARENT_SCOPE ) diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 0c3fceed8..c3d31fda9 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -124,29 +124,30 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : QHBoxLayout* av_enabled_layout = new QHBoxLayout(); video_enabled_ = new QCheckBox(tr("Export Video")); - video_enabled_->setChecked(true); av_enabled_layout->addWidget(video_enabled_); audio_enabled_ = new QCheckBox(tr("Export Audio")); - audio_enabled_->setChecked(true); av_enabled_layout->addWidget(audio_enabled_); + subtitles_enabled_ = new QCheckBox(tr("Export Subtitle")); + av_enabled_layout->addWidget(subtitles_enabled_); + preferences_layout->addLayout(av_enabled_layout, row, 0, 1, 4); row++; preferences_tabs_ = new QTabWidget(); - QScrollArea* video_area = new QScrollArea(); + color_manager_ = viewer_node_->project()->color_manager(); video_tab_ = new ExportVideoTab(color_manager_); - video_area->setWidgetResizable(true); - video_area->setWidget(video_tab_); - preferences_tabs_->addTab(video_area, tr("Video")); - QScrollArea* audio_area = new QScrollArea(); + AddPreferencesTab(video_tab_, tr("Video")); + audio_tab_ = new ExportAudioTab(); - audio_area->setWidgetResizable(true); - audio_area->setWidget(audio_tab_); - preferences_tabs_->addTab(audio_area, tr("Audio")); + AddPreferencesTab(audio_tab_, tr("Audio")); + + subtitle_tab_ = new ExportSubtitlesTab(); + AddPreferencesTab(subtitle_tab_, tr("Subtitles")); + preferences_layout->addWidget(preferences_tabs_, row, 0, 1, 4); row++; @@ -268,9 +269,9 @@ rational ExportDialog::GetSelectedTimebase() const void ExportDialog::StartExport() { - if (!video_enabled_->isChecked() && !audio_enabled_->isChecked()) { + if (!video_enabled_->isChecked() && !audio_enabled_->isChecked() && !subtitles_enabled_->isChecked()) { QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid parameters"), - tr("Both video and audio are disabled. There's nothing to export.")); + tr("Video, audio, and subtitles are disabled. There's nothing to export.")); return; } @@ -392,6 +393,14 @@ void ExportDialog::closeEvent(QCloseEvent *e) QDialog::closeEvent(e); } +void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title) +{ + QScrollArea* scroll_area = new QScrollArea(); + scroll_area->setWidgetResizable(true); + scroll_area->setWidget(inner_widget); + preferences_tabs_->addTab(scroll_area, title); +} + void ExportDialog::BrowseFilename() { ExportFormat::Format f = GetSelectedFormat(); @@ -437,6 +446,10 @@ void ExportDialog::FormatChanged(int index) bool has_audio_codecs = audio_tab_->SetFormat(current_format); audio_enabled_->setChecked(has_audio_codecs); audio_enabled_->setEnabled(has_audio_codecs); + + bool has_subtitle_codecs = subtitle_tab_->SetFormat(current_format); + subtitles_enabled_->setChecked(has_subtitle_codecs); + subtitles_enabled_->setEnabled(has_subtitle_codecs); } void ExportDialog::ResolutionChanged() @@ -549,6 +562,10 @@ ExportParams ExportDialog::GenerateParams() const params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->GetValue() * 1000); } + if (subtitles_enabled_->isChecked()) { + params.EnableSubtitles(subtitle_tab_->GetSubtitleEncoding(), subtitle_tab_->GetSubtitleCodec()); + } + return params; } diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 99f6f88f6..58d6e16c0 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -30,6 +30,7 @@ #include "codec/exportcodec.h" #include "codec/exportformat.h" #include "exportaudiotab.h" +#include "exportsubtitlestab.h" #include "exportvideotab.h" #include "task/export/export.h" #include "widget/viewer/viewer.h" @@ -50,6 +51,8 @@ protected: virtual void closeEvent(QCloseEvent *e) override; private: + void AddPreferencesTab(QWidget *inner_widget, const QString &title); + void LoadPresets(); void SetDefaultFilename(); @@ -75,6 +78,7 @@ private: QCheckBox* video_enabled_; QCheckBox* audio_enabled_; + QCheckBox* subtitles_enabled_; ViewerWidget* preview_viewer_; QLineEdit* filename_edit_; @@ -82,6 +86,7 @@ private: ExportVideoTab* video_tab_; ExportAudioTab* audio_tab_; + ExportSubtitlesTab* subtitle_tab_; double video_aspect_ratio_; diff --git a/app/dialog/export/exportsubtitlestab.cpp b/app/dialog/export/exportsubtitlestab.cpp new file mode 100644 index 000000000..5c535b60b --- /dev/null +++ b/app/dialog/export/exportsubtitlestab.cpp @@ -0,0 +1,47 @@ +#include "exportsubtitlestab.h" + +#include +#include + +namespace olive { + +ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout* outer_layout = new QVBoxLayout(this); + + QGridLayout* layout = new QGridLayout(); + outer_layout->addLayout(layout); + + int row = 0; + + layout->addWidget(new QLabel(tr("Codec:")), row, 0); + + codec_combobox_ = new QComboBox(); + layout->addWidget(codec_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Encoding:")), row, 0); + + encoding_combobox_ = new QComboBox(); + for (int i=0; iaddItem(SubtitleParams::GetEncodingName(static_cast(i)), i); + } + layout->addWidget(encoding_combobox_, row, 1); + + outer_layout->addStretch(); +} + +int ExportSubtitlesTab::SetFormat(ExportFormat::Format format) +{ + auto scodecs = ExportFormat::GetSubtitleCodecs(format); + setEnabled(!scodecs.isEmpty()); + codec_combobox_->clear(); + foreach (ExportCodec::Codec scodec, scodecs) { + codec_combobox_->addItem(ExportCodec::GetCodecName(scodec), scodec); + } + return scodecs.size(); +} + +} diff --git a/app/dialog/export/exportsubtitlestab.h b/app/dialog/export/exportsubtitlestab.h new file mode 100644 index 000000000..f3beaa10b --- /dev/null +++ b/app/dialog/export/exportsubtitlestab.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef EXPORTSUBTITLESTAB_H +#define EXPORTSUBTITLESTAB_H + +#include + +#include "codec/exportformat.h" +#include "render/subtitleparams.h" + +namespace olive { + +class ExportSubtitlesTab : public QWidget +{ +public: + ExportSubtitlesTab(QWidget *parent = nullptr); + + int SetFormat(ExportFormat::Format format); + + ExportCodec::Codec GetSubtitleCodec() + { + return static_cast(codec_combobox_->currentData().toInt()); + } + + SubtitleParams::Encoding GetSubtitleEncoding() + { + return static_cast(encoding_combobox_->currentData().toInt()); + } + +private: + QComboBox *codec_combobox_; + + QComboBox *encoding_combobox_; + +}; + +} + +#endif // EXPORTSUBTITLESTAB_H diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 6c9dea587..9c5b98b57 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -74,7 +74,7 @@ QWidget* ExportVideoTab::SetupResolutionSection() int row = 0; QGroupBox* resolution_group = new QGroupBox(); - resolution_group->setTitle(tr("Basic")); + resolution_group->setTitle(tr("General")); QGridLayout* layout = new QGridLayout(resolution_group); diff --git a/app/node/block/CMakeLists.txt b/app/node/block/CMakeLists.txt index 38eb10739..eb19f639d 100644 --- a/app/node/block/CMakeLists.txt +++ b/app/node/block/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(clip) add_subdirectory(gap) +add_subdirectory(subtitle) add_subdirectory(transition) set(OLIVE_SOURCES diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 954736a9c..fe8cdc6e2 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -26,9 +26,11 @@ namespace olive { const QString ClipBlock::kBufferIn = QStringLiteral("buffer_in"); -ClipBlock::ClipBlock() +ClipBlock::ClipBlock(bool create_buffer_in) { - AddInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); + if (create_buffer_in) { + AddInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); + } } Node *ClipBlock::copy() const @@ -108,7 +110,9 @@ void ClipBlock::Retranslate() { super::Retranslate(); - SetInputName(kBufferIn, tr("Buffer")); + if (HasInputWithID(kBufferIn)) { + SetInputName(kBufferIn, tr("Buffer")); + } } void ClipBlock::Hash(const QString &out, QCryptographicHash &hash, const rational &time, const VideoParams &video_params) const diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index 770be01ea..ef542526f 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -32,7 +32,7 @@ class ClipBlock : public Block { Q_OBJECT public: - ClipBlock(); + ClipBlock(bool create_buffer_in = true); NODE_DEFAULT_DESTRUCTOR(ClipBlock) diff --git a/app/node/block/subtitle/CMakeLists.txt b/app/node/block/subtitle/CMakeLists.txt new file mode 100644 index 000000000..81a745ab4 --- /dev/null +++ b/app/node/block/subtitle/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/subtitle/subtitle.cpp + node/block/subtitle/subtitle.h + PARENT_SCOPE +) diff --git a/app/node/block/subtitle/subtitle.cpp b/app/node/block/subtitle/subtitle.cpp new file mode 100644 index 000000000..3f5a9a212 --- /dev/null +++ b/app/node/block/subtitle/subtitle.cpp @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "subtitle.h" + +namespace olive { + +#define super ClipBlock + +const QString SubtitleBlock::kTextIn = QStringLiteral("text_in"); + +SubtitleBlock::SubtitleBlock() : + super(false) +{ + AddInput(kTextIn, NodeValue::kText, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); +} + +Node *SubtitleBlock::copy() const +{ + return new SubtitleBlock(); +} + +QString SubtitleBlock::Name() const +{ + return tr("Subtitle"); +} + +QString SubtitleBlock::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.subtitle"); +} + +QString SubtitleBlock::Description() const +{ + return tr("A time-based node representing a single subtitle element for a certain period of time."); +} + +void SubtitleBlock::Retranslate() +{ + super::Retranslate(); + + SetInputName(kTextIn, tr("Text")); +} + +} diff --git a/app/node/block/subtitle/subtitle.h b/app/node/block/subtitle/subtitle.h new file mode 100644 index 000000000..c88a2bfc1 --- /dev/null +++ b/app/node/block/subtitle/subtitle.h @@ -0,0 +1,60 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef SUBTITLEBLOCK_H +#define SUBTITLEBLOCK_H + +#include "node/block/clip/clip.h" + +namespace olive { + +class SubtitleBlock : public ClipBlock +{ + Q_OBJECT +public: + SubtitleBlock(); + + NODE_DEFAULT_DESTRUCTOR(SubtitleBlock) + + virtual Node* copy() const override; + + virtual QString Name() const override; + virtual QString id() const override; + virtual QString Description() const override; + + virtual void Retranslate() override; + + static const QString kTextIn; + + QString GetText() const + { + return GetStandardValue(kTextIn).toString(); + } + + void SetText(const QString &text) + { + SetStandardValue(kTextIn, text); + } + +}; + +} + +#endif // SUBTITLEBLOCK_H diff --git a/app/node/factory.cpp b/app/node/factory.cpp index c203f5a2d..9e18f0a99 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -26,6 +26,7 @@ #include "audio/volume/volume.h" #include "block/clip/clip.h" #include "block/gap/gap.h" +#include "block/subtitle/subtitle.h" #include "block/transition/crossdissolve/crossdissolvetransition.h" #include "block/transition/diptocolor/diptocolortransition.h" #include "distort/crop/cropdistortnode.h" @@ -236,6 +237,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new ValueNode(); case kTimeRemapNode: return new TimeRemapNode(); + case kSubtitleBlock: + return new SubtitleBlock(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index 5aa738237..8ba811cd3 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -58,6 +58,7 @@ public: kProjectSequence, kValueNode, kTimeRemapNode, + kSubtitleBlock, // Count value kInternalNodeCount diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 4d71d23cf..6f6898154 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -53,6 +53,8 @@ set(OLIVE_SOURCES render/renderprocessor.h render/shadercode.h render/stillimagecache.h + render/subtitleparams.cpp + render/subtitleparams.h render/texture.cpp render/texture.h render/videoparams.cpp diff --git a/app/render/subtitleparams.cpp b/app/render/subtitleparams.cpp new file mode 100644 index 000000000..d864e58e8 --- /dev/null +++ b/app/render/subtitleparams.cpp @@ -0,0 +1,188 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "subtitleparams.h" + +#include + +namespace olive { + +QString SubtitleParams::GetEncodingName(Encoding encoding) +{ + switch (encoding) { + case kISO8859_1: + return QCoreApplication::translate("SubtitleParams", "ASCII/ISO 8859-1"); + case kWindows1252: + return QCoreApplication::translate("SubtitleParams", "Windows-1252"); + case kUTF8: + return QCoreApplication::translate("SubtitleParams", "UTF-8"); + case kUTF8WithBOM: + return QCoreApplication::translate("SubtitleParams", "UTF-8 with BOM"); + case kUTF16LE: + return QCoreApplication::translate("SubtitleParams", "UTF-16LE"); + case kUTF16BE: + return QCoreApplication::translate("SubtitleParams", "UTF-16BE"); + case kEncodingInvalid: + case kEncodingCount: + break; + } + + return QCoreApplication::translate("SubtitleParams", "Unknown"); +} + +bool SubtitleParams::EncodingHasUnicodeBOM(Encoding encoding) +{ + switch (encoding) { + case kUTF8WithBOM: + case kUTF16LE: + case kUTF16BE: + return true; + + case kISO8859_1: + case kWindows1252: + case kUTF8: + case kEncodingInvalid: + case kEncodingCount: + break; + } + + return false; +} + +QByteArray SubtitleParams::GetUnicodeBOM(Encoding encoding) +{ + QByteArray arr; + + if (encoding == kUTF8WithBOM) { + arr.resize(3); + arr[0] = 0xEF; + arr[1] = 0xBB; + arr[2] = 0xBF; + } else if (encoding == kUTF16LE) { + arr.resize(2); + arr[0] = 0xFF; + arr[1] = 0xFE; + } else if (encoding == kUTF16BE) { + arr.resize(2); + arr[0] = 0xFE; + arr[1] = 0xFF; + } + + return arr; +} + +const char *SubtitleParams::GetQTextStreamCodec(Encoding encoding) +{ + switch (encoding) { + case SubtitleParams::kISO8859_1: + return "ISO 8859-1"; + case SubtitleParams::kWindows1252: + return "Windows-1252"; + case SubtitleParams::kUTF8: + case SubtitleParams::kUTF8WithBOM: + return "UTF-8"; + case SubtitleParams::kUTF16LE: + return "UTF-16LE"; + case SubtitleParams::kUTF16BE: + return "UTF-16BE"; + case SubtitleParams::kEncodingInvalid: + case SubtitleParams::kEncodingCount: + break; + } + + return nullptr; +} + +QString SubtitleParams::GenerateASSHeader() +{ + // NOTE: We'll probably implement more customization as we support ASS better. Right now, we only + // natively support SRT and only make this header because FFmpeg requires it. + static const int kAssDefaultPlayResX = 384; + static const int kAssDefaultPlayResY = 288; + static const QString kAssDefaultFont = QStringLiteral("Arial"); + static const int kAssDefaultFontSize = 16; + static const int kAssDefaultPrimaryColor = 0xFFFFFF; // White + static const int kAssDefaultSecondaryColor = 0xFFFFFF; // White + static const int kAssDefaultOutlineColor = 0x000000; // Black + static const int kAssDefaultBackColor = 0x000000; // Black + static const int kAssBold = 0; + static const int kAssItalic = 0; + static const int kAssUnderline = 0; + static const int kAssStrike = 0; + static const int kAssBorderStyle = 1; + static const int kAssAlignment = 2; + + static const QString kFormatHeader = QStringLiteral( + "[Script Info]\r\n" + "; Script generated by %1 %2\r\n" + "ScriptType: v4.00+\r\n" + "PlayResX: %3\r\n" + "PlayResY: %4\r\n" + "ScaledBorderAndShadow: yes\r\n" + "\r\n" + + /* ASSv4 header */ + "[V4+ Styles]\r\n" + "Format: Name, " + "Fontname, Fontsize, " + "PrimaryColour, SecondaryColour, OutlineColour, BackColour, " + "Bold, Italic, Underline, StrikeOut, " + "ScaleX, ScaleY, " + "Spacing, Angle, " + "BorderStyle, Outline, Shadow, " + "Alignment, MarginL, MarginR, MarginV, " + "Encoding\r\n" + + "Style: " + "Default," /* Name */ + "%5,%6," /* Font{name,size} */ + "&H%7,&H%8,&H%9,&H%10," /* {Primary,Secondary,Outline,Back}Colour */ + "%11,%12,%13,%14," /* Bold, Italic, Underline, StrikeOut */ + "100,100," /* Scale{X,Y} */ + "0,0," /* Spacing, Angle */ + "%15,1,0," /* BorderStyle, Outline, Shadow */ + "%16,10,10,10," /* Alignment, Margin[LRV] */ + "0\r\n" /* Encoding */ + + "\r\n" + "[Events]\r\n" + "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n" + ); + + return kFormatHeader.arg(QCoreApplication::applicationName(), + QCoreApplication::applicationVersion(), + QString::number(kAssDefaultPlayResX), + QString::number(kAssDefaultPlayResY), + kAssDefaultFont, + QString::number(kAssDefaultFontSize), + QString::number(kAssDefaultPrimaryColor, 16), + QString::number(kAssDefaultSecondaryColor, 16), + QString::number(kAssDefaultOutlineColor, 16), + QString::number(kAssDefaultBackColor, 16), + QString::number(kAssBold), + QString::number(kAssItalic), + QString::number(kAssUnderline), + QString::number(kAssStrike), + QString::number(kAssBorderStyle), + QString::number(kAssAlignment) + ); +} + +} diff --git a/app/render/subtitleparams.h b/app/render/subtitleparams.h new file mode 100644 index 000000000..241cccc33 --- /dev/null +++ b/app/render/subtitleparams.h @@ -0,0 +1,55 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef SUBTITLEPARAMS_H +#define SUBTITLEPARAMS_H + +#include + +namespace olive { + +class SubtitleParams { +public: + enum Encoding { + kEncodingInvalid = -1, + kISO8859_1, + kWindows1252, + kUTF8, + kUTF8WithBOM, + kUTF16LE, + kUTF16BE, + kEncodingCount + }; + + static QString GetEncodingName(Encoding encoding); + + static bool EncodingHasUnicodeBOM(Encoding encoding); + + static QByteArray GetUnicodeBOM(Encoding encoding); + + static const char *GetQTextStreamCodec(Encoding encoding); + + static QString GenerateASSHeader(); + +}; + +} + +#endif // SUBTITLEPARAMS_H diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index a2a0675c1..814c31125 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -56,7 +56,7 @@ bool ExportTask::Run() } if (!encoder_->Open()) { - SetError(tr("Failed to open file")); + SetError(tr("Failed to open file: %1").arg(encoder_->GetError())); encoder_->deleteLater(); return false; } @@ -102,6 +102,7 @@ bool ExportTask::Run() // Start render process TimeRangeList video_range, audio_range; + TimeRange subtitle_range; if (params_.video_enabled()) { video_range = {range}; @@ -111,7 +112,11 @@ bool ExportTask::Run() audio_range = {range}; } - Render(color_manager_, video_range, audio_range, RenderMode::kOnline, nullptr, + if (params_.subtitles_enabled()) { + subtitle_range = range; + } + + Render(color_manager_, video_range, audio_range, subtitle_range, RenderMode::kOnline, nullptr, video_force_size, video_force_matrix, encoder_->GetDesiredPixelFormat(), color_processor_); @@ -191,6 +196,11 @@ void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples } } +void ExportTask::EncodeSubtitle(const SubtitleBlock *sub) +{ + encoder_->WriteSubtitle(sub); +} + void ExportTask::WriteAudioLoop(const TimeRange& time, SampleBufferPtr samples) { encoder_->WriteAudio(samples); diff --git a/app/task/export/export.h b/app/task/export/export.h index b02691b54..7f1c0859a 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -42,6 +42,8 @@ protected: virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; + virtual void EncodeSubtitle(const SubtitleBlock *sub) override; + virtual bool TwoStepFrameRendering() const override { return false; diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index 679082a2a..1f6adc80e 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -67,6 +67,7 @@ bool PreCacheTask::Run() Render(project_->color_manager(), video_range, TimeRangeList(), + TimeRange(), RenderMode::kOnline, viewer()->video_frame_cache()); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index a4bd28aad..ee559fe2c 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -21,6 +21,7 @@ #include "render.h" #include "common/timecodefunctions.h" +#include "node/project/sequence/sequence.h" #include "render/rendermanager.h" namespace olive { @@ -40,7 +41,7 @@ RenderTask::~RenderTask() bool RenderTask::Render(ColorManager* manager, const TimeRangeList& video_range, - const TimeRangeList &audio_range, + const TimeRangeList &audio_range, const TimeRange &subtitle_range, RenderMode::Mode mode, FrameHashCache* cache, const QSize &force_size, const QMatrix4x4 &force_matrix, VideoParams::Format force_format, @@ -129,6 +130,50 @@ bool RenderTask::Render(ColorManager* manager, mode, cache, force_size, force_matrix, force_format, force_color_output); } + // Subtitle loop, loops over all blocks in sequence on all tracks + if (!subtitle_range.length().isNull()) { + Sequence *sequence = dynamic_cast(viewer_); + if (sequence) { + TrackList *list = sequence->track_list(Track::kSubtitle); + QVector block_indexes(list->GetTrackCount(), 0); + + QVector tracks_to_push; + do { + tracks_to_push.clear(); + + for (int i=0; iGetTrackAt(i); + int &this_block_index = block_indexes[i]; + if (this_block_index >= this_track->Blocks().size()) { + continue; + } + Block *this_block = this_track->Blocks().at(this_block_index); + + Track *compare_track = tracks_to_push.isEmpty() ? nullptr : list->GetTrackAt(tracks_to_push.first()); + const int &compare_block_index = tracks_to_push.isEmpty() ? -1 : block_indexes.at(tracks_to_push.first()); + Block *compare_block = compare_track ? compare_track->Blocks().at(compare_block_index) : nullptr; + if (!compare_track || compare_block->in() >= this_block->in()) { + if (compare_track && compare_block->in() != this_block->in()) { + tracks_to_push.clear(); + } + tracks_to_push.append(i); + } + } + + for (int i=0; iGetTrackAt(tracks_to_push.at(i)); + Block *this_block = this_track->Blocks().at(block_indexes.at(tracks_to_push.at(i))); + + if (const SubtitleBlock *sub = dynamic_cast(this_block)) { + EncodeSubtitle(sub); + } + + block_indexes[tracks_to_push.at(i)]++; + } + } while (!tracks_to_push.isEmpty()); + } + } + finished_watcher_mutex_.lock(); while (!IsCancelled()) { @@ -238,6 +283,11 @@ void RenderTask::DownloadFrame(QThread *thread, FramePtr frame, const QByteArray hash)); } +void RenderTask::EncodeSubtitle(const SubtitleBlock *subtitle) +{ + Q_UNUSED(subtitle) +} + void RenderTask::PrepareWatcher(RenderTicketWatcher *watcher, QThread *thread) { watcher->moveToThread(thread); diff --git a/app/task/render/render.h b/app/task/render/render.h index 328b78a19..af11071a7 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -23,6 +23,7 @@ #include +#include "node/block/subtitle/subtitle.h" #include "node/color/colormanager/colormanager.h" #include "node/output/viewer/viewer.h" #include "task/task.h" @@ -41,7 +42,8 @@ public: protected: bool Render(ColorManager *manager, const TimeRangeList &video_range, - const TimeRangeList &audio_range, RenderMode::Mode mode, + const TimeRangeList &audio_range, const TimeRange &subtitle_range, + RenderMode::Mode mode, FrameHashCache *cache, const QSize& force_size = QSize(0, 0), const QMatrix4x4& force_matrix = QMatrix4x4(), VideoParams::Format force_format = VideoParams::kFormatInvalid, @@ -53,6 +55,8 @@ protected: virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0; + virtual void EncodeSubtitle(const SubtitleBlock *subtitle); + ViewerOutput* viewer() const { return viewer_; diff --git a/app/tool/tool.h b/app/tool/tool.h index d19abab57..c277f4b0b 100644 --- a/app/tool/tool.h +++ b/app/tool/tool.h @@ -96,6 +96,9 @@ public: /// An audio clip with a sine connected to it kAddableTone, + /// A subtitle clip + kAddableSubtitle, + kAddableCount }; @@ -112,6 +115,8 @@ public: return QCoreApplication::translate("Tool", "Title"); case kAddableTone: return QCoreApplication::translate("Tool", "Tone"); + case kAddableSubtitle: + return QCoreApplication::translate("Tool", "Subtitle"); case kAddableCount: break; } diff --git a/app/widget/timelinewidget/timelineundo.h b/app/widget/timelinewidget/timelineundo.h index de868f2d6..8c472a658 100644 --- a/app/widget/timelinewidget/timelineundo.h +++ b/app/widget/timelinewidget/timelineundo.h @@ -1230,11 +1230,11 @@ public: if (timeline_->type() == Track::kVideo) { relevant_input = ViewerOutput::kTextureInput; - } else { + } else if (timeline_->type() == Track::kAudio) { relevant_input = ViewerOutput::kSamplesInput; } - if (!timeline_->parent()->IsInputConnected(relevant_input)) { + if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) { direct_ = NodeInput(timeline_->parent(), relevant_input); Node::ConnectEdge(track_, direct_); @@ -1289,15 +1289,20 @@ private: merge_ = new MergeNode(); base_ = NodeInput(merge_, MergeNode::kBaseIn); blend_ = NodeInput(merge_, MergeNode::kBlendIn); - } else { + } else if (timeline_->type() == Track::kAudio) { merge_ = new MathNode(); base_ = NodeInput(merge_, MathNode::kParamAIn); blend_ = NodeInput(merge_, MathNode::kParamBIn); + } else { + merge_ = nullptr; } - merge_->setParent(&memory_manager_); } else { merge_ = nullptr; } + + if (merge_) { + merge_->setParent(&memory_manager_); + } } TrackList* timeline_; diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index ddbed335b..d367489e4 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -77,7 +77,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : // Create list of TimelineViews - these MUST correspond to the ViewType enum view_splitter_ = new QSplitter(Qt::Vertical); - view_splitter_->setChildrenCollapsible(false); vert_layout->addWidget(view_splitter_); // Video view @@ -86,6 +85,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) : // Audio view views_.append(new TimelineAndTrackView(Qt::AlignTop)); + // Subtitle view + views_.append(new TimelineAndTrackView(Qt::AlignTop)); + // Create tools tools_.resize(olive::Tool::kCount); tools_.fill(nullptr); @@ -155,7 +157,17 @@ TimelineWidget::TimelineWidget(QWidget *parent) : } // Split viewer 50/50 - view_splitter_->setSizes({INT_MAX, INT_MAX}); + QList view_sizes; + view_sizes.reserve(views_.size()); + view_sizes.append(height()/2); // Video + view_sizes.append(height()/2); // Audio + view_sizes.append(0); // Subtitle (hidden by default) + view_splitter_->setSizes(view_sizes); + + // Video and audio are not collapsible, subtitle is + view_splitter_->setCollapsible(Track::kVideo, false); + view_splitter_->setCollapsible(Track::kAudio, false); + view_splitter_->setCollapsible(Track::kSubtitle, true); // FIXME: Magic number SetScale(90.0); @@ -786,11 +798,23 @@ void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) if (hover_tool) { hover_tool->HoverMove(event); + + // Special cast for subtitle adding - ensure section is visible + if (dynamic_cast(hover_tool) + && Core::instance()->GetSelectedAddableObject() == Tool::kAddableSubtitle) { + QList sz = view_splitter_->sizes(); + int &subtitle_section_height = sz[Track::kSubtitle]; + if (subtitle_section_height == 0) { + subtitle_section_height = height() / Track::kCount; + view_splitter_->setSizes(sz); + } + } } } } } + void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event) { if (active_tool_) { diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 06412b6c8..bd766735a 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -20,6 +20,7 @@ #include "add.h" #include "core.h" +#include "node/block/subtitle/subtitle.h" #include "node/factory.h" #include "node/generator/solid/solid.h" #include "node/generator/text/text.h" @@ -54,6 +55,9 @@ void AddTool::MousePress(TimelineViewMouseEvent *event) case olive::Tool::kAddableTone: add_type = Track::kAudio; break; + case olive::Tool::kAddableSubtitle: + add_type = Track::kSubtitle; + break; case olive::Tool::kAddableEmpty: // Leave as "none", which means this block can be placed on any track break; @@ -93,7 +97,12 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) if (!ghost_->GetAdjustedLength().isNull()) { MultiUndoCommand* command = new MultiUndoCommand(); - ClipBlock* clip = new ClipBlock(); + ClipBlock* clip; + if (Core::instance()->GetSelectedAddableObject() == olive::Tool::kAddableSubtitle) { + clip = new SubtitleBlock(); + } else { + clip = new ClipBlock(); + } clip->set_length_and_media_out(ghost_->GetAdjustedLength()); clip->SetLabel(olive::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); @@ -140,6 +149,9 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) // Not implemented yet qWarning() << "Unimplemented add object:" << Core::instance()->GetSelectedAddableObject(); break; + case olive::Tool::kAddableSubtitle: + // The block itself is the node we want + break; case olive::Tool::kAddableCount: // Invalid value, do nothing break;