diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 201b78ce6..9794b77c5 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -26,6 +26,7 @@ #include +#include "audio/packedprocessor.h" #include "config/config.h" namespace olive { @@ -68,6 +69,19 @@ int OutputCallback(const void *input, void *output, unsigned long frameCount, co return paContinue; } +int InputCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) +{ + FFmpegEncoder *f = static_cast(userData); + + SampleBufferPtr s = SampleBuffer::Create(); + s->set_sample_count(frameCount); + s->set_audio_params(f->params().audio_params()); + + f->WriteAudioData(f->params().audio_params(), false, reinterpret_cast(&input), frameCount); + + return paContinue; +} + void AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &samples) { if (output_device_ == paNoDevice) { @@ -79,13 +93,7 @@ void AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &sam CloseOutputStream(); - PaStreamParameters p; - - p.channelCount = output_params_.channel_count(); - p.device = output_device_; - p.hostApiSpecificStreamInfo = nullptr; - p.sampleFormat = GetPortAudioSampleFormat(output_params_.format()); - p.suggestedLatency = Pa_GetDeviceInfo(output_device_)->defaultLowOutputLatency; + PaStreamParameters p = GetPortAudioParams(params, output_device_); Pa_OpenStream(&output_stream_, nullptr, &p, output_params_.sample_rate(), paFramesPerBufferUnspecified, paNoFlag, OutputCallback, output_buffer_); @@ -176,6 +184,52 @@ void AudioManager::HardReset() Pa_Initialize(); } +bool AudioManager::StartRecording(const QString &filename, const AudioParams ¶ms) +{ + if (input_device_ == paNoDevice) { + return false; + } + + EncodingParams encode_param; + encode_param.EnableAudio(params, ExportCodec::kCodecMP3); + encode_param.SetFilename(filename); + + input_encoder_ = new FFmpegEncoder(encode_param); + if (!input_encoder_->Open()) { + qCritical() << "Failed to open encoder for recording"; + return false; + } + + PaStreamParameters p = GetPortAudioParams(params, input_device_); + + if (Pa_OpenStream(&input_stream_, &p, nullptr, params.sample_rate(), paFramesPerBufferUnspecified, paNoFlag, InputCallback, input_encoder_) == paNoError) { + if (Pa_StartStream(input_stream_) == paNoError) { + return true; + } + } + + StopRecording(); + return false; +} + +void AudioManager::StopRecording() +{ + if (input_stream_) { + if (Pa_IsStreamActive(input_stream_)) { + Pa_StopStream(input_stream_); + } + Pa_CloseStream(input_stream_); + + input_stream_ = nullptr; + } + + if (input_encoder_) { + input_encoder_->Close(); + delete input_encoder_; + input_encoder_ = nullptr; + } +} + PaDeviceIndex AudioManager::FindConfigDeviceByName(bool is_output_device) { QString entry = is_output_device ? QStringLiteral("AudioOutput") : QStringLiteral("AudioInput"); @@ -199,8 +253,23 @@ PaDeviceIndex AudioManager::FindDeviceByName(const QString &s, bool is_output_de return is_output_device ? Pa_GetDefaultOutputDevice() : Pa_GetDefaultInputDevice(); } +PaStreamParameters AudioManager::GetPortAudioParams(const AudioParams ¶ms, PaDeviceIndex device) +{ + PaStreamParameters p; + + p.channelCount = params.channel_count(); + p.device = device; + p.hostApiSpecificStreamInfo = nullptr; + p.sampleFormat = GetPortAudioSampleFormat(params.format()); + p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency; + + return p; +} + AudioManager::AudioManager() : - output_stream_(nullptr) + output_stream_(nullptr), + input_stream_(nullptr), + input_encoder_(nullptr) { #ifdef PA_HAS_JACK // PortAudio doesn't do a strcpy, so we need a const char that's readily accessible (i.e. not diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 0d0329bd6..62fd0e920 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -28,6 +28,7 @@ #include "audiovisualwaveform.h" #include "common/define.h" +#include "codec/ffmpeg/ffmpegencoder.h" #include "render/audioparams.h" #include "render/audioplaybackcache.h" #include "render/previewaudiodevice.h" @@ -73,9 +74,15 @@ public: void HardReset(); + bool StartRecording(const QString &filename, const AudioParams ¶ms); + + void StopRecording(); + static PaDeviceIndex FindConfigDeviceByName(bool is_output_device); static PaDeviceIndex FindDeviceByName(const QString &s, bool is_output_device); + static PaStreamParameters GetPortAudioParams(const AudioParams &p, PaDeviceIndex device); + signals: void OutputNotify(); @@ -96,6 +103,8 @@ private: PreviewAudioDevice *output_buffer_; PaDeviceIndex input_device_; + PaStream *input_stream_; + FFmpegEncoder *input_encoder_; }; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index c1ea0927a..9b38505b6 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -236,11 +236,6 @@ fail: bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) { - if (!InitializeResampleContext(audio)) { - qCritical() << "Failed to initialize resample context"; - return false; - } - bool result = true; // Create input buffer @@ -258,6 +253,25 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) } } + result = WriteAudioData(audio->audio_params(), true, const_cast(input_data), input_sample_count); + + if (input_data) { + av_freep(&input_data[0]); + av_freep(&input_data); + } + + return result; +} + +bool FFmpegEncoder::WriteAudioData(const AudioParams &audio_params, bool planar, const uint8_t **input_data, int input_sample_count) +{ + if (!InitializeResampleContext(audio_params, planar)) { + qCritical() << "Failed to initialize resample context"; + return false; + } + + bool result = true; + // Create output buffer int output_sample_count = input_sample_count ? swr_get_out_samples(audio_resample_ctx_, input_sample_count) : 102400; uint8_t** output_data = nullptr; @@ -308,11 +322,6 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) av_freep(&output_data); } - if (input_data) { - av_freep(&input_data[0]); - av_freep(&input_data); - } - return result; } @@ -774,7 +783,7 @@ void FFmpegEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream* stream) av_packet_free(&pkt); } -bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) +bool FFmpegEncoder::InitializeResampleContext(const AudioParams &audio, bool planar) { if (audio_resample_ctx_) { return true; @@ -785,9 +794,9 @@ bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) static_cast(audio_codec_ctx_->channel_layout), audio_codec_ctx_->sample_fmt, audio_codec_ctx_->sample_rate, - static_cast(audio->audio_params().channel_layout()), - FFmpegUtils::GetFFmpegSampleFormat(audio->audio_params().format(), true), - audio->audio_params().sample_rate(), + static_cast(audio.channel_layout()), + FFmpegUtils::GetFFmpegSampleFormat(audio.format(), planar), + audio.sample_rate(), 0, nullptr); if (!audio_resample_ctx_) { diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index d75d3f4ea..82e7040da 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -47,6 +47,8 @@ public: virtual bool WriteAudio(olive::SampleBufferPtr audio) override; + bool WriteAudioData(const AudioParams &audio_params, bool planar, const uint8_t **data, int input_sample_count); + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; virtual void Close() override; @@ -76,7 +78,7 @@ private: void FlushEncoders(); void FlushCodecCtx(AVCodecContext* codec_ctx, AVStream *stream); - bool InitializeResampleContext(SampleBufferPtr audio); + bool InitializeResampleContext(const AudioParams &audio, bool planar); static const AVCodec *GetEncoder(ExportCodec::Codec c); diff --git a/app/core.cpp b/app/core.cpp index d4e091959..c86f1a9e5 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -245,14 +245,14 @@ UndoStack *Core::undo_stack() return &undo_stack_; } -void Core::ImportFiles(const QStringList &urls, ProjectViewModel* model, Folder* parent) +void Core::ImportFiles(const QStringList &urls, Folder* parent) { if (urls.isEmpty()) { QMessageBox::critical(main_window_, tr("Import error"), tr("Nothing to import")); return; } - ProjectImportTask* pim = new ProjectImportTask(model, parent, urls); + ProjectImportTask* pim = new ProjectImportTask(parent, urls); if (!pim->GetFileCount()) { // No files to import @@ -364,7 +364,7 @@ void Core::DialogImportShow() // Get the selected folder in this panel Folder* folder = active_project_panel->GetSelectedFolder(); - ImportFiles(files, active_project_panel->model(), folder); + ImportFiles(files, folder); } } diff --git a/app/core.h b/app/core.h index 653769af6..759cdd522 100644 --- a/app/core.h +++ b/app/core.h @@ -182,7 +182,7 @@ public: * * @param urls */ - void ImportFiles(const QStringList& urls, ProjectViewModel *model, Folder *parent); + void ImportFiles(const QStringList& urls, Folder *parent); /** * @brief Get the currently active tool diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 845d4ab91..481feeed5 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -24,6 +24,8 @@ set(OLIVE_SOURCES dialog/export/exportadvancedvideodialog.h dialog/export/exportaudiotab.cpp dialog/export/exportaudiotab.h + dialog/export/exportformatcombobox.cpp + dialog/export/exportformatcombobox.h dialog/export/exportsubtitlestab.cpp dialog/export/exportsubtitlestab.h dialog/export/exportvideotab.cpp diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index c9aa7249c..ba5280897 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -118,7 +118,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : row++; preferences_layout->addWidget(new QLabel(tr("Format:")), row, 0); - format_combobox_ = new QComboBox(); + format_combobox_ = new ExportFormatComboBox(); preferences_layout->addWidget(format_combobox_, row, 1, 1, 3); row++; @@ -193,33 +193,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : // Set default filename SetDefaultFilename(); - // Populate combobox formats - for (int i=0; i(i)); - - bool inserted = false; - - for (int j=0; jcount(); j++) { - if (format_combobox_->itemText(j) > format_name) { - format_combobox_->insertItem(j, format_name, i); - inserted = true; - break; - } - } - - if (!inserted) { - format_combobox_->addItem(format_name, i); - } - } - // Set defaults previously_selected_format_ = ExportFormat::kFormatMPEG4; - SetCurrentFormat(ExportFormat::kFormatMPEG4); - connect(format_combobox_, - static_cast(&QComboBox::currentIndexChanged), - this, - &ExportDialog::FormatChanged); - FormatChanged(format_combobox_->currentIndex()); + format_combobox_->SetFormat(ExportFormat::kFormatMPEG4); + connect(format_combobox_, &ExportFormatComboBox::FormatChanged, this, &ExportDialog::FormatChanged); + FormatChanged(format_combobox_->GetFormat()); VideoParams vp = viewer_node_->GetVideoParams(); AudioParams ap = viewer_node_->GetAudioParams(); @@ -273,11 +251,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); } -ExportFormat::Format ExportDialog::GetSelectedFormat() const -{ - return static_cast(format_combobox_->currentData().toInt()); -} - rational ExportDialog::GetSelectedTimebase() const { return video_tab_->GetSelectedFrameRate().flipped(); @@ -293,7 +266,7 @@ void ExportDialog::StartExport() // Validate if the entered filename contains the correct extension (the extension is necessary // for both FFmpeg and OIIO to determine the output format) - QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(GetSelectedFormat())); + QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(format_combobox_->GetFormat())); QString proposed_filename = filename_edit_->text().trimmed(); // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. @@ -428,7 +401,7 @@ void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title void ExportDialog::BrowseFilename() { - ExportFormat::Format f = GetSelectedFormat(); + ExportFormat::Format f = format_combobox_->GetFormat(); QString browsed_fn = QFileDialog::getSaveFileName(this, "", @@ -444,11 +417,10 @@ void ExportDialog::BrowseFilename() } } -void ExportDialog::FormatChanged(int index) +void ExportDialog::FormatChanged(ExportFormat::Format current_format) { QString current_filename = filename_edit_->text().trimmed(); QString previously_selected_ext = ExportFormat::GetExtension(previously_selected_format_); - ExportFormat::Format current_format = static_cast(format_combobox_->itemData(index).toInt()); QString currently_selected_ext = ExportFormat::GetExtension(current_format); // If the previous extension was added, remove it @@ -546,7 +518,7 @@ ExportParams ExportDialog::GenerateParams() const AudioParams::kInternalFormat); ExportParams params; - params.set_encoder(Encoder::GetTypeFromFormat(GetSelectedFormat())); + params.set_encoder(Encoder::GetTypeFromFormat(format_combobox_->GetFormat())); params.SetFilename(filename_edit_->text().trimmed()); params.SetExportLength(viewer_node_->GetLength()); @@ -594,16 +566,6 @@ ExportParams ExportDialog::GenerateParams() const return params; } -void ExportDialog::SetCurrentFormat(ExportFormat::Format format) -{ - for (int i=0; icount(); i++) { - if (format_combobox_->itemData(i).toInt() == format) { - format_combobox_->setCurrentIndex(i); - break; - } - } -} - rational ExportDialog::GetExportLength() const { if (range_combobox_->currentIndex() == kRangeInToOut) { diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index ad8056fcc..d0938a4b1 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -29,6 +29,7 @@ #include "codec/exportcodec.h" #include "codec/exportformat.h" +#include "dialog/export/exportformatcombobox.h" #include "exportaudiotab.h" #include "exportsubtitlestab.h" #include "exportvideotab.h" @@ -43,8 +44,6 @@ class ExportDialog : public QDialog public: ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); - ExportFormat::Format GetSelectedFormat() const; - rational GetSelectedTimebase() const; protected: @@ -58,8 +57,6 @@ private: ExportParams GenerateParams() const; - void SetCurrentFormat(ExportFormat::Format format); - ViewerOutput* viewer_node_; ExportFormat::Format previously_selected_format_; @@ -82,7 +79,7 @@ private: ViewerWidget* preview_viewer_; QLineEdit* filename_edit_; - QComboBox* format_combobox_; + ExportFormatComboBox* format_combobox_; ExportVideoTab* video_tab_; ExportAudioTab* audio_tab_; @@ -98,7 +95,7 @@ private: private slots: void BrowseFilename(); - void FormatChanged(int index); + void FormatChanged(ExportFormat::Format current_format); void ResolutionChanged(); diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h index ef5155018..4e29e3d48 100644 --- a/app/dialog/export/exportaudiotab.h +++ b/app/dialog/export/exportaudiotab.h @@ -37,8 +37,6 @@ class ExportAudioTab : public QWidget public: ExportAudioTab(QWidget* parent = nullptr); - int SetFormat(ExportFormat::Format format); - QComboBox* codec_combobox() const { return codec_combobox_; @@ -59,6 +57,9 @@ public: return bit_rate_slider_; } +public slots: + int SetFormat(ExportFormat::Format format); + private: QComboBox* codec_combobox_; SampleRateComboBox* sample_rate_combobox_; diff --git a/app/dialog/export/exportformatcombobox.cpp b/app/dialog/export/exportformatcombobox.cpp new file mode 100644 index 000000000..9f1b1927f --- /dev/null +++ b/app/dialog/export/exportformatcombobox.cpp @@ -0,0 +1,83 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "exportformatcombobox.h" + +namespace olive { + +ExportFormatComboBox::ExportFormatComboBox(Mode mode, QWidget *parent) : + QComboBox(parent) +{ + // Populate combobox formats + for (int i=0; i(i); + + switch (mode) { + case kShowAllFormats: + break; + case kShowAudioOnly: + if (!ExportFormat::GetVideoCodecs(f).isEmpty()) { + continue; + } + break; + case kShowVideoOnly: + if (!ExportFormat::GetAudioCodecs(f).isEmpty()) { + continue; + } + break; + } + + QString format_name = ExportFormat::GetName(f); + + bool inserted = false; + + // Sort formats alphabetically + for (int j=0; j format_name) { + insertItem(j, format_name, i); + inserted = true; + break; + } + } + + if (!inserted) { + addItem(format_name, i); + } + } + + connect(this, static_cast(&QComboBox::currentIndexChanged), this, &ExportFormatComboBox::HandleIndexChange); +} + +void ExportFormatComboBox::SetFormat(ExportFormat::Format fmt) +{ + for (int i=0; i(itemData(index).toInt())); +} + +} diff --git a/app/dialog/export/exportformatcombobox.h b/app/dialog/export/exportformatcombobox.h new file mode 100644 index 000000000..3dd33e848 --- /dev/null +++ b/app/dialog/export/exportformatcombobox.h @@ -0,0 +1,63 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef EXPORTFORMATCOMBOBOX_H +#define EXPORTFORMATCOMBOBOX_H + +#include + +#include "codec/exportformat.h" + +namespace olive { + +class ExportFormatComboBox : public QComboBox +{ + Q_OBJECT +public: + enum Mode { + kShowAllFormats, + kShowAudioOnly, + kShowVideoOnly + }; + + ExportFormatComboBox(Mode mode, QWidget *parent = nullptr); + ExportFormatComboBox(QWidget *parent = nullptr) : + ExportFormatComboBox(kShowAllFormats, parent) + {} + + ExportFormat::Format GetFormat() const + { + return static_cast(currentData().toInt()); + } + +signals: + void FormatChanged(ExportFormat::Format fmt); + +public slots: + void SetFormat(ExportFormat::Format fmt); + +private slots: + void HandleIndexChange(int index); + +}; + +} + +#endif // EXPORTFORMATCOMBOBOX_H diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 8b16c350b..e8371cf9f 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -26,6 +26,8 @@ #include "audio/audiomanager.h" #include "config/config.h" +#include "dialog/export/exportaudiotab.h" +#include "dialog/export/exportformatcombobox.h" namespace olive { @@ -87,12 +89,24 @@ PreferencesAudioTab::PreferencesAudioTab() row++; - input_layout->addWidget(new QLabel(tr("Recording Mode:"), this), row, 0); + QGroupBox *recording_group = new QGroupBox(tr("Recording")); + input_layout->addWidget(recording_group, row, 0, 1, 2); - recording_combobox_ = new QComboBox(); - recording_combobox_->addItem(tr("Mono")); - recording_combobox_->addItem(tr("Stereo")); - input_layout->addWidget(recording_combobox_, row, 1); + QVBoxLayout *recording_layout = new QVBoxLayout(recording_group); + + QHBoxLayout *fmt_layout = new QHBoxLayout(); + recording_layout->addLayout(fmt_layout); + + fmt_layout->addWidget(new QLabel(tr("Format:"))); + + ExportFormatComboBox *fmt_combo = new ExportFormatComboBox(ExportFormatComboBox::kShowAudioOnly); + fmt_combo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + fmt_layout->addWidget(fmt_combo); + + ExportAudioTab *audio_recording_options = new ExportAudioTab(); + recording_layout->addWidget(audio_recording_options); + + connect(fmt_combo, &ExportFormatComboBox::FormatChanged, audio_recording_options, &ExportAudioTab::SetFormat); } QHBoxLayout* refresh_layout = new QHBoxLayout(); diff --git a/app/node/project/projectviewmodel.cpp b/app/node/project/projectviewmodel.cpp index 1e0c2c30b..e4af49620 100644 --- a/app/node/project/projectviewmodel.cpp +++ b/app/node/project/projectviewmodel.cpp @@ -405,7 +405,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action } // Trigger an import - Core::instance()->ImportFiles(urls, this, static_cast(drop_item)); + Core::instance()->ImportFiles(urls, static_cast(drop_item)); return true; } diff --git a/app/panel/sequenceviewer/sequenceviewer.cpp b/app/panel/sequenceviewer/sequenceviewer.cpp index 5cda8dda3..9a76f1a18 100644 --- a/app/panel/sequenceviewer/sequenceviewer.cpp +++ b/app/panel/sequenceviewer/sequenceviewer.cpp @@ -19,6 +19,7 @@ ***/ #include "sequenceviewer.h" +#include "panel/timeline/timeline.h" namespace olive { @@ -29,6 +30,12 @@ SequenceViewerPanel::SequenceViewerPanel(QWidget *parent) : Retranslate(); } +void SequenceViewerPanel::StartCapture(const TimeRange &time, const Track::Reference &track) +{ + TimelinePanel *tp = static_cast(sender()); + static_cast(GetTimeBasedWidget())->StartCapture(tp->timeline_widget(), time, track); +} + void SequenceViewerPanel::Retranslate() { ViewerPanel::Retranslate(); diff --git a/app/panel/sequenceviewer/sequenceviewer.h b/app/panel/sequenceviewer/sequenceviewer.h index 6f5f11e1e..486e762a8 100644 --- a/app/panel/sequenceviewer/sequenceviewer.h +++ b/app/panel/sequenceviewer/sequenceviewer.h @@ -31,6 +31,9 @@ class SequenceViewerPanel : public ViewerPanel public: SequenceViewerPanel(QWidget* parent); +public slots: + void StartCapture(const TimeRange &time, const Track::Reference &track); + protected: virtual void Retranslate() override; diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 461d94f16..e4fd4e274 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -34,6 +34,7 @@ TimelinePanel::TimelinePanel(QWidget *parent) : Retranslate(); connect(tw, &TimelineWidget::BlockSelectionChanged, this, &TimelinePanel::BlockSelectionChanged); + connect(tw, &TimelineWidget::RequestCaptureStart, this, &TimelinePanel::RequestCaptureStart ); } void TimelinePanel::SplitAtPlayhead() diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 360187b0a..9e40b232d 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -112,6 +112,8 @@ protected: signals: void BlockSelectionChanged(const QVector& selected_blocks); + void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); + }; } diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index 2e2823b15..352a8f175 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -30,9 +30,8 @@ namespace olive { -ProjectImportTask::ProjectImportTask(ProjectViewModel *model, Folder *folder, const QStringList &filenames) : +ProjectImportTask::ProjectImportTask(Folder *folder, const QStringList &filenames) : command_(nullptr), - model_(model), folder_(folder) { foreach (const QString& f, filenames) { @@ -117,6 +116,9 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte // Create undoable command that adds the items to the model AddItemToFolder(folder, footage, parent_command); + + // Add to vector + imported_footage_.push_back(footage); } else { // Add to list so we can tell the user about it later invalid_files_.append(file_info.absoluteFilePath()); @@ -220,7 +222,7 @@ void ProjectImportTask::ValidateImageSequence(Footage *footage, QFileInfoList& i void ProjectImportTask::AddItemToFolder(Folder *folder, Node *item, MultiUndoCommand *command) { // Create undoable command that adds the items to the model - Project* project = model_->project(); + Project* project = folder->project(); NodeAddCommand* nac = new NodeAddCommand(project, item); nac->PushToThread(project->thread()); diff --git a/app/task/project/import/import.h b/app/task/project/import/import.h index 79559b598..3f884478a 100644 --- a/app/task/project/import/import.h +++ b/app/task/project/import/import.h @@ -34,7 +34,7 @@ class ProjectImportTask : public Task { Q_OBJECT public: - ProjectImportTask(ProjectViewModel* model, Folder* folder, const QStringList& filenames); + ProjectImportTask(Folder* folder, const QStringList& filenames); const int& GetFileCount() const; @@ -53,6 +53,8 @@ public: return !invalid_files_.isEmpty(); } + const QVector &GetImportedFootage() const { return imported_footage_; } + protected: virtual bool Run() override; @@ -71,8 +73,6 @@ private: MultiUndoCommand* command_; - ProjectViewModel* model_; - Folder* folder_; QFileInfoList filenames_; @@ -83,6 +83,8 @@ private: QList image_sequence_ignore_files_; + QVector imported_footage_; + }; } diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 41f771a17..6a0c7ab6d 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -156,6 +156,10 @@ PlaybackControls::PlaybackControls(QWidget *parent) : SetAudioVideoDragButtonsVisible(false); connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &PlaybackControls::TimecodeChanged); + + play_blink_timer_ = new QTimer(this); + play_blink_timer_->setInterval(500); + connect(play_blink_timer_, &QTimer::timeout, this, &PlaybackControls::PlayBlink); } void PlaybackControls::SetTimecodeEnabled(bool enabled) @@ -231,10 +235,20 @@ void PlaybackControls::UpdateIcons() audio_drag_btn_->setIcon(icon::Audio); } +void PlaybackControls::SetButtonRecordingState(QPushButton *btn, bool on) +{ + btn->setStyleSheet(on ? QStringLiteral("background: red;") : QString()); +} + void PlaybackControls::TimecodeChanged() { // Update end time SetEndTime(end_time_); } +void PlaybackControls::PlayBlink() +{ + SetButtonRecordingState(play_btn_, play_btn_->styleSheet().isEmpty()); +} + } diff --git a/app/widget/playbackcontrols/playbackcontrols.h b/app/widget/playbackcontrols/playbackcontrols.h index b1d283f56..f467a1924 100644 --- a/app/widget/playbackcontrols/playbackcontrols.h +++ b/app/widget/playbackcontrols/playbackcontrols.h @@ -61,6 +61,23 @@ public slots: void ShowPlayButton(); + void StartPlayBlink() + { + play_blink_timer_->start(); + SetButtonRecordingState(play_btn_, true); + } + + void StopPlayBlink() + { + play_blink_timer_->stop(); + SetButtonRecordingState(play_btn_, false); + } + + void SetPauseButtonRecordingState(bool on) + { + SetButtonRecordingState(pause_btn_, on); + } + signals: /** * @brief Signal emitted when "Go to Start" is clicked @@ -108,6 +125,8 @@ protected: private: void UpdateIcons(); + static void SetButtonRecordingState(QPushButton *btn, bool on); + QWidget* lower_left_container_; QWidget* lower_right_container_; @@ -129,9 +148,13 @@ private: QStackedWidget* playpause_stack_; + QTimer *play_blink_timer_; + private slots: void TimecodeChanged(); + void PlayBlink(); + }; } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 8766cd2fb..58629b2c2 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -32,6 +32,7 @@ #include "dialog/speedduration/speeddurationdialog.h" #include "node/block/transition/transition.h" #include "node/project/serializer/serializer.h" +#include "task/project/import/import.h" #include "tool/add.h" #include "tool/beam.h" #include "tool/edit.h" @@ -788,6 +789,35 @@ void TimelineWidget::ShowSpeedDurationDialogForSelectedClips() } } +void TimelineWidget::RecordingCallback(const QString &filename, const TimeRange &time, const Track::Reference &track) +{ + ProjectImportTask task(GetConnectedNode()->project()->root(), {filename}); + task.Start(); + + MultiUndoCommand *import_command = task.GetCommand(); + Core::instance()->undo_stack()->pushIfHasChildren(import_command); + + if (task.GetImportedFootage().empty()) { + qCritical() << "Failed to import recorded audio file" << filename; + } else { + import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, track.index()); + } +} + +void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord) +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->EnableRecordingOverlay(coord); + } +} + +void TimelineWidget::DisableRecordingOverlay() +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->DisableRecordingOverlay(); + } +} + void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, MultiUndoCommand *command) { for (int i=0;i& selected_blocks); + void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); + protected: virtual void resizeEvent(QResizeEvent *event) override; diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index de84a94b5..fdcd49afc 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -173,7 +173,7 @@ void ImportTool::DragDrop(TimelineViewMouseEvent *event) } } -void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert, int track_offset) { DraggedFootageData refs; @@ -181,10 +181,10 @@ void ImportTool::PlaceAt(const QVector &footage, const rational refs.append({f, f->GetEnabledStreamsAsReferences()}); } - PlaceAt(refs, start, insert); + PlaceAt(refs, start, insert, track_offset); } -void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert, int track_offset) { dragged_footage_ = footage; @@ -192,7 +192,7 @@ void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &star return; } - PrepGhosts(start, 0); + PrepGhosts(start, track_offset); DropGhosts(insert); } diff --git a/app/widget/timelinewidget/tool/import.h b/app/widget/timelinewidget/tool/import.h index 70ad33676..965f5960d 100644 --- a/app/widget/timelinewidget/tool/import.h +++ b/app/widget/timelinewidget/tool/import.h @@ -37,8 +37,8 @@ public: using DraggedFootageData = QVector > >; - void PlaceAt(const QVector &footage, const rational& start, bool insert); - void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert); + void PlaceAt(const QVector &footage, const rational& start, bool insert, int track_offset = 0); + void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert, int track_offset = 0); enum DropWithoutSequenceBehavior { kDWSAsk, diff --git a/app/widget/timelinewidget/tool/record.cpp b/app/widget/timelinewidget/tool/record.cpp index 4a904fb14..c8a7fc001 100644 --- a/app/widget/timelinewidget/tool/record.cpp +++ b/app/widget/timelinewidget/tool/record.cpp @@ -1,11 +1,88 @@ #include "record.h" +#include "widget/timelinewidget/timelinewidget.h" + namespace olive { RecordTool::RecordTool(TimelineWidget *parent) : - BeamTool(parent) + BeamTool(parent), + ghost_(nullptr) { } +void RecordTool::MousePress(TimelineViewMouseEvent *event) +{ + const Track::Reference& track = event->GetTrack(); + + // Check if track is locked + Track* t = parent()->GetTrackFromReference(track); + if (t && t->IsLocked()) { + return; + } + + if (t->type() != Track::kAudio) { + // We only support audio tracks here + return; + } + + drag_start_point_ = ValidatedCoordinate(event->GetCoordinates(true)).GetFrame(); + + ghost_ = new TimelineViewGhostItem(); + ghost_->SetIn(drag_start_point_); + ghost_->SetOut(drag_start_point_); + ghost_->SetTrack(track); + parent()->AddGhost(ghost_); + + snap_points_.push_back(drag_start_point_); +} + +void RecordTool::MouseMove(TimelineViewMouseEvent *event) +{ + if (!ghost_) { + return; + } + + // Calculate movement + rational movement = event->GetFrame() - drag_start_point_; + + // Validation: Ensure in point never goes below 0 + if (movement < -ghost_->GetIn()) { + movement = -ghost_->GetIn(); + } + + // Snap movement + bool snapped; + + if (Core::instance()->snapping()) { + snapped = parent()->SnapPoint(snap_points_, &movement); + } else { + snapped = false; + } + + // Make adjustment + if (!movement) { + ghost_->SetInAdjustment(0); + ghost_->SetOutAdjustment(0); + } else if (movement > 0) { + ghost_->SetInAdjustment(0); + ghost_->SetOutAdjustment(movement); + } else if (movement < 0) { + ghost_->SetInAdjustment(movement); + ghost_->SetOutAdjustment(0); + } + + Q_UNUSED(snapped) +} + +void RecordTool::MouseRelease(TimelineViewMouseEvent *event) +{ + if (ghost_) { + emit parent()->RequestCaptureStart(TimeRange(ghost_->GetAdjustedIn(), ghost_->GetAdjustedOut()), ghost_->GetTrack()); + parent()->ClearGhosts(); + snap_points_.clear(); + ghost_ = nullptr; + } +} + } diff --git a/app/widget/timelinewidget/tool/record.h b/app/widget/timelinewidget/tool/record.h index 89c1a1738..4fbe7f85b 100644 --- a/app/widget/timelinewidget/tool/record.h +++ b/app/widget/timelinewidget/tool/record.h @@ -29,6 +29,18 @@ class RecordTool : public BeamTool { public: RecordTool(TimelineWidget* parent); + + virtual void MousePress(TimelineViewMouseEvent *event) override; + virtual void MouseMove(TimelineViewMouseEvent *event) override; + virtual void MouseRelease(TimelineViewMouseEvent *event) override; + +protected: + void MouseMoveInternal(const rational& cursor_frame, bool outwards); + + TimelineViewGhostItem* ghost_; + + rational drag_start_point_; + }; } diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index b1462efda..32f268c03 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -348,6 +348,16 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) track_y + GetTrackHeight(track_index)); } + // Draw recording overlay + if (recording_overlay_ && recording_coord_.GetTrack().type() == connected_track_list_->type()) { + painter->setPen(QPen(Qt::red, 2)); + painter->setBrush(QColor(255, 128, 128)); + + int x = TimeToScene(recording_coord_.GetFrame()); + painter->drawRect(x, GetTrackY(recording_coord_.GetTrack().index()), + TimeToScene(GetTime()) - x, GetTrackHeight(recording_coord_.GetTrack().index())); + } + // Draw standard TimelineViewBase things (such as playhead) super::drawForeground(painter, rect); } @@ -364,6 +374,7 @@ void TimelineView::ToolChangedEvent(Tool::Item tool) case Tool::kAdd: case Tool::kTransition: case Tool::kZoom: + case Tool::kRecord : setCursor(Qt::CrossCursor); break; case Tool::kTrackSelect: @@ -745,6 +756,19 @@ void TimelineView::SetTransitionOverlay(ClipBlock *out, ClipBlock *in) } } +void TimelineView::EnableRecordingOverlay(const TimelineCoordinate &coord) +{ + recording_overlay_ = true; + recording_coord_ = coord; + viewport()->update(); +} + +void TimelineView::DisableRecordingOverlay() +{ + recording_overlay_ = false; + viewport()->update(); +} + int TimelineView::SceneToTrack(double y) { int track = -1; diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 05568a03a..3d5513637 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -56,6 +56,8 @@ public: void SetBeamCursor(const TimelineCoordinate& coord); void SetTransitionOverlay(ClipBlock *out, ClipBlock *in); + void EnableRecordingOverlay(const TimelineCoordinate &coord); + void DisableRecordingOverlay(); void SetSelectionList(QHash* s) { @@ -157,6 +159,9 @@ private: QMap clip_marker_rects_; + bool recording_overlay_; + TimelineCoordinate recording_coord_; + private slots: void TrackListChanged(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 996e5e5bc..acc7b45c4 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -65,7 +65,9 @@ ViewerWidget::ViewerWidget(QWidget *parent) : color_menu_enabled_(true), time_changed_from_timer_(false), prequeuing_video_(false), - prequeuing_audio_(0) + prequeuing_audio_(0), + record_armed_(false), + recording_(false) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); @@ -158,6 +160,10 @@ void ViewerWidget::TimeChangedEvent(const rational &time) PauseInternal(); } + if (record_armed_) { + DisarmRecording(); + } + controls_->SetTime(time); waveform_view_->SetTime(time); @@ -377,6 +383,17 @@ void ViewerWidget::SetGizmos(Node *node) display_widget_->SetGizmos(node); } +void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track) +{ + SetTimeAndSignal(time.in()); + ArmForRecording(); + + recording_filename_ = QStringLiteral("/home/matt/Desktop/ass.mp3"); + recording_callback_ = source; + recording_range_ = time; + recording_track_ = track; +} + FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QByteArray& hash, const rational& time) { FramePtr frame = FrameHashCache::LoadCacheFrame(cache_path, hash); @@ -428,6 +445,18 @@ void ViewerWidget::DecrementPrequeuedAudio() } } +void ViewerWidget::ArmForRecording() +{ + controls_->StartPlayBlink(); + record_armed_ = true; +} + +void ViewerWidget::DisarmRecording() +{ + controls_->StopPlayBlink(); + record_armed_ = false; +} + void ViewerWidget::QueueNextAudioBuffer() { rational queue_end = audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_); @@ -595,13 +624,20 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) viewer->auto_cacher_.SetAudioPaused(true); } + // Disarm recording if armed + if (record_armed_) { + DisarmRecording(); + } + // If the playhead is beyond the end, restart at 0 - rational last_frame = GetConnectedNode()->GetLength() - timebase(); - if (!in_to_out_only && GetTime() >= last_frame) { - if (speed > 0) { - SetTimeAndSignal(0); - } else { - SetTimeAndSignal(last_frame); + if (!recording_) { + rational last_frame = GetConnectedNode()->GetLength() - timebase(); + if (!in_to_out_only && GetTime() >= last_frame) { + if (speed > 0) { + SetTimeAndSignal(0); + } else { + SetTimeAndSignal(last_frame); + } } } @@ -657,6 +693,15 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) void ViewerWidget::PauseInternal() { + if (recording_) { + AudioManager::instance()->StopRecording(); + recording_ = false; + controls_->SetPauseButtonRecordingState(false); + + recording_callback_->DisableRecordingOverlay(); + recording_callback_->RecordingCallback(recording_filename_, recording_range_, recording_track_); + } + if (IsPlaying()) { playback_speed_ = 0; controls_->ShowPlayButton(); @@ -1121,6 +1166,17 @@ void ViewerWidget::Play(bool in_to_out_only) } else { in_to_out_only = false; } + } else if (record_armed_) { + if (AudioManager::instance()->StartRecording(recording_filename_, GetConnectedNode()->GetAudioParams())) { + recording_ = true; + controls_->SetPauseButtonRecordingState(true); + recording_callback_->EnableRecordingOverlay(TimelineCoordinate(recording_range_.in(), recording_track_)); + } else { + QMessageBox::critical(this, tr("Audio Recording"), tr("Failed to start audio recording")); + return; + } + + DisarmRecording(); } PlayInternal(1, in_to_out_only); @@ -1205,7 +1261,13 @@ void ViewerWidget::PlaybackTimerUpdate() rational min_time, max_time; - if (play_in_to_out_only_ && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { + if (recording_ && recording_range_.out() != recording_range_.in()) { + + // Limit recording range if applicable + min_time = recording_range_.in(); + max_time = recording_range_.out(); + + } else if (play_in_to_out_only_ && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { // If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea min_time = GetConnectedNode()->GetTimelinePoints()->workarea()->in(); @@ -1229,8 +1291,9 @@ void ViewerWidget::PlaybackTimerUpdate() bool end_of_line = false; bool play_after_pause = false; - if ((playback_speed_ < 0 && current_time <= min_time) - || (playback_speed_ > 0 && current_time >= max_time)) { + if ((!recording_ || recording_range_.out() != recording_range_.in()) + && ((playback_speed_ < 0 && current_time <= min_time) + || (playback_speed_ > 0 && current_time >= max_time))) { // Determine which timestamp we tripped rational tripped_time; @@ -1245,7 +1308,7 @@ void ViewerWidget::PlaybackTimerUpdate() // or restart playback end_of_line = true; - if (Config::Current()[QStringLiteral("Loop")].toBool()) { + if (Config::Current()[QStringLiteral("Loop")].toBool() && !recording_) { // If we're looping, jump to the other side of the workarea and continue time_to_set = (tripped_time == min_time) ? max_time : min_time; diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index e8ef472c9..4bf043af1 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -41,6 +41,7 @@ #include "viewerwindow.h" #include "widget/playbackcontrols/playbackcontrols.h" #include "widget/timebased/timebasedwidget.h" +#include "widget/timelinewidget/timelinewidget.h" namespace olive { @@ -87,6 +88,8 @@ public: void SetGizmos(Node* node); + void StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track); + public slots: void Play(bool in_to_out_only); @@ -205,6 +208,10 @@ private: void DecrementPrequeuedAudio(); + void ArmForRecording(); + + void DisarmRecording(); + QStackedWidget* stack_; ViewerSizer* sizer_; @@ -255,6 +262,13 @@ private: static QVector instances_; + bool record_armed_; + bool recording_; + TimelineWidget *recording_callback_; + TimeRange recording_range_; + Track::Reference recording_track_; + QString recording_filename_; + private slots: void PlaybackTimerUpdate(); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index f956e9051..5a5fc6693 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -555,6 +555,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::TimeChanged, curve_panel_, &ParamPanel::SetTime); connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTime); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); + connect(panel, &TimelinePanel::RequestCaptureStart, sequence_viewer_panel_, &SequenceViewerPanel::StartCapture); connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime);