commit 244c09259189d0ba04a1f1fa22e56648b52c8161 Author: itsmattkc Date: Wed May 30 03:24:12 2018 +1000 First commit of brand new rewrite 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 000000000..1016612d6 Binary files /dev/null and b/icons/arrow.png differ diff --git a/icons/audiosource.png b/icons/audiosource.png new file mode 100644 index 000000000..54ec5b0cf Binary files /dev/null and b/icons/audiosource.png differ diff --git a/icons/beam.png b/icons/beam.png new file mode 100644 index 000000000..c056807fb Binary files /dev/null and b/icons/beam.png differ diff --git a/icons/ff.png b/icons/ff.png new file mode 100644 index 000000000..f7c7b3a01 Binary files /dev/null and b/icons/ff.png differ diff --git a/icons/full-icon.png b/icons/full-icon.png new file mode 100644 index 000000000..cbfa4f3b4 Binary files /dev/null and b/icons/full-icon.png differ diff --git a/icons/icons.qrc b/icons/icons.qrc new file mode 100644 index 000000000..009e0d108 --- /dev/null +++ b/icons/icons.qrc @@ -0,0 +1,19 @@ + + + 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 000000000..a36b72c7d Binary files /dev/null and b/icons/next.png differ diff --git a/icons/olive.icns b/icons/olive.icns new file mode 100644 index 000000000..b31d83455 Binary files /dev/null and b/icons/olive.icns differ diff --git a/icons/olive.ico b/icons/olive.ico new file mode 100644 index 000000000..8ab5b94b7 Binary files /dev/null and b/icons/olive.ico differ diff --git a/icons/pause.png b/icons/pause.png new file mode 100644 index 000000000..2f5dd9725 Binary files /dev/null and b/icons/pause.png differ diff --git a/icons/play.png b/icons/play.png new file mode 100644 index 000000000..8e059c5d6 Binary files /dev/null and b/icons/play.png differ diff --git a/icons/prev.png b/icons/prev.png new file mode 100644 index 000000000..25518f45d Binary files /dev/null and b/icons/prev.png differ diff --git a/icons/razor.png b/icons/razor.png new file mode 100644 index 000000000..3a53fa62d Binary files /dev/null and b/icons/razor.png differ diff --git a/icons/rew.png b/icons/rew.png new file mode 100644 index 000000000..ab84058f9 Binary files /dev/null and b/icons/rew.png differ diff --git a/icons/ripple.png b/icons/ripple.png new file mode 100644 index 000000000..1a13de403 Binary files /dev/null and b/icons/ripple.png differ diff --git a/icons/rolling.png b/icons/rolling.png new file mode 100644 index 000000000..ace97caad Binary files /dev/null and b/icons/rolling.png differ diff --git a/icons/slip.png b/icons/slip.png new file mode 100644 index 000000000..32d0b81ba Binary files /dev/null and b/icons/slip.png differ diff --git a/icons/videosource.png b/icons/videosource.png new file mode 100644 index 000000000..784f8c361 Binary files /dev/null and b/icons/videosource.png differ diff --git a/icons/win.rc b/icons/win.rc new file mode 100644 index 000000000..33edf5ce5 --- /dev/null +++ b/icons/win.rc @@ -0,0 +1 @@ +olicon ICON "olive.ico" \ No newline at end of file diff --git a/io/config.cpp b/io/config.cpp new file mode 100644 index 000000000..d751e595e --- /dev/null +++ b/io/config.cpp @@ -0,0 +1,49 @@ +#include "config.h" + +#ifdef _WIN32 +#include +#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