summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Szmigin <smidge@xsco.net>2020-05-07 21:50:58 +0100
committerMatthias Beyer <mail@beyermatthias.de>2021-01-10 10:41:01 +0100
commit629a4b56fc399d397fc0ac118c3089bf37cab67f (patch)
treeb97b4c08c77a064c815a53a342a17468c9860d86
parentaef1e1e2a96cbf922f8364d83c33e37c0168168d (diff)
Engine library export - initial implementation
-rw-r--r--CMakeLists.txt16
-rw-r--r--src/library/export/dlglibraryexport.cpp209
-rw-r--r--src/library/export/dlglibraryexport.h64
-rw-r--r--src/library/export/engineprimeexportjob.cpp539
-rw-r--r--src/library/export/engineprimeexportjob.h68
-rw-r--r--src/library/export/engineprimeexportrequest.h18
-rw-r--r--src/library/export/exportrequest.h22
-rw-r--r--src/library/export/libraryexporter.cpp60
-rw-r--r--src/library/export/libraryexporter.h49
-rw-r--r--src/library/library.cpp24
-rw-r--r--src/library/library.h17
-rw-r--r--src/library/mixxxlibraryfeature.cpp33
-rw-r--r--src/library/mixxxlibraryfeature.h39
-rw-r--r--src/library/trackset/crate/cratefeature.cpp38
-rw-r--r--src/library/trackset/crate/cratefeature.h14
-rw-r--r--src/mixxx.cpp33
-rw-r--r--src/mixxx.h11
-rw-r--r--src/widget/wmainmenubar.cpp11
-rw-r--r--src/widget/wmainmenubar.h3
19 files changed, 1255 insertions, 13 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0fae18018b..f60ab84633 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1619,14 +1619,12 @@ endif()
# Denon Engine Prime library export support (using libdjinterop)
option(ENGINEPRIME "Support for library export to Denon Engine Prime" OFF)
if(ENGINEPRIME)
- target_compile_definitions(mixxx-lib PUBLIC __ENGINEPRIME__)
-
# Look for an existing installation of libdjinterop and use that if available.
# Otherwise, download and build from GitHub.
find_package(DjInterop)
if(DjInterop_FOUND)
# An existing installation of djinterop is available.
- message(STATUS "Linking dynamically to existing installation of libdjinterop")
+ message(STATUS "Using existing system installation of libdjinterop")
target_include_directories(mixxx-lib PUBLIC ${DjInterop_INCLUDE_DIRS})
target_link_libraries(mixxx-lib PUBLIC DjInterop::DjInterop)
else()
@@ -1671,11 +1669,19 @@ if(ENGINEPRIME)
# Since we have built libdjinterop from sources as a static library, its
# transitive dependencies are not automatically recognised. libdjinterop
- # depends on zlib and sqlite3. Mixxx already has a dependency on sqlite3,
- # but not zlib, so we explicitly add that here.
+ # depends on zlib and optionally sqlite3. If libdjinterop was configured
+ # to depend on system SQLite, Mixxx will already have the dependency.
+ # But it does not have zlib, so we explicitly add that here.
find_package(ZLIB 1.2.8 REQUIRED)
target_link_libraries(mixxx-lib PUBLIC ${ZLIB_LIBRARIES})
endif()
+
+ # Include conditional sources only required with Engine Prime export support.
+ target_sources(mixxx-lib PRIVATE
+ src/library/export/dlglibraryexport.cpp
+ src/library/export/engineprimeexportjob.cpp
+ src/library/export/libraryexporter.cpp)
+ target_compile_definitions(mixxx-lib PUBLIC __ENGINEPRIME__)
endif()
# Ebur128
diff --git a/src/library/export/dlglibraryexport.cpp b/src/library/export/dlglibraryexport.cpp
new file mode 100644
index 0000000000..54992b3817
--- /dev/null
+++ b/src/library/export/dlglibraryexport.cpp
@@ -0,0 +1,209 @@
+#include "library/export/dlglibraryexport.h"
+
+#include <QFileDialog>
+#include <QFormLayout>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <QStandardPaths>
+#include <djinterop/enginelibrary.hpp>
+
+#include "library/export/engineprimeexportrequest.h"
+#include "library/trackcollection.h"
+#include "library/trackcollectionmanager.h"
+#include "library/trackset/crate/crateid.h"
+#include "library/trackset/crate/cratestorage.h"
+
+namespace el = djinterop::enginelibrary;
+
+namespace mixxx {
+
+namespace {
+const QString kDefaultMixxxExportDirName = QStringLiteral("mixxx-export");
+
+void populateCrates(
+ QListWidget& listWidget,
+ const TrackCollection& trackCollection) {
+ // Populate list of crates.
+ CrateSelectResult crates = trackCollection.crates().selectCrates();
+ Crate crate;
+ while (crates.populateNext(&crate)) {
+ auto pItem = std::make_unique<QListWidgetItem>(crate.getName());
+ pItem->setData(Qt::UserRole, crate.getId().value());
+ listWidget.addItem(pItem.release());
+ }
+}
+} // namespace
+
+DlgLibraryExport::DlgLibraryExport(
+ QWidget* parent,
+ UserSettingsPointer pConfig,
+ TrackCollectionManager* pTrackCollectionManager)
+ : QDialog(parent),
+ m_pConfig{pConfig},
+ m_pTrackCollectionManager{pTrackCollectionManager} {
+ // Selectable list of crates from the Mixxx library.
+ m_pCratesList = make_parented<QListWidget>();
+ m_pCratesList->setSelectionMode(QListWidget::ExtendedSelection);
+ populateCrates(*m_pCratesList, *m_pTrackCollectionManager->internalCollection());
+
+ // Read-only text fields showing key directories for export.
+ m_pBaseDirectoryTextField = make_parented<QLineEdit>();
+ m_pBaseDirectoryTextField->setReadOnly(true);
+ m_pDatabaseDirectoryTextField = make_parented<QLineEdit>();
+ m_pDatabaseDirectoryTextField->setReadOnly(true);
+ m_pMusicDirectoryTextField = make_parented<QLineEdit>();
+ m_pMusicDirectoryTextField->setReadOnly(true);
+
+ // Radio buttons to allow choice between exporting the whole music library
+ // or just tracks in a selection of crates.
+ m_pWholeLibraryRadio = make_parented<QRadioButton>(tr("Entire music library"));
+ m_pWholeLibraryRadio->setChecked(true);
+ m_pCratesList->setEnabled(false);
+ connect(m_pWholeLibraryRadio,
+ &QRadioButton::clicked,
+ this,
+ [this]() { m_pCratesList->setEnabled(false); });
+ m_pCratesRadio = make_parented<QRadioButton>(tr("Selected crates"));
+ connect(m_pCratesRadio,
+ &QRadioButton::clicked,
+ this,
+ [this]() { m_pCratesList->setEnabled(true); });
+
+ // Button to allow ability to browse for the export directory.
+ auto pExportDirBrowseButton = make_parented<QPushButton>(tr("Browse"));
+ connect(pExportDirBrowseButton,
+ &QPushButton::clicked,
+ this,
+ &DlgLibraryExport::browseExportDirectory);
+ auto pExportDirLayout = make_parented<QHBoxLayout>();
+ pExportDirLayout->addWidget(m_pBaseDirectoryTextField);
+ pExportDirLayout->addWidget(pExportDirBrowseButton);
+
+ auto pFormLayout = make_parented<QFormLayout>();
+ pFormLayout->addRow(tr("Base export directory"), pExportDirLayout);
+ pFormLayout->addRow(tr("Engine Prime database export directory"),
+ m_pDatabaseDirectoryTextField);
+ pFormLayout->addRow(tr("Copy music files to"), m_pMusicDirectoryTextField);
+
+ // Buttons to begin the export or cancel.
+ auto pExportButton = make_parented<QPushButton>(tr("Export"));
+ pExportButton->setDefault(true);
+ connect(pExportButton, &QPushButton::clicked, this, &DlgLibraryExport::exportRequested);
+ auto pCancelButton = make_parented<QPushButton>(tr("Cancel"));
+ connect(pCancelButton, &QPushButton::clicked, this, &QDialog::reject);
+
+ // Arrange action buttons at bottom of dialog.
+ auto pButtonBarLayout = make_parented<QHBoxLayout>();
+ pButtonBarLayout->addStretch(1);
+ pButtonBarLayout->addWidget(pExportButton);
+ pButtonBarLayout->addWidget(pCancelButton);
+
+ auto pLayout = make_parented<QGridLayout>();
+ pLayout->setColumnStretch(0, 1);
+ pLayout->setColumnStretch(1, 2);
+ pLayout->addWidget(m_pWholeLibraryRadio, 0, 0);
+ pLayout->addWidget(m_pCratesRadio, 1, 0);
+ pLayout->addWidget(m_pCratesList, 2, 0);
+ pLayout->addLayout(pFormLayout, 0, 1, 3, 1);
+ pLayout->addLayout(pButtonBarLayout, 3, 0, 1, 2);
+
+ setLayout(pLayout);
+ setWindowTitle(tr("Export Library"));
+
+ show();
+ raise();
+ activateWindow();
+}
+
+void DlgLibraryExport::setSelectedCrate(std::optional<CrateId> crateId) {
+ if (!crateId) {
+ m_pWholeLibraryRadio->setChecked(true);
+ m_pCratesList->setEnabled(false);
+ return;
+ }
+
+ m_pCratesRadio->setChecked(true);
+ m_pCratesList->setEnabled(true);
+ for (auto i = 0; i < m_pCratesList->count(); ++i) {
+ auto* pItem = m_pCratesList->item(i);
+ auto currCrateId = pItem->data(Qt::UserRole).toInt();
+ if (currCrateId == crateId.value().value()) {
+ m_pCratesList->setCurrentItem(pItem);
+ return;
+ }
+ }
+}
+
+void DlgLibraryExport::browseExportDirectory() {
+ QString lastExportDirectory =
+ m_pConfig->getValue(ConfigKey("[Library]", "LastLibraryExportDirectory"),
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+ auto baseDirectory =
+ QFileDialog::getExistingDirectory(NULL, tr("Export Library To"), lastExportDirectory);
+ if (baseDirectory.isEmpty()) {
+ return;
+ }
+ m_pConfig->set(
+ ConfigKey("[Library]", "LastLibraryExportDirectory"), ConfigValue(baseDirectory));
+
+ QDir baseExportDirectory{baseDirectory};
+ auto databaseDirectory = baseExportDirectory.filePath(
+ el::default_database_dir_name);
+ auto musicDirectory = baseExportDirectory.filePath(kDefaultMixxxExportDirName);
+
+ m_pBaseDirectoryTextField->setText(baseDirectory);
+ m_pDatabaseDirectoryTextField->setText(databaseDirectory);
+ m_pMusicDirectoryTextField->setText(musicDirectory);
+}
+
+void DlgLibraryExport::exportRequested() {
+ // Check a base export directory has been chosen
+ if (m_pBaseDirectoryTextField->text() == "") {
+ QMessageBox::information(this,
+ tr("No Export Directory Chosen"),
+ tr("No export directory was chosen. Please choose a directory "
+ "in order to export the music library."),
+ QMessageBox::Ok,
+ QMessageBox::Ok);
+ return;
+ }
+
+ // See if an EL DB exists in the chosen dir already, and ask the user for
+ // confirmation before proceeding if so.
+ if (el::database_exists(m_pDatabaseDirectoryTextField->text().toStdString())) {
+ int ret = QMessageBox::question(
+ this,
+ tr("Merge Into Existing Library?"),
+ tr("There is already an existing library in directory ") +
+ m_pDatabaseDirectoryTextField->text() +
+ tr("\nIf you proceed, the Mixxx library will be merged into "
+ "this existing library. Do you want to merge into the "
+ "the existing library?"),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ if (ret != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ // Construct a request to export the library/crates.
+ // Assumed to always be an Engine Prime export in this iteration of the
+ // dialog.
+ EnginePrimeExportRequest request;
+ request.engineLibraryDbDir = QDir{m_pDatabaseDirectoryTextField->text()};
+ request.musicFilesDir = QDir{m_pMusicDirectoryTextField->text()};
+ request.exportSelectedCrates = m_pCratesList->isEnabled();
+ if (request.exportSelectedCrates) {
+ for (auto* pItem : m_pCratesList->selectedItems()) {
+ CrateId id{pItem->data(Qt::UserRole).value<int>()};
+ request.crateIdsToExport.insert(id);
+ }
+ }
+
+ emit startEnginePrimeExport(std::move(request));
+ accept();
+}
+
+} // namespace mixxx
diff --git a/src/library/export/dlglibraryexport.h b/src/library/export/dlglibraryexport.h
new file mode 100644
index 0000000000..364acb2b94
--- /dev/null
+++ b/src/library/export/dlglibraryexport.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include <QDialog>
+#include <QLineEdit>
+#include <QListWidget>
+#include <QRadioButton>
+#include <QTreeWidget>
+#include <QWidget>
+#include <memory>
+
+#include "library/export/engineprimeexportrequest.h"
+#include "library/trackset/crate/crateid.h"
+#include "preferences/usersettings.h"
+#include "util/optional.h"
+#include "util/parented_ptr.h"
+
+class TrackCollectionManager;
+
+namespace mixxx {
+
+/// The DlgLibraryExport class is a UI window that gathers information from
+/// the user about how they would like to export the Mixxx library.
+///
+/// Currently, the dialog only supports exporting to the Engine Library format,
+/// but in future it is expected that this dialog could be expanded to include
+/// other formats, and generate different export signals accordingly.
+class DlgLibraryExport : public QDialog {
+ Q_OBJECT
+
+ public:
+ DlgLibraryExport(
+ QWidget* parent,
+ UserSettingsPointer pConfig,
+ TrackCollectionManager* pTrackCollectionManager);
+
+ /// Set the specified crate to be selected for export on the dialog. If no
+ /// crate is provided (i.e. `std::nullopt`), then the dialog will be ready
+ // to export the whole library. If an unknown crate is provided, then no
+ /// action is taken.
+ void setSelectedCrate(std::optional<CrateId> crateId);
+
+ signals:
+ /// The startEnginePrimeExport signal is emitted when sufficient information
+ /// has been gathered from the user to kick off an Engine Prime export, and
+ /// details of the request are provided as part of the signal.
+ void startEnginePrimeExport(EnginePrimeExportRequest) const;
+
+ private slots:
+ void browseExportDirectory();
+ void exportRequested();
+
+ private:
+ UserSettingsPointer m_pConfig;
+ TrackCollectionManager* m_pTrackCollectionManager;
+
+ parented_ptr<QRadioButton> m_pWholeLibraryRadio;
+ parented_ptr<QRadioButton> m_pCratesRadio;
+ parented_ptr<QListWidget> m_pCratesList;
+ parented_ptr<QLineEdit> m_pBaseDirectoryTextField;
+ parented_ptr<QLineEdit> m_pDatabaseDirectoryTextField;
+ parented_ptr<QLineEdit> m_pMusicDirectoryTextField;
+};
+
+} // namespace mixxx
diff --git a/src/library/export/engineprimeexportjob.cpp b/src/library/export/engineprimeexportjob.cpp
new file mode 100644
index 0000000000..b6d4e8dad9
--- /dev/null
+++ b/src/library/export/engineprimeexportjob.cpp
@@ -0,0 +1,539 @@
+#include "library/export/engineprimeexportjob.h"
+
+#include <QHash>
+#include <QMetaMethod>
+#include <QStringList>
+#include <array>
+#include <cstdint>
+#include <djinterop/djinterop.hpp>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+
+#include "library/trackcollection.h"
+#include "library/trackset/crate/crate.h"
+#include "track/track.h"
+#include "waveform/waveformfactory.h"
+
+namespace el = djinterop::enginelibrary;
+
+namespace mixxx {
+
+namespace {
+
+const std::string kMixxxRootCrateName = "Mixxx";
+
+const QStringList kSupportedFileTypes = {"mp3", "flac", "ogg"};
+
+std::optional<djinterop::musical_key> toDjinteropKey(
+ track::io::key::ChromaticKey key) {
+ static const std::array<std::optional<djinterop::musical_key>, 25> keyMap{{
+ std::nullopt, // INVALID = 0,
+ djinterop::musical_key::c_major, // C_MAJOR = 1,
+ djinterop::musical_key::d_flat_major, // D_FLAT_MAJOR = 2,
+ djinterop::musical_key::d_major, // D_MAJOR = 3,
+ djinterop::musical_key::e_flat_major, // E_FLAT_MAJOR = 4,
+ djinterop::musical_key::e_major, // E_MAJOR = 5,
+ djinterop::musical_key::f_major, // F_MAJOR = 6,
+ djinterop::musical_key::f_sharp_major, // F_SHARP_MAJOR = 7,
+ djinterop::musical_key::g_major, // G_MAJOR = 8,
+ djinterop::musical_key::a_flat_major, // A_FLAT_MAJOR = 9,
+ djinterop::musical_key::a_major, // A_MAJOR = 10,
+ djinterop::musical_key::b_flat_major, // B_FLAT_MAJOR = 11,
+ djinterop::musical_key::b_major, // B_MAJOR = 12,
+ djinterop::musical_key::c_minor, // C_MINOR = 13,
+ djinterop::musical_key::d_flat_minor, // C_SHARP_MINOR = 14,
+ djinterop::musical_key::d_minor, // D_MINOR = 15,
+ djinterop::musical_key::e_flat_minor, // E_FLAT_MINOR = 16,
+ djinterop::musical_key::e_minor, // E_MINOR = 17,
+ djinterop::musical_key::f_minor, // F_MINOR = 18,
+ djinterop::musical_key::f_sharp_minor, // F_SHARP_MINOR = 19,
+ djinterop::musical_key::g_minor, // G_MINOR = 20,
+ djinterop::musical_key::a_flat_minor, // G_SHARP_MINOR = 21,
+ djinterop::musical_key::a_minor, // A_MINOR = 22,
+ djinterop::musical_key::b_flat_minor, // B_FLAT_MINOR = 23,
+ djinterop::musical_key::b_minor, // B_MINOR = 24
+ }};
+
+ return keyMap[key];
+}
+
+std::string exportFile(const EnginePrimeExportRequest& request,
+ TrackPointer pTrack) {
+ if (!request.engineLibraryDbDir.exists()) {
+ auto msg = QString(
+ "Engine Library DB directory %1 has been removed from disk!")
+ .arg(request.engineLibraryDbDir.absolutePath());
+ throw std::runtime_error{msg.toStdString()};
+ } else if (!request.musicFilesDir.exists()) {
+ auto msg = QString(
+ "Music file export directory %1 has been removed from disk!")
+ .arg(request.musicFilesDir.absolutePath());
+ throw std::runtime_error{msg.toStdString()};
+ }
+
+ // Copy music files into the Mixxx export dir, if the source file has
+ // been modified (or the destination doesn't exist). To ensure no
+ // chance of filename clashes, and to keep things simple, we will prefix
+ // the destination files with the DB track identifier.
+ TrackFile srcFileInfo = pTrack->getFileInfo();
+ auto trackId = pTrack->getId().value();
+ QString dstFilename = QString::number(trackId) + " - " + srcFileInfo.fileName();
+ QString dstPath = request.musicFilesDir.filePath(dstFilename);
+ if (!QFile::exists(dstPath) ||
+ srcFileInfo.fileLastModified() > QFileInfo{dstPath}.lastModified()) {
+ auto srcPath = srcFileInfo.location();
+ QFile::copy(srcPath, dstPath);
+ }
+
+ return request.engineLibraryDbDir.relativeFilePath(dstPath).toStdString();
+}
+
+djinterop::track getTrackByRelativePath(
+ djinterop::database database, const std::string& relativePath) {
+ auto trackCandidates = database.tracks_by_relative_path(relativePath);
+ switch (trackCandidates.size()) {
+ case 0:
+ return database.create_track(relativePath);
+ case 1:
+ return trackCandidates.front();
+ default:
+ qInfo() << "Warning: More than one external track with the same relative path";
+ return trackCandidates.front();
+ }
+}
+
+void exportMetadata(djinterop::database& db,
+ QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap,
+ TrackPointer pTrack,
+ std::unique_ptr<Waveform> pWaveform,
+ const std::string& relativePath) {
+ // Create or load the track in the database, using the relative path to
+ // the music file. We will record the mapping from Mixxx track id to
+ // exported track id as well.
+ auto externalTrack = getTrackByRelativePath(db, relativePath);
+ mixxxToEnginePrimeTrackIdMap.insert(pTrack->getId(), externalTrack.id());
+
+ // Note that the Engine Prime format has the scope for recording meta-data
+ // about whether track was imported from an external database. However,
+ // that meta-data only extends as far as other Engine Prime databases,
+ // which Mixxx is not. So we do not set any import information on the
+ // exported track.
+ externalTrack.set_track_number(pTrack->getTrackNumber().toInt());
+ externalTrack.set_bpm(pTrack->getBpm());
+ externalTrack.set_year(pTrack->getYear().toInt());
+ externalTrack.set_title(pTrack->getTitle().toStdString());
+ externalTrack.set_artist(pTrack->getArtist().toStdString());
+ externalTrack.set_album(pTrack->getAlbum().toStdString());
+ externalTrack.set_genre(pTrack->getGenre().toStdString());
+ externalTrack.set_comment(pTrack->getComment().toStdString());
+ externalTrack.set_composer(pTrack->getComposer().toStdString());
+ externalTrack.set_key(toDjinteropKey(pTrack->getKey()));
+ int64_t lastModifiedMillisSinceEpoch =
+ pTrack->getFileInfo().fileLastModified().toMSecsSinceEpoch();
+ std::chrono::system_clock::time_point lastModifiedAt{
+ std::chrono::milliseconds{lastModifiedMillisSinceEpoch}};
+ externalTrack.set_last_modified_at(lastModifiedAt);
+ externalTrack.set_bitrate(pTrack->getBitrate());
+
+ // Frames used interchangeably with "samples" here.
+ auto sampleCount = static_cast<int64_t>(pTrack->getDuration() * pTrack->getSampleRate());
+ externalTrack.set_sampling({static_cast<double>(pTrack->getSampleRate()), sampleCount});
+
+ // Set track loudness.
+ // Note that the djinterop API method for setting loudness may be revised
+ // in future, as more is discovered about the exact meaning of the loudness
+ // field in the Engine Library format. Make the assumption for now that
+ // ReplayGain ratio is an appropriate value to set, which has been validated
+ // by basic experimental testing.
+ externalTrack.set_average_loudness(pTrack->getReplayGain().getRatio());
+
+ // Set main cue-point.
+ double cuePlayPos = pTrack->getCuePoint().getPosition();
+ externalTrack.set_default_main_cue(cuePlayPos / 2);
+ externalTrack.set_adjusted_main_cue(cuePlayPos / 2);
+
+ // Fill in beat grid. For now, assume a constant average BPM across
+ // the whole track. Note that points in the track are specified as
+ // "play positions", which are twice the sample offset.
+ BeatsPointer beats = pTrack->getBeats();
+ if (beats != nullptr) {
+ // Note that Mixxx does not (currently) store any information about
+ // which beat of a bar a given beat represents. As such, in order to
+ // make sure we have the right phrasing, assume that the main cue point
+ // starts at the beginning of a bar, then move backwards towards the
+ // beginning of the track in 4-beat decrements to find the first beat
+ // in the track that also aligns with the start of a bar.
+ double firstBeatPlayPos = beats->findNextBeat(0);
+ double cueBeatPlayPos = beats->findClosestBeat(cuePlayPos);
+ int numBeatsToCue = beats->numBeatsInRange(firstBeatPlayPos, cueBeatPlayPos);
+ double firstBarAlignedBeatPlayPos = beats->findNBeatsFromSample(
+ cueBeatPlayPos, numBeatsToCue & ~0x3);
+
+ // We will treat the first bar-aligned beat as beat zero. Find the
+ // number of beats from there until the end of the track in order to
+ // correctly assign an index for the last beat.
+ double lastBeatPlayPos = beats->findPrevBeat(sampleCount * 2);
+ int numBeats = beats->numBeatsInRange(firstBarAlignedBeatPlayPos, lastBeatPlayPos);
+ std::vector<djinterop::beatgrid_marker> beatgrid{
+ {0, firstBarAlignedBeatPlayPos / 2}, {numBeats, lastBeatPlayPos / 2}};
+ beatgrid = el::normalize_beatgrid(std::move(beatgrid), sampleCount);
+ externalTrack.set_default_beatgrid(beatgrid);
+ externalTrack.set_adjusted_beatgrid(beatgrid);
+ } else {
+ qInfo() << "No beats data found for track" << pTrack->getId()
+ << "(" << pTrack->getFileInfo().fileName() << ")";
+ }
+
+ auto cues = pTrack->getCuePoints();
+ for (const CuePointer& pCue : cues) {
+ // We are only interested in hot cues.
+ if (pCue->getType() != CueType::HotCue) {
+ continue;
+ }
+
+ int hot_cue_index = pCue->getHotCue(); // Note: Mixxx uses 0-based.
+ if (hot_cue_index < 0 || hot_cue_index >= 8) {
+ // Only supports a maximum of 8 hot cues.
+ qInfo() << "Skipping hot cue" << hot_cue_index
+ << "as the Engine Prime format only supports at most 8"
+ << "hot cues.";
+ continue;
+ }
+
+ QString label = pCue->getLabel();
+ if (label == "") {
+ label = QString("Cue %1").arg(hot_cue_index + 1);
+ }
+
+ djinterop::hot_cue hc{};
+ hc.label = label.toStdString();
+ hc.sample_offset = pCue->getPosition() / 2; // Convert "play pos".
+ hc.color = el::standard_pad_colors::pads[hot_cue_index];
+ externalTrack.set_hot_cue_at(hot_cue_index, hc);
+ }
+
+ // Note that Mixxx does not support pre-calculated stored loops, but it will
+ // remember the position of a single ad-hoc loop between track loads.
+ // However, since this single ad-hoc loop is likely to be different in use
+ // from a set of stored loops (and is easily overwritten), we do not export
+ // it to the external database here.
+ externalTrack.set_loops({});
+
+ // Write waveform.
+ // Note that writing a single waveform will automatically calculate an
+ // overview waveform too.
+ if (pWaveform) {
+ int64_t samplesPerEntry = externalTrack.required_waveform_samples_per_entry();
+ int64_t externalWaveformSize = (sampleCount / samplesPerEntry) + 1;
+ std::vector<djinterop::waveform_entry> externalWaveform;
+ externalWaveform.reserve(externalWaveformSize);
+ for (int64_t i = 0; i < externalWaveformSize; ++i) {
+ auto j = pWaveform->getDataSize() * i / externalWaveformSize;
+ externalWaveform.push_back({{pWaveform->getLow(j), 127},
+ {pWaveform->getMid(j), 127},
+ {pWaveform->getHigh(j), 127}});
+ }
+ externalTrack.set_waveform(std::move(externalWaveform));
+ } else {
+ qInfo() << "No waveform data found for track" << pTrack->getId()
+ << "(" << pTrack->getFileInfo().fileName() << ")";
+ }
+}
+
+void exportTrack(
+ const EnginePrimeExportRequest& request,
+ djinterop::database& db,
+ QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap,
+ const TrackPointer pTrack,
+ std::unique_ptr<Waveform> pWaveform) {
+ // Only export supported file types.
+ if (!kSupportedFileTypes.contains(pTrack->getType())) {
+ qInfo() << "Skipping file" << pTrack->getFileInfo().fileName()
+ << "(id" << pTrack->getId() << ") as its file type"
+ << pTrack->getType() << "is not supported";
+ return;
+ }
+
+ // Copy the file, if required.
+ auto musicFileRelativePath = exportFile(request, pTrack);
+
+ // Export meta-data.
+ exportMetadata(db,
+ mixxxToEnginePrimeTrackIdMap,
+ pTrack,
+ std::move(pWaveform),
+ musicFileRelativePath);
+}
+
+void exportCrate(
+ djinterop::crate& extRootCrate,
+ QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap,
+ const Crate& crate,
+ const QList<TrackId>& trackIds) {
+ // Create a new crate as a sub-crate of the top-level Mixxx crate.
+ auto extCrate = extRootCrate.create_sub_crate(crate.getName().toStdString());
+
+ // Loop through all track ids in this crate and add.
+ for (auto iter = trackIds.cbegin(); iter != trackIds.cend(); ++iter) {
+ auto trackId = *iter;
+ auto extTrackId = mixxxToEnginePrimeTrackIdMap[trackId];
+ extCrate.add_track(extTrackId);
+ }
+}
+
+} // namespace
+
+EnginePrimeExportJob::EnginePrimeExportJob(
+ QObject* parent,
+ TrackCollectionManager* pTrackCollectionManager,
+ EnginePrimeExportRequest request)
+ : QThread(parent),
+ m_pTrackCollectionManager(pTrackCollectionManager),
+ m_request{std::move(request)} {
+ // Must be collocated with the TrackCollectionManager.
+ DEBUG_ASSERT(m_pTrackCollectionManager != nullptr);
+ moveToThread(m_pTrackCollectionManager->thread());
+}
+
+void EnginePrimeExportJob::loadIds(QSet<CrateId> crateIds) {
+ // Note: this slot exists to ensure the track collection is never accessed
+ // from outside its own thread.
+ DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread());
+ QMutexLocker lock{&m_mainThreadLoadMutex};
+
+ if (crateIds.isEmpty()) {
+ // No explicit crate ids specified, meaning we want to export the
+ // whole library, plus all non-empty crates. Start by building a list
+ // of unique track refs from all directories in the library.
+ qDebug() << "Loading all track refs and crate ids...";
+ QSet<TrackRef> trackRefs;
+ auto dirs = m_pTrackCollectionManager->internalCollection()
+ ->getDirectoryDAO()
+ .getDirs();
+ for (auto& dir : dirs) {
+ trackRefs.unite(m_pTrackCollectionManager->internalCollection()
+ ->getTrackDAO()
+ .getAllTrackRefs(dir)
+ .toSet());
+ }
+
+ m_trackRefs = trackRefs.toList();
+
+ // Convert a list of track refs to a list of track ids, and use that
+ // to identify all crates that contain those tracks.
+ QList<TrackId> trackIds;
+ for (auto& trackRef : trackRefs) {
+ trackIds.append(trackRef.getId());
+ }
+ m_crateIds = m_pTrackCollectionManager->internalCollection()
+ ->crates()
+ .collectCrateIdsOfTracks(trackIds)
+ .toList();
+ } else {
+ // Explicit crates have been specified to export.
+ qDebug() << "Loading track refs from" << crateIds.size() << "crate(s)";
+ m_crateIds = crateIds.toList();
+
+ // Identify track refs from the specified crates.
+ m_trackRefs.clear();
+ for (auto& crateId : crateIds) {
+ auto result = m_pTrackCollectionManager->internalCollection()
+ ->crates()
+ .selectCrateTracksSorted(crateId);
+ while (result.next()) {
+ auto trackId = result.trackId();
+ auto location = m_pTrackCollectionManager->internalCollection()
+ ->getTrackDAO()
+ .getTrackLocation(trackId);
+ auto trackFile = TrackFile(location);
+ m_trackRefs.append(TrackRef::fromFileInfo(trackFile, trackId));
+ }
+ }
+ }
+
+ // Inform the worker thread that some main-thread loading has completed.
+ m_waitForMainThreadLoad.wakeAll();
+}
+
+void EnginePrimeExportJob::loadTrack(TrackRef trackRef) {
+ // Note: this slot exists to ensure the track collection is never accessed
+ // from outside its own thread.
+ DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread());
+ QMutexLocker lock{&m_mainThreadLoadMutex};
+
+ // Load the track.
+ m_pLastLoadedTrack = m_pTrackCollectionManager->getOrAddTrack(trackRef);
+
+ // Load high-resolution waveform from analysis info.
+ auto& analysisDao = m_pTrackCollectionManager->internalCollection()->getAnalysisDAO();
+ auto waveformAnalyses = analysisDao.getAnalysesForTrackByType(
+ m_pLastLoadedTrack->getId(), AnalysisDao::TYPE_WAVEFORM);
+ if (!waveformAnalyses.isEmpty()) {
+ auto& waveformAnalysis = waveformAnalyses.first();
+ m_pLastLoadedWaveform.reset(
+ WaveformFactory::loadWaveformFromAnalysis(waveformAnalysis));
+ }
+
+ // Inform the worker thread that some main-thread loading has completed.
+ m_waitForMainThreadLoad.wakeAll();
+}
+
+void EnginePrimeExportJob::loadCrate(CrateId crateId) {
+ // Note: this slot exists to ensure the track collection is never accessed
+ // from outside its own thread.
+ DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread());
+ QMutexLocker lock{&m_mainThreadLoadMutex};
+
+ // Load crate details.
+ m_pTrackCollectionManager->internalCollection()->crates().readCrateById(
+ crateId, &m_lastLoadedCrate);
+
+ // Loop through all track ids in this crate and add to a list.
+ auto result = m_pTrackCollectionManager->internalCollection()
+ ->crates()
+ .selectCrateTracksSorted(crateId);
+ m_lastLoadedCrateTrackIds.clear();
+ while (result.next()) {
+ m_lastLoadedCrateTrackIds.append(result.trackId());
+ }
+
+ // Inform the worker thread that some main-thread loading has completed.
+ m_waitForMainThreadLoad.wakeAll();
+}
+
+void EnginePrimeExportJob::run() {
+ // Crate music directory if it doesn't already exist.
+ QDir().mkpath(m_request.musicFilesDir.path());
+
+ // Load ids of tracks and crates to export.
+ // Note that loading must happen on the same thread as the track collection
+ // manager, which is not the same as this method's worker thread.
+ {
+ QMutexLocker lock{&m_mainThreadLoadMutex};
+ QMetaObject::invokeMethod(
+ this,
+ "loadIds",
+ Q_ARG(QSet<CrateId>, m_request.cr