From 244c09259189d0ba04a1f1fa22e56648b52c8161 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 30 May 2018 03:24:12 +1000 Subject: [PATCH] First commit of brand new rewrite --- dialogs/aboutdialog.cpp | 14 + dialogs/aboutdialog.h | 22 + dialogs/aboutdialog.ui | 77 ++++ dialogs/exportdialog.cpp | 437 +++++++++++++++++++ dialogs/exportdialog.h | 38 ++ dialogs/exportdialog.ui | 232 ++++++++++ dialogs/newsequencedialog.cpp | 110 +++++ dialogs/newsequencedialog.h | 30 ++ dialogs/newsequencedialog.ui | 360 +++++++++++++++ effects/effects.cpp | 16 + effects/effects.h | 61 +++ effects/paneffect.cpp | 50 +++ effects/transformeffect.cpp | 119 +++++ effects/volumeeffect.cpp | 38 ++ icons/arrow.png | Bin 0 -> 1459 bytes icons/audiosource.png | Bin 0 -> 3221 bytes icons/beam.png | Bin 0 -> 1393 bytes icons/ff.png | Bin 0 -> 3875 bytes icons/full-icon.png | Bin 0 -> 21806 bytes icons/icons.qrc | 19 + icons/next.png | Bin 0 -> 3533 bytes icons/olive.icns | Bin 0 -> 27395 bytes icons/olive.ico | Bin 0 -> 36382 bytes icons/pause.png | Bin 0 -> 1560 bytes icons/play.png | Bin 0 -> 3394 bytes icons/prev.png | Bin 0 -> 3527 bytes icons/razor.png | Bin 0 -> 1287 bytes icons/rew.png | Bin 0 -> 3862 bytes icons/ripple.png | Bin 0 -> 1158 bytes icons/rolling.png | Bin 0 -> 1165 bytes icons/slip.png | Bin 0 -> 1153 bytes icons/videosource.png | Bin 0 -> 2882 bytes icons/win.rc | 1 + io/config.cpp | 49 +++ io/config.h | 16 + io/exportthread.cpp | 342 +++++++++++++++ io/exportthread.h | 30 ++ io/media.cpp | 9 + io/media.h | 30 ++ main.cpp | 18 + mainwindow.cpp | 165 +++++++ mainwindow.h | 59 +++ mainwindow.ui | 296 +++++++++++++ olive.pro | 105 +++++ olive.pro.user | 375 ++++++++++++++++ olive.pro.user.3af0661 | 315 ++++++++++++++ olive.pro.user.89fae42 | 375 ++++++++++++++++ panels/effectcontrols.cpp | 59 +++ panels/effectcontrols.h | 29 ++ panels/effectcontrols.ui | 98 +++++ panels/panels.cpp | 6 + panels/panels.h | 14 + panels/project.cpp | 164 +++++++ panels/project.h | 34 ++ panels/project.ui | 95 ++++ panels/timeline.cpp | 320 ++++++++++++++ panels/timeline.h | 143 ++++++ panels/timeline.ui | 355 +++++++++++++++ panels/viewer.cpp | 79 ++++ panels/viewer.h | 42 ++ panels/viewer.ui | 319 ++++++++++++++ playback/audio.cpp | 72 +++ playback/audio.h | 19 + playback/cacher.cpp | 339 +++++++++++++++ playback/cacher.h | 31 ++ playback/playback.cpp | 288 ++++++++++++ playback/playback.h | 41 ++ project/clip.cpp | 81 ++++ project/clip.h | 95 ++++ project/effect.cpp | 18 + project/effect.h | 31 ++ project/sequence.cpp | 122 ++++++ project/sequence.h | 32 ++ ui/collapsiblewidget.cpp | 59 +++ ui/collapsiblewidget.h | 33 ++ ui/playbutton.cpp | 9 + ui/playbutton.h | 15 + ui/sourcetable.cpp | 30 ++ ui/sourcetable.h | 18 + ui/timeline-tools.h | 15 + ui/timelinewidget.cpp | 799 ++++++++++++++++++++++++++++++++++ ui/timelinewidget.h | 56 +++ ui/viewercontainer.cpp | 40 ++ ui/viewercontainer.h | 23 + ui/viewerwidget.cpp | 157 +++++++ ui/viewerwidget.h | 28 ++ 86 files changed, 8016 insertions(+) create mode 100644 dialogs/aboutdialog.cpp create mode 100644 dialogs/aboutdialog.h create mode 100644 dialogs/aboutdialog.ui create mode 100644 dialogs/exportdialog.cpp create mode 100644 dialogs/exportdialog.h create mode 100644 dialogs/exportdialog.ui create mode 100644 dialogs/newsequencedialog.cpp create mode 100644 dialogs/newsequencedialog.h create mode 100644 dialogs/newsequencedialog.ui create mode 100644 effects/effects.cpp create mode 100644 effects/effects.h create mode 100644 effects/paneffect.cpp create mode 100644 effects/transformeffect.cpp create mode 100644 effects/volumeeffect.cpp create mode 100644 icons/arrow.png create mode 100644 icons/audiosource.png create mode 100644 icons/beam.png create mode 100644 icons/ff.png create mode 100644 icons/full-icon.png create mode 100644 icons/icons.qrc create mode 100644 icons/next.png create mode 100644 icons/olive.icns create mode 100644 icons/olive.ico create mode 100644 icons/pause.png create mode 100644 icons/play.png create mode 100644 icons/prev.png create mode 100644 icons/razor.png create mode 100644 icons/rew.png create mode 100644 icons/ripple.png create mode 100644 icons/rolling.png create mode 100644 icons/slip.png create mode 100644 icons/videosource.png create mode 100644 icons/win.rc create mode 100644 io/config.cpp create mode 100644 io/config.h create mode 100644 io/exportthread.cpp create mode 100644 io/exportthread.h create mode 100644 io/media.cpp create mode 100644 io/media.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100644 olive.pro create mode 100644 olive.pro.user create mode 100644 olive.pro.user.3af0661 create mode 100644 olive.pro.user.89fae42 create mode 100644 panels/effectcontrols.cpp create mode 100644 panels/effectcontrols.h create mode 100644 panels/effectcontrols.ui create mode 100644 panels/panels.cpp create mode 100644 panels/panels.h create mode 100644 panels/project.cpp create mode 100644 panels/project.h create mode 100644 panels/project.ui create mode 100644 panels/timeline.cpp create mode 100644 panels/timeline.h create mode 100644 panels/timeline.ui create mode 100644 panels/viewer.cpp create mode 100644 panels/viewer.h create mode 100644 panels/viewer.ui create mode 100644 playback/audio.cpp create mode 100644 playback/audio.h create mode 100644 playback/cacher.cpp create mode 100644 playback/cacher.h create mode 100644 playback/playback.cpp create mode 100644 playback/playback.h create mode 100644 project/clip.cpp create mode 100644 project/clip.h create mode 100644 project/effect.cpp create mode 100644 project/effect.h create mode 100644 project/sequence.cpp create mode 100644 project/sequence.h create mode 100644 ui/collapsiblewidget.cpp create mode 100644 ui/collapsiblewidget.h create mode 100644 ui/playbutton.cpp create mode 100644 ui/playbutton.h create mode 100644 ui/sourcetable.cpp create mode 100644 ui/sourcetable.h create mode 100644 ui/timeline-tools.h create mode 100644 ui/timelinewidget.cpp create mode 100644 ui/timelinewidget.h create mode 100644 ui/viewercontainer.cpp create mode 100644 ui/viewercontainer.h create mode 100644 ui/viewerwidget.cpp create mode 100644 ui/viewerwidget.h diff --git a/dialogs/aboutdialog.cpp b/dialogs/aboutdialog.cpp new file mode 100644 index 000000000..40a2096c6 --- /dev/null +++ b/dialogs/aboutdialog.cpp @@ -0,0 +1,14 @@ +#include "aboutdialog.h" +#include "ui_aboutdialog.h" + +AboutDialog::AboutDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AboutDialog) +{ + ui->setupUi(this); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} diff --git a/dialogs/aboutdialog.h b/dialogs/aboutdialog.h new file mode 100644 index 000000000..18a87e537 --- /dev/null +++ b/dialogs/aboutdialog.h @@ -0,0 +1,22 @@ +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include + +namespace Ui { +class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = 0); + ~AboutDialog(); + +private: + Ui::AboutDialog *ui; +}; + +#endif // ABOUTDIALOG_H diff --git a/dialogs/aboutdialog.ui b/dialogs/aboutdialog.ui new file mode 100644 index 000000000..246699a14 --- /dev/null +++ b/dialogs/aboutdialog.ui @@ -0,0 +1,77 @@ + + + AboutDialog + + + + 0 + 0 + 320 + 180 + + + + Dialog + + + + + + Olive is a professional video editor. + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + AboutDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AboutDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/dialogs/exportdialog.cpp b/dialogs/exportdialog.cpp new file mode 100644 index 000000000..5457138ad --- /dev/null +++ b/dialogs/exportdialog.cpp @@ -0,0 +1,437 @@ +#include "exportdialog.h" +#include "ui_exportdialog.h" + +#include +#include +#include +#include + +#include "panels/panels.h" +#include "panels/viewer.h" +#include "panels/timeline.h" +#include "ui/viewerwidget.h" +#include "project/sequence.h" +#include "io/exportthread.h" + +extern "C" { + #include +} + +enum ExportFormats { + FORMAT_3GPP, + FORMAT_AIFF, + FORMAT_APNG, + FORMAT_AVI, + FORMAT_DNXHD, + FORMAT_AC3, + FORMAT_FLV, + FORMAT_GIF, + FORMAT_IMG, + FORMAT_MP2, + FORMAT_MP3, + FORMAT_MPEG1, + FORMAT_MPEG2, + FORMAT_MPEG4, + FORMAT_MPEGTS, + FORMAT_MKV, + FORMAT_OGG, + FORMAT_MOV, + FORMAT_WAV, + FORMAT_WEBM, + FORMAT_WMV, + FORMAT_SIZE +}; + +ExportDialog::ExportDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::ExportDialog) +{ + ui->setupUi(this); + + format_strings.resize(FORMAT_SIZE); + format_strings[FORMAT_3GPP] = "3GPP"; + format_strings[FORMAT_AIFF] = "AIFF"; + format_strings[FORMAT_APNG] = "Animated PNG"; + format_strings[FORMAT_AVI] = "AVI"; + format_strings[FORMAT_DNXHD] = "DNxHD"; + format_strings[FORMAT_AC3] = "Dolby Digital (AC3)"; + format_strings[FORMAT_FLV] = "FLV"; + format_strings[FORMAT_GIF] = "GIF"; + format_strings[FORMAT_IMG] = "Image Sequence"; + format_strings[FORMAT_MP2] = "MP2 Audio"; + format_strings[FORMAT_MP3] = "MP3 Audio"; + format_strings[FORMAT_MPEG1] = "MPEG-1 Video"; + format_strings[FORMAT_MPEG2] = "MPEG-2 Video"; + format_strings[FORMAT_MPEG4] = "MPEG-4 Video"; + format_strings[FORMAT_MPEGTS] = "MPEG-TS"; + format_strings[FORMAT_MKV] = "Matroska MKV"; + format_strings[FORMAT_OGG] = "Ogg"; + format_strings[FORMAT_MOV] = "QuickTime MOV"; + format_strings[FORMAT_WAV] = "WAVE Audio"; + format_strings[FORMAT_WEBM] = "WebM"; + format_strings[FORMAT_WMV] = "Windows Media"; + + for (int i=0;iformatCombobox->addItem(format_strings[i]); + } + ui->formatCombobox->setCurrentIndex(FORMAT_MPEG4); +} + +ExportDialog::~ExportDialog() +{ + delete ui; +} + +void ExportDialog::on_formatCombobox_currentIndexChanged(int index) +{ + format_vcodecs.clear(); + format_acodecs.clear(); + ui->vcodecCombobox->clear(); + ui->acodecCombobox->clear(); + + int default_vcodec = 0; + int default_acodec = 0; + + switch (index) { + case FORMAT_3GPP: + format_vcodecs.append(AV_CODEC_ID_MPEG4); + format_vcodecs.append(AV_CODEC_ID_H264); + + format_acodecs.append(AV_CODEC_ID_AAC); + + default_vcodec = 1; + break; + case FORMAT_AIFF: + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + break; + case FORMAT_APNG: + format_vcodecs.append(AV_CODEC_ID_APNG); + break; + case FORMAT_AVI: + format_vcodecs.append(AV_CODEC_ID_H264); + format_vcodecs.append(AV_CODEC_ID_MPEG4); + format_vcodecs.append(AV_CODEC_ID_MJPEG); + format_vcodecs.append(AV_CODEC_ID_MSVIDEO1); + format_vcodecs.append(AV_CODEC_ID_RAWVIDEO); + format_vcodecs.append(AV_CODEC_ID_HUFFYUV); + format_vcodecs.append(AV_CODEC_ID_DVVIDEO); + + format_acodecs.append(AV_CODEC_ID_AAC); + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_FLAC); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + + default_vcodec = 3; + default_acodec = 5; + break; + case FORMAT_DNXHD: + format_vcodecs.append(AV_CODEC_ID_DNXHD); + + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + break; + case FORMAT_AC3: + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_EAC3); + break; + case FORMAT_FLV: + format_vcodecs.append(AV_CODEC_ID_FLV1); + + format_acodecs.append(AV_CODEC_ID_MP3); + break; + case FORMAT_GIF: + format_vcodecs.append(AV_CODEC_ID_GIF); + break; + case FORMAT_IMG: + format_vcodecs.append(AV_CODEC_ID_BMP); + format_vcodecs.append(AV_CODEC_ID_MJPEG); + format_vcodecs.append(AV_CODEC_ID_JPEG2000); + format_vcodecs.append(AV_CODEC_ID_PSD); + format_vcodecs.append(AV_CODEC_ID_PNG); + format_vcodecs.append(AV_CODEC_ID_TIFF); + + default_vcodec = 4; + break; + case FORMAT_MP2: + format_acodecs.append(AV_CODEC_ID_MP2); + break; + case FORMAT_MP3: + format_acodecs.append(AV_CODEC_ID_MP3); + break; + case FORMAT_MPEG1: + format_vcodecs.append(AV_CODEC_ID_MPEG1VIDEO); + + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + + default_acodec = 1; + break; + case FORMAT_MPEG2: + format_vcodecs.append(AV_CODEC_ID_MPEG2VIDEO); + + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + + default_acodec = 1; + break; + case FORMAT_MPEG4: + format_vcodecs.append(AV_CODEC_ID_MPEG4); + format_vcodecs.append(AV_CODEC_ID_H264); + + format_acodecs.append(AV_CODEC_ID_AAC); + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + + default_vcodec = 1; + break; + case FORMAT_MPEGTS: + format_vcodecs.append(AV_CODEC_ID_MPEG2VIDEO); + + format_acodecs.append(AV_CODEC_ID_AAC); + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + + default_acodec = 2; + break; + case FORMAT_MKV: + format_vcodecs.append(AV_CODEC_ID_MPEG4); + format_vcodecs.append(AV_CODEC_ID_H264); + + format_acodecs.append(AV_CODEC_ID_AAC); + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_EAC3); + format_acodecs.append(AV_CODEC_ID_FLAC); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + format_acodecs.append(AV_CODEC_ID_OPUS); + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + format_acodecs.append(AV_CODEC_ID_VORBIS); + format_acodecs.append(AV_CODEC_ID_WAVPACK); + format_acodecs.append(AV_CODEC_ID_WMAV1); + format_acodecs.append(AV_CODEC_ID_WMAV2); + + default_vcodec = 1; + break; + case FORMAT_OGG: + format_vcodecs.append(AV_CODEC_ID_THEORA); + + format_acodecs.append(AV_CODEC_ID_OPUS); + format_acodecs.append(AV_CODEC_ID_VORBIS); + + default_acodec = AV_CODEC_ID_VORBIS; + break; + case FORMAT_MOV: + format_vcodecs.append(AV_CODEC_ID_QTRLE); + format_vcodecs.append(AV_CODEC_ID_MPEG4); + format_vcodecs.append(AV_CODEC_ID_H264); + format_vcodecs.append(AV_CODEC_ID_MJPEG); + format_vcodecs.append(AV_CODEC_ID_PRORES); + + format_acodecs.append(AV_CODEC_ID_AAC); + format_acodecs.append(AV_CODEC_ID_AC3); + format_acodecs.append(AV_CODEC_ID_MP2); + format_acodecs.append(AV_CODEC_ID_MP3); + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + + default_vcodec = 2; + break; + case FORMAT_WAV: + format_acodecs.append(AV_CODEC_ID_PCM_S16LE); + break; + case FORMAT_WEBM: + format_vcodecs.append(AV_CODEC_ID_VP8); + format_vcodecs.append(AV_CODEC_ID_VP9); + + format_acodecs.append(AV_CODEC_ID_OPUS); + format_acodecs.append(AV_CODEC_ID_VORBIS); + + default_vcodec = 1; + break; + case FORMAT_WMV: + format_vcodecs.append(AV_CODEC_ID_WMV1); + format_vcodecs.append(AV_CODEC_ID_WMV2); + + format_acodecs.append(AV_CODEC_ID_WMAV1); + format_acodecs.append(AV_CODEC_ID_WMAV2); + + default_vcodec = 1; + default_acodec = 1; + break; + default: + qDebug() << "[ERROR] Invalid format selection - this is a bug, please inform the developers"; + } + + AVCodec* codec_info; + for (int i=0;ivcodecCombobox->addItem("NULL"); + } else { + ui->vcodecCombobox->addItem(codec_info->long_name); + } + } + for (int i=0;iacodecCombobox->addItem("NULL"); + } else { + ui->acodecCombobox->addItem(codec_info->long_name); + } + } + + ui->vcodecCombobox->setCurrentIndex(default_vcodec); + ui->acodecCombobox->setCurrentIndex(default_acodec); + + bool video_enabled = format_vcodecs.size() != 0; + bool audio_enabled = format_acodecs.size() != 0; + ui->videoGroupbox->setChecked(video_enabled); + ui->audioGroupbox->setChecked(audio_enabled); + ui->videoGroupbox->setEnabled(video_enabled); + ui->audioGroupbox->setEnabled(audio_enabled); +} + +void ExportDialog::on_pushButton_2_clicked() +{ + close(); +} + +void ExportDialog::on_pushButton_clicked() +{ + QString ext; + switch (ui->formatCombobox->currentIndex()) { + case FORMAT_3GPP: + ext = "3gp"; + break; + case FORMAT_AIFF: + ext = "aiff"; + break; + case FORMAT_APNG: + ext = "apng"; + break; + case FORMAT_AVI: + ext = "avi"; + break; + case FORMAT_DNXHD: + ext = "mxf"; + break; + case FORMAT_AC3: + ext = "ac3"; + break; + case FORMAT_FLV: + ext = "flv"; + break; + case FORMAT_GIF: + ext = "gif"; + break; + case FORMAT_IMG: + // ext_name = ""; + // ext = ""; + // ext is determined from codec, but we prevent the switch from going to 'default' here + qDebug() << "[INFO] Image export not implemented yet"; + return; + break; + case FORMAT_MP3: + ext = "mp3"; + break; + case FORMAT_MPEG1: + if (ui->videoGroupbox->isChecked() && !ui->audioGroupbox->isChecked()) { + ext = "m1v"; + } else if (!ui->videoGroupbox->isChecked() && ui->audioGroupbox->isChecked()) { + ext = "m1a"; + } else { + ext = "mpg"; + } + break; + case FORMAT_MPEG2: + if (ui->videoGroupbox->isChecked() && !ui->audioGroupbox->isChecked()) { + ext = "m2v"; + } else if (!ui->videoGroupbox->isChecked() && ui->audioGroupbox->isChecked()) { + ext = "m2a"; + } else { + ext = "mpg"; + } + break; + case FORMAT_MPEG4: + if (ui->videoGroupbox->isChecked() && !ui->audioGroupbox->isChecked()) { + ext = "m4v"; + } else if (!ui->videoGroupbox->isChecked() && ui->audioGroupbox->isChecked()) { + ext = "m4a"; + } else { + ext = "mp4"; + } + break; + case FORMAT_MPEGTS: + ext = "ts"; + break; + case FORMAT_MKV: + if (!ui->videoGroupbox->isChecked()) { + ext = "mka"; + } else { + ext = "mkv"; + } + break; + case FORMAT_OGG: + ext = "ogg"; + break; + case FORMAT_MOV: + ext = "mov"; + break; + case FORMAT_WEBM: + ext = "webm"; + break; + case FORMAT_WMV: + if (ui->videoGroupbox->isChecked()) { + ext = "wmv"; + } else { + ext = "wma"; + } + break; + default: + qDebug() << "[ERROR] Invalid format - this is a bug, please inform the developers"; + return; + } + QString filename = QFileDialog::getSaveFileName(this, "Export Media", "", format_strings[ui->formatCombobox->currentIndex()] + " (*." + ext + ")"); + if (!filename.isEmpty()) { + ExportThread* et = new ExportThread(); + + et->surface.create(); + + connect(et, SIGNAL(finished()), et, SLOT(deleteLater())); + connect(et, SIGNAL(progress_changed(int)), this, SLOT(update_progress_bar(int))); + + panel_viewer->viewer_widget->context()->doneCurrent(); + panel_viewer->viewer_widget->context()->moveToThread(et); + + et->filename = filename; + et->video_enabled = ui->videoGroupbox->isChecked(); + et->video_codec = format_vcodecs.at(ui->vcodecCombobox->currentIndex()); + et->video_width = ui->widthSpinbox->value(); + et->video_height = ui->heightSpinbox->value(); + et->video_frame_rate = ui->framerateSpinbox->value(); + et->video_bitrate = ui->videobitrateSpinbox->value(); + et->audio_enabled = ui->audioGroupbox->isChecked(); + et->audio_codec = format_acodecs.at(ui->acodecCombobox->currentIndex()); + et->audio_sampling_rate = 48000; + et->audio_bitrate = ui->audiobitrateSpinbox->value(); + + et->start(); + } +} + +void ExportDialog::set_defaults(Sequence* s) { + ui->widthSpinbox->setValue(s->width); + ui->heightSpinbox->setValue(s->height); + ui->samplingRateSpinbox->setValue(s->audio_frequency); + ui->framerateSpinbox->setValue(s->frame_rate); +} + +void ExportDialog::update_progress_bar(int value) { + ui->progressBar->setValue(value); +} diff --git a/dialogs/exportdialog.h b/dialogs/exportdialog.h new file mode 100644 index 000000000..fcd0449db --- /dev/null +++ b/dialogs/exportdialog.h @@ -0,0 +1,38 @@ +#ifndef EXPORTDIALOG_H +#define EXPORTDIALOG_H + +#include + +namespace Ui { +class ExportDialog; +} + +struct Sequence; + +class ExportDialog : public QDialog +{ + Q_OBJECT +public: + explicit ExportDialog(QWidget *parent = 0); + ~ExportDialog(); + + void set_defaults(Sequence* s); + +private slots: + void on_formatCombobox_currentIndexChanged(int index); + + void on_pushButton_2_clicked(); + + void on_pushButton_clicked(); + + void update_progress_bar(int value); + +private: + Ui::ExportDialog *ui; + + QVector format_strings; + QVector format_vcodecs; + QVector format_acodecs; +}; + +#endif // EXPORTDIALOG_H diff --git a/dialogs/exportdialog.ui b/dialogs/exportdialog.ui new file mode 100644 index 000000000..7bcc52716 --- /dev/null +++ b/dialogs/exportdialog.ui @@ -0,0 +1,232 @@ + + + ExportDialog + + + + 0 + 0 + 299 + 372 + + + + Export + + + + + + + + Format: + + + + + + + + + + + + Video + + + false + + + true + + + + + + Codec: + + + + + + + + + + Width: + + + + + + + 16777216 + + + + + + + Height: + + + + + + + 16777216 + + + + + + + Frame Rate: + + + + + + + 60.000000000000000 + + + 0.000000000000000 + + + + + + + Bitrate (Mbps/CBR): + + + + + + + 100.000000000000000 + + + 2.000000000000000 + + + + + + + + + + Audio + + + true + + + + + + Codec: + + + + + + + + + + Sampling Rate: + + + + + + + 96000 + + + 0 + + + + + + + Bitrate (Kbps/CBR): + + + + + + + 320 + + + 256 + + + + + + + + + + false + + + 0 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Export + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/dialogs/newsequencedialog.cpp b/dialogs/newsequencedialog.cpp new file mode 100644 index 000000000..91663a651 --- /dev/null +++ b/dialogs/newsequencedialog.cpp @@ -0,0 +1,110 @@ +#include "newsequencedialog.h" +#include "ui_newsequencedialog.h" + +#include "panels/panels.h" +#include "panels/project.h" +#include "project/sequence.h" + +#include +#include + +extern "C" { + #include +} + +NewSequenceDialog::NewSequenceDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::NewSequenceDialog) +{ + ui->setupUi(this); + + ui->frame_rate_combobox->addItem("10 FPS", 10.0f); + ui->frame_rate_combobox->addItem("12.5 FPS", 12.5f); + ui->frame_rate_combobox->addItem("15 FPS", 15.0f); + ui->frame_rate_combobox->addItem("25 FPS", 25.0f); + ui->frame_rate_combobox->addItem("29.97 FPS", 29.97f); + ui->frame_rate_combobox->addItem("30 FPS", (float) 30.0f); + ui->frame_rate_combobox->addItem("50 FPS", (float) 50.0f); + ui->frame_rate_combobox->addItem("59.94 FPS", (float) 59.94f); + ui->frame_rate_combobox->addItem("60 FPS", (float) 60.0f); + ui->frame_rate_combobox->setCurrentIndex(4); + + ui->audio_frequency_combobox->addItem("22050 Hz", 22050); + ui->audio_frequency_combobox->addItem("24000 Hz", 24000); + ui->audio_frequency_combobox->addItem("32000 Hz", 32000); + ui->audio_frequency_combobox->addItem("44100 Hz", 44100); + ui->audio_frequency_combobox->addItem("48000 Hz", 48000); + ui->audio_frequency_combobox->addItem("88200 Hz", 88200); + ui->audio_frequency_combobox->addItem("96000 Hz", 96000); + ui->audio_frequency_combobox->setCurrentIndex(4); +} + +NewSequenceDialog::~NewSequenceDialog() +{ + delete ui; +} + +void NewSequenceDialog::set_sequence_name(const QString& s) { + ui->lineEdit->setText(s); +} + +void NewSequenceDialog::on_buttonBox_accepted() +{ + Sequence* s = new Sequence(); + + s->name = ui->lineEdit->text(); + s->width = ui->width_numeric->value(); + s->height = ui->height_numeric->value(); + s->frame_rate = ui->frame_rate_combobox->currentData().toFloat(); + s->audio_frequency = ui->audio_frequency_combobox->currentData().toInt(); + s->audio_layout = AV_CH_LAYOUT_STEREO; + + panel_project->new_sequence(s); +} + +void NewSequenceDialog::on_comboBox_currentIndexChanged(int index) +{ + switch (index) { + case 0: // FILM 4K + ui->width_numeric->setValue(4096); + ui->height_numeric->setValue(2160); + + break; + case 1: // TV 4K + ui->width_numeric->setValue(3840); + ui->height_numeric->setValue(2160); + break; + case 2: // 1080p + ui->width_numeric->setValue(1920); + ui->height_numeric->setValue(1080); + break; + case 3: // 720p + ui->width_numeric->setValue(1280); + ui->height_numeric->setValue(720); + break; + case 4: // 480p + ui->width_numeric->setValue(640); + ui->height_numeric->setValue(480); + break; + case 5: // 360p + ui->width_numeric->setValue(640); + ui->height_numeric->setValue(360); + break; + case 6: // 240p + ui->width_numeric->setValue(320); + ui->height_numeric->setValue(240); + break; + case 7: // 144p + ui->width_numeric->setValue(192); + ui->height_numeric->setValue(144); + break; + case 8: // NTSC (480i) + ui->width_numeric->setValue(720); + ui->height_numeric->setValue(480); + break; + case 9: // PAL (576i) + ui->width_numeric->setValue(720); + ui->height_numeric->setValue(576); + break; + } +} diff --git a/dialogs/newsequencedialog.h b/dialogs/newsequencedialog.h new file mode 100644 index 000000000..aff4347cd --- /dev/null +++ b/dialogs/newsequencedialog.h @@ -0,0 +1,30 @@ +#ifndef NEWSEQUENCEDIALOG_H +#define NEWSEQUENCEDIALOG_H + +#include + +class Project; + +namespace Ui { +class NewSequenceDialog; +} + +class NewSequenceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewSequenceDialog(QWidget *parent = 0); + ~NewSequenceDialog(); + void set_sequence_name(const QString& s); + +private slots: + void on_buttonBox_accepted(); + + void on_comboBox_currentIndexChanged(int index); + +private: + Ui::NewSequenceDialog *ui; +}; + +#endif // NEWSEQUENCEDIALOG_H diff --git a/dialogs/newsequencedialog.ui b/dialogs/newsequencedialog.ui new file mode 100644 index 000000000..de589f8ee --- /dev/null +++ b/dialogs/newsequencedialog.ui @@ -0,0 +1,360 @@ + + + NewSequenceDialog + + + + 0 + 0 + 298 + 341 + + + + New Sequence + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Preset: + + + + + + + 2 + + + + Film 4K + + + + + TV 4K (Ultra HD/2160p) + + + + + 1080p + + + + + 720p + + + + + 480p + + + + + 360p + + + + + 240p + + + + + 144p + + + + + NTSC (480i) + + + + + PAL (576i) + + + + + Custom + + + + + + + + + + + Video + + + + + + 9999 + + + 1080 + + + + + + + + 0 + 0 + + + + 9999 + + + 1920 + + + + + + + + 0 + 0 + + + + Width: + + + + + + + + 0 + 0 + + + + Pixel Aspect Ratio: + + + + + + + + 0 + 0 + + + + Interlacing: + + + + + + + Height: + + + + + + + + 0 + 0 + + + + + Square Pixels (1.0) + + + + + + + + + None (Progressive) + + + + + Upper Field First + + + + + Lower Field First + + + + + + + + + + + Frame Rate: + + + + + + + + + + + 0 + 0 + + + + Audio + + + + + + + 0 + 0 + + + + Sample Rate: + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Name: + + + + + + + Sequence 01 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + NewSequenceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + NewSequenceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/effects/effects.cpp b/effects/effects.cpp new file mode 100644 index 000000000..594c308c6 --- /dev/null +++ b/effects/effects.cpp @@ -0,0 +1,16 @@ +#include "effects/effects.h" + +#include + +QVector video_effect_names; +QVector audio_effect_names; + +void init_effects() { + video_effect_names.resize(VIDEO_EFFECT_COUNT); + audio_effect_names.resize(AUDIO_EFFECT_COUNT); + + video_effect_names[VIDEO_TRANSFORM_EFFECT] = "Transform"; + + audio_effect_names[AUDIO_VOLUME_EFFECT] = "Volume"; + audio_effect_names[AUDIO_PAN_EFFECT] = "Pan"; +} diff --git a/effects/effects.h b/effects/effects.h new file mode 100644 index 000000000..e061cba16 --- /dev/null +++ b/effects/effects.h @@ -0,0 +1,61 @@ +#ifndef EFFECTS_H +#define EFFECTS_H + +#include "project/effect.h" + +class QSpinBox; +class QCheckBox; + +enum VideoEffects { + VIDEO_TRANSFORM_EFFECT, + VIDEO_EFFECT_COUNT +}; + +enum AudioEffects { + AUDIO_VOLUME_EFFECT, + AUDIO_PAN_EFFECT, + AUDIO_EFFECT_COUNT +}; + +extern QVector video_effect_names; +extern QVector audio_effect_names; +void init_effects(); + +// video effects +class TransformEffect : public Effect { + Q_OBJECT +public: + TransformEffect(Clip* c); + void process_gl(int* anchor_x, int* anchor_y); +private: + QSpinBox* position_x; + QSpinBox* position_y; + QSpinBox* scale_x; + QSpinBox* scale_y; + QCheckBox* uniform_scale_box; + QSpinBox* rotation; + QSpinBox* anchor_x_box; + QSpinBox* anchor_y_box; + QSpinBox* opacity; +public slots: + void toggle_uniform_scale(bool enabled); +}; + +// audio effects +class VolumeEffect : public Effect { +public: + VolumeEffect(Clip* c); + void process_audio(uint8_t* samples, int nb_bytes); +private: + QSpinBox* volume_val; +}; + +class PanEffect : public Effect { +public: + PanEffect(Clip* c); + void process_audio(uint8_t* samples, int nb_bytes); +private: + QSpinBox* pan_val; +}; + +#endif // EFFECTS_H diff --git a/effects/paneffect.cpp b/effects/paneffect.cpp new file mode 100644 index 000000000..c10e35a64 --- /dev/null +++ b/effects/paneffect.cpp @@ -0,0 +1,50 @@ +#include "effects/effects.h" + +#include +#include +#include + +#include "ui/collapsiblewidget.h" + +PanEffect::PanEffect(Clip* c) : Effect(c) { + container = new CollapsibleWidget(); + container->setText(audio_effect_names[AUDIO_PAN_EFFECT]); + + ui = new QWidget(); + + QGridLayout* ui_layout = new QGridLayout(); + + ui_layout->addWidget(new QLabel("Pan:"), 0, 0); + pan_val = new QSpinBox(); + pan_val->setMinimum(-100); + pan_val->setMaximum(100); + ui_layout->addWidget(pan_val, 0, 1); + + ui->setLayout(ui_layout); + + container->setContents(ui); + + // set defaults + pan_val->setValue(0); +} + +void PanEffect::process_audio(uint8_t *samples, int nb_bytes) { + for (int i=0;ivalue()*0.01; + if (val < 0) { + // affect right channel + right_sample *= (1-abs(val)); + } else { + // affect left channel + left_sample *= (1-val); + } + + samples[i+3] = (uint8_t) (right_sample >> 8); + samples[i+2] = (uint8_t) right_sample; + samples[i+1] = (uint8_t) (left_sample >> 8); + samples[i] = (uint8_t) left_sample; + } +} diff --git a/effects/transformeffect.cpp b/effects/transformeffect.cpp new file mode 100644 index 000000000..314b9bf4f --- /dev/null +++ b/effects/transformeffect.cpp @@ -0,0 +1,119 @@ +#include "effects/effects.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "ui/collapsiblewidget.h" +#include "project/clip.h" +#include "project/sequence.h" +#include "io/media.h" + +TransformEffect::TransformEffect(Clip* c) : Effect(c) { + container = new CollapsibleWidget(); + container->setText(video_effect_names[VIDEO_TRANSFORM_EFFECT]); + + ui = new QWidget(); + + QGridLayout* ui_layout = new QGridLayout(); + + ui_layout->addWidget(new QLabel("Position:"), 0, 0); + position_x = new QSpinBox(); + position_x->setMinimum(-QWIDGETSIZE_MAX); + position_x->setMaximum(QWIDGETSIZE_MAX); + ui_layout->addWidget(position_x, 0, 1); + position_y = new QSpinBox(); + position_y->setMinimum(-QWIDGETSIZE_MAX); + position_y->setMaximum(QWIDGETSIZE_MAX); + ui_layout->addWidget(position_y, 0, 2); + + ui_layout->addWidget(new QLabel("Scale:"), 1, 0); + scale_x = new QSpinBox(); + scale_x->setMinimum(0); + scale_x->setMaximum(1200); + ui_layout->addWidget(scale_x, 1, 1); + scale_y = new QSpinBox(); + scale_y->setMinimum(0); + scale_y->setMaximum(1200); + ui_layout->addWidget(scale_y, 1, 2); + + uniform_scale_box = new QCheckBox(); + uniform_scale_box->setText("Uniform Scale"); + ui_layout->addWidget(uniform_scale_box, 2, 1); + + ui_layout->addWidget(new QLabel("Rotation:"), 3, 0); + rotation = new QSpinBox(); + rotation->setMinimum(-QWIDGETSIZE_MAX); + rotation->setMaximum(QWIDGETSIZE_MAX); + ui_layout->addWidget(rotation, 3, 1); + + ui_layout->addWidget(new QLabel("Anchor Point:"), 4, 0); + anchor_x_box = new QSpinBox(); + anchor_x_box->setMinimum(-QWIDGETSIZE_MAX); + anchor_x_box->setMaximum(QWIDGETSIZE_MAX); + ui_layout->addWidget(anchor_x_box, 4, 1); + anchor_y_box = new QSpinBox(); + anchor_y_box->setMinimum(-QWIDGETSIZE_MAX); + anchor_y_box->setMaximum(QWIDGETSIZE_MAX); + ui_layout->addWidget(anchor_y_box, 4, 2); + + ui_layout->addWidget(new QLabel("Opacity:"), 5, 0); + opacity = new QSpinBox(); + opacity->setMinimum(0); + opacity->setMaximum(100); + ui_layout->addWidget(opacity, 5, 1); + + ui->setLayout(ui_layout); + + container->setContents(ui); + + // set defaults + position_x->setValue(c->sequence->width/2); + position_y->setValue(c->sequence->height/2); + scale_x->setValue(100); + scale_y->setValue(100); + scale_y->setEnabled(false); + uniform_scale_box->setChecked(true); + anchor_x_box->setValue(c->media_stream->video_width/2); + anchor_y_box->setValue(c->media_stream->video_height/2); + opacity->setValue(100); + + connect(position_x, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(position_y, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(rotation, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(scale_x, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(scale_y, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(anchor_x_box, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(anchor_y_box, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(opacity, SIGNAL(valueChanged(int)), this, SLOT(field_changed())); + connect(uniform_scale_box, SIGNAL(toggled(bool)), this, SLOT(toggle_uniform_scale(bool))); + connect(uniform_scale_box, SIGNAL(toggled(bool)), this, SLOT(field_changed())); +} + +void TransformEffect::toggle_uniform_scale(bool enabled) { + scale_y->setEnabled(!enabled); +} + +void TransformEffect::process_gl(int* anchor_x, int* anchor_y) { + // position + glTranslatef(position_x->value()-(parent_clip->sequence->width/2), position_y->value()-(parent_clip->sequence->height/2), 0); + + // anchor point + *anchor_x += anchor_x_box->value(); + *anchor_y += anchor_y_box->value(); + + // rotation + glRotatef(rotation->value(), 0, 0, 1); + + // scale + float sx = scale_x->value()*0.01; + float sy = (uniform_scale_box->isChecked()) ? sx : scale_y->value()*0.01; + glScalef(sx, sy, 1); + + // opacity + glColor4f(1.0, 1.0, 1.0, opacity->value()*0.01); +} diff --git a/effects/volumeeffect.cpp b/effects/volumeeffect.cpp new file mode 100644 index 000000000..b29de4884 --- /dev/null +++ b/effects/volumeeffect.cpp @@ -0,0 +1,38 @@ +#include "effects/effects.h" + +#include +#include +#include + +#include "ui/collapsiblewidget.h" + +VolumeEffect::VolumeEffect(Clip* c) : Effect(c) { + container = new CollapsibleWidget(); + container->setText(audio_effect_names[AUDIO_VOLUME_EFFECT]); + + ui = new QWidget(); + + QGridLayout* ui_layout = new QGridLayout(); + + ui_layout->addWidget(new QLabel("Volume:"), 0, 0); + volume_val = new QSpinBox(); + volume_val->setMinimum(0); + volume_val->setMaximum(100); + ui_layout->addWidget(volume_val, 0, 1); + + ui->setLayout(ui_layout); + + container->setContents(ui); + + // set defaults + volume_val->setValue(100); +} + +void VolumeEffect::process_audio(uint8_t *samples, int nb_bytes) { + for (int i=0;ivalue()*0.01; + samples[i+1] = (uint8_t) (full_sample >> 8); + samples[i] = (uint8_t) full_sample; + } +} diff --git a/icons/arrow.png b/icons/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1016612d69db34d03f503600e641ea2e5a7a112c GIT binary patch literal 1459 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 literal 0 HcmV?d00001 diff --git a/icons/beam.png b/icons/beam.png new file mode 100644 index 0000000000000000000000000000000000000000..c056807fb755c42cadeaff0cac8c5a9bdb899854 GIT binary patch literal 1393 zcmV-%1&;cOP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<8AY({UO#lFTB>(_`g8%^e z{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ-(@8`@RCwC#nN27}Q547jG(#v$Lh?~o zqej^n8*3$r!b-7VYeOuQl!XOZ5Ee?p!fGs7YBm@xWN}LGuN4-v-BN2=BGtjo)r1ESvLV@ zXtgHgpR#xl+yrRmp@z^#5S-I|lWqcl6+F_?)p1apFFDk643NCyXTDlyozB%H0N^lF z?TsOcmJ9$KNJ{_CTA7j*0G!qU8l^gNfGl39^owE@WI}3hSXbi}vC0nsfIBrx{0+db zm;o>V2EYIq00UqE41iB`PZ!E?f(GU3#wqf!ic5i-ppaany`3@Ji>kPk>8;m`s`v>H zX=4d*9zjo<^)GnWm=v>?*_K^_Ru-{R)|mK2-%cEH0M}|B#g+LH82|%d01SWuFaQSN zUjhWxDDVRn@iueNQjYG*H>;&LW^hldh$p-Th@=$dBi;e{xntWGjC%tR+_qzaO@~Smv7{00000NkvXXu0mjfKw(*o literal 0 HcmV?d00001 diff --git a/icons/ff.png b/icons/ff.png new file mode 100644 index 0000000000000000000000000000000000000000..f7c7b3a0133f0a893a187a96b1921bbcb07f7a47 GIT binary patch literal 3875 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 literal 0 HcmV?d00001 diff --git a/icons/full-icon.png b/icons/full-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cbfa4f3b4b54a7aa8c1fbeedd6155bcf2baa28e1 GIT binary patch literal 21806 zcmd42byQu;vNyVL3lf3^cXxM(;1)bMtc4`_!X1JH2*KTh1wsM=f?FWCdkF6CvT)vH z?|t?;_l|qsx#N4^ACEC$&DFEIySlpSSJhQbgoc_tCOR=X2n51ZRFHiM0>J@~a3EA< z;KRt}lNIoR=A>W%1%WW0J^sRh(leg}56ZUM`mXw_D#DhI_Uz_hM+*qMr@a$E4FZWu zcsiL|zJa(>T0pFA9mJ>)8(XO1Ue!ZI!pb5}Tm;DzxMAqCD zB1ZkFMr>T%Y}`WHT!O;f0>Zp}EL?y-asElF3MeMn+|~SVVnIOL*tog0IRQ#eVJ^=9 zLhJ~(wetFpq+m;7D@PZ5b70$S?ai$r98M0_f6S(;Dy--Lbv1Xegec02Q3I^9+uDMK z`31l{JUoIHY*yTYoNPk8yxeSpTozz9D}IQ%Ij@Bfmyo6TKl;l$TDm=|)MNiYtqbgE z35@agUI<&5^H}om2=TM=b3ugI1bBEMY!=)+0&Essf?R?YmI7Rsg8cs&P2I&7FmQ9b ze;xIaD=;vkfHW6B4;Q}>zmS}aoB$V>kesZblpGhQkd&;Hj5IelHRV6&lyS6kbWwE# z1M=qi*LFo284VXlD_c9@1@xu7G^L`P3_q6;KR+8cI~O(Oqe2J+JOJx4e>6cc>VGuq zKWej`+nD<2iN1@zd9NGhxh+{t>a(|>;u5NZt+2V}p9`>j zR9^^~`mbi&|3L@-v$X#l(!&M z|7Izcjt=e+mw!0IqR! za|>~C3;q9Ni~i$M{&#r)-z?|f`KA9`+Q#u1{``qy9RD5O{`@Zcr%JqdRI)$93gQ0= z$o{vc1jHG_fb04X&WxYWg3C(4oR>Px z1|!M2gH1bdlSAlm;K2;LK0K`w8-q=0O^ETaSvP~5{wMJQ{aGoQ&|xvy9JIihrF(QB z0zN)aTvCsc+y+|~{%IgK&YX7EGovXyIDAPgCxRY$?m!wz<_ZkKaqX-VpZI#r}BHiMK;#>+fu<{K= zSBG)Itps@lIb%9I2Q6_^J-K9XqHz-OelOKLhC|S%+NKtAgn@Y8Dd?KkP}0I-lp_w3 zSi*NrkfX$z;II#b1>r4COB(nYRkyGPUbGQE5@QYyk3zo4_!*Un<4KyH1%~CFJFx{x zIyr~IOML8KpmQ$K3S4f(J=3=dk{t2;3HLduWUDV%%_tIylR*Cij(<>Y395X`k8f_x zlE(OLoJ%A>`Hg@!G~|||4^-!FY4B$&fg8!5!S!9Jgde?)@Y2!VkrD@$IQsdLmaD;+r{B}yn<9TTu;B1|gu#_-cBE#e zA0XA06cOT~BB~Az2PSRJ9&#wC7J-hp@>?Y-ZG@U4*&7MhEVMvqmxSJ=m5)u;Qj2jS zaHT0HQ3fChgsy4JVqn%)t`AWv^M&6QbxC9=2oz-07npERU(we~3M?7vIGXgpd!kd_ z!b>SLkX~B4IqDXJ8W37-Vh>OegS$4^OBH!vWj#Aml*N(=%Kg3s+Q?@h{jQmWG=fl< z7bHRp@Sh^Igl6o@I0a{62uF9O2-J ztK{K>az{}?4oKMo{+eFsPS!LY3Y0j!>=NA{L1kKjKeU&DaS^{nW^Ywu{bCE8Q5_`# zc4TV+xh8lXK6R4U?F2Y-3g*~@d6l1SP#COn4oQ| zXfFbo-Sx%WZusYu1;&s4qQ8Q8s~IV4B1C8&nSV~%7xdz#7EBiCg@ypmRo8+^KRz~- z+PK>9m;gec&-?tu)%G8c?@bQXDRGSaU7d1gU?>=f!Tn*3C^%CS&2?aA2;Uelreb_k z%;yz7_#Y6JPY9TWB|e(gbHU}d#5TDl>6;!lltHx0*B**vi_erKDLI`4d8=cK4&<^p z5sfFe$QCkopOrJz8*``0r8QNqBf+qoE=a1Q%-NM3;I69c={5T{<2( z^LU&$Vi=t4+3E5n$u$xj?-c2k#InTtfoUnw4C17^c*JlbC7sg7Evg<~r@wG<yjQ11nlNJk&Rd@4K~Ea~jdHPjKT;mIUu)bE6`{gL!S@R6~X4H;j&!$T9R{qKWdp3lxxqV#i85uK zN{JECC%N4E)*5SjYp3oXCWpmV1@Ye0`0}|3y?SFAF$L2KNm2=~7I**vkm@%=a;g%x zbW6hizPUvX4)@t#)O&HP6H8V}j$$@&m2piZ`|Jx*b@DcXq{q^}L~TyTCYDdL-Uh48 zOIk1m(TtvBd`!sNkqQ!ERv6zww6r`!0mm(!CfN7{q*h_~eUWx8{2LGSgG&a$I8289 z-$8R9nmH~4q|6QS_M63>{p?7B?8=eggA}8xK?C$s5uZaI?CxUr^KgY}N4P%b-|0WS zV@S6#`2u+bI(TXvf9`b`-Y7v^D>&`mqw}_0dYo6xribjIyyVq0H+@Q*>}nef$#_>f zjWUAMsjyl*Y<`acBRo9bVk>@amC%t1KSI^&jgiDu1Flos2ph95&QBE_Zzxt9-sjtE z7=onXr3IZ25O1n3b{7J|=@E+k43*LaFhtF{xU6B*GcQ6&0QV*kt1!X=M%JGVA2^@4>7SLB zp%_Ri`H*@Ks>Xh!u4s!k8)FYsfn!!j`uSeciD>ME4aE5cw8}?p*ZZq|>vB{!R2Gi2 zx?f|8TwmGUye8!cBOq|eMUJ?|%yXABm~X(0;)_v-&JHy_Wf->OO|0aiBuH&7bE!d~ zJYSnx4oxHWZk{Cc354=2aEV(cqP`)t3r;&I#7`|pFSC0)b6jPM3oei!&R*fY3b^+x zT{fL;?|6s=AdS4AM&sUiAkWP!kyy9Dtu`BPw=E<|Uu{wL;wUyELp!QYs}GVm$$?9i zc{&E5Kx@1d&f5F0B6T0)j%gwso?*7dvdjF+`j%lS)NQ8=LIs#UktV)(U^#}<=emVs zS2ca$(26*V0FRPIp<<(>~K&Xf;tX!kNDN+QjuPqCmfwNp^m`P$3`Nluz0q8e1f zXEBL%$UOL=>P*Un%D_us-psW=lLUQznZlr;5Q2`uPLr6L_Qej}EO$BSj)6xY;o}PR z3%U>Rx%N(mxYW-s7Y`obJk?P`h$wB=8MCb1(-$YQY;XoDK!cV;$`z-VY9HbAx8}bI zfQ&yj;A~KIl_Go=EoyyA$2eO~hf~d;m`e0z0^Z4ztW&kv?6>f>$Bw#+=M#{h<@HNl z-L9JV_7@N-ONgWj3gVZF4i9M@^Z4gGxg8hL_qQiD@c zy#|Z6-*{}CoB|`96idORAt-Q=!pxV0jRP`x&u26PQ-b$Jh^37QP)|rs>ndnTh@~Tv(@mZ5z$L+Up#4nQ zKZ|RlI2ud<$CtwY#p|i@);KARy!3jJcMHj;?JNBLf+RhoT!^nYe1W+mW@FP?o!fY< zmJFK~u(Z0OLq1WiQ#7qM3}uWo*rC^+a5F|{YosKVP0Pbt*lau7r}WaN2NY^CJ>+LL zvu_VH=eO~GNA%<>+>+fR-XN{I3Ng7QzkI1FJ#7OAdrfC zq#{^jY3wlbrKsC4etwBMxD9Q59~ZyaB#IBxZx3bB9>=W{SU2$(KiJ&JD_k#}t2!d* zCtv*K%NC>*Eb*y>(t*eoqhmE}I%CB2Ny?6e>6Ey;)d9Hwn9WYSB@I_E;wvI6ntoHF zlvaRyicm^1Jjju5X^7}ka;d0mr_3Dh4*lr}w>1&$Vbg!gs_dK*F1&bya-`y+N9Q(_ zSGB6OT*}u*w3X{zwJD}(fVs|uAvZF?nDzB_ zVO${Jj|Nd2F7@fcFZo{r46lEl!L?C|q)s^EcB@gWnoUWSLwES}KQXH}ck(z2Zk1t) zW;$s-WO!Qb@caz#p4O6Q#|+=2WT*a+=)g25c==*QW>qK&8o3v2cW2O66F>4%PyZF? z%ah%d5+pM-efTh~k&>Mt$wKDS;$iEl5rb?QzG&+f?=U$-!n{kJbgYR<+)sU4| zt@TRGqAE~{3G+a0hG^PbN|G$om#b$I+|VAC*=0xn2m11_-uN@7fag@9GlP0hF8ME| z)xJp&%UO|cb-&x1&Z7{^_a38ZK*^F8nLMoGN6=PXuVTwNZjU5nEYw!tKKOJ-6s!Rs z9hi8Vt^o>^N2{A|o^*-9_1rI-em?1!xPPSXe;XpfcJ(a2JY=Pq@=Bp6(x@5tlSRR_ zcJ$Y$#zZ6%$(qJ5MjQPO33%eCQMRqe>Nnr|2x@x5&*;?_fxD5!Wu%&EF#YU5X@HP* z>6R+>;UUER{*CW1%=hiX%NJVH!^QpIy)2o?#QJp~#A3Q2_S*w}`{zWqR@xX8{hRr> z=wt9;a#g5x{rVTf9?_hVfUp*6G+z3#5xr3Oiadh>mlTl~xhjS7ve@GH=iMgZ#aT+L z2S^?X#_9I=2+EP&C_9SD;C5l=znKfyE8qkUoh4JagOo5AB zbK12MvR9I+YH`KEe@ei%{$=c*+@Gg^kdF&qZs6I;$zMRB`MP)tX7Y+LLrQ-KCm-l!?IvGF>l>?C|HOdI`zJdP+P$?y6J zQMy}F(gF3?97bubkfiX>>3tZct!;XZEi2f14?&84^n5n3Om>DVxvMq&^_ldm##65i z*zM`nTFx*-EoVw_JUqzP&v$$AyF{;8yy8L9z>>pTG<4KbH;U`Uv7C=5)Z=zA*eL@) zbMm`b)ZaC>%CoUGvLUiKTNDbh0XX*Xvp8!MA>|!}5)u-(es`L9p$5al?N8Sv(KOB= zx|VKojDg59jDb<+jBv$ap9kbNmxg@ZVb+Y5!9}*8(sv;6BF1W@k@;PAcG15Oa)?h~4{8#VoL07xO)Rs;e}lK=G$s z=O{}NVe_i@I717z-|7wSDE&(CUF+(Mh*>;tM(o2?x zBMhVw8$mm&LU_mbG4fLnn&YQq29;2>aTGQAcv>ShLba4<5)=}rc;PE;g$=QE^)HGl zs96xNP@b+)$cFb|oxR)rai4!PQyG9^+dY2iycmO*Oz>OA7J}Akdi?XZ-hwYpW9Xcx zz0MPtLh<@L{B-42&t>Nih2*QV#*697AMIs+$B(cF4UJ}PkF{VN@2_tEs-cO?i<7O6 zpk(EJGNVptMpM^~_exndUlq>gz&E7qJwaETaZwIS>OW;D5PeOd6^v$~q9pk%?b=rhJF$a*2 z1x(-CRT1}sLlDyN>$iqX&->a)k6Abhb!rgHhOJRc_^>7Mz2FAsKnFn`b+vmeyZP=q zx{`Fu0w`pIV;rl(UFed9s8UiLlz5V=&KMD|@(zhJCEUcPpVW8x`dn?SpY6;=rEWc+ zwY}Ort=}y4(f?ps%Lw+m<`CJOa2)3_|{{ChXq0aB$sNmj7y`eA5K{v{mzrG zV#uE3^n6;#SJ=*xR(im*_-Mhr`0WKCqBNnjG9Uzu5%3?#s|s(zlY&4rbSgjeou2g> z1P-W3C#iVLeCvrUla~<|J#|-1-lJ)`ftHZ2YUT>5}grx9NrsXc2P02 zF-r%b8}J4bU=RAeLzAKR%xdwxvmp1nc`D`{wmj#O(P=#E2C-}XR&b_FRj*e$Sgb+P z$K0ndsIq?Jw^()lOLwetd;~mL!?DN=o)jxqCDI>{(77ig2>JpJ)%MyZzRZ+eiR}WG z{}EVzQ9M4*_Oo6AY~Q!#9nV@@SC_E9`s@j3H{B^Zw47L#D;JQfLKav~1(O2$4wh>K zS8nkG;u{$rl|<YV;m(e`cE!lk)Z6{pn&PI9nbs1X$pFf{V2G}PtVdu) zS#7*a+w1$)S#qm&Jy-t{s>uE!OQdUO{vL1VS-`!!nvd;O3# z%VW>gw1CfQlgZ!p+)6I{T4+7Szr3~WD0)gpV7w{M1Wo?7-9xWSrr((^^QMP+pfckq zDeE1wbc-S8*wSVw{3kvF)4*3nE)PF%>h1UJYh?KMXMbV_G{Aus_ELKcVXbn$H=gCn zel^QH_m_(jD~Xxc#0`+E0TrXyyfRPq@nnD#iKE1kd(!mRNg7+?lC%<5b>pEEzy98V z;p$FORLF?1rmz~WR^TfwMaY53w-}PJc{9VOTX>TZZ_`kOTz7E1=)71L>Qcpz{oX}I zR%OkK)CkZt#(-6Kh==qt1;}hGKXMck^>W({>a)P&dN_Kmw~Go@NQ9%80+)m(;%OCS zYa^^=S5O^D{b`uyU!uArLU-nv(4#QKY53?cCc)L+B&10;Fz(EJ~h12^q zsqVa1cIV^@heP2a>@1Vb$+vy7-y_D7e!L?gA{M|iND^{Ccn*O;7W%z6UT5joys>2@ z3!ui6%o<@`B|HX@%unEqu$5=Xh7> z8|2-J%gWvZC|YO4?${49$FHUhLDJA;Hh_byLoS!QV?(HFI<6VcHWMau9wTb`{fX5Q zJJs2~kOH*J8}H2UN?INus9I@X)z519Jy*GlPp5bFeO3uL#RN~h*QUo#H7ZwX17cML zo0PW10;U0S;SA1JPPI%isf8=i#x|0t!R>k@dz!+x1s%6V2eMI}mF!<_(;bj#hilT7 zTd+)lWYXXQUr{eR6K-04ns4K};fr%pIoWr9wZ^ay^NQPP?Y}3!ucfT{EPQ(1Jin5g zKpXLX@5u$l>@dEf-o$Hp3;Sc(xwYgb+#KgE^5fZW^osg}6@{Ez$wgfN&oFU2wM`f! z*;gF&rI!_5NQo&a`8MbsxZyJ&wx7(G!8ggWVfyn@fCAWFY<>Bf$P;wl;;wh(GkNp0SOH72Pm3J#W~cZ3Yty$N1u> zInzAE^e5(U6EuDzXY2DK3v@;H=Y~Eq@G_}!qHh76Cl&N+D?+3-pcWCxAn9JvU#|y3 zJWihpyRLI$`B<8FALFTq@UuVro7m5s*NRvQn0~#9cQ@ZBo+Jj2_vBgLPUlOXdwcqp zdbVk-7Wa(tGs4s1&Ebrg9e$i9+lE&+corSa9cR>TYgvkVz zuBR_+4i=RF4g4Ki`{tc`ff77m_`){Kad2^Ei?!s%{UxwLCopwcP?4mq#J9UggHo0s z=e4@X={>t2f8+asR`LC=z=zf1m?{ZqYxbA!{hr;q8lJnA>!+kR1GA>hs?#5g(T*@G zvPg*W`ic-TetFxljEAdi{$6`i?{zkKw*M;yBo6|jg~a5Pox23y+4$vSXt%*x2Q|Q0 zWKKuS(7td!<2i+Z2EO0QHRo5DpzfZLl%{Rm)xeZLx^nKZhOi1yUZGo7OLRWG^ofZ0 zWUNb}7@r!;8`Qk*qbQN(@F66!fYWV68)T7hnRa?fiVS9=i8$w&MuBmNt)2w+)oOFKGp7-H)50gK_{q0S_gOy{KiIS2s zDhAr=?|S`3*_w#8J~+KfdN<7aO{myx6y_UAeYof_4NyuXG7z@@V4I4_Pk6A-^QDXFfsO?8N$ZEvIP9R@Am_i`?KX7 z{VEU47V#EZcNN+^V|cx@=3o-}d4=L5wKanE_@oSo7JNLl<^l8OQtw;V*@pKX$xR*d zivd4leEiqiz9E3Etm6$+`>)1MKb1zd#RM}kkS2o(4NzE)($ROWqh7a23%wP#`YVhUiR3fW}gL1eGjqRPZf2sqpifMCQ{@c zDy?rN=B`o786{N5JuQz(nl7)RY${*K>LXTl4_FQT65=lg|1RMbH+VqHalFo48Czja z2=s;^@W;Pxpdlx#d`tO237mXvNq#4bd~JBU%V1=>4z1TFJPbG$H@t9MUaL#yq>ZsIz$5GPbLb!}rp zbIs$B?=|&7DZxdrZSaXHZch-ryU$t*-X!THavkSIfC;U}X=+eR2Os);%O54~5mQuA zfCJvb84a2~49F@7p)KDf9Te^}=2+;A8=i9#jO@Js?v3J3rmVIN9)L|6+An`@&yACcd!+&{{hPzE@I}GlX#Sn{ z7U_Iiu|q)Q4>9B_^I_y{&s{XNOrhnO2D{!X$3!>*^oZ2RwpZHv&>1n1aioo&k;eGFA%W9g4l|%Hkk1&jP z(e5@3Th~|ey4O{GA4GG%Xs@adC4a^C#8QL@-JK#(0~8%R=AWsL+(bL41+orJ*na!E z<%vF7L>8{s!z_%+BYFIbmcaE-7}w%1(R4!SBHPbUj9#idCFaG$+ibuKJRcvWD(B@TG>c_l)k$2(OL(KUx4QbBqDt6}PaKP)p?X((^V874bK$rCSvQk)tv}#? zJW(O-8PuCo$zk^sjCTtGbi<7b7N_Bh2hDbzxZxQLc!O_7b*l>S*91UBh{)E)&-!OZ zVwbO_;o0rc2B(+%^DM)EdH7Obg+E5mX@^LcNU?gU-4`9RV!;JHdqNH2m?b377Xw6t z{M}pIK8feQb0i!to&W6gS`BPaLr2^@G`!JBws<_69@E3tM^|s?lI4P?5HCB8Wn6E5 zi|HD<_p^p(_G~_;g}XAR@O5l??NQVmoNBn;Y$emHFB!`fFLjb&{+{K@6aka($v6oH zOTPKnGFS{PP4(7Nm&9j>%C~2#9}3pIH3-O*d;Owx`n(7`XQ(}#02iMWlD;`rKwT+A zUoIHqk=zRHRralw8z`|Ib->L*v4KqsZ3pE3ztCR!(?+Rv}Z(N!E1@kfyg7uUh&(B_4HamYkw0O zg{0VuOTB_fcGtjKS_COV;XiWX+Kk_|P7uQpo@CtuijH=Ir4S0D6?vhk+d03Xxz(g>;5cS;#J#mH`k+)I{%EVb3^;__)8G#!hJ zUo%bOhiG} zlVr2GGy3cFuD&c5CPqdxo0vSlp%FvRme?E9x|~z|%|SXIQwr0fCs`IE>}?!@^WpB$ zwTY+WC}xP0+C$Iuomv9T=fgE$nJCGU_#m$UF$Yf(?76O;;Sa!V=5CJz9Xy~qCpzcp zCZcT&f$IL3Kc6rWbjoIsKL&$SWbT#aCJnobEDwY4yR9p%rqiQ7xLcnNx%5A)bz+`2 z;k)d9E3xv@*ra=ySfpQ7t;c(QS$aU7?h9km9+qW z^k>sq?XIS%-<@CYE~6t|PgN5VDxJt#50ZAiAJlP-go&+Gns&5DnR$)Y*_+cVxKP}^ zV*)pB=jC%+Sz6HJWdvdZjuT9s4$pj=9nN<*0|v*wC?BJpNTH2%*bn{+P$gs^ELnVL zyjpy^Mbh)hE5FX=DCu_npw&}V70Bw;J$%2pa=7uJ+#VjKszwNC-8_7<$A1(ro;+BR zQGbs;y^89xgzB<&cmDlMZ)A&czw4b&ptpSK1h_d(*$qqibTrfisGlx@@%ZY# zJgfJ0w&kjlAG~NtD(^_?+$Hd-IV65_Go@{~MPn!J3jY=tSySUx+s%VdPL{1mny6i+ zecDK#U>*0BQst0UoVWI{p1%I89_!EI`E_-vRAS*1^(!N9C|bL|o*6g9L)GKnCt)lj z+Yl$Z=ZC`EFxoLSe95b=+)JYt#A`fiG|e!yo~Ky_j^A_6on$8M)Acge1Z;+~&R)*v z$L9{pZB#;aE+a{2@AiKLuVVti)9gT{%uRFiVIs>XEJ?Fu3l)@}9l3jfR{M+;M`fd4vIrokBB-tW<*qOYN!G#!H# z^TMsvZmxB-rpc=RYyJ-YdwEw9sqS|lWJBeSkFhCe6mh*aPcN+H3loqab$lS^1M6pGT* z*Uyb)daN@bOG%AwHcm}<$F3*{M?x~l!%=JQ5Dr6aEqbl8BNEX%e^QHKGPdpFa!#X{ zkes!#|0GcoYWoSF*+Z=^VR7IV6EyBG5q&2Q%YdO{qOLc~FVHZJiu8{JI}#ohW8Z`6 z4>+%j>Un3GLv~(@W&5$B56ep*pI-{7Z07J`#O97ZC6Lqtie{6%pJzTGx3*rx2+Q~p zuP3O#X;J5Vy`4CKI+Y{;l>K=$yWI-89@z%6>TsO?ZJv?JQ8NKXdhn31+$|;#d@?>y z3N69%v*$Mq7YvoEv@9NF)i*q>jc9}0?;)?W3Zdgij{3Lw!9`NG0!oa58-PCwK?ynC z`W`dYqjCVG#Q;?Y4x_;3k=QNLnKbNxQ!0c;EZ$IxGj7N;6m_s?`EuF9xAc`03uYsH z->Q2OWO^)G{@*PWxu(vAg+Zs(5Y^dp2 zbl%`I;6xdc`K_>vG3BMS6ZUy{)MGyyaQ+uHnAWV+>FR<7_cH zrBj=X7+(kX#wc&i7IFZYfBWu>4~MF*HoPn~<(xEFm~Yw!_BSsexn{%HhF%e4@3cNC z@)W69w7)8Ur?Is09@)?GGvQpRf|81bS-YgzR*#r~zz&AdJAdPwvEw+ie^s zmq>tip9;GVg$RjWjCYS@9$dBZA@V?eQ^gCH2_!T3*Y3ukAhU<1sLVX*=~{-@{^@iN%aF{=vo&g1 ziwj4<&H7^9>ouJkOUE={4EB_$=~l0-#2}`#!{0ey$r{GEKGHm;dJ1H75igN|yGNmP zp?FDnai*EWK;-^d?_g)fG7abO7xxG1LdtH=EoY+mL+Fu8QG$j}$z^k_r#B^YS_Rg{ z(6a$eV5hPLQ7n-)nJ$%RNlJ)bwxZmi;g3AwlJ+E&s7(F3|2ida)3?_Pc~LwSy&k?( z&wQq;ABZna^PxGnn+dG94tWgdPh+(sto`qwRCNuNR^~(t#dwskbE{cb04K@|vc>Lf zOP^nUt^#!UbVaA*$&QkYAsE!GfO?5Int~O;U(_+tQ7!Do^jl`{$w~_<1uPw-@!Pfv zMp-Z2gBY}D?1v;yE@%>yQjW)2{9lWh#M4$ns{lN!(m{I7h-W~RCj*a0hMUT-J)FAt z(ik{f^(4PF{FPc0H;5vjrSWR15GN#bDOv+Gh!4Zd{VL7pFd~?2Zvxy(V_j^@Ea@nk z+G_GgyBpy1;~hvs#7|-%6a}WqxjK%T&vRV&&}yQtgvyg7Bn_K?i}PBOR}VD!jh~f1*R>f|0sgwm5*Z|h4D!u4xg)Q>*HAu=M)x{g$T6z zCM+(V-9%+?t{#0$fsL`=Jdc%t7LFowmu{E1J?rx)`h-pBTjsbcQ@Roz2H)lG@`BDzPDMdkXn9%O(H8WS

sFT+4=shz#v$+~_!*h`qKL*z-S*7IL36!pb{@eV97Y;Z zeJwt9MVI`&y3#xa^w!pM*qqzGv77h8?|9S1;juyG$HcmwNaf&MgZ+WJ6NhqfbYDk2?r! zIuLH*Jz*st5r;UA$}#jez1P+4rlU#9xs6C(CaW03tiEAB_ukl@g`IBx2kE%PqTg%jVbp{{dn4jzjziT=E9)-*+nBkK=tmpqVe6<1NU8bNV z4WyhsE*{R__(4cM5%sRYV=ryJXT+<_Z?%P8DT1ijUp3gekWwC3%((SuhY>HWnJyR8Q zSnliVU0FR8FRCs`REx_(MMF35z9+lg8E#$C9%6zb#PG4ih5WRC=3u!{?NxMyfOw5k z2|F@bxZP5v?x5LT%x}FixvAM#1gcH~BrROc2UgOFrIDe!ukpX4CzqK@tC?Ws$G^y9 zJWu)9L|+|c)1A1+WW=|}tfZs_oGHWl*SR0~bqwU$FiVET{Hg)})*SR3qv_%3 z#;xInnm^W^HB26DMAdF)IKg5-MdS5=htA+x7|EnH&D${QR%rkBZ5oL0`z zy5@w#(5;b%#g0g!#lMx)n(r5?bm;Nfef6XbQzIVY!Go?&oF$&=8!nz&JuC` zI&Q5=u|}|Q@;jycIEUi?EptiDo-&);x+DM#c=>e-{+i+*CSKNADyo3WvqYJED<*~N zqrdA(#@p!9HQC)dTopR_+UucXcvzw8L{juVyFF5s<0MrPn9Iq4 z;>&TZ<_@=SO*LyWoYPzLu;HP>jf6vSDptn#EogojKhLm^Fw6kBw<;b$CM4|>1(#=x z!0SCp+ydu;(&-#{s+!6&d*3pQ*Qn0EGHoB8jv*vyGE<+>6mtIbElDr#?CwT<0ZoKg zbv4fb+t^8no0~PBh>1tP*`6<|#aLF_4qj}c86nMe^wEhqFM-s(6cDMV-yh%2xOo!Rhk6V1t_r=1H!!ChEmjoBv9*mN7(HB`Cw`)jahnwoQ227!w7)-*ZKTE2L1^q5~8HO|0WT((RfXS)Ec(Uehjno+{ zCZF1(wtQ#Gwqi!;h_^NW4EOTT6b)&_&N;@jRBL@R!GGv2l)P+m8%Yl7EPWJ%PVxuC z(HLG^<|s5ZVE6WwM7xh^OZRHIxSEq0C&leL0`dhZ>Q*C)+V8TL8`MG1Esw(}If;_Cbtla^z3J{7#nplD3HBf5d}gDok;nq^&Ws^tTX zOKU5<-B527_F@Ncw<23Dx8-|!sW-T{ zq*h2YJX!TI7KTdS*^ATrlaL3O9*T-oBZx`)TGaw{q*l#tzi9&(iW%zlT%y&*(){9) zOxcb;Abc9MS#JR%cv4aK*!pLiJ7^NGr8FdQDru|SZ(aeHs0=Pvxr?H+UnrQNlF^9Z zu28g>+J6Q`T{$978X1+y6?i@%bNDaXvSG#66t=4K28f z{iJT-OS^MxK7t0Ipv+q`Hp%>8G|pjwfP~(M?zxo|-jok}-4;FWFd^^G8*Bb>d^dMJ z@g*@a3ApLjBSCS;aE}!LTna1I>Rfl({!CfAqeJ)UHwWydwCLmO*GV~fqr8e$wf#rR z`XIKv`Z(H%&RxQcq2?%aVYp}x)zZ_uFS>0F6K<@F)lDz_D#qxFFy<{Acd^7_r+b3c zZyU4gqX7R1WKTVJVU9qtcS|eC1(ulmF|t(hg$0&J)%`*>HM@q+CqBCKy2Oo7=cbw? zu064aiT#y>{i*yGzvZy?jCB~k)g|62M}`zAu`X6-Dt6FMUgIXbP+M7IK#@8fn4LZ} z9_}a?AHaaMw7qMgxjdj)%C|xPhQ~8zPVtS8HXz zduQ|P$5s4kX0J%c6oR2aJ(Otx zH#79|(wyL-;+GGD8Cmmmk)Fw0KcX8O;C^Mn&w}5^vTxN>0(N@Q*Dgva1l(;dH zN(HKBB))hQK*}!yJ&Kx%DuO8AiW9n~(%0XwD3Tmj)o#0s{iNl7_*0Sq1&B88-`!bKAb+3QwoW0-gwG@ze zvFV=0;k`Jrq|t>U^Jcnj)j4%d=+06e&-coEeR>UV?N@xl^W?T`<6WR46{Og~aJ+x* zSPu*Hx!P+SzVw{wX4S6@OxRSzB_mgjsUttz9xI+oN@$mEuk+rUbbY_o6J|`ZhbC4w z?3uO&zl>;Si|My;yOqGmXB&I-*7`mh_=gJPfR?EE%6N>6PNmvk4%a8y z%B@9wAF+wZeNMS*D1Hrn(VOoh$!Yl!EW!SmF2Tbis5sgURcQ?nCUJNCbbQAQB(uLorgiuC8bmr)@R`|={WbD_g$rp`LQ*z`rt+#OKn0dC+1BLovo zKyileZiGyH&iLGm_Sb%HKLd$EVLU0Bw7jge?|12qax_U@)-~78pKVlch`ZJdHB%e&MblpSf z%`xn>*vH5t+jAdnA?txXAxs?}AAQ)gu1!((`22Xiq%nJC{l=7D5>1kAI)&`Z4}~u? z#5j3vY?#V6?6{(*ht{Q-eYPW|P4RRWCZm2&I07!Xm8aR7w@UVDW@kede?P?L;w%K* z`d!nVSxUm-=ENV|t<=+1e~raQ_^j~(Uk9xhr$s4C=~c-1l7@4xGB3A04f`rz zh0@ka0@+CT45CCDWjn>$_8aBTiX;bW+enxbgsc3?8nY-5$CNi?9D5lyBn!HP%%72# z$RRl`tF~b<@}XaFmR@_-F`Bxsy1v9L_R&cAgaB6Bm$P`qGrVxa3V3`{VfU?ISP2{2 zH#FblIYJdu&e1YdunyvvQC#5OGaWytyqQ7r)oy#M%k7ynbAmgqYJleC)%gUWrLaZf z12Z(qd2`&uh5aE@FW?EFlY2w_9=kv~qPI6;aP_xbwdH746eA@6YGNW6v z%R>~*Clu9Dao!@zo2MB2e0wII)zA6xV}IYE#e(UE4oM|Y0bM;8Q5{d^atWJ0Z1gx7 z3Lp0G`H_zzI-x0V|%ng2YB$%{yl>Qz=t0?Nb+ac-!gEE%B ztR;(NLo|o0p4jNpfEU3!SXxdYuR&`s9sE3yOY!E3l+0P@PFn&g?&B?hUV-caW&oz_ zwLq3&vS0RQN!_r$-wA{i{&ASI0ytZQsKiaT%l%DfIVpAiC+9%O#=U!BU^id&z3+6l z&rPEKg9k+bVZaQ<_cHWNGpz^g2^p>57mgyWCK7y7`4+5(wl;giztkN}*qPpH_4E!e z&jg6Qu_{mU-HTw^_486b@o&BvacFfh`x)? z<>X9p%E9yv{lOg66FWg(gG%H9_qpqjcd>5AIy2jinQfOw=#Sgt zFf#7^>0f_h!%ln9j*r8q%JbU>_F^xArc$4gN*%!QKO-SmV#KQkuNQiHUrvUphQAoS)D7xj7df-fy_qF%HGS z+?jKE``NcyvT_LnrVU`$yjlG7qkocqBE8O!)w^qN{{7Uy3GxZjrvfg`%ui60zBgB- z9yj%9!QB@Cpn--y8mTVJvXzDzGhirzYAf*6cYiGT`_&4SZXLVv+21}RC?JTk@-n*q zq${$ItQRYB7a`sB?j|DIL|pLF**V!H&q+p=RJMP-owi}s*GzP}oILjYW4!m@_e|xN z5SPFeqpl!jXbOY+3?einv{sMy-H+dK=hJuEsv5PVND;L^@txvnAJv{f|3G|weNk1F?3`>4 z967*-og4V<`_DLAc-EFruu+t11L0`N=8M3NgoGw(x}a;4xguazy0NYVG*W+zDS@oW zEZ?-8m;Un-vnI~+&Q7esu~WxtqySQeq_Ar9DrBiulwh^>f-sXUF($t~c9(^B#AY_if+t;GsIh>Sq33kf~#R}~|j@qVNC2T9-ij|>{T z^64v-l$Rj;$ol(+2@UK&x}Whw#$WWZ{d@LDb7>S67vbmYSEH`Xojw;oKRXFnM zU8_rM7c`ef)|spe`T#0(?WAjQx|}@x;=`VMS1vR1)2&afx-w_V9Lmbec;dAuObfs) z>WPGgX0N1V#ptiGbs4wwwFQfiSOyd{JjPVOZSq@l6IjjZ(>bR(k$vK#m-BJ>plKSr z_U)=ucW#+^3xBxt5BiFU;U}?F0y3)w`N-ZSz%5yfkXwHCATb3HG#bW~M8n&0CP}&j zbNhkq#6;ID$KdKstLxmCJFmNw(y~&XfB$(;e-tjfN>bfEiWV$G!U7O99>!)QN%CsO z?{^&7anZ}<733iyapd?Bw(Qe{uprePLwe z)gUr7k}(6vRD0;({`ohKXB{^q^IW+2@`o=83l1ZxM^c?4^L%}LnJ|0;i$j3Qe`SOPAX@WjZr1HsM|BjJ6ti*XMtk{hMZ&2$dHmJKx#pg0*p|Aj+Vd|&Z2MT= z{l9nhVx$$VI~ADGgbzT_Y%u%)2aX=#WcJA_kNI-hmsC_#*tQ&-Yxh1f+YfGM<~=jH z`JtOxy}9y=pP=@sz=5@$JPd~)zbEi>! z6g@acaW{8qF{5Qzafkw8a%2qZP z5DGxhe6YmUA3t@RTc+P)+wu3!if?%H!#BO7^b~ulGwT|DPJRwQZ2W<@7QMyJeLGQA zm6#4O7u-Z7OEL+u3GCXxi<4(gdag$!Zl`V}l(xNu0uVGaG$<%4AUZsne%<;3a4z>8 zw?BM4=S$ApLv`Jwf0Y47mLMJtjU&g8uw>;D-u&=Qw(Z$QS$P>zZKDVd4EE|mSer01 zj%RS>_z_CVOKgJQnydh++qKTNQMm<#v32K!SWxMn?46~3vugE07X*bti)U(tf_k<5SUm-Z`I{qjx+Y? zSCg2dN`Z8rVp;^1Ql?o8l-d-5u}o$pTXk4=1GX*&kmo=n51{}A%?U%+-D~}t!>$0V zn{#TC=FFTPNcwS-Z&rkwL)fO7HO(j|f=?>sX zQ};q3xFC%saE5$K)mRs;NAL{}JJ8tM)V&Z0S_G}f^f%Zi13R0#7XpExN!aB;;|y>D zs97dPfk4n2I02k?Xu4gq7Pz5l`y&tt8j-c&NeeC%fIx70u?{>5LB4ImjtB&T#%C#b zEP%uAcJb1z0)e2VW5~NLCD?zPrY{14ppp5^cwtolbx*D?5C~clpBOK!wE?#R`+=TK z&>?|9&^R0b%zA2-vjV@}xP1`_1dYr;tv^T!Q*C~~NZ=R{(3m|E2n3Bn2@r>#0ssIb z5Xj&xun@Sjar-3@2pWNfScD&{E)4PldL0yjK+t+nffpZMx(fuJ$C z6Igw*gDz$P7%T+dxuot01cJum9pL?%99TIN_(I$b^aGOX*eQWP(C};6<1Fs^d}=0O(q0EP56pfk4oB97C7AI*t4Wb)5oO*$oT_zC`RE6bKrF&A`>b z$xFS^rCI<6r_i;L2o@3u8kTp^W$luMAL>s50F(kh2X02}9S{iWn*!h#bQvqHuZt?- zUFvo|JB^Ve;!zmnrpf%wH-ay}hyS5>3)EEMJ7JYL_@N1xd1rR6dhk1&l%D9uTx_GSOr8*8%@SR{+fhDSljhQ}^LwL<33aj*%|z)(*XraX7j^A*jiE yC%BwB50nGB=nlKnK)So_L63>aZnEChfd2=V_EfGDk-GK(0000 + + play.png + ff.png + next.png + pause.png + prev.png + rew.png + arrow.png + beam.png + razor.png + full-icon.png + audiosource.png + videosource.png + ripple.png + rolling.png + slip.png + + diff --git a/icons/next.png b/icons/next.png new file mode 100644 index 0000000000000000000000000000000000000000..a36b72c7d0bab53f88c9f308e82f21e844f1fc6e GIT binary patch literal 3533 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 LZ+2&|PvU<7?Qw!I literal 0 HcmV?d00001 diff --git a/icons/olive.icns b/icons/olive.icns new file mode 100644 index 0000000000000000000000000000000000000000..b31d8345583441143f66ef315db24d5b29c97c4a GIT binary patch literal 27395 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 literal 0 HcmV?d00001 diff --git a/icons/olive.ico b/icons/olive.ico new file mode 100644 index 0000000000000000000000000000000000000000..8ab5b94b7b9a1967bce3b0b40fb86b551c783622 GIT binary patch literal 36382 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 rKpwio?i7&WZoAN9Vsd;v$n)|4m-bYyYW zV3x>qb`A*0$S=t+&d4uN@N{-oC@9KL%gjk-V5qp2oRBc@!HY*vPN^I?5D*X&^hEB7 zgruP4(gSt#8haaOHiCen;m#cg3>6NvgV_4^`coN24U7zGb{{)%;=q9e1(DY;9z8j= z(d59an1GN&K!JwF#)BHFTP=XPA96D@J6BbDI&v~IC-X2fH+uw?f!T~8cA3WnW1u)t zjj+FGmMc&Vb8~T>V`^Ypm0M@i!lg@>HYI!21+8)_NGQs=vWt+%m?J64+@o8ZdU3iXeipM4)se;BW`KzW1n zh0qUO0jn%?rM+51d(C+xeN)cKTUwbkpG*%r4Qti!9b*^}L6)lJh?#aY8E%~{J+`Ahv{^@a1L z<;(9U>}UD^r{ULuPDk(N^41uhTFG-FisG4~qJpu4#>Qp-cbZwYx!hf{;Yr&R)0O6z zEsb=Hj5j|^i$9Zke)IYD8aG0Itc%#KvbrSn=4`INrzWk6HSO6NR~r0U-t3j>Rg?L- zQ?s>m>~Cq_4ZTvkD}3wvjsM@d%=X-@{(r8N{o4;by)*xA-gn+}Ms?M`pG`}u?|s^qJKM)B|7O;v+~0hw=l(9={myc?e$M~qX3O?ExlDVt zx|*Y&uY;eTQA&co<>GcGwMKvcyn5aA{-Hf<-P~W7f1UsMzn+1Yf#2lUw2!gCjH8h35#-CjP^HSi z(9q1l@bf>Ae#yX4YQVtoDuIE)Y6b&?c>bjLqi(=lSLNyA7*cWT&Gmzv3<^9f2VE}J zpWetW-(_{Vq2N^0O~yZ2GZvr)(t*JTSWp}+^sxj~!Fo;x dhX4PW8U9^jXF0P!e>MXUc)I$ztaD0e0s!%B#Jd0h literal 0 HcmV?d00001 diff --git a/icons/play.png b/icons/play.png new file mode 100644 index 0000000000000000000000000000000000000000..8e059c5d6ac8d944e734a1e4edf4b9c2ef5d4a70 GIT binary patch literal 3394 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 ZfpePg$!ax}%g_rZusCdG`p}pV`yXsNgeU+2 literal 0 HcmV?d00001 diff --git a/icons/razor.png b/icons/razor.png new file mode 100644 index 0000000000000000000000000000000000000000..3a53fa62d37831fbff2d077e708ff3046e8fe773 GIT binary patch literal 1287 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 WoQs*l-oFm$X9iDKKbLh*2~7aS_2Cf! literal 0 HcmV?d00001 diff --git a/icons/rolling.png b/icons/rolling.png new file mode 100644 index 0000000000000000000000000000000000000000..ace97caadfca20d40ab8edb5beab0e79d1f48216 GIT binary patch literal 1165 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;&H|6fVg?3oVGw3ym^DX&fq_{f z)7d#7AS1sdzc?emK*7`5S)rgPKP@vSm4Tt+R&qkZyaz8HJvpUv;6Ok?NYE3xClZo^ zl1mTN&1>v!oY@EhiiSIP957Tk&<VC-0%FLPH%$&@_%-rk|R0d`{IyHO$S$b&jclX;p5WO$(PUUD}lFRTs3%sUWq`Gb?XnQ+ixcU7%xKV3KDR7f|sP zJ~kkToBl_L_4|=CHmu6bp=?^s9VawcIk*MwI1E`l^mMhF*;SdF8Jd}z8Jn4%8Jv}z zZ4WcGE#}FWI4T({;Vl_%a4%UXnJa15fs)3^8J3eO6+cUhc~9e;7QLow&Du5JZ|K}y zwy`#uKmGpMg-0$P{n%ZPvtN5kJT5>mzFQT zpRk|h|DT3m2Ra?So6B2ccxolji71L^ii!%x3K|=i`QK?~+2(S0$%ZFwQ%qNyU$!*T zF*4r#EG_;_>iNy**K6De`LQlyx6100(3`Wl{+^n&D%P}TYg}pYYk9L*rdLho=T6Pm z&auCxc{lV*?XK{x>o@*?=Q7)Kv-*O-+)#_@F zdcF>Reokfn-_p8Eu5Z2F*Q><+Ew4T2J3G4k{gwN{`|2Mh-b>!7{(FAvK7q>052gR2 zf0m2enbaEn{qyQ|)BA_^taWpLUH*0c8&EL#Y7+!>a@a2CEqi4C48d;*Yuk)xP(1aSW-r^=9Tlp=Jdhm*&gonZ6tr zw9Bo2QQ{TtnCnn=qk?5~zJpMi$%KuC!Fy(Jo*h5gr&+v#fklCV@t^zwuYjlgN#)#C zA2)I^1jw9UY?j`tyLt+n%r)ne{pTVd_Ax(sw3b0mpicfkeaCi|8!Z*${WrR%F)#=e zci4WEV_=vtPk`xQPmAD01823frCyS5XIAs+Obvapnf3H{_lR3v*BCg9-%1=?$FQ-C z$9wC8OHNnTi~KI$yCB!N==tnGzJ-h&Tm3j1C0fmwm?|?czTn1$91aXj0t?P=&G_>E b_iu*Z&t`gbG5?MRdY!@3)z4*}Q$iB}rETmX literal 0 HcmV?d00001 diff --git a/icons/slip.png b/icons/slip.png new file mode 100644 index 0000000000000000000000000000000000000000..32d0b81baeee20ead178f32d2f1fb9cfd7651da0 GIT binary patch literal 1153 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 wId4awKvzfSfIyN9>2Kic +#include +#endif + +const char* version = "Olive (May 2018 | Pre-Alpha)"; +float scale = 1.0f; +bool vsync = true; +bool hwaccel = false; +int padding = 8; +bool custom_scale = false; +bool saved_layout = false; +bool left_mouse_dominant = true; +bool show_track_lines = false; + +void load_config() { + /*if (!custom_scale) { + #ifdef _WIN32 + // Get Windows UI scale - TODO may not be compatible with XP + HDC screen = GetDC(0); + int dpiX = GetDeviceCaps (screen, LOGPIXELSX); + //int dpiY = GetDeviceCaps (screen, LOGPIXELSY); + + scale = (float) dpiX / 96; + + ReleaseDC (0, screen); + #endif + }*/ + + padding *= scale; + + /*DIR* dir = opendir("conf"); + if (dir) { + closedir(dir); + } else if (ENOENT == errno) { + // no config directory, assume this is the first time opening Olive + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "INFO", "NOTE: This version of Olive is INCOMPLETE and very likely to crash. It comes with NO WARRANTY or guarantee of functionality.\n\nRegardless, thanks for checking out the project and I hope you enjoy using it!", NULL); + }*/ +} + +void save_config() { + /*#ifdef _WIN32 + _mkdir("conf"); + #else + mkdir("conf", 0777); + #endif*/ +} diff --git a/io/config.h b/io/config.h new file mode 100644 index 000000000..1bd410f1b --- /dev/null +++ b/io/config.h @@ -0,0 +1,16 @@ +#ifndef CONFIG_H +#define CONFIG_H + +extern const char* version; +extern float scale; +extern bool vsync; +extern bool hwaccel; +extern int padding; +extern bool custom_scale; +extern bool saved_layout; +extern bool show_track_lines; + +void load_config(); +void save_config(); + +#endif // CONFIG_H diff --git a/io/exportthread.cpp b/io/exportthread.cpp new file mode 100644 index 000000000..7cdaaee6d --- /dev/null +++ b/io/exportthread.cpp @@ -0,0 +1,342 @@ +#include "exportthread.h" + +#include "project/sequence.h" + +#include "panels/panels.h" +#include "panels/timeline.h" +#include "panels/viewer.h" +#include "ui/viewerwidget.h" +#include "playback/playback.h" + +extern "C" { + #include + #include + #include + #include +} + +#include +#include +#include +#include +#include +#include + +bool encode(AVFormatContext* fmt_ctx, AVCodecContext* codec_ctx, AVFrame* frame, AVPacket* packet, AVStream* stream) { + int ret = avcodec_send_frame(codec_ctx, frame); + if (ret < 0) { + qDebug() << "[ERROR] Failed to send frame to encoder." << ret; + return false; + } else { + while (ret >= 0) { + ret = avcodec_receive_packet(codec_ctx, packet); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + // do nothing, encoder needs more input + } else if (ret < 0) { + qDebug() << "[ERROR] Failed to receive packet from encoder." << ret; + return false; + } else { + packet->stream_index = stream->index; + av_interleaved_write_frame(fmt_ctx, packet); + } + } + } + return true; +} + +void ExportThread::run() { + Sequence* sequence = panel_timeline->sequence; + + // TODO make customizable + long start = 0; + long end = sequence->getEndFrame(); + + if (panel_viewer->viewer_widget->context()->makeCurrent(&surface)) { + qDebug() << "make current succeeded"; + } else { + qDebug() << "make current failed"; + } + panel_viewer->viewer_widget->multithreaded = false; + panel_viewer->viewer_widget->initializeOpenGLFunctions(); + + AVFormatContext* fmt_ctx = NULL; + QByteArray ba = filename.toLatin1(); + char* c_filename = new char[ba.size()+1]; + strcpy(c_filename, ba.data()); + + avformat_alloc_output_context2(&fmt_ctx, NULL, NULL, c_filename); + if (!fmt_ctx) { + qDebug() << "[ERROR] Could not create output context"; + } else { + AVStream* video_stream; + AVCodec* vcodec; + AVCodecContext* vcodec_ctx; + AVFrame* video_frame; + SwsContext* sws_ctx = NULL; +// AVStream* audio_stream; +// AVCodec* acodec; +// AVFrame* audio_frame; +// AVCodecContext* acodec_ctx; + AVPacket video_pkt; +// AVPacket audio_pkt; +// SwrContext* swr_ctx = NULL; + int ret; + + bool fail = false; + + if (video_enabled) { + // initialize array contain opengl data + video_stream = avformat_new_stream(fmt_ctx, NULL); + + if (!video_stream) { + qDebug() << "[ERROR] Could not allocate output streams"; + fail = true; + } else { + vcodec = avcodec_find_encoder((enum AVCodecID) video_codec); + + if (!vcodec) { + qDebug() << "[ERROR] Could not find video encoder"; + fail = true; + } else { + vcodec_ctx = avcodec_alloc_context3(vcodec); + + if (!vcodec_ctx) { + qDebug() << "[ERROR] Could not find allocate video encoding context"; + fail = true; + } else { + vcodec_ctx->width = sequence->width; + vcodec_ctx->height = sequence->height; + vcodec_ctx->sample_aspect_ratio = av_d2q(sequence->width/sequence->height, INT_MAX); + vcodec_ctx->pix_fmt = vcodec->pix_fmts[0]; // maybe be breakable code + vcodec_ctx->framerate = av_d2q(sequence->frame_rate, INT_MAX); + vcodec_ctx->bit_rate = video_bitrate * 1000000; + vcodec_ctx->time_base = av_inv_q(vcodec_ctx->framerate); + + ret = avcodec_open2(vcodec_ctx, vcodec, NULL); + if (ret < 0) { + qDebug() << "[ERROR] Could not open output video encoder." << ret; + fail = true; + } else { + ret = avcodec_parameters_from_context(video_stream->codecpar, vcodec_ctx); + if (ret < 0) { + qDebug() << "[ERROR] Could not copy video encoder parameters to output stream." << ret; + fail = true; + } else { + if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) { + vcodec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + video_stream->time_base = vcodec_ctx->time_base; + + // initialize raw video frame + video_frame = av_frame_alloc(); + av_frame_make_writable(video_frame); + video_frame->format = AV_PIX_FMT_RGBA; + video_frame->width = video_width; + video_frame->height = video_height; + av_frame_get_buffer(video_frame, 0); + + av_init_packet(&video_pkt); + + sws_ctx = sws_getContext( + video_width, + video_height, + AV_PIX_FMT_RGBA, + video_width, + video_height, + AV_PIX_FMT_YUV420P, + SWS_FAST_BILINEAR, + NULL, + NULL, + NULL + ); + } + } + } + } + } + } + +// if (audio_enabled && !fail) { +// audio_stream = avformat_new_stream(fmt_ctx, NULL); +// if (!audio_stream) { +// qDebug() << "[ERROR] Could not allocate output streams"; +// fail = true; +// } else { +// acodec = avcodec_find_encoder((enum AVCodecID) audio_codec); +// if (!acodec) { +// qDebug() << "[ERROR] Could not find audio encoder"; +// fail = true; +// } else { +// acodec_ctx = avcodec_alloc_context3(acodec); +// if (!acodec_ctx) { +// qDebug() << "[ERROR] Could not find allocate audio encoding context"; +// fail = true; +// } else { +// acodec_ctx->sample_rate = audio_sampling_rate; +// acodec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; +// acodec_ctx->channels = av_get_channel_layout_nb_channels(acodec_ctx->channel_layout); +// acodec_ctx->sample_fmt = acodec->sample_fmts[0]; +// acodec_ctx->time_base = {1, audio_sampling_rate}; +// acodec_ctx->bit_rate = audio_bitrate * 1000; + +// ret = avcodec_open2(acodec_ctx, acodec, NULL); +// if (ret < 0) { +// qDebug() << "[ERROR] Could not open output audio encoder." << ret; +// fail = true; +// } else { +// ret = avcodec_parameters_from_context(audio_stream->codecpar, acodec_ctx); +// if (ret < 0) { +// qDebug() << "[ERROR] Could not copy audio encoder parameters to output stream." << ret; +// fail = true; +// } else { +// if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) { +// acodec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; +// } + +// audio_stream->time_base = acodec_ctx->time_base; + +// // init audio resampler context +// swr_ctx = swr_alloc_set_opts( +// NULL, +// acodec_ctx->channel_layout, +// acodec_ctx->sample_fmt, +// acodec_ctx->sample_rate, +// sequence->audio_layout, +// AV_SAMPLE_FMT_S16, +// sequence->audio_frequency, +// 0, +// NULL +// ); +// swr_init(swr_ctx); + +// // initialize raw audio frame +// audio_frame = av_frame_alloc(); +// audio_frame->nb_samples = acodec_ctx->frame_size; +// if (audio_frame->nb_samples == 0) audio_frame->nb_samples = 2048; // should probably be way smaller? +// audio_frame->format = acodec_ctx->sample_fmt; +// audio_frame->channels = acodec_ctx->channels; +// audio_frame->channel_layout = acodec_ctx->channel_layout; +// ret = av_frame_get_buffer(audio_frame, 0); +// if (ret < 0) { +// qDebug() << "[ERROR] Could not allocate audio buffer." << ret; +// } +// // av_samples_alloc(&audio_frame->data[0], NULL, av_get_channel_layout_nb_channels(audio_frame->channel_layout), audio_frame->nb_samples, acodec_ctx->sample_fmt, 0); +// av_frame_make_writable(audio_frame); + +// av_init_packet(&audio_pkt); +// } +// } +// } +// } +// } +// } + + if (!fail) { + av_dump_format(fmt_ctx, 0, c_filename, 1); + + ret = avio_open(&fmt_ctx->pb, c_filename, AVIO_FLAG_WRITE); + if (ret < 0) { + qDebug() << "[ERROR] Could not open output file." << ret; + } else { + ret = avformat_write_header(fmt_ctx, NULL); + if (ret < 0) { + qDebug() << "[ERROR] Could not write output file header." << ret; + } else { + panel_timeline->seek(0); + + // clean up - close all open clips + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + if (c.open) { + close_clip(&c); + } + } + + QOpenGLFramebufferObject fbo(video_width, video_height, QOpenGLFramebufferObject::CombinedDepthStencil, GL_TEXTURE_RECTANGLE); + fbo.bind(); + QOpenGLPaintDevice fbo_dev(video_width, video_height); + QPainter painter(&fbo_dev); + painter.beginNativePainting(); + + AVFrame* sws_frame = av_frame_alloc(); + sws_frame->format = AV_PIX_FMT_YUV420P; + sws_frame->width = video_frame->width; + sws_frame->height = video_frame->height; + av_frame_get_buffer(sws_frame, 0); + uint8_t* flip_buffer = new uint8_t[video_frame->width * video_frame->height * 4]; + + while (panel_timeline->playhead < end && !fail) { + panel_viewer->viewer_widget->paintGL(); + + double timecode_secs = (double) panel_timeline->playhead / sequence->frame_rate; + if (video_enabled) { + // get image from opengl + glReadPixels(0, 0, video_width, video_height, GL_RGBA, GL_UNSIGNED_BYTE, flip_buffer); + + // flip image vertically because opengl sucks ass + int linesize = video_width*4; + for (int i=video_height-1;i>=0;i--) { + int src_index = i*linesize; + int dst_index = (video_height-i)*linesize; + memcpy(video_frame->data[0]+dst_index, flip_buffer+src_index, linesize); + } + + // change pixel format + sws_scale(sws_ctx, video_frame->data, video_frame->linesize, 0, video_frame->height, sws_frame->data, sws_frame->linesize); + sws_frame->pts = round(timecode_secs/av_q2d(video_stream->time_base)); + + // send to encoder + if (!encode(fmt_ctx, vcodec_ctx, sws_frame, &video_pkt, video_stream)) fail = true; + } + emit progress_changed(((float) panel_timeline->playhead / (float) end) * 100); + panel_timeline->playhead++; + } + + delete [] flip_buffer; + painter.endNativePainting(); + fbo.release(); + + av_frame_free(&sws_frame); + + if (!fail) { + // flush remaining packets + if (video_enabled) { + if (!encode(fmt_ctx, vcodec_ctx, NULL, &video_pkt, video_stream)) fail = true; + } + // if (audio_enabled) { + // if (!encode(fmt_ctx, acodec_ctx, NULL, &audio_pkt, audio_stream)) return false; + // } + } + + if (!fail) { + ret = av_write_trailer(fmt_ctx); + if (ret < 0) { + qDebug() << "[ERROR] Could not write output file trailer." << ret; + } + + emit progress_changed(100); + } + } + } + + av_packet_unref(&video_pkt); + av_frame_free(&video_frame); + avcodec_free_context(&vcodec_ctx); +// avcodec_free_context(&acodec_ctx); + avio_closep(&fmt_ctx->pb); + avformat_free_context(fmt_ctx); + + // qDebug() << "Render took: %ih %im %is\n", render_time/3600000, render_time/60000, render_time/1000; + + if (sws_ctx != NULL) sws_freeContext(sws_ctx); +// if (swr_ctx != NULL) swr_free(&swr_ctx); + } + } + + delete [] c_filename; + + panel_viewer->viewer_widget->context()->doneCurrent(); + panel_viewer->viewer_widget->context()->moveToThread(qApp->thread()); + panel_viewer->viewer_widget->multithreaded = true; +} diff --git a/io/exportthread.h b/io/exportthread.h new file mode 100644 index 000000000..c0d76365b --- /dev/null +++ b/io/exportthread.h @@ -0,0 +1,30 @@ +#ifndef EXPORTTHREAD_H +#define EXPORTTHREAD_H + +#include +#include + +class ExportThread : public QThread { + Q_OBJECT +public: + void run() override; + + // export parameters + QString filename; + bool video_enabled; + int video_codec; + int video_width; + int video_height; + double video_frame_rate; + double video_bitrate; + bool audio_enabled; + int audio_codec; + int audio_sampling_rate; + int audio_bitrate; + + QOffscreenSurface surface; +signals: + void progress_changed(int value); +}; + +#endif // EXPORTTHREAD_H diff --git a/io/media.cpp b/io/media.cpp new file mode 100644 index 000000000..3280986ee --- /dev/null +++ b/io/media.cpp @@ -0,0 +1,9 @@ +#include "media.h" + +extern "C" { + #include +} + +long Media::get_length_in_frames(float frame_rate) { + return ceil((float) length / (float) AV_TIME_BASE * frame_rate); +} diff --git a/io/media.h b/io/media.h new file mode 100644 index 000000000..e7aff6ddc --- /dev/null +++ b/io/media.h @@ -0,0 +1,30 @@ +#ifndef MEDIA_H +#define MEDIA_H + +#include +#include +#include +#include + +struct Sequence; + +struct MediaStream { + int file_index; + int video_width; + int video_height; + bool infinite_length; +}; + +struct Media +{ + QString url; + QString name; + bool is_sequence; + int64_t length; + QVector video_tracks; + QVector audio_tracks; + Sequence* sequence; + long get_length_in_frames(float frame_rate); +}; + +#endif // MEDIA_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 000000000..483c6bfcf --- /dev/null +++ b/main.cpp @@ -0,0 +1,18 @@ +#include "mainwindow.h" +#include + +extern "C" { + #include +} + +int main(int argc, char *argv[]) +{ + // init ffmpeg subsystem + av_register_all(); + + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 000000000..7c7e19c37 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,165 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "io/config.h" + +#include "panels/panels.h" +#include "panels/project.h" +#include "panels/effectcontrols.h" +#include "panels/viewer.h" +#include "panels/timeline.h" + +#include "dialogs/aboutdialog.h" +#include "dialogs/newsequencedialog.h" +#include "dialogs/exportdialog.h" + +#include +#include + +void MainWindow::setup_layout() { + panel_project = new Project(this); + panel_effect_controls = new EffectControls(this); + panel_viewer = new Viewer(this); + panel_timeline = new Timeline(this); + + addDockWidget(Qt::TopDockWidgetArea, panel_project); + addDockWidget(Qt::TopDockWidgetArea, panel_effect_controls); + addDockWidget(Qt::TopDockWidgetArea, panel_viewer); + addDockWidget(Qt::BottomDockWidgetArea, panel_timeline); + + setCentralWidget(NULL); +} + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + // set up style? + + qApp->setStyle(QStyleFactory::create("Fusion")); + + QPalette darkPalette; + darkPalette.setColor(QPalette::Window, QColor(53,53,53)); + darkPalette.setColor(QPalette::WindowText, Qt::white); + darkPalette.setColor(QPalette::Base, QColor(25,25,25)); + darkPalette.setColor(QPalette::AlternateBase, QColor(53,53,53)); + darkPalette.setColor(QPalette::ToolTipBase, Qt::white); + darkPalette.setColor(QPalette::ToolTipText, Qt::white); + darkPalette.setColor(QPalette::Text, Qt::white); + darkPalette.setColor(QPalette::Button, QColor(53,53,53)); + darkPalette.setColor(QPalette::ButtonText, Qt::white); darkPalette.setColor(QPalette::BrightText, Qt::red); + darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(128, 128, 128)); + darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); + + darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::HighlightedText, Qt::black); + + + + qApp->setPalette(darkPalette); + + // end style + + setWindowState(Qt::WindowMaximized); + + ui->setupUi(this); + + setWindowTitle("Olive (May 2018 | Pre-Alpha)"); + statusBar()->showMessage("Welcome to Olive::Qt"); + + setup_layout(); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::on_action_Import_triggered() +{ + panel_project->import_dialog(); +} + +void MainWindow::on_actionExit_triggered() +{ + QApplication::quit(); +} + +void MainWindow::on_actionAbout_triggered() +{ + AboutDialog a(this); + a.exec(); +} + +void MainWindow::on_actionDelete_triggered() +{ + if (panel_timeline->focused()) { + panel_timeline->delete_selection(false); + } +} + +void MainWindow::on_actionSelect_All_triggered() +{ + if (panel_timeline->focused()) { + panel_timeline->select_all(); + } +} + +void MainWindow::on_actionSequence_triggered() +{ + NewSequenceDialog nsd; + nsd.set_sequence_name(panel_project->get_next_sequence_name()); + nsd.exec(); +} + +void MainWindow::on_actionZoom_In_triggered() +{ + if (panel_timeline->focused()) { + panel_timeline->zoom_in(); + } +} + +void MainWindow::on_actionZoom_out_triggered() +{ + if (panel_timeline->focused()) { + panel_timeline->zoom_out(); + } +} + +void MainWindow::on_actionTimeline_Track_Lines_toggled(bool e) +{ + show_track_lines = e; + panel_timeline->redraw_all_clips(); +} + +void MainWindow::on_actionExport_triggered() +{ + ExportDialog e; + if (panel_timeline->sequence != NULL) e.set_defaults(panel_timeline->sequence); + e.exec(); +} + +void MainWindow::on_actionProject_2_toggled(bool arg1) +{ + panel_project->setVisible(arg1); +} + +void MainWindow::on_actionEffect_Controls_toggled(bool arg1) +{ + panel_effect_controls->setVisible(arg1); +} + +void MainWindow::on_actionViewer_toggled(bool arg1) +{ + panel_viewer->setVisible(arg1); +} + +void MainWindow::on_actionTimeline_toggled(bool arg1) +{ + panel_timeline->setVisible(arg1); +} + +void MainWindow::on_actionRipple_Delete_triggered() +{ + panel_timeline->delete_selection(true); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 000000000..5556aae3a --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,59 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +class Project; +class EffectControls; +class Viewer; +class Timeline; + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private slots: + void on_action_Import_triggered(); + + void on_actionExit_triggered(); + + void on_actionAbout_triggered(); + + void on_actionDelete_triggered(); + + void on_actionSelect_All_triggered(); + + void on_actionSequence_triggered(); + + void on_actionZoom_In_triggered(); + + void on_actionZoom_out_triggered(); + + void on_actionTimeline_Track_Lines_toggled(bool arg1); + + void on_actionExport_triggered(); + + void on_actionProject_2_toggled(bool arg1); + + void on_actionEffect_Controls_toggled(bool arg1); + + void on_actionViewer_toggled(bool arg1); + + void on_actionTimeline_toggled(bool arg1); + + void on_actionRipple_Delete_triggered(); + +private: + Ui::MainWindow *ui; + void setup_layout(); +}; + +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 000000000..7c7d44b55 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,296 @@ + + + MainWindow + + + + 0 + 0 + 653 + 394 + + + + MainWindow + + + + :/icons/full-icon.png:/icons/full-icon.png + + + + + + 0 + 0 + 653 + 21 + + + + + &File + + + + &New + + + + + + + + + + + + + + + + + + + &Edit + + + + + + + + + + + + + + + &Window + + + + + + + + + &Help + + + + + + &View + + + + + + + + + + + + + + + &Open Project + + + Ctrl+O + + + + + &Save Project + + + Ctrl+S + + + + + Save Project &As + + + + + &Import... + + + Ctrl+I + + + + + &Export... + + + Ctrl+M + + + + + E&xit + + + + + About... + + + + + &Undo + + + Ctrl+Z + + + + + &Redo + + + Ctrl+Y + + + + + Cu&t + + + Ctrl+X + + + + + Cop&y + + + Ctrl+C + + + + + &Paste + + + Ctrl+V + + + + + Delete + + + Del + + + + + Select &All + + + Ctrl+A + + + + + true + + + Track Lines + + + + + Project... + + + Ctrl+N + + + + + Sequence... + + + + + Zoom In + + + = + + + + + Zoom Out + + + - + + + + + true + + + true + + + Project + + + + + true + + + true + + + Effect Controls + + + + + true + + + true + + + Viewer + + + + + true + + + true + + + Timeline + + + + + Ripple Delete + + + Shift+Del + + + + + + + + + diff --git a/olive.pro b/olive.pro new file mode 100644 index 000000000..7cce76716 --- /dev/null +++ b/olive.pro @@ -0,0 +1,105 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2018-05-11T10:31:59 +# +#------------------------------------------------- + +QT += core gui multimedia opengl + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = olive-qt +TEMPLATE = app + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which has been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + panels/project.cpp \ + panels/effectcontrols.cpp \ + panels/viewer.cpp \ + panels/timeline.cpp \ + ui/sourcetable.cpp \ + dialogs/aboutdialog.cpp \ + ui/timelinewidget.cpp \ + io/media.cpp \ + project/sequence.cpp \ + project/clip.cpp \ + playback/playback.cpp \ + playback/audio.cpp \ + io/config.cpp \ + dialogs/newsequencedialog.cpp \ + ui/viewerwidget.cpp \ + ui/viewercontainer.cpp \ + dialogs/exportdialog.cpp \ + ui/collapsiblewidget.cpp \ + effects/transformeffect.cpp \ + panels/panels.cpp \ + effects/volumeeffect.cpp \ + effects/paneffect.cpp \ + project/effect.cpp \ + effects/effects.cpp \ + playback/cacher.cpp \ + io/exportthread.cpp + +HEADERS += \ + mainwindow.h \ + panels/project.h \ + panels/effectcontrols.h \ + panels/viewer.h \ + panels/timeline.h \ + ui/sourcetable.h \ + dialogs/aboutdialog.h \ + ui/timelinewidget.h \ + io/media.h \ + project/sequence.h \ + project/clip.h \ + playback/playback.h \ + playback/audio.h \ + effects/effects.h \ + io/config.h \ + ui/timeline-tools.h \ + dialogs/newsequencedialog.h \ + ui/viewerwidget.h \ + ui/viewercontainer.h \ + dialogs/exportdialog.h \ + ui/collapsiblewidget.h \ + project/effect.h \ + panels/panels.h \ + playback/cacher.h \ + io/exportthread.h + +FORMS += \ + mainwindow.ui \ + panels/project.ui \ + panels/effectcontrols.ui \ + panels/viewer.ui \ + panels/timeline.ui \ + dialogs/aboutdialog.ui \ + dialogs/newsequencedialog.ui \ + dialogs/exportdialog.ui + +win32 { + LIBS += -L../ffmpeg/lib -lavutil -lavformat -lavcodec -lswscale -lswresample opengl32.lib + INCLUDEPATH = ../ffmpeg/include + RC_FILE = icons/win.rc +} + +linux { + LIBS += -lavutil -lavformat -lavcodec -lswscale -lswresample +} + +RESOURCES += \ + icons/icons.qrc \ + styles/styles.qrc diff --git a/olive.pro.user b/olive.pro.user new file mode 100644 index 000000000..9f1a191e9 --- /dev/null +++ b/olive.pro.user @@ -0,0 +1,375 @@ + + + + + + EnvironmentId + {89fae42a-e0bc-4377-8782-ec7014933bdd} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.10.1 MSVC2017 64bit + Desktop Qt 5.10.1 MSVC2017 64bit + qt.qt5.5101.win64_msvc2017_64_kit + 0 + 0 + 1 + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + E:/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + + E:/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Profile + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + true + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 3 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug/debug/olive-qt.exe + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug/debug/ + Run E:\olive\build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug\debug\olive-qt.exe + VC run + ProjectExplorer.CustomExecutableRunConfiguration + 3768 + false + true + false + false + true + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + olive + + Qt4ProjectManager.Qt4RunConfiguration:E:/olive/olive/olive.pro + true + + olive.pro + false + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug + 3768 + false + true + false + false + true + + 2 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 18 + + + Version + 18 + + diff --git a/olive.pro.user.3af0661 b/olive.pro.user.3af0661 new file mode 100644 index 000000000..24db8b91f --- /dev/null +++ b/olive.pro.user.3af0661 @@ -0,0 +1,315 @@ + + + + + + EnvironmentId + {3af06612-75ce-48d7-b444-7dc372f1566d} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.10.1 MSVC2017 64bit + Desktop Qt 5.10.1 MSVC2017 64bit + qt.qt5.5101.win64_msvc2017_64_kit + 0 + 0 + 0 + + C:/Users/Matt/Documents/temp + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + //neptune/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + + //neptune/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Profile + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + true + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 3 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + + + + + ProjectExplorer.CustomExecutableRunConfiguration + 3768 + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 18 + + + Version + 18 + + diff --git a/olive.pro.user.89fae42 b/olive.pro.user.89fae42 new file mode 100644 index 000000000..72aef63ee --- /dev/null +++ b/olive.pro.user.89fae42 @@ -0,0 +1,375 @@ + + + + + + EnvironmentId + {89fae42a-e0bc-4377-8782-ec7014933bdd} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.10.1 MSVC2017 64bit + Desktop Qt 5.10.1 MSVC2017 64bit + qt.qt5.5101.win64_msvc2017_64_kit + 0 + 0 + 0 + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + E:/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + + E:/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Profile + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + true + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 3 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug/debug/olive-qt.exe + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug/debug/ + Run E:\olive\build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug\debug\olive-qt.exe + VC run + ProjectExplorer.CustomExecutableRunConfiguration + 3768 + false + true + false + false + true + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + olive-qt + olive-qt2 + Qt4ProjectManager.Qt4RunConfiguration:E:/olive/olive-qt/olive-qt.pro + true + + olive-qt.pro + false + + E:/olive/build-olive-qt-Desktop_Qt_5_10_1_MSVC2017_64bit-Debug + 3768 + false + true + false + false + true + + 2 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 18 + + + Version + 18 + + diff --git a/panels/effectcontrols.cpp b/panels/effectcontrols.cpp new file mode 100644 index 000000000..ac5134bff --- /dev/null +++ b/panels/effectcontrols.cpp @@ -0,0 +1,59 @@ +#include "effectcontrols.h" +#include "ui_effectcontrols.h" + +#include +#include +#include + +#include "effects/effects.h" +#include "project/clip.h" +#include "project/effect.h" +#include "ui/collapsiblewidget.h" + + +EffectControls::EffectControls(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::EffectControls) +{ + ui->setupUi(this); + + init_effects(); + + clip = NULL; + set_clip(NULL); +} + +EffectControls::~EffectControls() +{ + delete ui; +} + +void EffectControls::on_pushButton_clicked() +{ + QMenu effects_menu(this); + for (int i=0;iscrollAreaWidgetContents + if (clip != NULL) { + for (int i=0;ieffects.size();i++) { + clip->effects.at(i)->container->setParent(NULL); + } + } + + ui->pushButton->setEnabled(c != NULL); + if (c != NULL) { + for (int i=0;ieffects.size();i++) { + static_cast(ui->scrollAreaWidgetContents->layout())->insertWidget(i, c->effects.at(i)->container); + } + } + clip = c; +} diff --git a/panels/effectcontrols.h b/panels/effectcontrols.h new file mode 100644 index 000000000..c536a8144 --- /dev/null +++ b/panels/effectcontrols.h @@ -0,0 +1,29 @@ +#ifndef EFFECTCONTROLS_H +#define EFFECTCONTROLS_H + +#include + +struct Clip; + +namespace Ui { +class EffectControls; +} + +class EffectControls : public QDockWidget +{ + Q_OBJECT + +public: + explicit EffectControls(QWidget *parent = 0); + ~EffectControls(); + void set_clip(Clip* c); + +private slots: + void on_pushButton_clicked(); + +private: + Ui::EffectControls *ui; + Clip* clip; +}; + +#endif // EFFECTCONTROLS_H diff --git a/panels/effectcontrols.ui b/panels/effectcontrols.ui new file mode 100644 index 000000000..0d329d3be --- /dev/null +++ b/panels/effectcontrols.ui @@ -0,0 +1,98 @@ + + + EffectControls + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + QDockWidget::AllDockWidgetFeatures + + + Effect Controls + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + 0 + 0 + 398 + 253 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + [+] Add Effect + + + + + + + + + diff --git a/panels/panels.cpp b/panels/panels.cpp new file mode 100644 index 000000000..2761c74da --- /dev/null +++ b/panels/panels.cpp @@ -0,0 +1,6 @@ +#include "panels.h" + +Project* panel_project; +EffectControls* panel_effect_controls; +Viewer* panel_viewer; +Timeline* panel_timeline; diff --git a/panels/panels.h b/panels/panels.h new file mode 100644 index 000000000..9800d8f6c --- /dev/null +++ b/panels/panels.h @@ -0,0 +1,14 @@ +#ifndef PANELS_H +#define PANELS_H + +class Project; +class EffectControls; +class Viewer; +class Timeline; + +extern Project* panel_project; +extern EffectControls* panel_effect_controls; +extern Viewer* panel_viewer; +extern Timeline* panel_timeline; + +#endif // PANELS_H diff --git a/panels/project.cpp b/panels/project.cpp new file mode 100644 index 000000000..49c4227ba --- /dev/null +++ b/panels/project.cpp @@ -0,0 +1,164 @@ +#include "project.h" +#include "ui_project.h" +#include "io/media.h" + +#include +#include +#include +#include +#include +#include + +#include "panels/timeline.h" +#include "project/sequence.h" + +extern "C" { + #include + #include +} + +Project::Project(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::Project) +{ + ui->setupUi(this); +} + +Project::~Project() +{ + delete ui; +} + +QString Project::get_next_sequence_name() { + int n = 1; + bool found = true; + QString name; + while (found) { + found = false; + name = "Sequence "; + if (n < 10) { + name += "0"; + } + name += QString::number(n); + for (int i=0;itreeWidget->topLevelItemCount();i++) { + if (QString::compare(ui->treeWidget->topLevelItem(i)->text(0), name, Qt::CaseInsensitive) == 0) { + found = true; + n++; + break; + } + } + } + return name; +} + +void Project::new_sequence(Sequence *s) { + Media* m = new Media(); + m->is_sequence = true; + m->sequence = s; + + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setText(0, s->name); + item->setData(0, Qt::UserRole + 1, QVariant::fromValue(reinterpret_cast(m))); + + ui->treeWidget->addTopLevelItem(item); + source_table = ui->treeWidget; +} + +void Project::import_dialog() { + QStringList files = QFileDialog::getOpenFileNames(this, "Import media...", "", "All Files (*.*)"); + for (int i=0;iis_sequence = false; + m->url = file; + + // detect video/audio streams in file + for (int i=0;i<(int)pFormatCtx->nb_streams;i++) { + // Find the decoder for the video stream + if (avcodec_find_decoder(pFormatCtx->streams[i]->codecpar->codec_id) == NULL) { + qDebug() << "[ERROR] Unsupported codec in stream %d.\n"; + } else { + if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + qDebug() << "[WARNING] INFINITE_LENGTH calculation is inaccurate in this build\n"; + // TODO BETTER infinite length calculator + bool infinite_length = (pFormatCtx->streams[i]->nb_frames == 0); +// bool infinite_length = false; + + m->video_tracks.append({i, pFormatCtx->streams[i]->codecpar->width, pFormatCtx->streams[i]->codecpar->height, infinite_length}); + } else if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + m->audio_tracks.append({i, 0, 0, false}); + } + } + } + m->name = files.at(i).mid(files.at(i).lastIndexOf('/')+1); + m->length = pFormatCtx->duration; + + QTreeWidgetItem* item = new QTreeWidgetItem(); + if (m->video_tracks.size() == 0) { + item->setIcon(0, QIcon(":/icons/audiosource.png")); + } else { + item->setIcon(0, QIcon(":/icons/videosource.png")); + } + item->setText(0, m->name); + item->setText(1, QString::number(m->length)); + item->setData(0, Qt::UserRole + 1, QVariant::fromValue(reinterpret_cast(m))); + + ui->treeWidget->addTopLevelItem(item); + } + } + avformat_close_input(&pFormatCtx); + delete [] filename; + } +} diff --git a/panels/project.h b/panels/project.h new file mode 100644 index 000000000..b184082dd --- /dev/null +++ b/panels/project.h @@ -0,0 +1,34 @@ +#ifndef PROJECT_H +#define PROJECT_H + +#include +#include + +struct Media; +struct Sequence; +class Timeline; +class Viewer; +class SourceTable; + +namespace Ui { +class Project; +} + +class Project : public QDockWidget +{ + Q_OBJECT + +public: + explicit Project(QWidget *parent = 0); + ~Project(); + void import_dialog(); + void new_sequence(Sequence* s); + QString get_next_sequence_name(); + + SourceTable* source_table; + +private: + Ui::Project *ui; +}; + +#endif // PROJECT_H diff --git a/panels/project.ui b/panels/project.ui new file mode 100644 index 000000000..080ed310f --- /dev/null +++ b/panels/project.ui @@ -0,0 +1,95 @@ + + + Project + + + + 0 + 0 + 504 + 371 + + + + + 0 + 0 + + + + Project + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::CurrentChanged|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + false + + + QAbstractItemView::DragOnly + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + false + + + 200 + + + true + + + + Name + + + + + Duration + + + + + + + + + + SourceTable + QTreeWidget +

ui/sourcetable.h
+ + + + + diff --git a/panels/timeline.cpp b/panels/timeline.cpp new file mode 100644 index 000000000..d585285cb --- /dev/null +++ b/panels/timeline.cpp @@ -0,0 +1,320 @@ +#include "timeline.h" +#include "ui_timeline.h" + +#include "panels/panels.h" +#include "panels/effectcontrols.h" +#include "ui/timelinewidget.h" +#include "project/sequence.h" +#include "project/clip.h" +#include "ui/viewerwidget.h" +#include "playback/audio.h" +#include "panels/viewer.h" +#include "playback/cacher.h" +#include "playback/playback.h" + +#include + +Timeline::Timeline(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::Timeline) +{ + selecting = moving_init = moving_proc = splitting = importing = playing = trim_in = false; + last_frame = playhead = 0; + trim_target = -1; + + ui->setupUi(this); + + ui->video_area->bottom_align = true; + + tool_buttons.append(ui->toolArrowButton); + tool_buttons.append(ui->toolEditButton); + tool_buttons.append(ui->toolRippleButton); + tool_buttons.append(ui->toolRazorButton); + tool_buttons.append(ui->toolSlipButton); + tool_buttons.append(ui->toolRollingButton); + + ui->toolArrowButton->click(); + + zoom = 1.0f; + + sequence = NULL; + set_sequence(NULL); + + connect(&playback_updater, SIGNAL(timeout()), this, SLOT(repaint_timeline())); +} + +Timeline::~Timeline() +{ + delete ui; +} + +void Timeline::go_to_start() { + seek(0); +} + +void Timeline::previous_frame() { + seek(playhead-1); +} + +void Timeline::next_frame() { + seek(playhead+1); +} + +void Timeline::seek(long p) { + pause(); + + // reset all clip audio + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + c.reset_audio = true; + c.frame_sample_index = 0; + } + clear_cache(true, true); + switch_audio_cache = true; + reading_audio_cache_A = false; + + playhead = p; +} + +bool Timeline::toggle_play() { + if (playing) { + pause(); + } else { + play(); + } + return playing; +} + +void Timeline::play() { + playhead_start = playhead; + playback_timer.start(); + playback_updater.start(); + playing = true; + + if (switch_audio_cache) { + // add something to pre-cache audio + } +} + +void Timeline::pause() { + playing = false; +} + +void Timeline::go_to_end() { + seek(sequence->getEndFrame()); +} + +void Timeline::set_sequence(Sequence *s) { + if (sequence != NULL) { + // clean up - close all open clips + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + if (c.open) { + close_clip(&c); + } + } + } + + sequence = s; + bool null_sequence = (s == NULL); + + for (int i=0;isetEnabled(!null_sequence); + } + ui->pushButton_4->setEnabled(!null_sequence); + ui->pushButton_5->setEnabled(!null_sequence); + ui->video_area->setEnabled(!null_sequence); + ui->audio_area->setEnabled(!null_sequence); + + if (null_sequence) { + setWindowTitle("Timeline: "); + } else { + setWindowTitle("Timeline: " + sequence->name); + redraw_all_clips(); + playback_updater.setInterval(floor(1000 / sequence->frame_rate)); + } +} + +bool Timeline::focused() { + return (ui->video_area->hasFocus() || ui->audio_area->hasFocus()); +} + +void Timeline::repaint_timeline() { + if (playing) { + playhead = playhead_start + (playback_timer.elapsed() * 0.001 * sequence->frame_rate); + } + ui->video_area->update(); + ui->audio_area->update(); + if (last_frame != playhead) { + panel_viewer->viewer_widget->update(); + last_frame = playhead; + } +} + +void Timeline::redraw_all_clips() { + ui->video_area->redraw_clips(); + ui->audio_area->redraw_clips(); +} + +void Timeline::select_all() { + selections.clear(); + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + selections.append({c.timeline_in, c.timeline_out, c.track}); + } +} + +void Timeline::delete_selection(bool ripple_delete) { + if (selections.size() > 0) { + panel_effect_controls->set_clip(NULL); + + long ripple_point = selections.at(0).in; + long ripple_length = selections.at(0).out - selections.at(0).in; + + for (int i=0;idelete_area(s.in, s.out, s.track); + if (ripple_delete) { + if (ripple_point > s.in) ripple_point = s.in; + if (ripple_length > s.out - s.in) ripple_length = s.out - s.in; + } + } + selections.clear(); + + if (ripple_delete) { + long validator; + for (int i=0;iclip_count();i++) { + // check every clip after and see if it'll collide + // NOTE, we could probably re-use the validation code for the ripple tool here for optimization (since it's technically better code I think) + Clip& c = sequence->get_clip(i); + if (c.timeline_in >= ripple_point) { + for (int j=0;jclip_count();j++) { + Clip& cc = sequence->get_clip(j); + if (cc.timeline_in < ripple_point) { + validator = c.timeline_in - ripple_length - cc.timeline_out; + if (validator < 0) ripple_length += validator; + + if (ripple_length <= 0) { + // we've seen all we need to see here (can't ripple so stop looping) + i = j = sequence->clip_count(); + } + } + } + } + } + if (ripple_length > 0) ripple(ripple_point, -ripple_length); + } + + redraw_all_clips(); + } +} + +void Timeline::zoom_in() { + zoom *= 2; + redraw_all_clips(); +} + +void Timeline::zoom_out() { + zoom /= 2; + redraw_all_clips(); +} + +void Timeline::ripple(long ripple_point, long ripple_length) { + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + if (c.timeline_in >= ripple_point) { + c.timeline_in += ripple_length; + c.timeline_out += ripple_length; + } + } + for (int i=0;i= ripple_point) { + s.in += ripple_length; + s.out += ripple_length; + } + } +} + +void Timeline::decheck_tool_buttons(QObject* sender) { + for (int i=0;isetChecked(false); + } + } +} + +void Timeline::on_toolEditButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::IBeamCursor); + tool = TIMELINE_TOOL_EDIT; + } +} + +void Timeline::on_toolArrowButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::ArrowCursor); + tool = TIMELINE_TOOL_POINTER; + } +} + +void Timeline::on_toolRazorButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::IBeamCursor); + tool = TIMELINE_TOOL_RAZOR; + } +} + +void Timeline::on_pushButton_4_clicked() +{ + zoom_in(); +} + +void Timeline::on_pushButton_5_clicked() +{ + zoom_out(); +} + +bool Timeline::is_clip_selected(int clip_index) { + Clip& clip = sequence->get_clip(clip_index); + for (int i=0;i= s.in && clip.timeline_out <= s.out) { + return true; + } + } + return false; +} + +void Timeline::on_toolRippleButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::ArrowCursor); + tool = TIMELINE_TOOL_RIPPLE; + } +} + +void Timeline::on_toolRollingButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::ArrowCursor); + tool = TIMELINE_TOOL_ROLLING; + } +} + +void Timeline::on_toolSlipButton_toggled(bool checked) +{ + if (checked) { + decheck_tool_buttons(sender()); + ui->timeline_area->setCursor(Qt::ArrowCursor); + tool = TIMELINE_TOOL_SLIP; + } +} diff --git a/panels/timeline.h b/panels/timeline.h new file mode 100644 index 000000000..a911f5bd6 --- /dev/null +++ b/panels/timeline.h @@ -0,0 +1,143 @@ +#ifndef TIMELINE_H +#define TIMELINE_H + +#include "ui/timeline-tools.h" +#include +#include +#include +#include + +class QPushButton; +class SourceTable; +class ViewerWidget; +struct Sequence; +struct Clip; +struct Media; +struct MediaStream; + +struct Ghost { + Clip* clip; + long in; + long out; + int track; + long clip_in; + + long old_in; + long old_out; + int old_track; + long old_clip_in; + + // importing variables + Media* media; + MediaStream* media_stream; + + // other variables + long ghost_length; + long media_length; +}; + +struct Selection { + long in; + long out; + int track; + + long old_in; + long old_out; + int old_track; +}; + +namespace Ui { +class Timeline; +} + +class Timeline : public QDockWidget +{ + Q_OBJECT + +public: + explicit Timeline(QWidget *parent = 0); + ~Timeline(); + + bool focused(); + void zoom_in(); + void zoom_out(); + void set_sequence(Sequence* s); + + Sequence* sequence; + long playhead; + + // playback functions + void go_to_start(); + void previous_frame(); + void next_frame(); + void seek(long p); + bool toggle_play(); + void play(); + void pause(); + void go_to_end(); + bool playing; + long playhead_start; + QTime playback_timer; + QTimer playback_updater; + + // shared information + int tool; + float zoom; + long drag_frame_start; + int drag_track_start; + void redraw_all_clips(); + + // selecting functions + bool selecting; + QVector selections; + bool is_clip_selected(int clip_index); + void delete_selection(bool ripple); + void select_all(); + + // moving + bool moving_init; + bool moving_proc; + QVector ghosts; + + // trimming + int trim_target; + bool trim_in; + + // splitting + bool splitting; + + // importing + bool importing; + + // ripple + void ripple(long ripple_point, long ripple_length); + +public slots: + void repaint_timeline(); + +private slots: + void on_toolEditButton_toggled(bool checked); + + void on_toolArrowButton_toggled(bool checked); + + void on_toolRazorButton_toggled(bool checked); + + void on_pushButton_4_clicked(); + + void on_pushButton_5_clicked(); + + void on_toolRippleButton_toggled(bool checked); + + void on_toolRollingButton_toggled(bool checked); + + void on_toolSlipButton_toggled(bool checked); + +private: + Ui::Timeline *ui; + QVector tool_buttons; + void decheck_tool_buttons(QObject* sender); + void set_tool(int tool); + long last_frame; +}; + +#endif // TIMELINE_H diff --git a/panels/timeline.ui b/panels/timeline.ui new file mode 100644 index 000000000..5fb2a3045 --- /dev/null +++ b/panels/timeline.ui @@ -0,0 +1,355 @@ + + + Timeline + + + + 0 + 0 + 840 + 535 + + + + Timeline + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + Pointer Tool (V) + + + + + + + :/icons/arrow.png:/icons/arrow.png + + + + 16 + 16 + + + + true + + + + + + + + 30 + 16777215 + + + + Edit Tool (X) + + + + + + + :/icons/beam.png:/icons/beam.png + + + + 16 + 16 + + + + true + + + + + + + + 30 + 16777215 + + + + Ripple Tool (B) + + + + + + + :/icons/ripple.png:/icons/ripple.png + + + + 16 + 16 + + + + true + + + + + + + + 30 + 16777215 + + + + Rolling Tool (N) + + + + + + + :/icons/rolling.png:/icons/rolling.png + + + true + + + + + + + + 30 + 16777215 + + + + Razor Tool (C) + + + + + + + :/icons/razor.png:/icons/razor.png + + + + 16 + 16 + + + + true + + + + + + + Slip Tool (Y) + + + + + + + :/icons/slip.png + + + + true + + + + + + + + 30 + 16777215 + + + + Zoom In (=) + + + + + + + + + + + + 30 + 16777215 + + + + Zoom Out (-) + + + - + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOn + + + true + + + + + 0 + 0 + 789 + 494 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + 0 + 0 + + + + Qt::ClickFocus + + + + + + 0 + 0 + + + + Qt::ClickFocus + + + + + + + + + + + + + + TimelineWidget + QWidget +
ui/timelinewidget.h
+ 1 +
+
+ + + + +
diff --git a/panels/viewer.cpp b/panels/viewer.cpp new file mode 100644 index 000000000..5afee6c74 --- /dev/null +++ b/panels/viewer.cpp @@ -0,0 +1,79 @@ +#include "viewer.h" +#include "ui_viewer.h" + +#include "playback/audio.h" +#include "timeline.h" +#include "project/sequence.h" +#include "panels/panels.h" + +extern "C" { + #include +} + +Viewer::Viewer(QWidget *parent) : + QDockWidget(parent), + ui(new Ui::Viewer) +{ + ui->setupUi(this); + ui->glViewerPane->child = ui->openGLWidget; + viewer_widget = ui->openGLWidget; + set_sequence(NULL); +} + +Viewer::~Viewer() +{ + init_audio(NULL); + + delete ui; +} + +void Viewer::set_sequence(Sequence* s) { + bool null_sequence = (s == NULL); + sequence = s; + + ui->openGLWidget->setEnabled(!null_sequence); + ui->openGLWidget->setVisible(!null_sequence); + ui->pushButton->setEnabled(!null_sequence); + ui->pushButton_2->setEnabled(!null_sequence); + ui->pushButton_3->setEnabled(!null_sequence); + ui->pushButton_4->setEnabled(!null_sequence); + ui->pushButton_5->setEnabled(!null_sequence); + + if (!null_sequence) { + ui->glViewerPane->aspect_ratio = (float) sequence->width / (float) sequence->height; + ui->glViewerPane->adjust(); + } + + init_audio(s); + update(); +} + +void Viewer::on_pushButton_clicked() +{ + panel_timeline->go_to_start(); +} + +void Viewer::on_pushButton_5_clicked() +{ + panel_timeline->go_to_end(); +} + +void Viewer::on_pushButton_2_clicked() +{ + panel_timeline->previous_frame(); +} + +void Viewer::on_pushButton_4_clicked() +{ + panel_timeline->next_frame(); +} + +void Viewer::on_pushButton_3_clicked() +{ + if (panel_timeline->toggle_play()) { + // playing + ui->pushButton_3->setIcon(QIcon(":/icons/pause.png")); + } else { + ui->pushButton_3->setIcon(QIcon(":/icons/play.png")); + } +} diff --git a/panels/viewer.h b/panels/viewer.h new file mode 100644 index 000000000..8ef7a4f69 --- /dev/null +++ b/panels/viewer.h @@ -0,0 +1,42 @@ +#ifndef VIEWER_H +#define VIEWER_H + +#include + +class Timeline; +class ViewerWidget; +struct Sequence; + +namespace Ui { +class Viewer; +} + +class Viewer : public QDockWidget +{ + Q_OBJECT + +public: + explicit Viewer(QWidget *parent = 0); + ~Viewer(); + void set_sequence(Sequence* s); + void compose(); + + Sequence* sequence; + ViewerWidget* viewer_widget; + +private slots: + void on_pushButton_clicked(); + + void on_pushButton_5_clicked(); + + void on_pushButton_2_clicked(); + + void on_pushButton_4_clicked(); + + void on_pushButton_3_clicked(); + +private: + Ui::Viewer *ui; +}; + +#endif // VIEWER_H diff --git a/panels/viewer.ui b/panels/viewer.ui new file mode 100644 index 000000000..3ad67cbc0 --- /dev/null +++ b/panels/viewer.ui @@ -0,0 +1,319 @@ + + + Viewer + + + + 0 + 0 + 583 + 396 + + + + + 0 + 0 + + + + Viewer + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + 0 + 0 + + + + + + 130 + 60 + 301 + 221 + + + + + 16777215 + 16777215 + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + 0 + 0 + + + + + 36 + 0 + + + + + 16777215 + 16777215 + + + + + + + + :/icons/prev.png:/icons/prev.png + + + + 14 + 14 + + + + + + + + + 0 + 0 + + + + + 36 + 0 + + + + + 16777215 + 16777215 + + + + + + + + :/icons/rew.png:/icons/rew.png + + + + 14 + 14 + + + + + + + + + 0 + 0 + + + + + 36 + 0 + + + + + 16777215 + 16777215 + + + + + + + + :/icons/play.png + + + + + 14 + 14 + + + + + + + + + 0 + 0 + + + + + 36 + 0 + + + + + 16777215 + 16777215 + + + + + + + + :/icons/ff.png + + + + + 14 + 14 + + + + + + + + + 0 + 0 + + + + + 36 + 0 + + + + + 16777215 + 16777215 + + + + + + + + :/icons/next.png:/icons/next.png + + + + 14 + 14 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + ViewerWidget + QOpenGLWidget +
ui/viewerwidget.h
+
+ + ViewerContainer + QWidget +
ui/viewercontainer.h
+ 1 +
+
+ + + + +
diff --git a/playback/audio.cpp b/playback/audio.cpp new file mode 100644 index 000000000..963df89ff --- /dev/null +++ b/playback/audio.cpp @@ -0,0 +1,72 @@ +#include "audio.h" + +#include "project/sequence.h" + +#include +#include + +extern "C" { + #include +} + +QAudioOutput* audio_output; +QIODevice* audio_io_device; +bool audio_device_set = false; + +uint8_t* audio_cache_A = NULL; +uint8_t* audio_cache_B = NULL; +int audio_cache_size = 0; +bool switch_audio_cache = true; +bool reading_audio_cache_A = false; + +void init_audio(Sequence* s) { + if (audio_cache_A != NULL) { + delete [] audio_cache_A; + audio_cache_A = NULL; + } + + if (audio_cache_B != NULL) { + delete [] audio_cache_B; + audio_cache_B = NULL; + } + + if (audio_device_set) { + audio_output->stop(); + delete audio_output; + audio_device_set = false; + } + + if (s != NULL) { + QAudioFormat audio_format; + audio_format.setSampleRate(s->audio_frequency); + audio_format.setChannelCount(av_get_channel_layout_nb_channels(s->audio_layout)); + audio_format.setSampleSize(16); + audio_format.setCodec("audio/pcm"); + audio_format.setByteOrder(QAudioFormat::LittleEndian); + audio_format.setSampleType(QAudioFormat::SignedInt); + + QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); + if (!info.isFormatSupported(audio_format)) { + qWarning() << "[WARNING] Couldn't initialize audio. Audio format is not supported by backend"; + } else { + audio_output = new QAudioOutput(audio_format); + // connect + audio_io_device = audio_output->start(); + audio_device_set = true; + + audio_cache_size = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(s->audio_layout), s->audio_frequency/8, AV_SAMPLE_FMT_S16, 1); + audio_cache_A = new uint8_t[audio_cache_size]; + audio_cache_B = new uint8_t[audio_cache_size]; + clear_cache(true, true); + } + } +} + +void clear_cache(bool clear_A, bool clear_B) { + if (clear_A) { + memset(audio_cache_A, 0, audio_cache_size); + } + if (clear_B) { + memset(audio_cache_B, 0, audio_cache_size); + } +} diff --git a/playback/audio.h b/playback/audio.h new file mode 100644 index 000000000..812666cb8 --- /dev/null +++ b/playback/audio.h @@ -0,0 +1,19 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include + +struct Sequence; + +extern QIODevice* audio_io_device; + +extern uint8_t* audio_cache_A; +extern uint8_t* audio_cache_B; +extern int audio_cache_size; +extern bool switch_audio_cache; +extern bool reading_audio_cache_A; +void init_audio(Sequence* s); +void clear_cache(bool clear_A, bool clear_B); + +#endif // AUDIO_H diff --git a/playback/cacher.cpp b/playback/cacher.cpp new file mode 100644 index 000000000..a342cabc8 --- /dev/null +++ b/playback/cacher.cpp @@ -0,0 +1,339 @@ +#include "cacher.h" + +#include "project/clip.h" +#include "project/sequence.h" +#include "io/media.h" +#include "playback/audio.h" +#include "playback/playback.h" +#include "effects/effects.h" + +extern "C" { + #include + #include + #include + #include +} + +#include + +void cache_audio_worker(Clip* c, bool write_A) { + // gets one frame worth of audio and sends it to the audio buffer + AVFrame* frame = c->cache_A.frames[0]; + uint8_t* cache = (write_A) ? audio_cache_A : audio_cache_B; + + int bytes_written = 0; + int j = 0; + while (bytes_written < audio_cache_size && !c->reached_end) { + // is there audio left in the frame + if (c->frame_sample_index == 0) { + // no more audio left in frame, get a new one + retrieve_next_frame_raw_data(c, frame); + } + if (!c->reached_end) { + int nb_bytes = av_samples_get_buffer_size(NULL, frame->channels, frame->nb_samples, static_cast(frame->format), 1); + int limit = std::min(c->frame_sample_index + audio_cache_size - bytes_written, nb_bytes); + // perform all audio effects + for (unsigned int j=0;jeffects.size();j++) { + c->effects[j]->process_audio(frame->data[0], limit); + } + // mix audio into cache + for (int i=c->frame_sample_index;idata[0][i]; + j++; + } + bytes_written += limit - c->frame_sample_index; + c->frame_sample_index = (limit == nb_bytes) ? 0 : limit; + } + } +} + +void cache_video_worker(Clip* c, long playhead, ClipCache* cache) { + cache->mutex.lock(); + + cache->offset = playhead; + for (size_t i=0;icache_size;i++) { + retrieve_next_frame_raw_data(c, cache->frames[i]); + } + cache->written = true; + cache->unread = true; + + cache->mutex.unlock(); +} + +void reset_cache(Clip* c, long target_frame) { + // if we seek to a whole other place in the timeline, we'll need to reset the cache with new values + if (c->media_stream->infinite_length) { + // if this clip is a still image, we only need one frame + if (!c->cache_A.written) { + retrieve_next_frame_raw_data(c, c->cache_A.frames[0]); + c->cache_A.written = true; + } + } else { + // flush ffmpeg codecs + avcodec_flush_buffers(c->codecCtx); + + double timebase = av_q2d(c->stream->time_base); + + if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + // seeks to nearest keyframe (target_frame represents internal clip frame) + av_seek_frame(c->formatCtx, c->media_stream->file_index, clip_frame_to_seconds(c, target_frame) / timebase, AVSEEK_FLAG_BACKWARD); + + // play up to the frame we actually want + long retrieved_frame = 0; + AVFrame* temp = av_frame_alloc(); + do { + retrieve_next_frame(c, temp); + if (retrieved_frame == 0) { + if (target_frame != 0) { +// retrieved_frame = floor(av_frame_get_best_effort_timestamp(temp) * timebase * av_q2d(c->stream->avg_frame_rate)); + retrieved_frame = floor(temp->pts * timebase * av_q2d(av_guess_frame_rate(c->formatCtx, c->stream, temp))); + } + } else { + retrieved_frame++; + } + } while (retrieved_frame < target_frame); + av_frame_free(&temp); + } else if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + // seek (target_frame represents timeline timecode in frames, not clip timecode) + swr_drop_output(c->swr_ctx, swr_get_out_samples(c->swr_ctx, 0)); + av_seek_frame(c->formatCtx, c->media_stream->file_index, playhead_to_seconds(c, target_frame) / timebase, AVSEEK_FLAG_ANY); + } + } +} + +Cacher::Cacher(Clip* c) : clip(c) {} + +void open_clip_worker(Clip* clip) { + // opens file resource for FFmpeg and prepares Clip struct for playback + QByteArray ba = clip->media->url.toUtf8(); + const char* filename = ba.constData(); + + int errCode = avformat_open_input( + &clip->formatCtx, + filename, + NULL, + NULL + ); + if (errCode != 0) { + char err[1024]; + av_strerror(errCode, err, 1024); + qDebug() << "[ERROR] Could not open" << filename << "-" << err; + } + + errCode = avformat_find_stream_info(clip->formatCtx, NULL); + if (errCode < 0) { + char err[1024]; + av_strerror(errCode, err, 1024); + qDebug() << "[ERROR] Could not open" << filename << "-" << err; + } + + av_dump_format(clip->formatCtx, 0, filename, 0); + + clip->stream = clip->formatCtx->streams[clip->media_stream->file_index]; + clip->codec = avcodec_find_decoder(clip->stream->codecpar->codec_id); + clip->codecCtx = avcodec_alloc_context3(clip->codec); + avcodec_parameters_to_context(clip->codecCtx, clip->stream->codecpar); + + AVDictionary* opts = NULL; + + // decoding optimization configuration + if (clip->stream->codecpar->codec_id != AV_CODEC_ID_PNG && + clip->stream->codecpar->codec_id != AV_CODEC_ID_APNG && + clip->stream->codecpar->codec_id != AV_CODEC_ID_TIFF && + clip->stream->codecpar->codec_id != AV_CODEC_ID_PSD) { + av_dict_set(&opts, "threads", "auto", 0); + } + if (clip->stream->codecpar->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&opts, "tune", "fastdecode", 0); + av_dict_set(&opts, "tune", "zerolatency", 0); + } + + // Open codec + if (avcodec_open2(clip->codecCtx, clip->codec, &opts) < 0) { + qDebug() << "[ERROR] Could not open codec"; + } + + if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + // set up swscale context - primarily used for colorspace conversion + // as "scaling" is actually done by OpenGL + int dest_format = AV_PIX_FMT_RGBA; + + clip->sws_ctx = sws_getContext( + clip->stream->codecpar->width, + clip->stream->codecpar->height, + static_cast(clip->stream->codecpar->format), + ceil(clip->stream->codecpar->width/2)*2, + ceil(clip->stream->codecpar->height/2)*2, + static_cast(dest_format), + SWS_FAST_BILINEAR, + NULL, + NULL, + NULL + ); + + // create memory cache for video + if (clip->media_stream->infinite_length) { + clip->cache_size = 1; + } else { + clip->cache_size = ceil(av_q2d(av_guess_frame_rate(clip->formatCtx, clip->stream, NULL))/4); // cache is half a second in total + + // infinite length doesn't need cache B + clip->cache_B.frames = new AVFrame* [clip->cache_size]; + + } + clip->cache_A.frames = new AVFrame* [clip->cache_size]; + + + for (size_t i=0;icache_size;i++) { + clip->cache_A.frames[i] = av_frame_alloc(); + av_frame_make_writable(clip->cache_A.frames[i]); + clip->cache_A.frames[i]->width = clip->stream->codecpar->width; + clip->cache_A.frames[i]->height = clip->stream->codecpar->height; + clip->cache_A.frames[i]->format = dest_format; + av_frame_get_buffer(clip->cache_A.frames[i], 0); + clip->cache_A.frames[i]->linesize[0] = clip->stream->codecpar->width*4; + + if (!clip->media_stream->infinite_length) { + clip->cache_B.frames[i] = av_frame_alloc(); + av_frame_make_writable(clip->cache_B.frames[i]); + clip->cache_B.frames[i]->width = clip->stream->codecpar->width; + clip->cache_B.frames[i]->height = clip->stream->codecpar->height; + clip->cache_B.frames[i]->format = dest_format; + av_frame_get_buffer(clip->cache_B.frames[i], 0); + clip->cache_B.frames[i]->linesize[0] = clip->stream->codecpar->width*4; + } + } + } else if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + // if FFmpeg can't pick up the channel layout (usually WAV), assume + // based on channel count (doesn't support surround sound sources yet) + if (clip->codecCtx->channel_layout == 0) { + switch (clip->stream->codecpar->channels) { + case 1: clip->codecCtx->channel_layout = AV_CH_LAYOUT_MONO; + default: + clip->codecCtx->channel_layout = AV_CH_LAYOUT_STEREO; + qDebug() << "[WARNING] Could not detect audio channel layout - assuming stereo"; + break; + } + } + + int sample_format = AV_SAMPLE_FMT_S16; + + // init resampling context + clip->swr_ctx = swr_alloc_set_opts( + NULL, + clip->sequence->audio_layout, + static_cast(sample_format), + clip->sequence->audio_frequency, + clip->stream->codecpar->channel_layout, + static_cast(clip->stream->codecpar->format), + clip->stream->codecpar->sample_rate, + 0, + NULL + ); + swr_init(clip->swr_ctx); + + // set up cache + clip->cache_A.frames = new AVFrame* [1]; + clip->cache_A.frames[0] = av_frame_alloc(); + clip->cache_A.frames[0]->format = sample_format; + clip->cache_A.frames[0]->channel_layout = clip->sequence->audio_layout; + clip->cache_A.frames[0]->channels = av_get_channel_layout_nb_channels(clip->cache_A.frames[0]->channel_layout); + clip->cache_A.frames[0]->sample_rate = clip->sequence->audio_frequency; + av_frame_make_writable(clip->cache_A.frames[0]); + + clip->reset_audio = true; + } + + clip->frame = av_frame_alloc(); + + qDebug() << "[INFO] Clip opened on track" << clip->track; + + clip->open = true; +} + +void cache_clip_worker(Clip* clip, long playhead, bool write_A, bool write_B, bool reset) { + if (reset) { + // note: for video, playhead is in "internal clip" frames - for audio, it's the timeline playhead + reset_cache(clip, playhead); + clip->reset_audio = false; + } + + if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + if (write_A) { + cache_video_worker(clip, playhead, &clip->cache_A); + playhead += clip->cache_size; + } + + if (write_B) { + cache_video_worker(clip, playhead, &clip->cache_B); + } + } else if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + if (write_A) { + cache_audio_worker(clip, true); + } + if (write_B) { + cache_audio_worker(clip, false); + } + } +} + +void close_clip_worker(Clip* clip) { + // closes ffmpeg file handle and frees any memory used for caching + if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + sws_freeContext(clip->sws_ctx); + } else if (clip->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + swr_free(&clip->swr_ctx); + } + + avcodec_close(clip->codecCtx); + avcodec_free_context(&clip->codecCtx); + avformat_close_input(&clip->formatCtx); + + for (size_t i=0;icache_size;i++) { + av_frame_free(&clip->cache_A.frames[i]); + if (!clip->media_stream->infinite_length) av_frame_free(&clip->cache_B.frames[i]); + } + delete [] clip->cache_A.frames; + if (!clip->media_stream->infinite_length) delete [] clip->cache_B.frames; + + av_frame_free(&clip->frame); + + clip->reset(); + + // remove clip from current_clips + cc_lock.lock(); + bool found = false; + for (int i=0;itrack; +} + +void Cacher::run() { + // open_lock is used to prevent the clip from being destroyed before the cacher has closed it properly + clip->open_lock.lock(); + + open_clip_worker(clip); + + caching = true; + while (true) { + clip->can_cache.wait(&clip->lock); + if (!caching) { + break; + } else { + cache_clip_worker(clip, playhead, write_A, write_B, reset); + } + } + + close_clip_worker(clip); + clip->lock.unlock(); + + clip->open_lock.unlock(); +} diff --git a/playback/cacher.h b/playback/cacher.h new file mode 100644 index 000000000..d99924864 --- /dev/null +++ b/playback/cacher.h @@ -0,0 +1,31 @@ +#ifndef CACHER_H +#define CACHER_H + +#include + +struct Clip; + +class Cacher : public QThread +{ +// Q_OBJECT +public: + Cacher(Clip* c); + void run() override; + + bool caching; + + // must be set before caching + long playhead; + bool write_A; + bool write_B; + bool reset; + +private: + Clip* clip; +}; + +void open_clip_worker(Clip* clip); +void cache_clip_worker(Clip* clip, long playhead, bool write_A, bool write_B, bool reset); +void close_clip_worker(Clip* clip); + +#endif // CACHER_H diff --git a/playback/playback.cpp b/playback/playback.cpp new file mode 100644 index 000000000..b58725a77 --- /dev/null +++ b/playback/playback.cpp @@ -0,0 +1,288 @@ +#include "playback.h" + +#include "project/clip.h" +#include "project/sequence.h" +#include "io/media.h" +#include "playback/audio.h" +#include "playback/cacher.h" +#include "panels/timeline.h" +#include + +extern "C" { + #include + #include + #include + #include +} + +#include +#include +#include +#include + +QList current_clips; +bool texture_failed = false; + +QMutex cc_lock; + +void handle_media(Sequence* sequence, long playhead, bool multithreaded) { + for (int i=0;iclip_count();i++) { + Clip& c = sequence->get_clip(i); + + // if clip starts within one second and/or hasn't finished yet + if (is_clip_active(&c, playhead)) { + // if thread is already working, we don't want to touch this, + // but we also don't want to hang the UI thread + if (!c.open) { + if (c.lock.tryLock()) { + open_clip(&c, multithreaded); + + // add to current_clips, (insertion) sorted by track so composite them in order + cc_lock.lock(); + bool found = false; + for (int j=0;jtrack < c.track) { + current_clips.insert(current_clips.begin()+j, &c); + found = true; + break; + } + } + if (!found) { + current_clips.push_back(&c); + } + cc_lock.unlock(); + } + } + } else if (c.open) { + close_clip(&c); + } + } +} + +void open_clip(Clip* clip, bool multithreaded) { + clip->multithreaded = multithreaded; + if (multithreaded) { + // maybe keep cacher instance in memory while clip exists for performance? + clip->cacher = new Cacher(clip); + QObject::connect(clip->cacher, SIGNAL(finished()), clip->cacher, SLOT(deleteLater())); + + clip->cacher->start(QThread::LowPriority); + } else { + open_clip_worker(clip); + clip->lock.unlock(); + } +} + +void close_clip(Clip* clip) { + // destroy opengl texture in main thread + if (clip->texture != NULL) { + clip->texture->destroy(); + clip->texture = NULL; + } + + if (clip->multithreaded) { + clip->cacher->caching = false; + clip->can_cache.wakeAll(); + } else { + close_clip_worker(clip); + } +} + +void cache_clip(Clip* clip, long playhead, bool write_A, bool write_B, bool reset) { + if (clip->multithreaded) { + clip->cacher->playhead = playhead; + clip->cacher->write_A = write_A; + clip->cacher->write_B = write_B; + clip->cacher->reset = reset; + + clip->can_cache.wakeAll(); + } else { + cache_clip_worker(clip, playhead, write_A, write_B, reset); + } +} + +void get_clip_frame(Clip* c, long playhead) { + if (c->open) { + long clip_time = seconds_to_clip_frame(c, playhead_to_seconds(c, playhead)); + + // do we need to update the texture? + if ((!c->media_stream->infinite_length && c->texture_frame != clip_time) || + (c->media_stream->infinite_length && c->texture_frame == -1)) { + AVFrame* current_frame = NULL; + + // get frame data + if (c->media_stream->infinite_length) { // if clip is a still frame, we only need one + if (c->cache_A.written) { + // retrieve cached frame + current_frame = c->cache_A.frames[0]; + } else if (c->multithreaded) { + if (c->lock.tryLock()) { + // grab image (multi-threaded) + cache_clip(c, 0, false, false, true); + c->lock.unlock(); + } + } else { + // grab image (single-threaded) + reset_cache(c, playhead); + } + } else { + // keeping a RAM cache improves performance, however it's detrimental when rendering + // determine which cache contains the requested frame + bool using_cache_A = false; + bool using_cache_B = false; + AVFrame** cache = NULL; + long cache_offset = 0; + bool cache_needs_reset = false; + + if (c->cache_A.written && clip_time >= c->cache_A.offset && clip_time < c->cache_A.offset + c->cache_size) { + if (c->cache_A.mutex.tryLock()) { // lock in case cacher is still writing to it + using_cache_A = true; + c->cache_A.unread = false; + cache = c->cache_A.frames; + cache_offset = c->cache_A.offset; + c->cache_A.mutex.unlock(); + } + } else if (c->cache_B.written && clip_time >= c->cache_B.offset && clip_time < c->cache_B.offset + c->cache_size) { + if (c->cache_B.mutex.tryLock()) { // lock in case cacher is still writing to it + using_cache_B = true; + c->cache_B.unread = false; + cache = c->cache_B.frames; + cache_offset = c->cache_B.offset; + c->cache_B.mutex.unlock(); + } + } else { + // this is technically bad, unless we just seeked + c->cache_A.unread = c->cache_B.unread = false; + cache_needs_reset = true; + } + + if (cache != NULL) { + current_frame = cache[clip_time - cache_offset]; + } + + // determine whether we should start filling the other cache + if (!using_cache_A || !using_cache_B) { + if (c->lock.tryLock()) { + bool write_A = (!using_cache_A && !c->cache_A.unread); + bool write_B = (!using_cache_B && !c->cache_B.unread); + if (write_A || write_B) { + long playhead; + if (cache_needs_reset) { + // if we have no cache and need to seek, start us at the current playhead... + playhead = clip_time; + } else { + // ...otherwise start at the end of the current cache + playhead = cache_offset + c->cache_size; + } + cache_clip(c, playhead, write_A, write_B, cache_needs_reset); + } + c->lock.unlock(); + } + } + } + + if (current_frame != NULL) { + // set up opengl texture + if (c->texture == NULL) { + c->texture = new QOpenGLTexture(QOpenGLTexture::Target2D); + c->texture->setSize(c->media_stream->video_width, c->media_stream->video_height); + c->texture->setFormat(QOpenGLTexture::RGBA8_UNorm); + c->texture->setMipLevels(c->texture->maximumMipLevels()); + c->texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear); + c->texture->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8); + } + + c->texture->setData(0, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, current_frame->data[0]); + c->texture_frame = clip_time; + } else { + texture_failed = true; + qDebug() << "[ERROR] Failed to retrieve frame from cache (R:" << clip_time << "| A:" << c->cache_A.offset << "-" << c->cache_A.offset+c->cache_size-1 << "| B:" << c->cache_B.offset << "-" << c->cache_B.offset+c->cache_size-1 << "| WA:" << c->cache_A.written << "| WB:" << c->cache_B.written << ")"; + } + } + } +} + +float playhead_to_seconds(Clip* c, long playhead) { + // returns time in seconds + return (std::max((long) 0, playhead - c->timeline_in) + c->clip_in)/c->sequence->frame_rate; +} + +long seconds_to_clip_frame(Clip* c, float seconds) { + // returns time as frame number (according to clip's frame rate) + if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + return floor(seconds*av_q2d(av_guess_frame_rate(c->formatCtx, c->stream, c->frame))); + } else { + qDebug() << "[ERROR] seconds_to_clip_frame only works on video streams"; + return 0; + } +} + +float clip_frame_to_seconds(Clip* c, long clip_frame) { + // returns frame number as seconds (decimal) + if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + return (double) clip_frame / av_q2d(c->stream->avg_frame_rate); + } else { + qDebug() << "[ERROR] clip_frame_to_seconds only works on video streams"; + return 0; + } +} + +int retrieve_next_frame(Clip* c, AVFrame* f) { + int result = 0; + int receive_ret; + + // could be optimized if "linked stream" shares timeline_in and clip_in, only use one read_frame? + // depends how much time av_read_frame needs, the decoding is probably far more consuming anyway + + // do we need to retrieve a new packet for a new frame? + while ((receive_ret = avcodec_receive_frame(c->codecCtx, f)) == AVERROR(EAGAIN)) { + int read_ret = 0; + do { + if (c->pkt_written) { + av_packet_unref(c->pkt); + } + read_ret = av_read_frame(c->formatCtx, c->pkt); + c->pkt_written = true; + } while (read_ret >= 0 && c->pkt->stream_index != c->media_stream->file_index); + + if (read_ret >= 0) { + int send_ret = avcodec_send_packet(c->codecCtx, c->pkt); + if (send_ret < 0) { + qDebug() << "[ERROR] Failed to send packet to decoder." << send_ret; + result = send_ret; + break; + } + } else { + if (read_ret != AVERROR_EOF) qDebug() << "[ERROR] Could not read frame." << read_ret; + return read_ret; // skips trying to find a frame at all + } + } + if (receive_ret < 0) { + qDebug() << "[ERROR] Failed to receive packet from decoder." << receive_ret; + result = receive_ret; + } + + return result; +} + +void retrieve_next_frame_raw_data(Clip* c, AVFrame* output) { + int ret = retrieve_next_frame(c, c->frame); + if (ret >= 0) { + if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + sws_scale(c->sws_ctx, c->frame->data, c->frame->linesize, 0, c->stream->codecpar->height, output->data, output->linesize); + } else if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { + ret = swr_convert_frame(c->swr_ctx, output, c->frame); + if (ret < 0) { + qDebug() << "[ERROR] Failed to resample audio." << ret; + } + } + } else if (ret == AVERROR_EOF) { + c->reached_end = true; + } else { + qDebug() << "[WARNING] Raw frame data could not be retrieved." << ret; + } +} + +bool is_clip_active(Clip* c, long playhead) { + return c->timeline_in < playhead + ceil(c->sequence->frame_rate) && c->timeline_out > playhead; +} diff --git a/playback/playback.h b/playback/playback.h new file mode 100644 index 000000000..6bd77a702 --- /dev/null +++ b/playback/playback.h @@ -0,0 +1,41 @@ +#ifndef PLAYBACK_H +#define PLAYBACK_H + +#include +#include + +struct Clip; +struct ClipCache; +struct Sequence; +struct AVFrame; + +extern QList current_clips; +extern QMutex cc_lock; + +extern bool texture_failed; + +void open_clip(Clip* clip, bool multithreaded); +void cache_clip(Clip* clip, long playhead, bool write_A, bool write_B, bool reset); +void close_clip(Clip* clip); +void cache_audio_worker(Clip* c, bool write_A); +void cache_video_worker(Clip* c, long playhead, ClipCache* cache); +void handle_media(Sequence* sequence, long playhead, bool multithreaded); +void reset_cache(Clip* c, long target_frame); +void get_clip_frame(Clip* c, long playhead); +float playhead_to_seconds(Clip* c, long playhead); +long seconds_to_clip_frame(Clip* c, float seconds); +float clip_frame_to_seconds(Clip* c, long clip_frame); +int retrieve_next_frame(Clip* c, AVFrame* f); +void retrieve_next_frame_raw_data(Clip* c, AVFrame* output); +bool is_clip_active(Clip* c, long playhead); +void get_next_audio(Clip* c, bool mix); + +struct ClipCacheData { + Clip& clip; + long playhead; + bool write_A; + bool write_B; + bool reset; +}; + +#endif // PLAYBACK_H diff --git a/project/clip.cpp b/project/clip.cpp new file mode 100644 index 000000000..f8f2c25d9 --- /dev/null +++ b/project/clip.cpp @@ -0,0 +1,81 @@ +#include "clip.h" + +#include "project/effect.h" +#include "io/media.h" +#include "playback/playback.h" +#include "playback/cacher.h" + +#include + +extern "C" { + #include +} + +Clip::Clip() { + init(); +} + +Clip::Clip(const Clip &c) { + init(); + copy(c); +} + +Clip& Clip::operator= (const Clip& c) { + init(); + copy(c); + return *this; +} + +void Clip::copy(const Clip& c) { + name = c.name; + clip_in = c.clip_in; + timeline_in = c.timeline_in; + timeline_out = c.timeline_out; + track = c.track; + color_r = c.color_r; + color_g = c.color_g; + color_b = c.color_b; + sequence = c.sequence; + media = c.media; + media_stream = c.media_stream; +} + +void Clip::init() { + reset(); + clip_in = timeline_in = timeline_out = track = undeletable = 0; + texture = NULL; + pkt = new AVPacket(); +} + +void Clip::reset() { + cache_size = cache_A.offset = cache_B.offset = open = pkt_written = cache_A.written = cache_B.written = cache_A.unread = cache_B.unread = reached_end = reset_audio = frame_sample_index = 0; + texture_frame = -1; + formatCtx = NULL; + stream = NULL; + codec = NULL; + codecCtx = NULL; + texture = NULL; + cache_A.frames = NULL; + cache_B.frames = NULL; +} + +Clip::~Clip() { + if (open) { + close_clip(this); + } + + // make sure clip has closed before clip is destroyed + open_lock.lock(); + open_lock.unlock(); + + for (int i=0;i +#include +#include + +class Cacher; +class Effect; +struct Sequence; +struct Media; +struct MediaStream; + +struct AVFormatContext; +struct AVStream; +struct AVCodec; +struct AVCodecContext; +struct AVFrame; +struct AVPacket; +struct SwsContext; +struct SwrContext; +class QOpenGLTexture; + +struct ClipCache { + AVFrame** frames; + long offset; + bool written; + bool unread; + QMutex mutex; +}; + +struct Clip +{ + Clip(); + Clip(const Clip &c); + Clip& operator= (const Clip&); // explicitly defaulted copy assignment + ~Clip(); + void copy(const Clip& c); + void init(); + void reset(); + bool undeletable; + + // timeline variables + QString name; + long clip_in; + long timeline_in; + long timeline_out; + int track; + uint8_t color_r; + uint8_t color_g; + uint8_t color_b; + long getLength(); + + // inherited information (should be copied in copy()) + Media* media; // attached media + MediaStream* media_stream; + Sequence* sequence; + + // other variables (should be "duplicated" in copy()) + QList effects; +// QVector linkedClips; + + // media handling + AVFormatContext* formatCtx; + AVStream* stream; + AVCodec* codec; + AVCodecContext* codecCtx; + AVPacket* pkt; + AVFrame* frame; + bool pkt_written; + bool reached_end; + bool open; + + // caching functions + bool multithreaded; + Cacher* cacher; + QWaitCondition can_cache; + uint16_t cache_size; + ClipCache cache_A; + ClipCache cache_B; + QMutex lock; + QMutex open_lock; + + // video playback variables + SwsContext* sws_ctx; + QOpenGLTexture* texture; + long texture_frame; + + // audio playback variables + SwrContext* swr_ctx; + int frame_sample_index; + bool reset_audio; +}; + +#endif // CLIP_H diff --git a/project/effect.cpp b/project/effect.cpp new file mode 100644 index 000000000..25c222020 --- /dev/null +++ b/project/effect.cpp @@ -0,0 +1,18 @@ +#include "effect.h" + +#include "panels/panels.h" +#include "panels/viewer.h" +#include "ui/viewerwidget.h" + +Effect::Effect(Clip* c) : parent_clip(c) +{ + name = ""; + type = EFFECT_TYPE_INVALID; +} + +void Effect::field_changed() { + panel_viewer->viewer_widget->update(); +} + +void Effect::process_gl(int* anchor_x, int* anchor_y) {} +void Effect::process_audio(uint8_t* samples, int nb_bytes) {} diff --git a/project/effect.h b/project/effect.h new file mode 100644 index 000000000..443ac5591 --- /dev/null +++ b/project/effect.h @@ -0,0 +1,31 @@ +#ifndef EFFECT_H +#define EFFECT_H + +#include +#include +class QWidget; +class CollapsibleWidget; + +struct Clip; + +enum EffectTypes { EFFECT_TYPE_INVALID, EFFECT_TYPE_VIDEO, EFFECT_TYPE_AUDIO }; + +class Effect : public QObject +{ + Q_OBJECT +public: + Effect(Clip* c); + int type; + QString name; + CollapsibleWidget* container; + QWidget* ui; + Clip* parent_clip; + + virtual void process_gl(int* anchor_x, int* anchor_y); + virtual void process_audio(uint8_t* samples, int nb_bytes); + +public slots: + void field_changed(); +}; + +#endif // EFFECT_H diff --git a/project/sequence.cpp b/project/sequence.cpp new file mode 100644 index 000000000..7140bec67 --- /dev/null +++ b/project/sequence.cpp @@ -0,0 +1,122 @@ +#include "sequence.h" + +#include "project/clip.h" + +#include + +Sequence::Sequence() { +} + +Sequence::~Sequence() { +} + +Clip& Sequence::new_clip() { + clips.append(Clip()); + return clips[clips.size() - 1]; +} + +Clip& Sequence::insert_clip(const Clip& c) { + clips.append(c); + return clips[clips.size() - 1]; +} + +int Sequence::clip_count() { + return clips.count(); +} + +Clip& Sequence::get_clip(int i) { + return clips[i]; +} + +void Sequence::delete_clip(int i) { + // remove any potential link references to clip +// for (size_t j=0;jlinkedClips.size();k++) { +// if (c->linkedClips[k] == clips[i]) { +// c->linkedClips.erase(c->linkedClips.begin()+k); +// } +// } +// } + + // finally remove from vector + clips.removeAt(i); +} + +long Sequence::getEndFrame() { + long end = 0; + for (int j=0;j end) { + end = c.timeline_out; + } + } + return end; +} + +void Sequence::get_track_limits(int* video_tracks, int* audio_tracks) { + int vt = 0; + int at = 0; + for (int j=0;j at) { + at = c.track; + } + } + if (video_tracks != NULL) *video_tracks = vt; + if (audio_tracks != NULL) *audio_tracks = at; +} + +void Sequence::delete_area(long in, long out, int track) { + for (int i=0;i= in && c.timeline_out <= out) { + // clips falls entirely within deletion area + delete_clip(i); + i--; + } else if (c.timeline_in < in && c.timeline_out > out) { + // middle of clip is within deletion area + clips.append(clips.at(i)); // copy clip + + Clip& pre = get_clip(i); + Clip& post = get_clip(clips.size()-1); + + pre.timeline_out = in; + post.timeline_in = out; + post.clip_in = pre.clip_in + pre.getLength() + (out - in); + } else if (c.timeline_in < in && c.timeline_out > in) { + // only out point is in deletion area + c.timeline_out = in; + } else if (c.timeline_in < out && c.timeline_out > out) { + // only in point is in deletion area + c.clip_in += out - c.timeline_in; + c.timeline_in = out; + } + } + } +} + +void Sequence::split_clip(int i, long frame) { + Clip& pre = get_clip(i); + if (pre.timeline_in < frame && pre.timeline_out > frame) { // guard against attempts to split at in/out points + clips.append(pre); // copy clip + + Clip& post = get_clip(clips.size()-1); + + pre.timeline_out = frame; + post.timeline_in = frame; + post.clip_in = pre.clip_in + pre.getLength(); + } +} + +void Sequence::split_at_playhead(long frame) { + for (int j=0;j frame) { + split_clip(j, frame); + } + } +} diff --git a/project/sequence.h b/project/sequence.h new file mode 100644 index 000000000..d63abe68f --- /dev/null +++ b/project/sequence.h @@ -0,0 +1,32 @@ +#ifndef SEQUENCE_H +#define SEQUENCE_H + +#include + +#include "project/clip.h" + +struct Sequence { +public: + Sequence(); + ~Sequence(); + QString name; + Clip& new_clip(); + Clip& insert_clip(const Clip& c); + int clip_count(); + Clip& get_clip(int i); + void delete_clip(int i); + void delete_area(long in, long out, int track); + void split_clip(int i, long frame); + void split_at_playhead(long frame); + void get_track_limits(int* video_tracks, int* audio_tracks); + long getEndFrame(); + int width; + int height; + float frame_rate; + int audio_frequency; + int audio_layout; +private: + QList clips; +}; + +#endif // SEQUENCE_H diff --git a/ui/collapsiblewidget.cpp b/ui/collapsiblewidget.cpp new file mode 100644 index 000000000..a4b57de5a --- /dev/null +++ b/ui/collapsiblewidget.cpp @@ -0,0 +1,59 @@ +#include "collapsiblewidget.h" + +#include +#include +#include +#include +#include +#include +#include + +CollapsibleWidget::CollapsibleWidget(QWidget* parent) : QWidget(parent) +{ + layout = new QVBoxLayout(this); + layout->setMargin(0); + + title_bar = new QHBoxLayout(); + title_bar->setMargin(0); + enabled_check = new QCheckBox(); + enabled_check->setChecked(true); + header = new QLabel(); + collapse_button = new QPushButton("-"); + collapse_button->setMaximumWidth(25); + setText(""); + title_bar->addWidget(collapse_button); + title_bar->addWidget(enabled_check); + title_bar->addWidget(header); + title_bar->addStretch(); + layout->addLayout(title_bar); + + line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + layout->addWidget(line); + + contents = NULL; +} + +void CollapsibleWidget::setContents(QWidget* c) { + bool existing = (contents != NULL); + contents = c; + if (!existing) { + layout->addWidget(contents); + connect(enabled_check, SIGNAL(toggled(bool)), this, SLOT(on_enabled_change(bool))); + connect(collapse_button, SIGNAL(clicked()), this, SLOT(on_visible_change())); + } +} + +void CollapsibleWidget::setText(const QString &s) { + header->setText(s); +} + +void CollapsibleWidget::on_enabled_change(bool b) { + contents->setEnabled(b); +} + +void CollapsibleWidget::on_visible_change() { + contents->setVisible(!contents->isVisible()); + collapse_button->setText(contents->isVisible() ? "-" : "+"); +} diff --git a/ui/collapsiblewidget.h b/ui/collapsiblewidget.h new file mode 100644 index 000000000..b771c8387 --- /dev/null +++ b/ui/collapsiblewidget.h @@ -0,0 +1,33 @@ +#ifndef COLLAPSIBLEWIDGET_H +#define COLLAPSIBLEWIDGET_H + +#include +class QLabel; +class QCheckBox; +class QHBoxLayout; +class QVBoxLayout; +class QPushButton; +class QFrame; + +class CollapsibleWidget : public QWidget +{ + Q_OBJECT +public: + CollapsibleWidget(QWidget* parent = 0); + void setContents(QWidget* c); + void setText(const QString &); +private: + QLabel* header; + QCheckBox* enabled_check; + QHBoxLayout* title_bar; + QVBoxLayout* layout; + QPushButton* collapse_button; + QWidget* contents; + QFrame* line; + +private slots: + void on_enabled_change(bool b); + void on_visible_change(); +}; + +#endif // COLLAPSIBLEWIDGET_H diff --git a/ui/playbutton.cpp b/ui/playbutton.cpp new file mode 100644 index 000000000..4a2eea08b --- /dev/null +++ b/ui/playbutton.cpp @@ -0,0 +1,9 @@ +#include "playbutton.h" + +PlayButton::PlayButton(QWidget* parent) : QPushButton(parent) +{ + play_text = ">"; + pause_text = "||"; + + setText(play_text); +} diff --git a/ui/playbutton.h b/ui/playbutton.h new file mode 100644 index 000000000..3dc8b5a88 --- /dev/null +++ b/ui/playbutton.h @@ -0,0 +1,15 @@ +#ifndef PLAYBUTTON_H +#define PLAYBUTTON_H + +#include + +class PlayButton : public QPushButton +{ +public: + PlayButton(QWidget* parent = 0); +private: + QString play_text; + QString pause_text; +}; + +#endif // PLAYBUTTON_H diff --git a/ui/sourcetable.cpp b/ui/sourcetable.cpp new file mode 100644 index 000000000..06f632b06 --- /dev/null +++ b/ui/sourcetable.cpp @@ -0,0 +1,30 @@ +#include "sourcetable.h" +#include "panels/project.h" + +#include "io/media.h" +#include "panels/timeline.h" +#include "panels/viewer.h" +#include "panels/panels.h" + +#include + +SourceTable::SourceTable(QWidget* parent) : QTreeWidget(parent) { + this->sortByColumn(0, Qt::AscendingOrder); +} + +void SourceTable::mouseDoubleClickEvent(QMouseEvent* ) +{ + if (selectedItems().count() == 0) { + panel_project->import_dialog(); + } else if (selectedItems().count() == 1) { + Media* m = reinterpret_cast(selectedItems().at(0)->data(0, Qt::UserRole + 1).value()); + if (m->is_sequence) { + panel_timeline->set_sequence(m->sequence); + panel_viewer->set_sequence(m->sequence); + } + } +} + +//void SourceTable::dragEnterEvent(QDragEnterEvent *event) { +// event->accept(); +//} diff --git a/ui/sourcetable.h b/ui/sourcetable.h new file mode 100644 index 000000000..e42c2acb1 --- /dev/null +++ b/ui/sourcetable.h @@ -0,0 +1,18 @@ +#ifndef SOURCETABLE_H +#define SOURCETABLE_H + +#include + +class Project; + +class SourceTable : public QTreeWidget +{ +public: + SourceTable(QWidget* parent = 0); +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; +// void dragEnterEvent(QDragEnterEvent *event) override; +private: +}; + +#endif // SOURCETABLE_H diff --git a/ui/timeline-tools.h b/ui/timeline-tools.h new file mode 100644 index 000000000..28cdb10da --- /dev/null +++ b/ui/timeline-tools.h @@ -0,0 +1,15 @@ +#ifndef TIMELINETOOLS_H +#define TIMELINETOOLS_H + +enum TimelineTools { + TIMELINE_TOOL_POINTER, + TIMELINE_TOOL_EDIT, + TIMELINE_TOOL_RAZOR, + TIMELINE_TOOL_RIPPLE, + TIMELINE_TOOL_ROLLING, + TIMELINE_TOOL_SLIP, + TIMELINE_TOOL_HAND, + TIMELINE_TOOL_ZOOM +}; + +#endif // TIMELINETOOLS_H diff --git a/ui/timelinewidget.cpp b/ui/timelinewidget.cpp new file mode 100644 index 000000000..54e569bdd --- /dev/null +++ b/ui/timelinewidget.cpp @@ -0,0 +1,799 @@ +#include "timelinewidget.h" + +#include "panels/panels.h" +#include "io/config.h" +#include "project/sequence.h" +#include "project/clip.h" +#include "panels/project.h" +#include "panels/timeline.h" +#include "io/media.h" +#include "ui/sourcetable.h" +#include "panels/effectcontrols.h" + +#include "effects/effects.h" + +#include +#include +#include +#include +#include +#include +#include + +TimelineWidget::TimelineWidget(QWidget *parent) : QWidget(parent) +{ + bottom_align = false; + setMouseTracking(true); + track_height = 40; + + clip_pixmap = NULL; + + setAcceptDrops(true); +} + +bool same_sign(int a, int b) { + return (a < 0) == (b < 0); +} + +void TimelineWidget::dragEnterEvent(QDragEnterEvent *event) { + if (static_cast(event->source()) == panel_project->source_table) { + QPoint pos = event->pos(); + event->accept(); + QList items = panel_project->source_table->selectedItems(); + long entry_point = getFrameFromScreenPoint(pos.x(), false); + panel_timeline->drag_frame_start = entry_point + getFrameFromScreenPoint(50, false); + panel_timeline->drag_track_start = (bottom_align) ? -1 : 0; + for (int i=0;i(items.at(i)->data(0, Qt::UserRole + 1).value()); + long duration = m->get_length_in_frames(panel_timeline->sequence->frame_rate); + Ghost g = {NULL, entry_point, entry_point + duration}; + g.media = m; + g.clip_in = 0; + for (int j=0;jaudio_tracks.size();j++) { + g.track = j; + g.media_stream = &m->audio_tracks[j]; + ignore_infinite_length = true; + panel_timeline->ghosts.append(g); + } + for (int j=0;jvideo_tracks.size();j++) { + g.track = -1-j; + g.media_stream = &m->video_tracks[j]; + if (m->video_tracks[j].infinite_length && !ignore_infinite_length) g.out = g.in + 100; + panel_timeline->ghosts.append(g); + } + entry_point += duration; + } + init_ghosts(); + panel_timeline->importing = true; + } +} + +void TimelineWidget::dragMoveEvent(QDragMoveEvent *event) { + if (panel_timeline->importing) { + QPoint pos = event->pos(); + update_ghosts(pos); + panel_timeline->repaint_timeline(); + } +} + +void TimelineWidget::dragLeaveEvent(QDragLeaveEvent *event) { + if (panel_timeline->importing) { + panel_timeline->ghosts.clear(); + panel_timeline->importing = false; + panel_timeline->repaint_timeline(); + } +} + +void TimelineWidget::dropEvent(QDropEvent* event) { + if (panel_timeline->importing) { + event->accept(); + + for (int i=0;ighosts.size();i++) { + const Ghost& g = panel_timeline->ghosts.at(i); + + panel_timeline->sequence->delete_area(g.in, g.out, g.track); + + Clip& c = panel_timeline->sequence->new_clip(); + c.media = g.media; + c.media_stream = g.media_stream; + c.timeline_in = g.in; + c.timeline_out = g.out; + c.clip_in = g.clip_in; + c.color_r = 128; + c.color_g = 128; + c.color_b = 192; + c.sequence = panel_timeline->sequence; + c.track = g.track; + c.name = c.media->name; + + if (c.track < 0) { + // add default video effects + c.effects.append(new TransformEffect(&c)); + } else { + // add default audio effects + c.effects.append(new VolumeEffect(&c)); + c.effects.append(new PanEffect(&c)); + } + } + + panel_timeline->ghosts.clear(); + panel_timeline->importing = false; + + panel_timeline->redraw_all_clips(); + } +} + +void TimelineWidget::mousePressEvent(QMouseEvent *event) { + QPoint pos = event->pos(); + + panel_timeline->drag_frame_start = getFrameFromScreenPoint(pos.x(), false); + panel_timeline->drag_track_start = getTrackFromScreenPoint(pos.y()); + int clip_index = panel_timeline->trim_target; + if (clip_index == -1) clip_index = getClipIndexFromCoords(panel_timeline->drag_frame_start, panel_timeline->drag_track_start); + + switch (panel_timeline->tool) { + case TIMELINE_TOOL_POINTER: + case TIMELINE_TOOL_RIPPLE: + { + if (clip_index >= 0) { + if (panel_timeline->is_clip_selected(clip_index)) { + // TODO if shift is down, deselect it + } else { + // if "shift" is not down + if (!(event->modifiers() & Qt::ShiftModifier)) { + panel_timeline->selections.clear(); + } + + Clip& clip = panel_timeline->sequence->get_clip(clip_index); + panel_timeline->selections.append({clip.timeline_in, clip.timeline_out, clip.track}); + } + panel_timeline->moving_init = true; + } else { + panel_timeline->selections.clear(); + } + panel_timeline->repaint_timeline(); + } + break; + case TIMELINE_TOOL_EDIT: + panel_timeline->seek(panel_timeline->drag_frame_start); + panel_timeline->selecting = true; + panel_timeline->repaint_timeline(); + break; + case TIMELINE_TOOL_RAZOR: + { + if (clip_index >= 0) { + panel_timeline->sequence->split_clip(clip_index, panel_timeline->drag_frame_start); + } + panel_timeline->splitting = true; + panel_timeline->redraw_all_clips(); + } + break; + } +} + +void TimelineWidget::mouseReleaseEvent(QMouseEvent *event) { + if (panel_timeline->moving_proc) { + if (event->modifiers() & Qt::AltModifier) { // if holding alt, duplicate rather than move + // duplicate clips + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + Clip& c = panel_timeline->sequence->insert_clip(*g.clip); + + c.timeline_in = g.in; + c.timeline_out = g.out; + c.track = g.track; + c.undeletable = true; + + // step 2 - delete anything that exists in area that clip is moving to + panel_timeline->sequence->delete_area(g.in, g.out, g.track); + + c.undeletable = false; + } + } else { + // move clips + // TODO can we do this better than 3 consecutive for loops? + for (int i=0;ighosts.size();i++) { + // step 1 - set clips that are moving to "undeletable" (to avoid step 2 deleting any part of them) + panel_timeline->ghosts[i].clip->undeletable = true; + } + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + + if (panel_timeline->tool == TIMELINE_TOOL_POINTER) { + // step 2 - delete anything that exists in area that clip is moving to + // note: ripples are non-destructive so this is pointer-tool exclusive + panel_timeline->sequence->delete_area(g.in, g.out, g.track); + } + + // step 3 - move clips + g.clip->timeline_in = g.in; + g.clip->timeline_out = g.out; + g.clip->track = g.track; + g.clip->clip_in = g.clip_in; + } + for (int i=0;ighosts.size();i++) { + // step 4 - set clips back to deletable + panel_timeline->ghosts[i].clip->undeletable = false; + } + } + + // ripple ops + if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { + long ripple_length, ripple_point; + if (panel_timeline->trim_in) { + ripple_length = panel_timeline->ghosts.at(0).old_in - panel_timeline->ghosts.at(0).in; + ripple_point = (ripple_length < 0) ? panel_timeline->ghosts.at(0).old_in : panel_timeline->ghosts.at(0).in; + } else { + ripple_length = panel_timeline->ghosts.at(0).old_out - panel_timeline->ghosts.at(0).out; + ripple_point = (ripple_length < 0) ? panel_timeline->ghosts.at(0).old_out : panel_timeline->ghosts.at(0).out; + } + for (int i=0;ighosts.size();i++) { + long comp_point; + if (panel_timeline->trim_in) { + comp_point = (ripple_length < 0) ? panel_timeline->ghosts.at(i).old_in : panel_timeline->ghosts.at(i).in; + } else { + comp_point = (ripple_length < 0) ? panel_timeline->ghosts.at(i).old_out : panel_timeline->ghosts.at(i).out; + } + if (ripple_point > comp_point) ripple_point = comp_point; + } + if (panel_timeline->trim_in) { + panel_timeline->ripple(ripple_point, ripple_length); + } else { + panel_timeline->ripple(ripple_point, -ripple_length); + } + } + + panel_timeline->redraw_all_clips(); + } + + // destroy all ghosts + panel_timeline->ghosts.clear(); + + panel_timeline->selecting = false; + panel_timeline->moving_proc = false; + panel_timeline->moving_init = false; + panel_timeline->splitting = false; + pre_clips.clear(); + post_clips.clear(); + + // find out how many clips are selected + bool single_select = false; + int selected_clip = 0; + for (int i=0;isequence->clip_count();i++) { + if (panel_timeline->is_clip_selected(i)) { + if (!single_select) { + // found ONE selected clip + selected_clip = i; + single_select = true; + } else { + // more than one clip is selected + single_select = false; + break; + } + } + } + if (single_select) { + panel_effect_controls->set_clip(&panel_timeline->sequence->get_clip(selected_clip)); + } else { + panel_effect_controls->set_clip(NULL); + } +} + +void TimelineWidget::init_ghosts() { + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + g.old_in = g.in; + g.old_out = g.out; + g.old_track = g.track; + g.old_clip_in = g.clip_in; + + if (panel_timeline->trim_target > -1) { + // used for trim ops + g.ghost_length = g.old_out - g.old_in; + g.media_length = g.clip->media->get_length_in_frames(panel_timeline->sequence->frame_rate); + } + } + for (int i=0;iselections.size();i++) { + Selection& s = panel_timeline->selections[i]; + s.old_in = s.in; + s.old_out = s.out; + s.old_track = s.track; + } +} + +void TimelineWidget::update_ghosts(QPoint& mouse_pos) { + int mouse_track = getTrackFromScreenPoint(mouse_pos.y()); + long frame_diff = getFrameFromScreenPoint(mouse_pos.x(), false) - panel_timeline->drag_frame_start; + int track_diff = mouse_track - panel_timeline->drag_track_start; + + long validator; + if (panel_timeline->trim_target > -1) { + // trim ops + + // validate ghosts + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + + if (panel_timeline->trim_in) { + // prevent clip length from being less than 1 frame long + validator = g.ghost_length - frame_diff; + if (validator < 1) frame_diff -= (1 - validator); + + // prevent timeline in from going below 0 + validator = g.old_in + frame_diff; + if (validator < 0) frame_diff -= validator; + + if (!g.clip->media_stream->infinite_length) { + // prevent clip_in from going below 0 + validator = g.old_clip_in + frame_diff; + if (validator < 0) frame_diff -= validator; + } + + // ripple ops + if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { + for (int j=0;jtimeline_in - frame_diff; + if (validator < 0) frame_diff += validator; + + // prevent any post-clips colliding with pre-clips + for (int k=0;ktrack == post->track) { + validator = post->timeline_in - frame_diff - pre->timeline_out; + if (validator < 0) frame_diff += validator; + } + } + } + } + } else { + // prevent clip length from being less than 1 frame long + validator = g.ghost_length + frame_diff; + if (validator < 1) frame_diff += (1 - validator); + + if (!g.clip->media_stream->infinite_length) { + // prevent clip length exceeding media length + validator = g.ghost_length + frame_diff; + if (validator > g.media_length) frame_diff -= validator - g.media_length; + } + + // ripple ops + if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { + for (int j=0;jtrack == post->track) { + validator = post->timeline_in + frame_diff - pre->timeline_out; + if (validator < 0) frame_diff -= validator; + } + } + } + } + } + } + + // resize ghosts + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + + if (panel_timeline->trim_in) { + g.in = g.old_in + frame_diff; + g.clip_in = g.old_clip_in + frame_diff; + } else { + g.out = g.old_out + frame_diff; + } + } + + // resize selections + for (int i=0;iselections.size();i++) { + Selection& s = panel_timeline->selections[i]; + + if (panel_timeline->trim_in) { + s.in = s.old_in + frame_diff; + } else { + s.out = s.old_out + frame_diff; + } + } + } else if (panel_timeline->tool == TIMELINE_TOOL_POINTER || panel_timeline->importing) { // only move clips on pointer (not ripple or rolling) + // validate ghosts + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + + // prevent clips from moving below 0 on the timeline + validator = g.old_in + frame_diff; + if (validator < 0) frame_diff -= validator; + + // prevent clips from crossing tracks + if (same_sign(g.old_track, panel_timeline->drag_track_start)) { + while (!same_sign(g.old_track, g.old_track + track_diff)) { + if (g.old_track < 0) { + track_diff--; + } else { + track_diff++; + } + } + } + } + + // move ghosts + for (int i=0;ighosts.size();i++) { + Ghost& g = panel_timeline->ghosts[i]; + g.in = g.old_in + frame_diff; + g.out = g.old_out + frame_diff; + + g.track = g.old_track; + + if (panel_timeline->importing) { + int abs_track_diff = abs(track_diff); + if (g.old_track < 0) { // clip is video + g.track -= abs_track_diff; + } else { // clip is audio + g.track += abs_track_diff; + } + } else { + if (same_sign(g.old_track, panel_timeline->drag_track_start)) g.track += track_diff; + } + } + + // move selections + if (!panel_timeline->importing) { + for (int i=0;iselections.size();i++) { + Selection& s = panel_timeline->selections[i]; + s.in = s.old_in + frame_diff; + s.out = s.old_out + frame_diff; + s.track = s.old_track; + if (panel_timeline->importing) { + int abs_track_diff = abs(track_diff); + if (s.old_track < 0) { + s.track -= abs_track_diff; + } else { + s.track += abs_track_diff; + } + } else { + if (same_sign(s.track, panel_timeline->drag_track_start)) s.track += track_diff; + } + } + } + } +} + +void TimelineWidget::mouseMoveEvent(QMouseEvent *event) { + if (panel_timeline->selecting) { + QPoint pos = event->pos(); + + int current_selection_track = getTrackFromScreenPoint(pos.y()); + long current_selection_frame = getFrameFromScreenPoint(pos.x(), false);; + int selection_count = 1 + qMax(current_selection_track, panel_timeline->drag_track_start) - qMin(current_selection_track, panel_timeline->drag_track_start); + if (panel_timeline->selections.size() != selection_count) { + panel_timeline->selections.resize(selection_count); + } + int minimum_selection_track = qMin(current_selection_track, panel_timeline->drag_track_start); + for (int i=0;iselections[i]; + s->track = minimum_selection_track + i; + long in = panel_timeline->drag_frame_start; + long out = current_selection_frame; + s->in = qMin(in, out); + s->out = qMax(in, out); + } + panel_timeline->playhead = qMin(panel_timeline->drag_frame_start, current_selection_frame); + panel_timeline->repaint_timeline(); + } else if (panel_timeline->moving_init) { + QPoint pos = event->pos(); + + if (panel_timeline->moving_proc) { + update_ghosts(pos); + } else { + // set up movement + // create ghosts + for (int i=0;isequence->clip_count();i++) { + if (panel_timeline->is_clip_selected(i)) { + Clip& c = panel_timeline->sequence->get_clip(i); + panel_timeline->ghosts.append({&c, c.timeline_in, c.timeline_out, c.track, c.clip_in}); + } + } + + // ripple edit prep + if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { + for (int i=0;ighosts.size();i++) { + // get clips before and after ripple point + for (int j=0;jsequence->clip_count();j++) { + // don't cache any currently selected clips + Clip* c = panel_timeline->ghosts.at(i).clip; + Clip& cc = panel_timeline->sequence->get_clip(j); + bool is_selected = false; + for (int k=0;kghosts.size();k++) { + if (panel_timeline->ghosts.at(k).clip == &cc) { + is_selected = true; + break; + } + } + + if (!is_selected) { + if (cc.timeline_in < c->timeline_in) { + // add clip to pre-cache UNLESS there is already a clip on that track closer to the ripple point + bool found = false; + for (int k=0;ktrack == cc.track) { + if (ccc->timeline_in < cc.timeline_in) { + // clip is closer to ripple point than the one in cache, replace it + ccc = &cc; + } + found = true; + } + } + if (!found) { + // no clip from that track in the cache, add it + pre_clips.append(&cc); + } + } else { + // add clip to post-cache UNLESS there is already a clip on that track closer to the ripple point + bool found = false; + for (int k=0;ktrack == cc.track) { + if (ccc->timeline_in > cc.timeline_in) { + // clip is closer to ripple point than the one in cache, replace it + ccc = &cc; + } + found = true; + } + } + if (!found) { + // no clip from that track in the cache, add it + post_clips.append(&cc); + } + } + } + } + } + + // debug code - print the information we got + qDebug() << "found" << pre_clips.size() << "preceding clips and" << post_clips.size() << "following"; + } + + init_ghosts(); + + panel_timeline->moving_proc = true; + } + panel_timeline->repaint_timeline(); + } else if (panel_timeline->splitting) { + QPoint pos = event->pos(); + + int track = getTrackFromScreenPoint(pos.y()); + bool repaint = false; + for (int i=0;isequence->clip_count();i++) { + if (panel_timeline->sequence->get_clip(i).track == track) { + panel_timeline->sequence->split_clip(i, panel_timeline->drag_frame_start); + repaint = true; + } + } + + // redraw clips since we changed them + if (repaint) panel_timeline->redraw_all_clips(); + } else if (panel_timeline->tool == TIMELINE_TOOL_POINTER || panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { + QPoint pos = event->pos(); + + int lim = 5; + int mouse_track = getTrackFromScreenPoint(pos.y()); + long mouse_frame_lower = getFrameFromScreenPoint(pos.x()-lim, false)-1; + long mouse_frame_upper = getFrameFromScreenPoint(pos.x()+lim, false)+1; + bool found = false; + for (int i=0;isequence->clip_count();i++) { + Clip& c = panel_timeline->sequence->get_clip(i); + if (c.track == mouse_track) { + if (c.timeline_in > mouse_frame_lower && c.timeline_in < mouse_frame_upper) { + panel_timeline->trim_target = i; + panel_timeline->trim_in = true; + found = true; + break; + } else if (c.timeline_out > mouse_frame_lower && c.timeline_out < mouse_frame_upper) { + panel_timeline->trim_target = i; + panel_timeline->trim_in = false; + found = true; + break; + } + } + } + if (found) { + setCursor(Qt::SizeHorCursor); + } else { + unsetCursor(); + panel_timeline->trim_target = -1; + } + } else if (panel_timeline->tool == TIMELINE_TOOL_EDIT || panel_timeline->tool == TIMELINE_TOOL_RAZOR) { + // redraw because we have a cursor + panel_timeline->repaint_timeline(); + } +} + +int color_brightness(int r, int g, int b) { + return (0.2126*r + 0.7152*g + 0.0722*b); +} + +void TimelineWidget::redraw_clips() { + // Draw clips + if (clip_pixmap != NULL) delete clip_pixmap; + + int panel_width = getScreenPointFromFrame(panel_timeline->sequence->getEndFrame()) + 100; + setMinimumWidth(panel_width); + + clip_pixmap = new QPixmap(panel_width, height()); + clip_pixmap->fill(Qt::transparent); + QPainter clip_painter(clip_pixmap); + int video_track_limit = 0; + int audio_track_limit = 0; + for (int i=0;isequence->clip_count();i++) { + Clip& clip = panel_timeline->sequence->get_clip(i); + if (is_track_visible(clip.track)) { + if (clip.track < 0 && clip.track < video_track_limit) { // video clip + video_track_limit = clip.track; + } else if (clip.track > audio_track_limit) { + audio_track_limit = clip.track; + } + + QRect clip_rect(getScreenPointFromFrame(clip.timeline_in), getScreenPointFromTrack(clip.track), clip.getLength() * panel_timeline->zoom, track_height); + clip_painter.fillRect(clip_rect, QColor(clip.color_r, clip.color_g, clip.color_b)); + clip_painter.setPen(Qt::white); + clip_painter.drawLine(clip_rect.bottomLeft(), clip_rect.topLeft()); + clip_painter.drawLine(clip_rect.topLeft(), clip_rect.topRight()); + clip_painter.setPen(QColor(0, 0, 0, 128)); + clip_painter.drawLine(clip_rect.bottomLeft(), clip_rect.bottomRight()); + clip_painter.drawLine(clip_rect.bottomRight(), clip_rect.topRight()); + + if (color_brightness(clip.color_r, clip.color_g, clip.color_b) > 160) { + clip_painter.setPen(Qt::black); + } else { + clip_painter.setPen(Qt::white); + } + QRect text_rect(clip_rect.left() + CLIP_TEXT_PADDING, clip_rect.top() + CLIP_TEXT_PADDING, clip_rect.width() - CLIP_TEXT_PADDING - CLIP_TEXT_PADDING, clip_rect.height() - CLIP_TEXT_PADDING - CLIP_TEXT_PADDING); + clip_painter.drawText(text_rect, 0, clip.name, &text_rect); + } + } + + // Draw track lines + if (show_track_lines) { + clip_painter.setPen(QColor(0, 0, 0, 96)); + audio_track_limit++; + if (video_track_limit == 0) video_track_limit--; + + if (bottom_align) { + // only draw lines for video tracks + for (int i=video_track_limit;i<0;i++) { + int line_y = getScreenPointFromTrack(i) - 1; + clip_painter.drawLine(0, line_y, rect().width(), line_y); + } + } else { + // only draw lines for audio tracks + for (int i=0;isequence != NULL) { + QPainter p(this); + + if (clip_pixmap != NULL) p.drawPixmap(0, 0, minimumWidth(), height(), *clip_pixmap); + + // Draw selections + for (int i=0;iselections.size();i++) { + const Selection& s = panel_timeline->selections.at(i); + if (is_track_visible(s.track)) { + int selection_y = getScreenPointFromTrack(s.track); + int selection_x = getScreenPointFromFrame(s.in); + p.fillRect(selection_x, selection_y, getScreenPointFromFrame(s.out) - selection_x, track_height, QColor(0, 0, 0, 64)); + } + } + + // Draw ghosts + for (int i=0;ighosts.size();i++) { + const Ghost& g = panel_timeline->ghosts.at(i); + if (is_track_visible(g.track)) { + int ghost_x = getScreenPointFromFrame(g.in); + int ghost_y = getScreenPointFromTrack(g.track); + int ghost_width = getScreenPointFromFrame(g.out - g.in) - 1; + int ghost_height = track_height - 1; + p.setPen(QColor(255, 255, 0)); + for (int j=0;jtool == TIMELINE_TOOL_EDIT || panel_timeline->tool == TIMELINE_TOOL_RAZOR) { + QPoint mouse_pos = mapFromGlobal(QCursor::pos()); + int track = getTrackFromScreenPoint(mouse_pos.y()); + if (is_track_visible(track)) { + int cursor_x = getScreenPointFromFrame(getFrameFromScreenPoint(mouse_pos.x(), false)); + int cursor_y = getScreenPointFromTrack(track); + + p.setPen(Qt::gray); + p.drawLine(cursor_x, cursor_y, cursor_x, cursor_y + track_height); + } + } + + // Draw playhead + p.setPen(Qt::red); + int playhead_x = getScreenPointFromFrame(panel_timeline->playhead); + p.drawLine(playhead_x, rect().top(), playhead_x, rect().bottom()); + + p.setPen(QColor(0, 0, 0, 64)); + int edge_y = (bottom_align) ? rect().height()-1 : 0; + p.drawLine(0, edge_y, rect().width(), edge_y); + } +} + +bool TimelineWidget::is_track_visible(int track) { + return ((bottom_align && track < 0) || (!bottom_align && track >= 0)); +} + +// ************************************** +// screen point <-> frame/track functions +// ************************************** + +long TimelineWidget::getFrameFromScreenPoint(int x, bool f) { + float div = (float) x / panel_timeline->zoom; + if (div < 0) { + return 0; + } + if (f) { + return floor(div); + } else { + return round(div); + } +} + +int TimelineWidget::getTrackFromScreenPoint(int y) { + if (bottom_align) { + y -= rect().bottom(); + if (show_track_lines) y -= 1; + } else { + y -= 1; + } + int temp_track_height = track_height; + if (show_track_lines) temp_track_height--; + return (int)floor((float) (y)/ (float) temp_track_height); +} + +int TimelineWidget::getScreenPointFromFrame(long frame) { + return (int) round(frame*panel_timeline->zoom); +} + +int TimelineWidget::getScreenPointFromTrack(int track) { + int temp_track_height = track_height; + if (show_track_lines) temp_track_height++; + int y = track * temp_track_height; + + if (bottom_align) { // video track + y += rect().bottom(); + if (show_track_lines) y += 1; + } else { // audio track + y += 1; + } + return y; +} + +int TimelineWidget::getClipIndexFromCoords(long frame, int track) { + for (int i=0;isequence->clip_count();i++) { + Clip& c = panel_timeline->sequence->get_clip(i); + if (c.track == track) { + if (frame >= c.timeline_in && frame < c.timeline_out) { + return i; + } + } + } + return -1; +} diff --git a/ui/timelinewidget.h b/ui/timelinewidget.h new file mode 100644 index 000000000..fb14a3bb6 --- /dev/null +++ b/ui/timelinewidget.h @@ -0,0 +1,56 @@ +#ifndef TIMELINEWIDGET_H +#define TIMELINEWIDGET_H + +#include +#include "timeline-tools.h" + +#define GHOST_THICKNESS 2 // thiccccc +#define CLIP_TEXT_PADDING 3 + +struct Sequence; +struct Clip; +class Timeline; + +class TimelineWidget : public QWidget +{ + Q_OBJECT +public: + explicit TimelineWidget(QWidget *parent = nullptr); + + bool bottom_align; + + void redraw_clips(); +protected: + void paintEvent(QPaintEvent*) override; + + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + + void dragEnterEvent(QDragEnterEvent *event); + void dragLeaveEvent(QDragLeaveEvent *event); + void dropEvent(QDropEvent* event); + void dragMoveEvent(QDragMoveEvent *event); +private: + void init_ghosts(); + void update_ghosts(QPoint& mouse_pos); + bool is_track_visible(int track); + long getFrameFromScreenPoint(int x, bool floor); + int getTrackFromScreenPoint(int y); + int getScreenPointFromFrame(long frame); + int getScreenPointFromTrack(int track); + int getClipIndexFromCoords(long frame, int track); + int track_height; + + QList pre_clips; + QList post_clips; + + QPixmap* clip_pixmap; +// QPixmap selection_pixmap; + +signals: + +public slots: +}; + +#endif // TIMELINEWIDGET_H diff --git a/ui/viewercontainer.cpp b/ui/viewercontainer.cpp new file mode 100644 index 000000000..57e085001 --- /dev/null +++ b/ui/viewercontainer.cpp @@ -0,0 +1,40 @@ +#include "viewercontainer.h" + +#include +#include + +// enforces aspect ratio +ViewerContainer::ViewerContainer(QWidget *parent) : QWidget(parent) +{ + child = NULL; + aspect_ratio = 1; +} + +void ViewerContainer::adjust() { + if (child != NULL) { + QSize widget_size = size(); + int widget_x = 0; + int widget_y = 0; + int widget_width = widget_size.width(); + int widget_height = widget_size.height(); + float widget_ar = (float) widget_width /(float) widget_height; + + bool widget_is_larger_than_sequence = widget_ar > aspect_ratio; + + if (widget_is_larger_than_sequence) { + widget_width = widget_height * aspect_ratio; + widget_x = (widget_size.width() / 2) - (widget_width / 2); + } else { + widget_height = widget_width / aspect_ratio; + widget_y = (widget_size.height() / 2) - (widget_height / 2); + } + + child->move(widget_x, widget_y); + child->resize(widget_width, widget_height); + } +} + +void ViewerContainer::resizeEvent(QResizeEvent *event) { + event->accept(); + adjust(); +} diff --git a/ui/viewercontainer.h b/ui/viewercontainer.h new file mode 100644 index 000000000..136d3c479 --- /dev/null +++ b/ui/viewercontainer.h @@ -0,0 +1,23 @@ +#ifndef VIEWERCONTAINER_H +#define VIEWERCONTAINER_H + +#include + +class ViewerContainer : public QWidget +{ + Q_OBJECT +public: + explicit ViewerContainer(QWidget *parent = nullptr); + float aspect_ratio; + QWidget* child; + void adjust(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +signals: + +public slots: +}; + +#endif // VIEWERCONTAINER_H diff --git a/ui/viewerwidget.cpp b/ui/viewerwidget.cpp new file mode 100644 index 000000000..5ea1f6d87 --- /dev/null +++ b/ui/viewerwidget.cpp @@ -0,0 +1,157 @@ +#include "viewerwidget.h" + +#include "panels/panels.h" +#include "panels/viewer.h" +#include "panels/timeline.h" +#include "project/sequence.h" +#include "project/effect.h" +#include "playback/playback.h" +#include "playback/audio.h" +#include "io/media.h" + +#include + +extern "C" { + #include +} + +ViewerWidget::ViewerWidget(QWidget *parent) : QOpenGLWidget(parent) +{ + multithreaded = true; + + QSurfaceFormat format; + format.setDepthBufferSize(24); + setFormat(format); + + // error handler - retries after 200ms if we couldn't get the entire image + retry_timer.setInterval(200); + connect(&retry_timer, SIGNAL(timeout()), this, SLOT(retry())); +} + +void ViewerWidget::retry() { + update(); +} + +void ViewerWidget::initializeGL() { + initializeOpenGLFunctions(); + + glClearColor(0, 0, 0, 1); + glMatrixMode(GL_PROJECTION); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +//void ViewerWidget::resizeGL(int w, int h) +//{ +//} + +int audio_bytes_written = 0; + +void ViewerWidget::paintGL() +{ + if (multithreaded) retry_timer.stop(); + + glClear(GL_COLOR_BUFFER_BIT); + + long playhead = panel_timeline->playhead; + + handle_media(panel_viewer->sequence, playhead, multithreaded); + texture_failed = false; + + bool render_audio = (panel_timeline->playing); + + if (render_audio && switch_audio_cache) { + reading_audio_cache_A = !reading_audio_cache_A; + audio_bytes_written = 0; + clear_cache(!reading_audio_cache_A, reading_audio_cache_A); + } + + cc_lock.lock(); + for (int i=0;iopen) { + qDebug() << "[WARNING] Tried to display clip" << i << "but it's closed"; + texture_failed = true; + } else if (is_clip_active(c, playhead)) { + if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + // start preparing cache + get_clip_frame(c, playhead); + + if (c->texture == NULL) { + qDebug() << "[WARNING] Texture hasn't been created yet"; + texture_failed = true; + } else if (playhead >= c->timeline_in) { + glLoadIdentity(); + int half_width = c->sequence->width/2; + int half_height = c->sequence->height/2; + glOrtho(-half_width, half_width, half_height,- half_height, -1, 1); + int anchor_x = 0; + int anchor_y = 0; + + // perform all transform effects + for (unsigned int j=0;jeffects.size();j++) { + c->effects.at(j)->process_gl(&anchor_x, &anchor_y); + } + + int anchor_right = c->media_stream->video_width - anchor_x; + int anchor_bottom = c->media_stream->video_height - anchor_y; + + c->texture->bind(); + + glBegin(GL_QUADS); + glTexCoord2f(0.0, 0.0); + glVertex2f(-anchor_x, -anchor_y); + glTexCoord2f(1.0, 0.0); + glVertex2f(anchor_right, -anchor_y); + glTexCoord2f(1.0, 1.0); + glVertex2f(anchor_right, anchor_bottom); + glTexCoord2f(0.0, 1.0); + glVertex2f(-anchor_x, anchor_bottom); + glEnd(); + + c->texture->release(); + } + } else if (c->stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && + playhead >= c->timeline_in && + render_audio && + !c->reached_end && + (switch_audio_cache || c->reset_audio)) { + // TODO doesn't new clips appearing in the timeline (they'll ALWAYS end up in the other cache with a 1/8 sec delay) + + // cache audio + if (c->lock.tryLock()) { + // clip is not caching, start cache + c->lock.unlock(); + cache_clip(c, playhead, !reading_audio_cache_A, reading_audio_cache_A, c->reset_audio); + } + } + } + } + + if (render_audio) { + switch_audio_cache = false; + + if (audio_cache_A != NULL && audio_bytes_written < audio_cache_size) { + // send cached/buffered audio to QIODevice/QAudioOutput + uint8_t* cache = reading_audio_cache_A ? audio_cache_A : audio_cache_B; + audio_bytes_written += audio_io_device->write((const char*) cache+audio_bytes_written, audio_cache_size-audio_bytes_written); + } + + if (audio_bytes_written == audio_cache_size) { + // switch_cache + switch_audio_cache = true; + } + } + + cc_lock.unlock(); + + if (texture_failed) { + if (multithreaded) { + retry_timer.start(); + } else { + paintGL(); + } + } +} diff --git a/ui/viewerwidget.h b/ui/viewerwidget.h new file mode 100644 index 000000000..33000d84c --- /dev/null +++ b/ui/viewerwidget.h @@ -0,0 +1,28 @@ +#ifndef VIEWERWIDGET_H +#define VIEWERWIDGET_H + +#include +#include +#include +#include +#include + +class Viewer; + +class ViewerWidget : public QOpenGLWidget, public QOpenGLFunctions +{ + Q_OBJECT +public: + ViewerWidget(QWidget *parent = 0); + void initializeGL(); +// void resizeGL(int w, int h); + void paintGL(); + + bool multithreaded; +private: + QTimer retry_timer; +private slots: + void retry(); +}; + +#endif // VIEWERWIDGET_H