diff --git a/include/AutomationTrack.h b/include/AutomationTrack.h index 9bcb537f41e..8d185f6b9b2 100644 --- a/include/AutomationTrack.h +++ b/include/AutomationTrack.h @@ -53,6 +53,8 @@ class AutomationTrack : public Track void saveTrackSpecificSettings(QDomDocument& doc, QDomElement& parent, bool presetMode) override; void loadTrackSpecificSettings( const QDomElement & _this ) override; + bool isRenderable() override { return false; } + private: friend class AutomationTrackView; diff --git a/include/ExportProjectDialog.h b/include/ExportProjectDialog.h index 27a97f3f295..280484f0b42 100644 --- a/include/ExportProjectDialog.h +++ b/include/ExportProjectDialog.h @@ -44,15 +44,24 @@ namespace lmms::gui { class ExportProjectDialog : public QDialog { public: + static ExportProjectDialog* exportProjectDialog(const QString& path); + static ExportProjectDialog* exportTracksDialog(const QString& path); + static ExportProjectDialog* exportTrackDialog(const QString& path, Track* track); + + //! @returns `true` if the exported track should be imported as a sample track, `false` otherwise + //! @note only applies when exporting a single track + bool importExportedTrack(); + +private: enum class Mode { ExportProject, ExportTracks, + ExportTrack }; - ExportProjectDialog(const QString& path, Mode mode, QWidget* parent = nullptr); + ExportProjectDialog(const QString& path, Mode mode, Track* track = nullptr, QWidget* parent = nullptr); -private: void accept() override; void reject() override; void onFileFormatChanged(int index); @@ -82,6 +91,7 @@ class ExportProjectDialog : public QDialog QCheckBox* m_exportAsLoopBox = nullptr; QCheckBox* m_exportBetweenLoopMarkersBox = nullptr; + QCheckBox* m_importExportedTrackBox = nullptr; QLabel* m_loopRepeatLabel = nullptr; QSpinBox* m_loopRepeatBox = nullptr; QPushButton* m_startButton = nullptr; @@ -90,6 +100,7 @@ class ExportProjectDialog : public QDialog QString m_path; Mode m_mode; + Track* m_track = nullptr; std::unique_ptr m_renderManager; }; diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index b8777f88a44..0cb23eeedb9 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -239,6 +239,8 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor void autoAssignMidiDevice( bool ); + bool isRenderable() override { return true; } + signals: void instrumentChanged(); void midiNoteOn( const lmms::Note& ); diff --git a/include/PatternTrack.h b/include/PatternTrack.h index 704cbc997c3..abca40da597 100644 --- a/include/PatternTrack.h +++ b/include/PatternTrack.h @@ -80,6 +80,8 @@ class LMMS_EXPORT PatternTrack : public Track m_disabledTracks.removeAll( _track ); } + bool isRenderable() override { return true; } + protected: inline QString nodeName() const override { diff --git a/include/RenderManager.h b/include/RenderManager.h index 267e2dd4e27..bc6c63d4fac 100644 --- a/include/RenderManager.h +++ b/include/RenderManager.h @@ -27,6 +27,7 @@ #define LMMS_RENDER_MANAGER_H #include +#include #include "ProjectRenderer.h" #include "OutputSettings.h" @@ -40,15 +41,17 @@ class RenderManager : public QObject { Q_OBJECT public: - RenderManager(const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt, QString outputPath); - + RenderManager(const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt); ~RenderManager() override; /// Export all unmuted tracks into a single file - void renderProject(); + void renderProject(const QString& outputPath); /// Export all unmuted tracks into individual file - void renderTracks(); + void renderTracks(const QString& outputPath); + + /// Export a a track into a single file + void renderTrack(Track* track, const QString& outputPath); void abortProcessing(); @@ -57,23 +60,29 @@ class RenderManager : public QObject void finished(); private slots: - void renderNextTrack(); void updateConsoleProgress(); private: - QString pathForTrack( const Track *track, int num ); - void restoreMutedState(); + struct RenderJob + { + QString path; + std::vector tracksToRender; + }; - void render( QString outputPath ); + void render(); + void startRender(); + void renderFinished(); + + void storeMuteStates(); + void restoreMuteStates(); const OutputSettings m_outputSettings; ProjectRenderer::ExportFileFormat m_format; - QString m_outputPath; - std::unique_ptr m_activeRenderer; - std::vector m_tracksToRender; - std::vector m_unmuted; + std::queue m_renderJobQueue; + std::unordered_map m_muteStates; + int m_totalRenderJobs = 0; } ; diff --git a/include/SampleTrack.h b/include/SampleTrack.h index d333cd59394..1049d450ffc 100644 --- a/include/SampleTrack.h +++ b/include/SampleTrack.h @@ -83,6 +83,8 @@ class SampleTrack : public Track m_isPlaying = playing; } + bool isRenderable() override { return true; } + signals: void playingChanged(); diff --git a/include/Track.h b/include/Track.h index 274e7a1fc13..e103abd3a35 100644 --- a/include/Track.h +++ b/include/Track.h @@ -110,6 +110,9 @@ class LMMS_EXPORT Track : public Model, public JournallingObject virtual void saveTrackSpecificSettings(QDomDocument& doc, QDomElement& parent, bool presetMode) = 0; virtual void loadTrackSpecificSettings( const QDomElement & element ) = 0; + //! @returns `true` if the track is renderable (i.e., it can be exported to audio), returns `false` otherwise + virtual bool isRenderable() = 0; + // Saving and loading of presets which do not necessarily contain all the track information void savePreset(QDomDocument & doc, QDomElement & element); void loadPreset(const QDomElement & element); diff --git a/include/TrackContainerView.h b/include/TrackContainerView.h index 010cda60280..49f0274c397 100644 --- a/include/TrackContainerView.h +++ b/include/TrackContainerView.h @@ -153,7 +153,10 @@ class TrackContainerView : public QWidget, public ModelView, public slots: void realignTracks(); - lmms::gui::TrackView * createTrackView( lmms::Track * _t ); + + //! Creates a track view for @a t if one doesn't exist already. + //! Otherwise, the already existing track view for @a t is returned. + lmms::gui::TrackView* createTrackView(lmms::Track* _t); void deleteTrackView( lmms::gui::TrackView * _tv ); void dropEvent( QDropEvent * _de ) override; diff --git a/include/TrackOperationsWidget.h b/include/TrackOperationsWidget.h index ed3b72c81b2..979530f5a53 100644 --- a/include/TrackOperationsWidget.h +++ b/include/TrackOperationsWidget.h @@ -63,6 +63,7 @@ private slots: void recordingOn(); void recordingOff(); void clearTrack(); + void exportTrack(); private: TrackView * m_trackView; diff --git a/src/core/RenderManager.cpp b/src/core/RenderManager.cpp index 1d8cd3f9781..ec65ce2687a 100644 --- a/src/core/RenderManager.cpp +++ b/src/core/RenderManager.cpp @@ -22,23 +22,22 @@ * */ +#include "RenderManager.h" + #include #include +#include -#include "RenderManager.h" - -#include "PatternStore.h" +#include "Engine.h" #include "Song.h" - namespace lmms { RenderManager::RenderManager( - const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt, QString outputPath) + const OutputSettings& outputSettings, ProjectRenderer::ExportFileFormat fmt) : m_outputSettings(outputSettings) , m_format(fmt) - , m_outputPath(outputPath) { Engine::audioEngine()->storeAudioDevice(); } @@ -50,146 +49,151 @@ RenderManager::~RenderManager() void RenderManager::abortProcessing() { - if ( m_activeRenderer ) { - disconnect( m_activeRenderer.get(), SIGNAL(finished()), - this, SLOT(renderNextTrack())); + if (m_activeRenderer) + { m_activeRenderer->abortProcessing(); + m_activeRenderer.reset(); } - restoreMutedState(); + + restoreMuteStates(); } -// Called to render each new track when rendering tracks individually. -void RenderManager::renderNextTrack() +// Render the song into individual tracks +void RenderManager::renderTracks(const QString& outputPath) { - m_activeRenderer.reset(); + auto trackNum = 1; - if (m_tracksToRender.empty()) + // TODO: Currently, only the song is exported (it will require changes and refactors in Song), but in the future we + // may want to generalize this function to work with any track container (e.g. the pattern store) + for (const auto& track : Engine::getSong()->tracks()) { - // nothing left to render - restoreMutedState(); - emit finished(); + if (!track->isRenderable()) { continue; } + + auto extension = ProjectRenderer::getFileExtensionFromFormat(m_format); + auto name = track->name(); + name = name.remove(QRegularExpression(FILENAME_FILTER)); + name = QString{"%1_%2%3"}.arg(trackNum++).arg(name).arg(extension); + + const auto pathForTrack = QDir{outputPath}.filePath(name); + m_renderJobQueue.emplace(pathForTrack, std::vector{track}); } - else - { - // pop the next track from our rendering queue - Track* renderTrack = m_tracksToRender.back(); - m_tracksToRender.pop_back(); - // mute everything but the track we are about to render - for (auto track : m_unmuted) - { - track->setMuted(track != renderTrack); - } + startRender(); +} - // for multi-render, prefix each output file with a different number - int trackNum = m_tracksToRender.size() + 1; +// Render the song into a single track +void RenderManager::renderProject(const QString& outputPath) +{ + auto tracks = std::vector{}; - render( pathForTrack(renderTrack, trackNum) ); + // TODO: Currently, only the song is exported (it will require changes and refactors in Song), but in the future we + // may want to generalize this function to work with any track container (e.g. the pattern store) + for (const auto& track : Engine::getSong()->tracks()) + { + if (!track->isRenderable()) { continue; } + tracks.emplace_back(track); } + + m_renderJobQueue.emplace(outputPath, tracks); + startRender(); } -// Render the song into individual tracks -void RenderManager::renderTracks() +void RenderManager::renderTrack(Track* track, const QString& outputPath) { - const TrackContainer::TrackList& tl = Engine::getSong()->tracks(); + assert(track); + assert(track->trackContainer()); - // find all currently unnmuted tracks -- we want to render these. - for (const auto& tk : tl) + if (!track->isRenderable()) { - Track::Type type = tk->type(); - - // Don't render automation tracks - if ( tk->isMuted() == false && - ( type == Track::Type::Instrument || type == Track::Type::Sample ) ) - { - m_unmuted.push_back(tk); - } + qDebug("Track is not renderable"); + return; } - const TrackContainer::TrackList& t2 = Engine::patternStore()->tracks(); - for (const auto& tk : t2) + // TODO: Currently, only the song is exported (it will require changes and refactors in Song), but in the future we + // may want to generalize this function to work with any track container (e.g. the pattern store) + if (!dynamic_cast(track->trackContainer())) { - Track::Type type = tk->type(); - - // Don't render automation tracks - if ( tk->isMuted() == false && - ( type == Track::Type::Instrument || type == Track::Type::Sample ) ) - { - m_unmuted.push_back(tk); - } + qDebug("Can only export tracks from the song"); + return; } - // copy the list of unmuted tracks into our rendering queue. - // we need to remember which tracks were unmuted to restore state at the end. - m_tracksToRender = m_unmuted; + m_renderJobQueue.emplace(outputPath, std::vector{track}); + startRender(); +} - renderNextTrack(); +void RenderManager::startRender() +{ + m_totalRenderJobs = m_renderJobQueue.size(); + storeMuteStates(); + render(); } -// Render the song into a single track -void RenderManager::renderProject() +void RenderManager::renderFinished() { - render( m_outputPath ); + restoreMuteStates(); + render(); } -void RenderManager::render(QString outputPath) +void RenderManager::render() { - m_activeRenderer = std::make_unique(m_outputSettings, m_format, outputPath); + if (m_renderJobQueue.empty()) + { + emit finished(); + return; + } - if( m_activeRenderer->isReady() ) + const auto job = m_renderJobQueue.front(); + m_renderJobQueue.pop(); + + for (const auto& track : Engine::getSong()->tracks()) { - // pass progress signals through - connect( m_activeRenderer.get(), SIGNAL(progressChanged(int)), - this, SIGNAL(progressChanged(int))); + if (!track->isRenderable()) { continue; } + track->setMuted( + std::find(job.tracksToRender.begin(), job.tracksToRender.end(), track) == job.tracksToRender.end()); + } - // when it is finished, render the next track. - // if we have not queued any tracks, renderNextTrack will just clean up - connect( m_activeRenderer.get(), SIGNAL(finished()), - this, SLOT(renderNextTrack())); + // TODO: We shouldn't need to allocate a new ProjectRenderer each time... we might also want to remove + // ProjectRenderer, merge it with RenderManager and use ThreadPool instead + m_activeRenderer = std::make_unique(m_outputSettings, m_format, job.path); + if (m_activeRenderer->isReady()) + { + connect(m_activeRenderer.get(), &ProjectRenderer::progressChanged, this, &RenderManager::progressChanged); + connect(m_activeRenderer.get(), &ProjectRenderer::finished, this, &RenderManager::renderFinished); m_activeRenderer->startProcessing(); } else { - qDebug( "Renderer failed to acquire a file device!" ); - renderNextTrack(); + qDebug("Renderer failed to acquire a file device"); + m_renderJobQueue = std::queue{}; } } -// Unmute all tracks that were muted while rendering tracks -void RenderManager::restoreMutedState() +void RenderManager::updateConsoleProgress() { - while (!m_unmuted.empty()) + if (m_activeRenderer) { - Track* restoreTrack = m_unmuted.back(); - m_unmuted.pop_back(); - restoreTrack->setMuted( false ); + m_activeRenderer->updateConsoleProgress(); + std::cerr << "(" << (m_totalRenderJobs - m_renderJobQueue.size()) << "/" << m_totalRenderJobs << ")"; } } -// Determine the output path for a track when rendering tracks individually -QString RenderManager::pathForTrack(const Track *track, int num) +void RenderManager::storeMuteStates() { - QString extension = ProjectRenderer::getFileExtensionFromFormat( m_format ); - QString name = track->name(); - name = name.remove(QRegularExpression(FILENAME_FILTER)); - name = QString( "%1_%2%3" ).arg( num ).arg( name ).arg( extension ); - return QDir(m_outputPath).filePath(name); + // TODO: Currently, only the song is exported (it will require changes and refactors in Song), but in the future we + // may want to generalize this function to work with any track container (e.g. the pattern store) + for (const auto& track : Engine::getSong()->tracks()) + { + if (!track->isRenderable()) { continue; } + m_muteStates[track] = track->isMuted(); + } } -void RenderManager::updateConsoleProgress() +void RenderManager::restoreMuteStates() { - if ( m_activeRenderer ) + for (const auto& [track, muted] : m_muteStates) { - m_activeRenderer->updateConsoleProgress(); - - int totalNum = m_unmuted.size(); - if ( totalNum > 0 ) - { - // we are rendering multiple tracks, append a track counter to the output - int trackNum = totalNum - m_tracksToRender.size(); - fprintf( stderr, "(%d/%d)", trackNum, totalNum ); - } + track->setMuted(muted); } } diff --git a/src/core/main.cpp b/src/core/main.cpp index a63e6633c57..0dfd00aef9a 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -737,7 +737,7 @@ int main( int argc, char * * argv ) } // create renderer - auto r = new RenderManager(os, eff, renderOut); + auto r = new RenderManager(os, eff); QCoreApplication::instance()->connect( r, SIGNAL(finished()), SLOT(quit())); @@ -755,11 +755,11 @@ int main( int argc, char * * argv ) // start now! if ( renderTracks ) { - r->renderTracks(); + r->renderTracks(renderOut); } else { - r->renderProject(); + r->renderProject(renderOut); } } else // otherwise, start the GUI diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 7e9c16ab97f..034036a9cc1 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1508,10 +1508,8 @@ void MainWindow::exportProject(bool multiExport) } } - ExportProjectDialog epd(exportFileName, - multiExport ? ExportProjectDialog::Mode::ExportTracks : ExportProjectDialog::Mode::ExportProject, - getGUI()->mainWindow()); - epd.exec(); + multiExport ? ExportProjectDialog::exportTracksDialog(exportFileName)->exec() + : ExportProjectDialog::exportProjectDialog(exportFileName)->exec(); } } diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 6bc0e28d539..e8efef3c5d7 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -32,6 +32,7 @@ #include #include "AutomationClip.h" +#include "AutomationClipView.h" #include "Clipboard.h" #include "ColorChooser.h" #include "DataFile.h" diff --git a/src/gui/modals/ExportProjectDialog.cpp b/src/gui/modals/ExportProjectDialog.cpp index 9e354696b06..7487c0eb17f 100644 --- a/src/gui/modals/ExportProjectDialog.cpp +++ b/src/gui/modals/ExportProjectDialog.cpp @@ -51,7 +51,7 @@ constexpr auto defaultStereoMode = OutputSettings::StereoMode::Stereo; constexpr auto maxLoopRepeat = std::numeric_limits::max(); } // namespace -ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget* parent) +ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, Track* track, QWidget* parent) : QDialog(parent) , m_fileFormatLabel(new QLabel(tr("File format:"))) , m_fileFormatComboBox(new QComboBox()) @@ -69,6 +69,7 @@ ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget , m_fileFormatSettingsLayout(new QFormLayout(m_fileFormatSettingsGroupBox)) , m_exportAsLoopBox(new QCheckBox(tr("Export as loop (remove extra bar)"))) , m_exportBetweenLoopMarkersBox(new QCheckBox(tr("Export between loop markers"))) + , m_importExportedTrackBox(new QCheckBox(tr("Import exported track"))) , m_loopRepeatLabel(new QLabel(tr("Render looped section:"))) , m_loopRepeatBox(new QSpinBox()) , m_startButton(new QPushButton(tr("Start"))) @@ -76,8 +77,10 @@ ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget , m_progressBar(new QProgressBar()) , m_path(path) , m_mode(mode) + , m_track(track) { setWindowTitle(tr("Export project")); + setAttribute(Qt::WA_DeleteOnClose); for (const auto& device : ProjectRenderer::fileEncodeDevices) { @@ -152,7 +155,6 @@ ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget } } - auto loopRepeatLayout = new QHBoxLayout{}; loopRepeatLayout->addWidget(m_loopRepeatLabel); loopRepeatLayout->addWidget(m_loopRepeatBox); @@ -161,6 +163,13 @@ ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget auto exportSettingsLayout = new QVBoxLayout{exportSettingsGroupBox}; exportSettingsLayout->addWidget(m_exportAsLoopBox); exportSettingsLayout->addWidget(m_exportBetweenLoopMarkersBox); + + if (mode == Mode::ExportTrack) + { + m_importExportedTrackBox->setChecked(true); + exportSettingsLayout->addWidget(m_importExportedTrackBox); + } + exportSettingsLayout->addLayout(loopRepeatLayout); m_fileFormatSettingsLayout->addRow(m_fileFormatLabel, m_fileFormatComboBox); @@ -202,9 +211,8 @@ ExportProjectDialog::ExportProjectDialog(const QString& path, Mode mode, QWidget void ExportProjectDialog::onFileFormatChanged(int index) { - if (m_mode == Mode::ExportProject) + if (const auto fileInfo = QFileInfo{m_path}; !fileInfo.suffix().isEmpty()) { - const auto fileInfo = QFileInfo{m_path}; const auto extension = ProjectRenderer::getFileExtensionFromFormat(static_cast(index)); m_path = fileInfo.path() + QDir::separator() + fileInfo.completeBaseName() + extension; @@ -253,7 +261,7 @@ void ExportProjectDialog::onStartButtonClicked() outputSettings.setCompressionLevel(compressionLevel); const auto format = static_cast(m_fileFormatComboBox->currentData().toInt()); - m_renderManager = std::make_unique(outputSettings, format, m_path); + m_renderManager = std::make_unique(outputSettings, format); m_startButton->setEnabled(false); Engine::getSong()->setExportLoop(m_exportAsLoopBox->isChecked()); @@ -267,10 +275,14 @@ void ExportProjectDialog::onStartButtonClicked() switch (m_mode) { case Mode::ExportProject: - m_renderManager->renderProject(); + m_renderManager->renderProject(m_path); + break; + case Mode::ExportTrack: + assert(m_track && "no track assigned for export"); + m_renderManager->renderTrack(m_track, m_path); break; case Mode::ExportTracks: - m_renderManager->renderTracks(); + m_renderManager->renderTracks(m_path); break; } } @@ -293,4 +305,25 @@ void ExportProjectDialog::updateTitleBar(int prog) setWindowTitle(tr("Rendering: %1%").arg(prog)); } +bool ExportProjectDialog::importExportedTrack() +{ + assert(m_mode == Mode::ExportTrack); + return m_importExportedTrackBox->isChecked(); +} + +ExportProjectDialog* ExportProjectDialog::exportProjectDialog(const QString& path) +{ + return new ExportProjectDialog{path, Mode::ExportProject, nullptr, nullptr}; +} + +ExportProjectDialog* ExportProjectDialog::exportTrackDialog(const QString& path, Track* track) +{ + return new ExportProjectDialog{path, Mode::ExportTrack, track, nullptr}; +} + +ExportProjectDialog* ExportProjectDialog::exportTracksDialog(const QString& path) +{ + return new ExportProjectDialog{path, Mode::ExportTracks, nullptr, nullptr}; +} + } // namespace lmms::gui diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index 16c0ca3e424..42828d4b8e7 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -38,6 +38,11 @@ #include "ColorChooser.h" #include "ConfigManager.h" #include "DataFile.h" +#include "ExportProjectDialog.h" +#include "FileDialog.h" +#include "PatternStore.h" +#include "ProjectRenderer.h" +#include "SampleClip.h" #include "embed.h" #include "Engine.h" #include "InstrumentTrackView.h" @@ -247,6 +252,51 @@ void TrackOperationsWidget::removeTrack() } } +void TrackOperationsWidget::exportTrack() +{ + auto dialog = FileDialog{nullptr, tr("Select audio file")}; + auto types = QStringList{}; + + for (const auto& fileEncodeDevice : ProjectRenderer::fileEncodeDevices) + { + if (!fileEncodeDevice.isAvailable()) { continue; } + types << tr(fileEncodeDevice.m_description); + } + + dialog.setFileMode(FileDialog::AnyFile); + dialog.setNameFilters(types); + dialog.setAcceptMode(FileDialog::AcceptSave); + dialog.setDefaultSuffix(ProjectRenderer::fileEncodeDevices[0].m_extension); + + const auto projectFileName = Engine::getSong()->projectFileName(); + const auto exportName = (projectFileName.isEmpty() ? tr("untitled") : QFileInfo{projectFileName}.baseName()) + "_" + + m_trackView->getTrack()->name() + ProjectRenderer::fileEncodeDevices[0].m_extension; + dialog.selectFile(exportName); + + if (dialog.exec() != QDialog::Accepted) { return; } + + const auto exportPath = dialog.selectedFiles()[0]; + const auto exportDialog = ExportProjectDialog::exportTrackDialog(exportPath, m_trackView->getTrack()); + + connect(exportDialog, &ExportProjectDialog::accepted, [this, exportDialog, exportPath] { + if (!exportDialog->importExportedTrack()) { return; } + + const auto trackContainerView = m_trackView->trackContainerView(); + auto sampleTrack = Track::create(Track::Type::Sample, trackContainerView->model()); + auto sampleTrackView = trackContainerView->createTrackView(sampleTrack); + auto sampleClip = new SampleClip(sampleTrack); + + const auto indexOfTrack = trackContainerView->trackViews().indexOf(m_trackView); + assert(indexOfTrack != -1); + + trackContainerView->moveTrackView(sampleTrackView, indexOfTrack + 1); + m_trackView->getTrack()->setMuted(true); + sampleClip->setSampleFile(exportPath); + }); + + exportDialog->open(); +} + void TrackOperationsWidget::selectTrackColor() { const auto newColor = ColorChooser{this} @@ -313,6 +363,14 @@ void TrackOperationsWidget::updateMenu() { toMenu->addAction( tr( "Clear this track" ), this, SLOT(clearTrack())); } + + // TODO: Allow for exporting tracks inside patterns + if (!dynamic_cast(this) + && !dynamic_cast(m_trackView->getTrack()->trackContainer())) + { + toMenu->addAction(tr("Export this track"), this, &TrackOperationsWidget::exportTrack); + } + if (QMenu *mixerMenu = m_trackView->createMixerMenu(tr("Channel %1: %2"), tr("Assign to new Mixer Channel"))) { toMenu->addMenu(mixerMenu);