From af5a36f26a49eef374f95f1cec906170bc65f556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 11 Aug 2019 16:17:00 +0200 Subject: [PATCH 001/103] Remove unused unremove flag from addMultipleTracks() --- src/library/analysisfeature.cpp | 2 +- src/library/autodj/autodjfeature.cpp | 2 +- src/library/crate/cratefeature.cpp | 2 +- src/library/crate/cratetablemodel.cpp | 2 +- src/library/dao/trackdao.cpp | 46 ++++++++++++--------------- src/library/dao/trackdao.h | 2 +- src/library/librarytablemodel.cpp | 2 +- src/library/mixxxlibraryfeature.cpp | 2 +- src/library/playlistfeature.cpp | 2 +- src/library/playlisttablemodel.cpp | 2 +- 10 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/library/analysisfeature.cpp b/src/library/analysisfeature.cpp index 548c7c1f05c3..ee9ee88834f8 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysisfeature.cpp @@ -196,7 +196,7 @@ bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { Q_UNUSED(pSource); QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); + QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); analyzeTracks(trackIds); return trackIds.size() > 0; } diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 0edbbbae7fa4..f0ce44d49cda 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -147,7 +147,7 @@ bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { trackIds = m_pTrackCollection->getTrackDAO().getTrackIds(files); m_pTrackCollection->unhideTracks(trackIds); } else { - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); + trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); } // remove tracks that could not be added diff --git a/src/library/crate/cratefeature.cpp b/src/library/crate/cratefeature.cpp index 5487ff36dc6f..38ab09adb56c 100644 --- a/src/library/crate/cratefeature.cpp +++ b/src/library/crate/cratefeature.cpp @@ -210,7 +210,7 @@ bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, m_pTrackCollection->unhideTracks(trackIds); } else { // Adds track, does not insert duplicates, handles unremoving logic. - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); + trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); } qDebug() << "CrateFeature::dropAcceptChild adding tracks" << trackIds.size() << " to crate "<< crateId; diff --git a/src/library/crate/cratetablemodel.cpp b/src/library/crate/cratetablemodel.cpp index ca059ffe10a9..7e808a4430b8 100644 --- a/src/library/crate/cratetablemodel.cpp +++ b/src/library/crate/cratetablemodel.cpp @@ -154,7 +154,7 @@ int CrateTableModel::addTracks(const QModelIndex& index, } } - QList trackIds(m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList, true)); + QList trackIds(m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList)); if (m_pTrackCollection->addCrateTracks(m_selectedCrate, trackIds)) { select(); return trackIds.size(); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 282b05a86f72..39d439b4057a 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -688,8 +688,7 @@ TrackPointer TrackDAO::addSingleTrack(const QFileInfo& fileInfo, bool unremove) } QList TrackDAO::addMultipleTracks( - const QList& fileInfoList, - bool unremove) { + const QList& fileInfoList) { // Prepare to add tracks to the database. // This also begins an SQL transaction. addTracksPrepare(); @@ -717,29 +716,26 @@ QList TrackDAO::addMultipleTracks( index++; } - // If imported-playlist tracks are to be unremoved, do that for all playlist - // tracks that were already in the database. - if (unremove) { - query.prepare("SELECT library.id FROM playlist_import, " - "track_locations, library WHERE library.location = track_locations.id " - "AND playlist_import.location = track_locations.location"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } + // Unremove all playlist tracks, that were already in the database. + query.prepare("SELECT library.id FROM playlist_import, " + "track_locations, library WHERE library.location = track_locations.id " + "AND playlist_import.location = track_locations.location"); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } - int idColumn = query.record().indexOf("id"); - QStringList idStringList; - while (query.next()) { - TrackId trackId(query.value(idColumn)); - idStringList.append(trackId.toString()); - } + int idColumn = query.record().indexOf("id"); + QStringList idStringList; + while (query.next()) { + TrackId trackId(query.value(idColumn)); + idStringList.append(trackId.toString()); + } - query.prepare(QString("UPDATE library SET mixxx_deleted=0 " - "WHERE id in (%1) AND mixxx_deleted=1") - .arg(idStringList.join(","))); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } + query.prepare(QString("UPDATE library SET mixxx_deleted=0 " + "WHERE id in (%1) AND mixxx_deleted=1") + .arg(idStringList.join(","))); + if (!query.exec()) { + LOG_FAILED_QUERY(query); } // Any tracks not already in the database need to be added. @@ -753,7 +749,7 @@ QList TrackDAO::addMultipleTracks( while (query.next()) { int addIndex = query.value(addIndexColumn).toInt(); const QFileInfo fileInfo(fileInfoList.at(addIndex)); - addTracksAddFile(fileInfo, unremove); + addTracksAddFile(fileInfo, true); } // Now that we have imported any tracks that were not already in the @@ -768,7 +764,7 @@ QList TrackDAO::addMultipleTracks( if (!query.exec()) { LOG_FAILED_QUERY(query); } - int idColumn = query.record().indexOf("id"); + idColumn = query.record().indexOf("id"); while (query.next()) { TrackId trackId(query.value(idColumn)); trackIds.append(trackId); diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index f3ce490ccadb..76549c7446a4 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -51,7 +51,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC QString getTrackLocation(TrackId trackId); TrackPointer addSingleTrack(const QFileInfo& fileInfo, bool unremove); - QList addMultipleTracks(const QList& fileInfoList, bool unremove); + QList addMultipleTracks(const QList& fileInfoList); void addTracksPrepare(); TrackPointer addTracksAddFile(const QFileInfo& fileInfo, bool unremove); diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index 3d79457bea54..f4a4c838e763 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -65,7 +65,7 @@ int LibraryTableModel::addTracks(const QModelIndex& index, foreach (QString fileLocation, locations) { fileInfoList.append(QFileInfo(fileLocation)); } - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList, true); + QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList); select(); return trackIds.size(); } diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 199a2926df48..36323c4f105f 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -184,7 +184,7 @@ bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_trackDao.addMultipleTracks(files, true); + QList trackIds = m_trackDao.addMultipleTracks(files); return trackIds.size() > 0; } } diff --git a/src/library/playlistfeature.cpp b/src/library/playlistfeature.cpp index 197f57fb76de..2266769f02d0 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/playlistfeature.cpp @@ -116,7 +116,7 @@ bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls // library, then add the track to the library before adding it to the // playlist. // Adds track, does not insert duplicates, handles unremoving logic. - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files, true); + trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); } // remove tracks that could not be added diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index e1bc686c7d37..c6bc0d48a3bc 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -94,7 +94,7 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, } } - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList, true); + QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList); int tracksAdded = m_pTrackCollection->getPlaylistDAO().insertTracksIntoPlaylist( trackIds, m_iPlaylistId, position); From 5ad6d78b831d490072e705cc0aef52ea411aab81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 11 Aug 2019 16:22:22 +0200 Subject: [PATCH 002/103] Remove superfluid transaction around a single sql statment, which was an a wron application layer anyway. --- src/library/dao/trackdao.cpp | 2 +- src/library/dao/trackdao.h | 2 +- src/library/trackcollection.cpp | 10 +--------- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 39d439b4057a..a16135d31951 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -807,7 +807,7 @@ void TrackDAO::afterHidingTracks( // up in the library views again. // This function should get called if you drag-and-drop a file that's been // "hidden" from Mixxx back into the library view. -bool TrackDAO::onUnhidingTracks( +bool TrackDAO::unhideTracks( const QList& trackIds) { QStringList idList; for (const auto& trackId: trackIds) { diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 76549c7446a4..e67a23ec7494 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -63,7 +63,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC void afterHidingTracks( const QList& trackIds); - bool onUnhidingTracks( + bool unhideTracks( const QList& trackIds); void afterUnhidingTracks( const QList& trackIds); diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 62c0b64ab4cb..b652d013e18d 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -156,15 +156,7 @@ bool TrackCollection::hideTracks(const QList& trackIds) { bool TrackCollection::unhideTracks(const QList& trackIds) { DEBUG_ASSERT(QApplication::instance()->thread() == QThread::currentThread()); - // Transactional - SqlTransaction transaction(m_database); - VERIFY_OR_DEBUG_ASSERT(transaction) { - return false; - } - VERIFY_OR_DEBUG_ASSERT(m_trackDao.onUnhidingTracks(trackIds)) { - return false; - } - VERIFY_OR_DEBUG_ASSERT(transaction.commit()) { + VERIFY_OR_DEBUG_ASSERT(m_trackDao.unhideTracks(trackIds)) { return false; } From 901095c3a513eb542f983f0b64b98a1c696c0f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 11 Aug 2019 23:27:37 +0200 Subject: [PATCH 003/103] introduce new function getAndEnsureTrackIds() --- src/library/autodj/autodjfeature.cpp | 3 +-- src/library/crate/cratefeature.cpp | 3 +-- src/library/playlistfeature.cpp | 3 +-- src/library/trackcollection.cpp | 14 ++++++++++++-- src/library/trackcollection.h | 4 ++++ 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index f0ce44d49cda..798e30f8a20f 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -144,8 +144,7 @@ bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); QList trackIds; if (pSource) { - trackIds = m_pTrackCollection->getTrackDAO().getTrackIds(files); - m_pTrackCollection->unhideTracks(trackIds); + trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); } else { trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); } diff --git a/src/library/crate/cratefeature.cpp b/src/library/crate/cratefeature.cpp index 38ab09adb56c..f59f7f4f56aa 100644 --- a/src/library/crate/cratefeature.cpp +++ b/src/library/crate/cratefeature.cpp @@ -206,8 +206,7 @@ bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); QList trackIds; if (pSource) { - trackIds = m_pTrackCollection->getTrackDAO().getTrackIds(files); - m_pTrackCollection->unhideTracks(trackIds); + trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); } else { // Adds track, does not insert duplicates, handles unremoving logic. trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); diff --git a/src/library/playlistfeature.cpp b/src/library/playlistfeature.cpp index 2266769f02d0..73644efea63f 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/playlistfeature.cpp @@ -109,8 +109,7 @@ bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls QList trackIds; if (pSource) { - trackIds = m_pTrackCollection->getTrackDAO().getTrackIds(files); - m_pTrackCollection->unhideTracks(trackIds); + trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); } else { // If a track is dropped onto a playlist's name, but the track isn't in the // library, then add the track to the library before adding it to the diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index b652d013e18d..314b46d97b0e 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -87,6 +87,13 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { GlobalTrackCacheLocker().relocateCachedTracks(&m_trackDao); } +QList TrackCollection::getAndEnsureTrackIds( + const QList& files) { + QList trackIds = m_trackDao.getTrackIds(files); + unhideTracks(trackIds); + return trackIds; +} + bool TrackCollection::hideTracks(const QList& trackIds) { DEBUG_ASSERT(QApplication::instance()->thread() == QThread::currentThread()); @@ -162,13 +169,16 @@ bool TrackCollection::unhideTracks(const QList& trackIds) { // Post-processing // TODO(XXX): Move signals from TrackDAO to TrackCollection + // To update BaseTrackCache m_trackDao.afterUnhidingTracks(trackIds); // TODO(XXX): Move signals from TrackDAO to TrackCollection // Emit signal(s) // TODO(XXX): Emit signals here instead of from DAOs - QSet modifiedCrateSummaries( - m_crates.collectCrateIdsOfTracks(trackIds)); + // To update labels of CrateFeature, because unhiding might make a + // crate track visible again. + QSet modifiedCrateSummaries = + m_crates.collectCrateIdsOfTracks(trackIds); emit(crateSummaryChanged(modifiedCrateSummaries)); return true; diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index eefd00bb9a78..ed63a8b4b866 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -66,6 +66,10 @@ class TrackCollection : public QObject, void relocateDirectory(QString oldDir, QString newDir); + // This function returns a track ID of all file in the list not already visible, + // it adds and unhides the tracks as well. + QList getAndEnsureTrackIds(const QList& files); + bool hideTracks(const QList& trackIds); bool unhideTracks(const QList& trackIds); From 0e9b7272de191aed85cb7ea8aae2b539860a7a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Mon, 12 Aug 2019 18:36:36 +0200 Subject: [PATCH 004/103] Use a temp table to keep sorting unchanged in TrackDAO::getTrackIds() --- src/library/dao/trackdao.cpp | 53 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index a16135d31951..9b1c4d6f553f 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -139,18 +139,45 @@ QList TrackDAO::getTrackIds(const QList& files) { QList trackIds; trackIds.reserve(files.size()); + // Create a temporary database of the paths of all the imported tracks. QSqlQuery query(m_database); - { - QStringList pathList; - pathList.reserve(files.size()); - for (const auto& file: files) { - pathList << file.absoluteFilePath(); - } - query.prepare(QString("SELECT library.id FROM library INNER JOIN " - "track_locations ON library.location = track_locations.id " - "WHERE track_locations.location in (%1)").arg( - SqlStringFormatter::formatList(m_database, pathList))); + query.prepare( + "CREATE TEMP TABLE playlist_import " + "(location varchar (512))"); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return trackIds; + } + + QStringList pathList; + pathList.reserve(files.size()); + for (const auto& file: files) { + pathList << "(" + SqlStringFormatter::format(m_database, file.absoluteFilePath()) + ")"; + } + + // Add all the track paths temporary to this database. + query.prepare( + "INSERT INTO playlist_import (location) " + "VALUES " + pathList.join(',')); + if (!query.exec()) { + LOG_FAILED_QUERY(query); } + + query.prepare( + "SELECT library.id FROM playlist_import " + "INNER JOIN track_locations ON playlist_import.location = track_locations.location " + "INNER JOIN library ON library.location = track_locations.id " + // the order by clause enforces the native sorting which is used anyway + // hopefully optimized away. TODO() verify. + "ORDER BY playlist_import.ROWID"); + + // Old syntax for a shorter but less readable query. TODO() check performance gain + // query.prepare( + // "SELECT library.id FROM playlist_import, " + // "track_locations, library WHERE library.location = track_locations.id " + // "AND playlist_import.location = track_locations.location"); + // "ORDER BY playlist_import.ROWID"); + if (query.exec()) { const int idColumn = query.record().indexOf("id"); while (query.next()) { @@ -164,6 +191,12 @@ QList TrackDAO::getTrackIds(const QList& files) { LOG_FAILED_QUERY(query); } + // Drop the temporary playlist-import table. + query.prepare("DROP TABLE IF EXISTS playlist_import"); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + return trackIds; } From af85c1c4e5b3584402095c85e760b83cb476ffc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Mon, 12 Aug 2019 23:32:52 +0200 Subject: [PATCH 005/103] Improve query in TrackDAO::unhideTracks() --- src/library/dao/trackdao.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 9b1c4d6f553f..30f761ad985f 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -846,9 +846,10 @@ bool TrackDAO::unhideTracks( for (const auto& trackId: trackIds) { idList.append(trackId.toString()); } - FwdSqlQuery query(m_database, QString( + FwdSqlQuery query(m_database, "UPDATE library SET mixxx_deleted=0 " - "WHERE id in (%1)").arg(idList.join(","))); + "WHERE mixxx_deleted!=0 " + "AND id in (" + idList.join(",") + ")"); return !query.hasError() && query.execPrepared(); } From 5eb8ac0f398e0c81a967e9b0403e9ec52c339d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 13 Aug 2019 01:35:27 +0200 Subject: [PATCH 006/103] Add the addMissingTracks fature to TrackDAO::getTrackIds() and make use of it. --- src/library/autodj/autodjfeature.cpp | 27 ++++++++++++------------ src/library/crate/cratefeature.cpp | 31 ++++++++++++++-------------- src/library/dao/trackdao.cpp | 26 ++++++++++++++++++++++- src/library/dao/trackdao.h | 2 +- src/library/playlistfeature.cpp | 31 ++++++++++++++-------------- src/library/trackcollection.cpp | 4 ++-- src/library/trackcollection.h | 2 +- 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 798e30f8a20f..6387c8a27a79 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -138,25 +138,24 @@ void AutoDJFeature::activate() { } bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { - // If a track is dropped onto a playlist's name, but the track isn't in the - // library, then add the track to the library before adding it to the - // playlist. QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - QList trackIds; - if (pSource) { - trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); - } else { - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); + if (!files.size()) { + return false; } - // remove tracks that could not be added - for (int trackIdIndex = 0; trackIdIndex < trackIds.size(); trackIdIndex++) { - if (!trackIds.at(trackIdIndex).isValid()) { - trackIds.removeAt(trackIdIndex--); - } + // If a track is dropped onto the Auto DJ tree node, but the track isn't in the + // library, then add the track to the library before adding it to the + // Auto DJ playlist. getAndEnsureTrackIds(), does not insert duplicates and handles + // unremove logic. + // pSource != nullptr it is a drop from inside Mixxx and indicates all + // tracks already in the DB + bool addMissingTracks = (pSource == nullptr); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + if (!trackIds.size()) { + return false; } - // Return whether the tracks were appended. + // Return whether appendTracksToPlaylist succeeded. return m_playlistDao.appendTracksToPlaylist(trackIds, m_iAutoDJPlaylistId); } diff --git a/src/library/crate/cratefeature.cpp b/src/library/crate/cratefeature.cpp index f59f7f4f56aa..1dbd770fef80 100644 --- a/src/library/crate/cratefeature.cpp +++ b/src/library/crate/cratefeature.cpp @@ -200,25 +200,26 @@ void updateTreeItemForTrackSelection( bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource) { CrateId crateId(crateIdFromIndex(index)); - if (!crateId.isValid()) { + VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) { return false; } QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - QList trackIds; - if (pSource) { - trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); - } else { - // Adds track, does not insert duplicates, handles unremoving logic. - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); - } - qDebug() << "CrateFeature::dropAcceptChild adding tracks" - << trackIds.size() << " to crate "<< crateId; - // remove tracks that could not be added - for (int trackIdIndex = 0; trackIdIndex < trackIds.size(); ++trackIdIndex) { - if (!trackIds.at(trackIdIndex).isValid()) { - trackIds.removeAt(trackIdIndex--); - } + if (!files.size()) { + return false; } + + // If a track is dropped onto a crate's name, but the track isn't in the + // library, then add the track to the library before adding it to the + // playlist. getAndEnsureTrackIds(), does not insert duplicates and handles + // unremove logic. + // pSource != nullptr it is a drop from inside Mixxx and indicates all + // tracks already in the DB + bool addMissingTracks = (pSource == nullptr); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + if (!trackIds.size()) { + return false; + } + m_pTrackCollection->addCrateTracks(crateId, trackIds); return true; } diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 30f761ad985f..1189e6827f47 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -135,7 +135,8 @@ TrackId TrackDAO::getTrackId(const QString& absoluteFilePath) { return trackId; } -QList TrackDAO::getTrackIds(const QList& files) { +QList TrackDAO::getTrackIds(const QList& files, + bool addMissingTracks) { QList trackIds; trackIds.reserve(files.size()); @@ -163,6 +164,29 @@ QList TrackDAO::getTrackIds(const QList& files) { LOG_FAILED_QUERY(query); } + if (addMissingTracks) { + // Prepare to add tracks to the database. + // This also begins an SQL transaction. + addTracksPrepare(); + + // Any tracks not already in the database need to be added. + query.prepare("SELECT location FROM playlist_import " + "WHERE NOT EXISTS (SELECT location FROM track_locations " + "WHERE playlist_import.location = track_locations.location)"); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } + const int locationColumn = query.record().indexOf("location"); + while (query.next()) { + QString location = query.value(locationColumn).toString(); + const QFileInfo fileInfo(location); + addTracksAddFile(fileInfo, true); + } + + // Finish adding tracks to the database. + addTracksFinish(); + } + query.prepare( "SELECT library.id FROM playlist_import " "INNER JOIN track_locations ON playlist_import.location = track_locations.location " diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index e67a23ec7494..3abfebf7ab59 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -40,7 +40,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC void finish(); TrackId getTrackId(const QString& absoluteFilePath); - QList getTrackIds(const QList& files); + QList getTrackIds(const QList& files, bool addMissingTracks); QList getTrackIds(const QDir& dir); // WARNING: Only call this from the main thread instance of TrackDAO. diff --git a/src/library/playlistfeature.cpp b/src/library/playlistfeature.cpp index 73644efea63f..0966fc8b4331 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/playlistfeature.cpp @@ -103,26 +103,25 @@ void PlaylistFeature::onRightClickChild(const QPoint& globalPos, QModelIndex ind bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls, QObject* pSource) { int playlistId = playlistIdFromIndex(index); - //m_playlistDao.appendTrackToPlaylist(url.toLocalFile(), playlistId); + VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) { + return false; + } QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - - QList trackIds; - if (pSource) { - trackIds = m_pTrackCollection->getAndEnsureTrackIds(files); - } else { - // If a track is dropped onto a playlist's name, but the track isn't in the - // library, then add the track to the library before adding it to the - // playlist. - // Adds track, does not insert duplicates, handles unremoving logic. - trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); + if (!files.size()) { + return false; } - // remove tracks that could not be added - for (int trackIdIndex = 0; trackIdIndex < trackIds.size(); ++trackIdIndex) { - if (!trackIds.at(trackIdIndex).isValid()) { - trackIds.removeAt(trackIdIndex--); - } + // If a track is dropped onto a playlist's name, but the track isn't in the + // library, then add the track to the library before adding it to the + // playlist. getAndEnsureTrackIds(), does not insert duplicates and handles + // unremove logic. + // pSource != nullptr it is a drop from inside Mixxx and indicates all + // tracks already in the DB + bool addMissingTracks = (pSource == nullptr); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + if (!trackIds.size()) { + return false; } // Return whether appendTracksToPlaylist succeeded. diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 314b46d97b0e..ff68351687cb 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -88,8 +88,8 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { } QList TrackCollection::getAndEnsureTrackIds( - const QList& files) { - QList trackIds = m_trackDao.getTrackIds(files); + const QList& files, bool addMissingTracks) { + QList trackIds = m_trackDao.getTrackIds(files, addMissingTracks); unhideTracks(trackIds); return trackIds; } diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index ed63a8b4b866..c880ae54edd2 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -68,7 +68,7 @@ class TrackCollection : public QObject, // This function returns a track ID of all file in the list not already visible, // it adds and unhides the tracks as well. - QList getAndEnsureTrackIds(const QList& files); + QList getAndEnsureTrackIds(const QList& files, bool addMissingTracks); bool hideTracks(const QList& trackIds); bool unhideTracks(const QList& trackIds); From acb844454ea098c89a3aa72a72196b4fcbd17219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 13 Aug 2019 02:13:23 +0200 Subject: [PATCH 007/103] Completely replace obsolete TrackDAO::addMultipleTracks() calls by getAndEnsureTrackIds() --- src/library/analysisfeature.cpp | 2 +- src/library/crate/cratetablemodel.cpp | 6 +- src/library/dao/trackdao.cpp | 96 --------------------------- src/library/dao/trackdao.h | 2 - src/library/librarytablemodel.cpp | 2 +- src/library/mixxxlibraryfeature.cpp | 2 +- src/library/playlisttablemodel.cpp | 8 +-- 7 files changed, 8 insertions(+), 110 deletions(-) diff --git a/src/library/analysisfeature.cpp b/src/library/analysisfeature.cpp index ee9ee88834f8..39ac2d3cf641 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysisfeature.cpp @@ -196,7 +196,7 @@ bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { Q_UNUSED(pSource); QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(files); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, true); analyzeTracks(trackIds); return trackIds.size() > 0; } diff --git a/src/library/crate/cratetablemodel.cpp b/src/library/crate/cratetablemodel.cpp index 7e808a4430b8..ef3e5e57df9c 100644 --- a/src/library/crate/cratetablemodel.cpp +++ b/src/library/crate/cratetablemodel.cpp @@ -149,12 +149,10 @@ int CrateTableModel::addTracks(const QModelIndex& index, QList fileInfoList; foreach(QString fileLocation, locations) { QFileInfo fileInfo(fileLocation); - if (fileInfo.exists()) { - fileInfoList.append(fileInfo); - } + fileInfoList.append(fileInfo); } - QList trackIds(m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList)); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); if (m_pTrackCollection->addCrateTracks(m_selectedCrate, trackIds)) { select(); return trackIds.size(); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 1189e6827f47..d15fe064cebe 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -744,102 +744,6 @@ TrackPointer TrackDAO::addSingleTrack(const QFileInfo& fileInfo, bool unremove) return pTrack; } -QList TrackDAO::addMultipleTracks( - const QList& fileInfoList) { - // Prepare to add tracks to the database. - // This also begins an SQL transaction. - addTracksPrepare(); - - // Create a temporary database of the paths of all the imported tracks. - QSqlQuery query(m_database); - query.prepare("CREATE TEMP TABLE playlist_import " - "(add_index INTEGER PRIMARY KEY, location varchar (512))"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - addTracksFinish(true); - return QList(); - } - - // Add all the track paths to this database. - query.prepare("INSERT INTO playlist_import (add_index, location) " - "VALUES (:add_index, :location)"); - int index = 0; - for (const auto& fileInfo: fileInfoList) { - query.bindValue(":add_index", index); - query.bindValue(":location", fileInfo.absoluteFilePath()); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - index++; - } - - // Unremove all playlist tracks, that were already in the database. - query.prepare("SELECT library.id FROM playlist_import, " - "track_locations, library WHERE library.location = track_locations.id " - "AND playlist_import.location = track_locations.location"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - - int idColumn = query.record().indexOf("id"); - QStringList idStringList; - while (query.next()) { - TrackId trackId(query.value(idColumn)); - idStringList.append(trackId.toString()); - } - - query.prepare(QString("UPDATE library SET mixxx_deleted=0 " - "WHERE id in (%1) AND mixxx_deleted=1") - .arg(idStringList.join(","))); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - - // Any tracks not already in the database need to be added. - query.prepare("SELECT add_index, location FROM playlist_import " - "WHERE NOT EXISTS (SELECT location FROM track_locations " - "WHERE playlist_import.location = track_locations.location)"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - const int addIndexColumn = query.record().indexOf("add_index"); - while (query.next()) { - int addIndex = query.value(addIndexColumn).toInt(); - const QFileInfo fileInfo(fileInfoList.at(addIndex)); - addTracksAddFile(fileInfo, true); - } - - // Now that we have imported any tracks that were not already in the - // library, re-select ordering by playlist_import.add_index to return - // the list of track ids in the order that they were requested to be - // added. - QList trackIds; - query.prepare("SELECT library.id FROM playlist_import, " - "track_locations, library WHERE library.location = track_locations.id " - "AND playlist_import.location = track_locations.location " - "ORDER BY playlist_import.add_index"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - idColumn = query.record().indexOf("id"); - while (query.next()) { - TrackId trackId(query.value(idColumn)); - trackIds.append(trackId); - } - - // Drop the temporary playlist-import table. - query.prepare("DROP TABLE IF EXISTS playlist_import"); - if (!query.exec()) { - LOG_FAILED_QUERY(query); - } - - // Finish adding tracks to the database. - addTracksFinish(); - - // Return the list of track IDs added to the database. - return trackIds; -} - bool TrackDAO::onHidingTracks( const QList& trackIds) { QStringList idList; diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 3abfebf7ab59..52824ad03017 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -51,8 +51,6 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC QString getTrackLocation(TrackId trackId); TrackPointer addSingleTrack(const QFileInfo& fileInfo, bool unremove); - QList addMultipleTracks(const QList& fileInfoList); - void addTracksPrepare(); TrackPointer addTracksAddFile(const QFileInfo& fileInfo, bool unremove); TrackId addTracksAddTrack(const TrackPointer& pTrack, bool unremove); diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index f4a4c838e763..5805125c15ca 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -65,7 +65,7 @@ int LibraryTableModel::addTracks(const QModelIndex& index, foreach (QString fileLocation, locations) { fileInfoList.append(QFileInfo(fileLocation)); } - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); select(); return trackIds.size(); } diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 36323c4f105f..7eb29f42a4d1 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -184,7 +184,7 @@ bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_trackDao.addMultipleTracks(files); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, true); return trackIds.size() > 0; } } diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index c6bc0d48a3bc..3b419f889aac 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -89,15 +89,13 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, QList fileInfoList; foreach (QString fileLocation, locations) { QFileInfo fileInfo(fileLocation); - if (fileInfo.exists()) { - fileInfoList.append(fileInfo); - } + fileInfoList.append(fileInfo); } - QList trackIds = m_pTrackCollection->getTrackDAO().addMultipleTracks(fileInfoList); + QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); int tracksAdded = m_pTrackCollection->getPlaylistDAO().insertTracksIntoPlaylist( - trackIds, m_iPlaylistId, position); + trackIds, m_iPlaylistId, position); if (locations.size() - tracksAdded > 0) { qDebug() << "PlaylistTableModel::addTracks could not add" From 29b81a7e58a9ac93acedb4f153308e1e2c8f8cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 18 Aug 2019 12:43:18 +0200 Subject: [PATCH 008/103] Renamed resolveTrackIds() and add enum fag option parameter --- src/library/analysisfeature.cpp | 5 +++-- src/library/autodj/autodjfeature.cpp | 10 ++++++---- src/library/crate/cratefeature.cpp | 10 ++++++---- src/library/crate/cratetablemodel.cpp | 4 +++- src/library/dao/trackdao.cpp | 6 +++--- src/library/dao/trackdao.h | 13 ++++++++++++- src/library/librarytablemodel.cpp | 4 +++- src/library/mixxxlibraryfeature.cpp | 6 +++--- src/library/playlistfeature.cpp | 10 ++++++---- src/library/playlisttablemodel.cpp | 4 +++- src/library/trackcollection.cpp | 10 ++++++---- src/library/trackcollection.h | 2 +- 12 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/library/analysisfeature.cpp b/src/library/analysisfeature.cpp index 39ac2d3cf641..058b7bc23e83 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysisfeature.cpp @@ -195,8 +195,9 @@ void AnalysisFeature::cleanupAnalyzer() { bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { Q_UNUSED(pSource); QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, true); + QList trackIds = m_pTrackCollection->resolveTrackIds(files, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); analyzeTracks(trackIds); return trackIds.size() > 0; } diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 6387c8a27a79..1d4eed271faf 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -145,12 +145,14 @@ bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { // If a track is dropped onto the Auto DJ tree node, but the track isn't in the // library, then add the track to the library before adding it to the - // Auto DJ playlist. getAndEnsureTrackIds(), does not insert duplicates and handles - // unremove logic. + // Auto DJ playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - bool addMissingTracks = (pSource == nullptr); - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; + if (pSource == nullptr) { + options |= TrackDAO::ResolveTrackIdOption::AddMissing; + } + QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); if (!trackIds.size()) { return false; } diff --git a/src/library/crate/cratefeature.cpp b/src/library/crate/cratefeature.cpp index 1dbd770fef80..14d375b6e9be 100644 --- a/src/library/crate/cratefeature.cpp +++ b/src/library/crate/cratefeature.cpp @@ -210,12 +210,14 @@ bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, // If a track is dropped onto a crate's name, but the track isn't in the // library, then add the track to the library before adding it to the - // playlist. getAndEnsureTrackIds(), does not insert duplicates and handles - // unremove logic. + // playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - bool addMissingTracks = (pSource == nullptr); - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; + if (pSource == nullptr) { + options |= TrackDAO::ResolveTrackIdOption::AddMissing; + } + QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); if (!trackIds.size()) { return false; } diff --git a/src/library/crate/cratetablemodel.cpp b/src/library/crate/cratetablemodel.cpp index ef3e5e57df9c..05b594632874 100644 --- a/src/library/crate/cratetablemodel.cpp +++ b/src/library/crate/cratetablemodel.cpp @@ -152,7 +152,9 @@ int CrateTableModel::addTracks(const QModelIndex& index, fileInfoList.append(fileInfo); } - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); + QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); if (m_pTrackCollection->addCrateTracks(m_selectedCrate, trackIds)) { select(); return trackIds.size(); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index d15fe064cebe..76362dee0e04 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -135,8 +135,8 @@ TrackId TrackDAO::getTrackId(const QString& absoluteFilePath) { return trackId; } -QList TrackDAO::getTrackIds(const QList& files, - bool addMissingTracks) { +QList TrackDAO::resolveTrackIds(const QList& files, + ResolveTrackIdOptions options) { QList trackIds; trackIds.reserve(files.size()); @@ -164,7 +164,7 @@ QList TrackDAO::getTrackIds(const QList& files, LOG_FAILED_QUERY(query); } - if (addMissingTracks) { + if (options & ResolveTrackIdOption::AddMissing) { // Prepare to add tracks to the database. // This also begins an SQL transaction. addTracksPrepare(); diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 52824ad03017..b007dbd9e4c0 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -24,6 +24,14 @@ class LibraryHashDAO; class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackCacheRelocator { Q_OBJECT public: + + enum class ResolveTrackIdOption : int { + ResolveOnly = 0, + UnhideHidden = 1, + AddMissing = 2 + }; + Q_DECLARE_FLAGS(ResolveTrackIdOptions, ResolveTrackIdOption) + // The 'config object' is necessary because users decide ID3 tags get // synchronized on track metadata change TrackDAO( @@ -40,7 +48,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC void finish(); TrackId getTrackId(const QString& absoluteFilePath); - QList getTrackIds(const QList& files, bool addMissingTracks); + QList resolveTrackIds(const QList& files, ResolveTrackIdOptions options); QList getTrackIds(const QDir& dir); // WARNING: Only call this from the main thread instance of TrackDAO. @@ -157,4 +165,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC DISALLOW_COPY_AND_ASSIGN(TrackDAO); }; + +Q_DECLARE_OPERATORS_FOR_FLAGS(TrackDAO::ResolveTrackIdOptions) + #endif //TRACKDAO_H diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index 5805125c15ca..e3c2059edf16 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -65,7 +65,9 @@ int LibraryTableModel::addTracks(const QModelIndex& index, foreach (QString fileLocation, locations) { fileInfoList.append(QFileInfo(fileLocation)); } - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); + QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); select(); return trackIds.size(); } diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 7eb29f42a4d1..cf38c3a8c3b3 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -182,9 +182,9 @@ bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { return false; } else { QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - - // Adds track, does not insert duplicates, handles unremoving logic. - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, true); + QList trackIds = m_pTrackCollection->resolveTrackIds(files, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); return trackIds.size() > 0; } } diff --git a/src/library/playlistfeature.cpp b/src/library/playlistfeature.cpp index 0966fc8b4331..5b284030a6c4 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/playlistfeature.cpp @@ -114,12 +114,14 @@ bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls // If a track is dropped onto a playlist's name, but the track isn't in the // library, then add the track to the library before adding it to the - // playlist. getAndEnsureTrackIds(), does not insert duplicates and handles - // unremove logic. + // playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - bool addMissingTracks = (pSource == nullptr); - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(files, addMissingTracks); + TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; + if (pSource == nullptr) { + options |= TrackDAO::ResolveTrackIdOption::AddMissing; + } + QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); if (!trackIds.size()) { return false; } diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 3b419f889aac..1abe0b666ee7 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -92,7 +92,9 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, fileInfoList.append(fileInfo); } - QList trackIds = m_pTrackCollection->getAndEnsureTrackIds(fileInfoList, true); + QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); int tracksAdded = m_pTrackCollection->getPlaylistDAO().insertTracksIntoPlaylist( trackIds, m_iPlaylistId, position); diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index ff68351687cb..f2c41811765a 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -87,10 +87,12 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { GlobalTrackCacheLocker().relocateCachedTracks(&m_trackDao); } -QList TrackCollection::getAndEnsureTrackIds( - const QList& files, bool addMissingTracks) { - QList trackIds = m_trackDao.getTrackIds(files, addMissingTracks); - unhideTracks(trackIds); +QList TrackCollection::resolveTrackIds( + const QList& files, TrackDAO::ResolveTrackIdOptions options) { + QList trackIds = m_trackDao.resolveTrackIds(files, options); + if (options & TrackDAO::ResolveTrackIdOption::UnhideHidden) { + unhideTracks(trackIds); + } return trackIds; } diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index c880ae54edd2..21f18e103550 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -68,7 +68,7 @@ class TrackCollection : public QObject, // This function returns a track ID of all file in the list not already visible, // it adds and unhides the tracks as well. - QList getAndEnsureTrackIds(const QList& files, bool addMissingTracks); + QList resolveTrackIds(const QList& files, TrackDAO::ResolveTrackIdOptions options); bool hideTracks(const QList& trackIds); bool unhideTracks(const QList& trackIds); From 2b900a09e949382e5ed92ef09430e7ca601000bf Mon Sep 17 00:00:00 2001 From: Be Date: Sat, 31 Aug 2019 19:43:20 -0500 Subject: [PATCH 009/103] don't identify PortAudio devices by PaDeviceIndex in configuration PortAudio provides no guarantees that the PaDeviceIndex for any device will be stable across restarts of Mixxx. Abusing the PaDeviceIndex as a persistent identifier caused Mixxx to falsely claim that audio interfaces were not available and annoyingly require the user to reconfigure all their sound I/O even when nothing about the available sound hardware actually changed. It is the responsibility of the sound API to provide persistent names for devices to PortAudio. --- src/soundio/sounddeviceportaudio.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index 6b82b536797a..c502b58c73c1 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -107,8 +107,7 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, // Setting parent class members: m_hostAPI = Pa_GetHostApiInfo(deviceInfo->hostApi)->name; m_dSampleRate = deviceInfo->defaultSampleRate; - m_strInternalName = QString("%1, %2").arg(QString::number(m_devId), - deviceInfo->name); + m_strInternalName = deviceInfo->name; m_strDisplayName = QString::fromLocal8Bit(deviceInfo->name); m_iNumInputChannels = m_deviceInfo->maxInputChannels; m_iNumOutputChannels = m_deviceInfo->maxOutputChannels; From 4c8c9856165d18f84df9c0e1b89194677d0b318d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 4 Sep 2019 18:09:50 +0200 Subject: [PATCH 010/103] Increase number of cached chunks --- src/engine/cachingreader.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index d52404df8399..b73e467c34d4 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -20,10 +20,17 @@ mixxx::Logger kLogger("CachingReader"); // TODO() Do we suffer cache misses if we use an audio buffer of above 23 ms? const SINT kDefaultHintFrames = 1024; -// currently CachingReaderWorker::kCachingReaderChunkLength is 65536 (0x10000); -// For 80 chunks we need 5242880 (0x500000) bytes (5 MiB) of Memory -//static -const SINT kNumberOfCachedChunksInMemory = 80; +// With CachingReaderChunk::kFrames = 8192 each chunk consumes +// 8192 frames * 2 channels/frame * 4-bytes per sample = 65 kB. +// +// NOTE(2019-09-04): https://bugs.launchpad.net/mixxx/+bug/1842679 +// According to the linked bug report reserving only 80 chunks in +// version 2.2.2 for doesn't seem to be sufficient to prevent running +// out of free chunks. Therefore we increased the number of cached +// chunks to 256: +// 80 chunks -> 5120 KB = 5 MB +// 256 chunks -> 16384 KB = 16 MB +const SINT kNumberOfCachedChunksInMemory = 256; } // anonymous namespace From 0c663d1729c0b0c3ddb6fdf4309389c36740ae5a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 4 Sep 2019 23:19:12 +0200 Subject: [PATCH 011/103] Reduce and attach size of FIFOs to number of cached chunks --- src/engine/cachingreader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index b73e467c34d4..39629bd42b2f 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -38,8 +38,8 @@ const SINT kNumberOfCachedChunksInMemory = 256; CachingReader::CachingReader(QString group, UserSettingsPointer config) : m_pConfig(config), - m_chunkReadRequestFIFO(1024), - m_readerStatusFIFO(1024), + m_chunkReadRequestFIFO(kNumberOfCachedChunksInMemory), + m_readerStatusFIFO(kNumberOfCachedChunksInMemory), m_readerStatus(INVALID), m_mruCachingReaderChunk(nullptr), m_lruCachingReaderChunk(nullptr), From 5c66430a633c1313622f1966133a2ff7213bfe4c Mon Sep 17 00:00:00 2001 From: Philip Gottschling Date: Mon, 2 Sep 2019 21:55:19 +0200 Subject: [PATCH 012/103] Don't immediately jump to loop start when loopOut is pressed in quantized mode Fixes LP1837077 --- src/engine/loopingcontrol.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/engine/loopingcontrol.cpp b/src/engine/loopingcontrol.cpp index 201a1e0e1bf7..adf9953968f1 100644 --- a/src/engine/loopingcontrol.cpp +++ b/src/engine/loopingcontrol.cpp @@ -353,18 +353,28 @@ double LoopingControl::nextTrigger(bool reverse, LoopSamples loopSamples = m_loopSamples.getValue(); + // m_bAdjustingLoopIn is true while the LoopIn button is pressed while a loop is active (slotLoopIn) if (m_bAdjustingLoopInOld != m_bAdjustingLoopIn) { m_bAdjustingLoopInOld = m_bAdjustingLoopIn; - if (reverse && !m_bAdjustingLoopIn) { + + // When the LoopIn button is released in reverse mode we jump to the end of the loop to not fall out and disable the active loop + // This must not happen in quantized mode. The newly set start is always ahead (in time, but behind spacially) of the current position so we don't jump. + // Jumping to the end is then handled when the loop's start is reached later in this function. + if (reverse && !m_bAdjustingLoopIn && !m_pQuantizeEnabled->toBool()) { m_oldLoopSamples = loopSamples; *pTarget = loopSamples.end; return currentSample; } } + // m_bAdjustingLoopOut is true while the LoopOut button is pressed while a loop is active (slotLoopOut) if (m_bAdjustingLoopOutOld != m_bAdjustingLoopOut) { m_bAdjustingLoopOutOld = m_bAdjustingLoopOut; - if (!reverse && !m_bAdjustingLoopOut) { + + // When the LoopOut button is released in forward mode we jump to the start of the loop to not fall out and disable the active loop + // This must not happen in quantized mode. The newly set end is always ahead of the current position so we don't jump. + // Jumping to the start is then handled when the loop's end is reached later in this function. + if (!reverse && !m_bAdjustingLoopOut && !m_pQuantizeEnabled->toBool()) { m_oldLoopSamples = loopSamples; *pTarget = loopSamples.start; return currentSample; From 09e96b8c9a043f9d40a2b5a4c83ff314a18e1a93 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 09:00:13 +0200 Subject: [PATCH 013/103] Add debug assertions to detect memory-leaks in cache --- src/engine/cachingreaderchunk.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/engine/cachingreaderchunk.cpp b/src/engine/cachingreaderchunk.cpp index 9eb2dbbf085c..2c7af9f84162 100644 --- a/src/engine/cachingreaderchunk.cpp +++ b/src/engine/cachingreaderchunk.cpp @@ -126,22 +126,32 @@ CachingReaderChunkForOwner::~CachingReaderChunkForOwner() { } void CachingReaderChunkForOwner::init(SINT index) { - DEBUG_ASSERT(READ_PENDING != m_state); + // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(!m_pNext); + DEBUG_ASSERT(!m_pPrev); + // Must not be accessed by a worker! + DEBUG_ASSERT(m_state != READ_PENDING); CachingReaderChunk::init(index); m_state = READY; } void CachingReaderChunkForOwner::free() { - DEBUG_ASSERT(READ_PENDING != m_state); + // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(!m_pNext); + DEBUG_ASSERT(!m_pPrev); + // Must not be accessed by a worker! + DEBUG_ASSERT(m_state != READ_PENDING); CachingReaderChunk::init(kInvalidChunkIndex); m_state = FREE; } void CachingReaderChunkForOwner::insertIntoListBefore( CachingReaderChunkForOwner* pBefore) { - DEBUG_ASSERT(m_pNext == nullptr); - DEBUG_ASSERT(m_pPrev == nullptr); - DEBUG_ASSERT(m_state != READ_PENDING); // Must not be accessed by a worker! + // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(!m_pNext); + DEBUG_ASSERT(!m_pPrev); + // Must not be accessed by a worker! + DEBUG_ASSERT(m_state != READ_PENDING); m_pNext = pBefore; if (pBefore) { @@ -157,6 +167,7 @@ void CachingReaderChunkForOwner::insertIntoListBefore( void CachingReaderChunkForOwner::removeFromList( CachingReaderChunkForOwner** ppHead, CachingReaderChunkForOwner** ppTail) { + DEBUG_ASSERT(m_pNext || m_pPrev); // Remove this chunk from the double-linked list... CachingReaderChunkForOwner* pNext = m_pNext; CachingReaderChunkForOwner* pPrev = m_pPrev; From a66af2ecd6c94a531ba49143b7f0afa0d46c6926 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 13:11:41 +0200 Subject: [PATCH 014/103] Simplify double-linked list removal --- src/engine/cachingreaderchunk.cpp | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/engine/cachingreaderchunk.cpp b/src/engine/cachingreaderchunk.cpp index 2c7af9f84162..2b7e41c3702e 100644 --- a/src/engine/cachingreaderchunk.cpp +++ b/src/engine/cachingreaderchunk.cpp @@ -167,28 +167,36 @@ void CachingReaderChunkForOwner::insertIntoListBefore( void CachingReaderChunkForOwner::removeFromList( CachingReaderChunkForOwner** ppHead, CachingReaderChunkForOwner** ppTail) { - DEBUG_ASSERT(m_pNext || m_pPrev); + DEBUG_ASSERT(ppHead); + DEBUG_ASSERT(ppTail); + if (!m_pPrev && !m_pNext) { + // Not in linked list -> nothing to do + return; + } + // Remove this chunk from the double-linked list... - CachingReaderChunkForOwner* pNext = m_pNext; - CachingReaderChunkForOwner* pPrev = m_pPrev; - m_pNext = nullptr; + const auto pPrev = m_pPrev; + const auto pNext = m_pNext; m_pPrev = nullptr; + m_pNext = nullptr; - // ...reconnect the remaining list elements... - if (pNext) { - DEBUG_ASSERT(this == pNext->m_pPrev); - pNext->m_pPrev = pPrev; - } + // ...reconnect the adjacent list items and adjust head/tail if (pPrev) { DEBUG_ASSERT(this == pPrev->m_pNext); pPrev->m_pNext = pNext; - } - - // ...and adjust head/tail. - if (ppHead && (this == *ppHead)) { + } else { + // No predecessor + DEBUG_ASSERT(this == *ppHead); + // pNext becomes the new head *ppHead = pNext; } - if (ppTail && (this == *ppTail)) { + if (pNext) { + DEBUG_ASSERT(this == pNext->m_pPrev); + pNext->m_pPrev = pPrev; + } else { + // No successor + DEBUG_ASSERT(this == *ppTail); + // pPrev becomes the new tail *ppTail = pPrev; } } From b05a7181ec48be209fa28d3650a58b345f53e07c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 13:21:08 +0200 Subject: [PATCH 015/103] Ensure complete initialization of caching worker update messages --- src/engine/cachingreader.cpp | 24 ++++++++++-------------- src/engine/cachingreaderworker.cpp | 21 +++++++++------------ src/engine/cachingreaderworker.h | 16 +++++++++++++++- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 39629bd42b2f..8ed0b4509cfd 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -189,15 +189,11 @@ void CachingReader::newTrack(TrackPointer pTrack) { } void CachingReader::process() { - ReaderStatusUpdate status; - while (m_readerStatusFIFO.read(&status, 1) == 1) { - CachingReaderChunkForOwner* pChunk = static_cast(status.chunk); + ReaderStatusUpdate update; + while (m_readerStatusFIFO.read(&update, 1) == 1) { + auto pChunk = update.takeFromWorker(); if (pChunk) { - // Take over control of the chunk from the worker. - // This has to be done before freeing all chunks - // after a new track has been loaded (see below)! - pChunk->takeFromWorker(); - if (status.status == CHUNK_READ_SUCCESS) { + if (update.status == CHUNK_READ_SUCCESS) { // Insert or freshen the chunk in the MRU/LRU list after // obtaining ownership from the worker. freshenChunk(pChunk); @@ -206,12 +202,12 @@ void CachingReader::process() { freeChunk(pChunk); } } - if (status.status == TRACK_NOT_LOADED) { - m_readerStatus = status.status; - } else if (status.status == TRACK_LOADED) { - m_readerStatus = status.status; + if (update.status == TRACK_NOT_LOADED) { + m_readerStatus = update.status; + } else if (update.status == TRACK_LOADED) { + m_readerStatus = update.status; // Reset the max. readable frame index - m_readableFrameIndexRange = status.readableFrameIndexRange(); + m_readableFrameIndexRange = update.readableFrameIndexRange(); // Free all chunks with sample data from a previous track freeAllChunks(); } @@ -219,7 +215,7 @@ void CachingReader::process() { // Adjust the readable frame index range after loading or reading m_readableFrameIndexRange = intersect( m_readableFrameIndexRange, - status.readableFrameIndexRange()); + update.readableFrameIndexRange()); } else { // Reset the readable frame index range m_readableFrameIndexRange = mixxx::IndexRange(); diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index 34f9a9ac836d..84b52e937e4b 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -133,14 +133,14 @@ mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack, } // anonymous namespace void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { - ReaderStatusUpdate status; - status.init(TRACK_NOT_LOADED); + ReaderStatusUpdate update; + update.init(TRACK_NOT_LOADED); if (!pTrack) { // Unload track m_pAudioSource.reset(); // Close open file handles m_readableFrameIndexRange = mixxx::IndexRange(); - m_pReaderStatusFIFO->writeBlocking(&status, 1); + m_pReaderStatusFIFO->writeBlocking(&update, 1); return; } @@ -152,7 +152,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Must unlock before emitting to avoid deadlock kLogger.debug() << m_group << "loadTrack() load failed for\"" << filename << "\", unlocked reader lock"; - m_pReaderStatusFIFO->writeBlocking(&status, 1); + m_pReaderStatusFIFO->writeBlocking(&update, 1); emit(trackLoadFailed( pTrack, QString("The file '%1' could not be found.") .arg(QDir::toNativeSeparators(filename)))); @@ -167,7 +167,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Must unlock before emitting to avoid deadlock kLogger.debug() << m_group << "loadTrack() load failed for\"" << filename << "\", file invalid, unlocked reader lock"; - m_pReaderStatusFIFO->writeBlocking(&status, 1); + m_pReaderStatusFIFO->writeBlocking(&update, 1); emit(trackLoadFailed( pTrack, QString("The file '%1' could not be loaded.").arg(filename))); return; @@ -183,18 +183,15 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // be decreased to avoid repeated reading of corrupt audio data. m_readableFrameIndexRange = m_pAudioSource->frameIndexRange(); - status.status = TRACK_LOADED; - status.readableFrameIndexRangeStart = m_readableFrameIndexRange.start(); - status.readableFrameIndexRangeEnd = m_readableFrameIndexRange.end(); - m_pReaderStatusFIFO->writeBlocking(&status, 1); + update.init(TRACK_LOADED, nullptr, m_pAudioSource->frameIndexRange()); + m_pReaderStatusFIFO->writeBlocking(&update, 1); // Clear the chunks to read list. CachingReaderChunkReadRequest request; while (m_pChunkReadRequestFIFO->read(&request, 1) == 1) { kLogger.debug() << "Cancelling read request for " << request.chunk->getIndex(); - status.status = CHUNK_READ_INVALID; - status.chunk = request.chunk; - m_pReaderStatusFIFO->writeBlocking(&status, 1); + update.init(CHUNK_READ_INVALID, request.chunk); + m_pReaderStatusFIFO->writeBlocking(&update, 1); } // Emit that the track is loaded. diff --git a/src/engine/cachingreaderworker.h b/src/engine/cachingreaderworker.h index 54c5a4c77e45..eab3c4a8c980 100644 --- a/src/engine/cachingreaderworker.h +++ b/src/engine/cachingreaderworker.h @@ -36,11 +36,14 @@ enum ReaderStatus { // POD with trivial ctor/dtor/copy for passing through FIFO typedef struct ReaderStatusUpdate { - ReaderStatus status; + private: CachingReaderChunk* chunk; SINT readableFrameIndexRangeStart; SINT readableFrameIndexRangeEnd; + public: + ReaderStatus status; + void init( ReaderStatus statusArg = INVALID, CachingReaderChunk* chunkArg = nullptr, @@ -51,6 +54,17 @@ typedef struct ReaderStatusUpdate { readableFrameIndexRangeEnd = readableFrameIndexRangeArg.end(); } + CachingReaderChunkForOwner* takeFromWorker() { + CachingReaderChunkForOwner* pChunk = nullptr; + if (chunk) { + DEBUG_ASSERT(dynamic_cast(chunk)); + pChunk = static_cast(chunk); + chunk = nullptr; + pChunk->takeFromWorker(); + } + return pChunk; + } + mixxx::IndexRange readableFrameIndexRange() const { return mixxx::IndexRange::between( readableFrameIndexRangeStart, From 5c89e8b6cbc7b1cfea4812f7e147b768358fcd25 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 14:40:49 +0200 Subject: [PATCH 016/103] Use default destructors and std::move --- src/engine/cachingreaderchunk.cpp | 12 +++--------- src/engine/cachingreaderchunk.h | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/engine/cachingreaderchunk.cpp b/src/engine/cachingreaderchunk.cpp index 2b7e41c3702e..09065bd3d2ac 100644 --- a/src/engine/cachingreaderchunk.cpp +++ b/src/engine/cachingreaderchunk.cpp @@ -33,11 +33,8 @@ const SINT CachingReaderChunk::kSamples = CachingReaderChunk::CachingReaderChunk( mixxx::SampleBuffer::WritableSlice sampleBuffer) : m_index(kInvalidChunkIndex), - m_sampleBuffer(sampleBuffer) { - DEBUG_ASSERT(sampleBuffer.length() == kSamples); -} - -CachingReaderChunk::~CachingReaderChunk() { + m_sampleBuffer(std::move(sampleBuffer)) { + DEBUG_ASSERT(m_sampleBuffer.length() == kSamples); } void CachingReaderChunk::init(SINT index) { @@ -116,15 +113,12 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CachingReaderChunkForOwner::CachingReaderChunkForOwner( mixxx::SampleBuffer::WritableSlice sampleBuffer) - : CachingReaderChunk(sampleBuffer), + : CachingReaderChunk(std::move(sampleBuffer)), m_state(FREE), m_pPrev(nullptr), m_pNext(nullptr) { } -CachingReaderChunkForOwner::~CachingReaderChunkForOwner() { -} - void CachingReaderChunkForOwner::init(SINT index) { // Must not be referenced in MRU/LRU list! DEBUG_ASSERT(!m_pNext); diff --git a/src/engine/cachingreaderchunk.h b/src/engine/cachingreaderchunk.h index 463e7366665d..c7e282e69443 100644 --- a/src/engine/cachingreaderchunk.h +++ b/src/engine/cachingreaderchunk.h @@ -67,7 +67,7 @@ class CachingReaderChunk { protected: explicit CachingReaderChunk( mixxx::SampleBuffer::WritableSlice sampleBuffer); - virtual ~CachingReaderChunk(); + virtual ~CachingReaderChunk() = default; void init(SINT index); @@ -91,7 +91,7 @@ class CachingReaderChunkForOwner: public CachingReaderChunk { public: explicit CachingReaderChunkForOwner( mixxx::SampleBuffer::WritableSlice sampleBuffer); - ~CachingReaderChunkForOwner() override; + ~CachingReaderChunkForOwner() override = default; void init(SINT index); void free(); From 510a871c27a501069bbffe3074de1269a5950cec Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 14:51:19 +0200 Subject: [PATCH 017/103] Fix management of MRU/LRU chunk list --- src/engine/cachingreader.cpp | 109 ++++++++++++++++-------------- src/engine/cachingreader.h | 1 + src/engine/cachingreaderchunk.cpp | 101 ++++++++++++++++++++------- src/engine/cachingreaderchunk.h | 24 ++++--- 4 files changed, 150 insertions(+), 85 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 8ed0b4509cfd..2afffecc779a 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -80,8 +80,16 @@ CachingReader::~CachingReader() { qDeleteAll(m_chunks); } +void CachingReader::freeChunkFromList(CachingReaderChunkForOwner* pChunk) { + pChunk->removeFromList( + &m_mruCachingReaderChunk, + &m_lruCachingReaderChunk); + pChunk->free(); + m_freeChunks.push_back(pChunk); +} + void CachingReader::freeChunk(CachingReaderChunkForOwner* pChunk) { - DEBUG_ASSERT(pChunk != nullptr); + DEBUG_ASSERT(pChunk); DEBUG_ASSERT(pChunk->getState() != CachingReaderChunkForOwner::READ_PENDING); const int removed = m_allocatedCachingReaderChunks.remove(pChunk->getIndex()); @@ -89,10 +97,7 @@ void CachingReader::freeChunk(CachingReaderChunkForOwner* pChunk) { // because sometime you free a chunk right after you allocated it. DEBUG_ASSERT(removed <= 1); - pChunk->removeFromList( - &m_mruCachingReaderChunk, &m_lruCachingReaderChunk); - pChunk->free(); - m_freeChunks.push_back(pChunk); + freeChunkFromList(pChunk); } void CachingReader::freeAllChunks() { @@ -104,15 +109,13 @@ void CachingReader::freeAllChunks() { } if (pChunk->getState() != CachingReaderChunkForOwner::FREE) { - pChunk->removeFromList( - &m_mruCachingReaderChunk, &m_lruCachingReaderChunk); - pChunk->free(); - m_freeChunks.push_back(pChunk); + freeChunkFromList(pChunk); } } + DEBUG_ASSERT(!m_mruCachingReaderChunk); + DEBUG_ASSERT(!m_lruCachingReaderChunk); m_allocatedCachingReaderChunks.clear(); - m_mruCachingReaderChunk = nullptr; } CachingReaderChunkForOwner* CachingReader::allocateChunk(SINT chunkIndex) { @@ -129,55 +132,53 @@ CachingReaderChunkForOwner* CachingReader::allocateChunk(SINT chunkIndex) { } CachingReaderChunkForOwner* CachingReader::allocateChunkExpireLRU(SINT chunkIndex) { - CachingReaderChunkForOwner* pChunk = allocateChunk(chunkIndex); + auto pChunk = allocateChunk(chunkIndex); if (!pChunk) { - if (m_lruCachingReaderChunk == nullptr) { - kLogger.warning() << "ERROR: No LRU chunk to free in allocateChunkExpireLRU."; - return nullptr; + if (m_lruCachingReaderChunk) { + freeChunk(m_lruCachingReaderChunk); + pChunk = allocateChunk(chunkIndex); + } else { + kLogger.warning() << "No cached LRU chunk available for freeing"; } - freeChunk(m_lruCachingReaderChunk); - pChunk = allocateChunk(chunkIndex); } - //kLogger.debug() << "allocateChunkExpireLRU" << chunk << pChunk; + if (kLogger.traceEnabled()) { + kLogger.trace() << "allocateChunkExpireLRU" << chunkIndex << pChunk; + } return pChunk; } CachingReaderChunkForOwner* CachingReader::lookupChunk(SINT chunkIndex) { // Defaults to nullptr if it's not in the hash. - CachingReaderChunkForOwner* chunk = m_allocatedCachingReaderChunks.value(chunkIndex, nullptr); - - // Make sure the allocated number matches the indexed chunk number. - DEBUG_ASSERT(chunk == nullptr || chunkIndex == chunk->getIndex()); - - return chunk; + auto pChunk = m_allocatedCachingReaderChunks.value(chunkIndex, nullptr); + DEBUG_ASSERT(!pChunk || pChunk->getIndex() == chunkIndex); + return pChunk; } void CachingReader::freshenChunk(CachingReaderChunkForOwner* pChunk) { - DEBUG_ASSERT(pChunk != nullptr); - DEBUG_ASSERT(pChunk->getState() != CachingReaderChunkForOwner::READ_PENDING); - - // Remove the chunk from the LRU list - pChunk->removeFromList(&m_mruCachingReaderChunk, &m_lruCachingReaderChunk); - - // Adjust the least-recently-used item before inserting the - // chunk as the new most-recently-used item. - if (m_lruCachingReaderChunk == nullptr) { - if (m_mruCachingReaderChunk == nullptr) { - m_lruCachingReaderChunk = pChunk; - } else { - m_lruCachingReaderChunk = m_mruCachingReaderChunk; - } + DEBUG_ASSERT(pChunk); + DEBUG_ASSERT(pChunk->getState() == CachingReaderChunkForOwner::READY); + if (kLogger.traceEnabled()) { + kLogger.trace() + << "freshenChunk()" + << pChunk->getIndex() + << pChunk; } - // Insert the chunk as the new most-recently-used item. - pChunk->insertIntoListBefore(m_mruCachingReaderChunk); - m_mruCachingReaderChunk = pChunk; + // Remove the chunk from the MRU/LRU list + pChunk->removeFromList( + &m_mruCachingReaderChunk, + &m_lruCachingReaderChunk); + + // Reinsert has new head of MRU list + pChunk->insertIntoListBefore( + &m_mruCachingReaderChunk, + &m_lruCachingReaderChunk, + m_mruCachingReaderChunk); } CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex) { - CachingReaderChunkForOwner* pChunk = lookupChunk(chunkIndex); - if (pChunk && - (pChunk->getState() != CachingReaderChunkForOwner::READ_PENDING)) { + auto pChunk = lookupChunk(chunkIndex); + if (pChunk && (pChunk->getState() == CachingReaderChunkForOwner::READY)) { freshenChunk(pChunk); } return pChunk; @@ -476,27 +477,33 @@ void CachingReader::hintAndMaybeWake(const HintVector& hintList) { const int lastChunkIndex = CachingReaderChunk::indexForFrame(readableFrameIndexRange.end() - 1); for (int chunkIndex = firstChunkIndex; chunkIndex <= lastChunkIndex; ++chunkIndex) { CachingReaderChunkForOwner* pChunk = lookupChunk(chunkIndex); - if (pChunk == nullptr) { + if (!pChunk) { shouldWake = true; pChunk = allocateChunkExpireLRU(chunkIndex); - if (pChunk == nullptr) { - kLogger.warning() << "ERROR: Couldn't allocate spare CachingReaderChunk to make CachingReaderChunkReadRequest."; + if (!pChunk) { + kLogger.warning() + << "Failed to allocate chunk" + << chunkIndex + << "for read request"; continue; } // Do not insert the allocated chunk into the MRU/LRU list, // because it will be handed over to the worker immediately CachingReaderChunkReadRequest request; request.giveToWorker(pChunk); - // kLogger.debug() << "Requesting read of chunk" << current << "into" << pChunk; - // kLogger.debug() << "Requesting read into " << request.chunk->data; + if (kLogger.traceEnabled()) { + kLogger.trace() + << "Requesting read of chunk" + << request.chunk; + } if (m_chunkReadRequestFIFO.write(&request, 1) != 1) { - kLogger.warning() << "ERROR: Could not submit read request for " - << chunkIndex; + kLogger.warning() + << "Failed to submit read request for chunk" + << chunkIndex; // Revoke the chunk from the worker and free it pChunk->takeFromWorker(); freeChunk(pChunk); } - //kLogger.debug() << "Checking chunk " << current << " shouldWake:" << shouldWake << " chunksToRead" << m_chunksToRead.size(); } else if (pChunk->getState() == CachingReaderChunkForOwner::READY) { // This will cause the chunk to be 'freshened' in the cache. The // chunk will be moved to the end of the LRU list. diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index 029b44246da1..e148cd637e1f 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -140,6 +140,7 @@ class CachingReader : public QObject { // Returns a CachingReaderChunk to the free list void freeChunk(CachingReaderChunkForOwner* pChunk); + void freeChunkFromList(CachingReaderChunkForOwner* pChunk); // Returns all allocated chunks to the free list void freeAllChunks(); diff --git a/src/engine/cachingreaderchunk.cpp b/src/engine/cachingreaderchunk.cpp index 09065bd3d2ac..701fb00bf50c 100644 --- a/src/engine/cachingreaderchunk.cpp +++ b/src/engine/cachingreaderchunk.cpp @@ -38,6 +38,7 @@ CachingReaderChunk::CachingReaderChunk( } void CachingReaderChunk::init(SINT index) { + DEBUG_ASSERT(m_index == kInvalidChunkIndex || index == kInvalidChunkIndex); m_index = index; m_bufferedSampleFrames.frameIndexRange() = mixxx::IndexRange(); } @@ -45,6 +46,7 @@ void CachingReaderChunk::init(SINT index) { // Frame index range of this chunk for the given audio source. mixxx::IndexRange CachingReaderChunk::frameIndexRange( const mixxx::AudioSourcePointer& pAudioSource) const { + DEBUG_ASSERT(m_index != kInvalidChunkIndex); if (!pAudioSource) { return mixxx::IndexRange(); } @@ -59,6 +61,7 @@ mixxx::IndexRange CachingReaderChunk::frameIndexRange( mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( const mixxx::AudioSourcePointer& pAudioSource, mixxx::SampleBuffer::WritableSlice tempOutputBuffer) { + DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto sourceFrameIndexRange = frameIndexRange(pAudioSource); mixxx::AudioSourceStereoProxy audioSourceProxy( pAudioSource, @@ -76,6 +79,7 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( CSAMPLE* sampleBuffer, const mixxx::IndexRange& frameIndexRange) const { + DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { @@ -95,6 +99,7 @@ mixxx::IndexRange CachingReaderChunk::readBufferedSampleFrames( mixxx::IndexRange CachingReaderChunk::readBufferedSampleFramesReverse( CSAMPLE* reverseSampleBuffer, const mixxx::IndexRange& frameIndexRange) const { + DEBUG_ASSERT(m_index != kInvalidChunkIndex); const auto copyableFrameIndexRange = intersect(frameIndexRange, m_bufferedSampleFrames.frameIndexRange()); if (!copyableFrameIndexRange.empty()) { @@ -120,77 +125,123 @@ CachingReaderChunkForOwner::CachingReaderChunkForOwner( } void CachingReaderChunkForOwner::init(SINT index) { + // Must not be accessed by a worker! + DEBUG_ASSERT(m_state != READ_PENDING); // Must not be referenced in MRU/LRU list! DEBUG_ASSERT(!m_pNext); DEBUG_ASSERT(!m_pPrev); - // Must not be accessed by a worker! - DEBUG_ASSERT(m_state != READ_PENDING); + CachingReaderChunk::init(index); m_state = READY; } void CachingReaderChunkForOwner::free() { + // Must not be accessed by a worker! + DEBUG_ASSERT(m_state != READ_PENDING); // Must not be referenced in MRU/LRU list! DEBUG_ASSERT(!m_pNext); DEBUG_ASSERT(!m_pPrev); - // Must not be accessed by a worker! - DEBUG_ASSERT(m_state != READ_PENDING); + CachingReaderChunk::init(kInvalidChunkIndex); m_state = FREE; } void CachingReaderChunkForOwner::insertIntoListBefore( + CachingReaderChunkForOwner** ppHead, + CachingReaderChunkForOwner** ppTail, CachingReaderChunkForOwner* pBefore) { - // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(m_state == READY); + // Both head and tail need to be adjusted + DEBUG_ASSERT(ppHead); + DEBUG_ASSERT(ppTail); + // Cannot insert before itself + DEBUG_ASSERT(this != pBefore); + // Must not yet be referenced in MRU/LRU list + DEBUG_ASSERT(this != *ppHead); + DEBUG_ASSERT(this != *ppTail); DEBUG_ASSERT(!m_pNext); DEBUG_ASSERT(!m_pPrev); - // Must not be accessed by a worker! - DEBUG_ASSERT(m_state != READ_PENDING); + if (kLogger.traceEnabled()) { + kLogger.trace() + << "insertIntoListBefore()" + << this + << ppHead << *ppHead + << ppTail << *ppTail + << pBefore; + } - m_pNext = pBefore; if (pBefore) { - if (pBefore->m_pPrev) { - m_pPrev = pBefore->m_pPrev; - DEBUG_ASSERT(m_pPrev->m_pNext == pBefore); + // List must already contain one or more item, i.e. has both + // a head and a tail + DEBUG_ASSERT(*ppHead); + DEBUG_ASSERT(*ppTail); + m_pPrev = pBefore->m_pPrev; + pBefore->m_pPrev = this; + m_pNext = pBefore; + if (*ppHead == pBefore) { + // Replace head + *ppHead = this; + } + } else { + // Append as new tail + m_pPrev = *ppTail; + *ppTail = this; + if (m_pPrev) { m_pPrev->m_pNext = this; } - pBefore->m_pPrev = this; + if (!*ppHead) { + // Initialize new head if the list was empty before + *ppHead = this; + } } } void CachingReaderChunkForOwner::removeFromList( CachingReaderChunkForOwner** ppHead, CachingReaderChunkForOwner** ppTail) { + DEBUG_ASSERT(m_state == READY); + // Both head and tail need to be adjusted DEBUG_ASSERT(ppHead); DEBUG_ASSERT(ppTail); - if (!m_pPrev && !m_pNext) { - // Not in linked list -> nothing to do - return; + if (kLogger.traceEnabled()) { + kLogger.trace() + << "removeFromList()" + << this + << ppHead << *ppHead + << ppTail << *ppTail; } - // Remove this chunk from the double-linked list... + // Disconnect this chunk from the double-linked list const auto pPrev = m_pPrev; const auto pNext = m_pNext; m_pPrev = nullptr; m_pNext = nullptr; - // ...reconnect the adjacent list items and adjust head/tail + // Reconnect the adjacent list items and adjust head/tail if needed if (pPrev) { DEBUG_ASSERT(this == pPrev->m_pNext); pPrev->m_pNext = pNext; } else { - // No predecessor - DEBUG_ASSERT(this == *ppHead); - // pNext becomes the new head - *ppHead = pNext; + // Only the current head item doesn't have a predecessor + if (this == *ppHead) { + // pNext becomes the new head + *ppHead = pNext; + } else { + // Item was not part the list and must not have any successor + DEBUG_ASSERT(!pPrev); + } } if (pNext) { DEBUG_ASSERT(this == pNext->m_pPrev); pNext->m_pPrev = pPrev; } else { - // No successor - DEBUG_ASSERT(this == *ppTail); - // pPrev becomes the new tail - *ppTail = pPrev; + // Only the current tail item doesn't have a successor + if (this == *ppTail) { + // pPrev becomes the new tail + *ppTail = pPrev; + } else { + // Item was not part the list and must not have any predecessor + DEBUG_ASSERT(!pPrev); + } } } diff --git a/src/engine/cachingreaderchunk.h b/src/engine/cachingreaderchunk.h index c7e282e69443..9f44ec3d7b93 100644 --- a/src/engine/cachingreaderchunk.h +++ b/src/engine/cachingreaderchunk.h @@ -108,24 +108,30 @@ class CachingReaderChunkForOwner: public CachingReaderChunk { // The state is controlled by the cache as the owner of each chunk! void giveToWorker() { - DEBUG_ASSERT(READY == m_state); + // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(!m_pPrev); + DEBUG_ASSERT(!m_pNext); + DEBUG_ASSERT(m_state == READY); m_state = READ_PENDING; } void takeFromWorker() { - DEBUG_ASSERT(READ_PENDING == m_state); + // Must not be referenced in MRU/LRU list! + DEBUG_ASSERT(!m_pPrev); + DEBUG_ASSERT(!m_pNext); + DEBUG_ASSERT(m_state == READ_PENDING); m_state = READY; } // Inserts a chunk into the double-linked list before the - // given chunk. If the list is currently empty simply pass - // pBefore = nullptr. Please note that if pBefore points to - // the head of the current list this chunk becomes the new - // head of the list. + // given chunk and adjusts the head/tail pointers. The + // chunk is inserted at the tail of the list if + // pBefore == nullptr. void insertIntoListBefore( + CachingReaderChunkForOwner** ppHead, + CachingReaderChunkForOwner** ppTail, CachingReaderChunkForOwner* pBefore); - // Removes a chunk from the double-linked list and optionally - // adjusts head/tail pointers. Pass ppHead/ppTail = nullptr if - // you prefer to adjust those pointers manually. + // Removes a chunk from the double-linked list and adjusts + // the head/tail pointers. void removeFromList( CachingReaderChunkForOwner** ppHead, CachingReaderChunkForOwner** ppTail); From cc2c242cf2fd364a003f902443f0bf320ef33317 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 16:31:58 +0200 Subject: [PATCH 018/103] Add notes about how to test the caching of audio data --- src/engine/cachingreader.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 2afffecc779a..16caf31f1d94 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -28,8 +28,14 @@ const SINT kDefaultHintFrames = 1024; // version 2.2.2 for doesn't seem to be sufficient to prevent running // out of free chunks. Therefore we increased the number of cached // chunks to 256: +// // 80 chunks -> 5120 KB = 5 MB // 256 chunks -> 16384 KB = 16 MB +// +// NOTE(uklotzde, 2019-09-05): Reduce this number to just few chunks +// (kNumberOfCachedChunksInMemory = 1, 2, 3, ...) for testing purposes +// to verify that the MRU/LRU cache works as expected. Even though +// massive drop outs are expected to occur Mixxx should run reliably! const SINT kNumberOfCachedChunksInMemory = 256; } // anonymous namespace From dc92cad298505e2655980da02983ddb40b4ade48 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 21:15:37 +0200 Subject: [PATCH 019/103] Clean up code and logs in CachingReaderWorker --- src/engine/cachingreaderworker.cpp | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index 84b52e937e4b..955267563cf8 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -55,6 +55,7 @@ ReaderStatusUpdate CachingReaderWorker::processReadRequest( ReaderStatus status = bufferedFrameIndexRange.empty() ? CHUNK_READ_EOF : CHUNK_READ_SUCCESS; if (chunkFrameIndexRange != bufferedFrameIndexRange) { kLogger.warning() + << m_group << "Failed to read chunk samples for frame index range:" << "actual =" << bufferedFrameIndexRange << ", expected =" << chunkFrameIndexRange; @@ -120,18 +121,6 @@ void CachingReaderWorker::run() { } } -namespace { - -mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack, const mixxx::AudioSource::OpenParams& params) { - auto pAudioSource = SoundSourceProxy(pTrack).openAudioSource(params); - if (!pAudioSource) { - kLogger.warning() << "Failed to open file:" << pTrack->getLocation(); - } - return pAudioSource; -} - -} // anonymous namespace - void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { ReaderStatusUpdate update; update.init(TRACK_NOT_LOADED); @@ -149,9 +138,10 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { QString filename = pTrack->getLocation(); if (filename.isEmpty() || !pTrack->exists()) { - // Must unlock before emitting to avoid deadlock - kLogger.debug() << m_group << "loadTrack() load failed for\"" - << filename << "\", unlocked reader lock"; + kLogger.warning() + << m_group + << "File not found" + << filename; m_pReaderStatusFIFO->writeBlocking(&update, 1); emit(trackLoadFailed( pTrack, QString("The file '%1' could not be found.") @@ -161,12 +151,13 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { mixxx::AudioSource::OpenParams config; config.setChannelCount(CachingReaderChunk::kChannels); - m_pAudioSource = openAudioSourceForReading(pTrack, config); + m_pAudioSource = SoundSourceProxy(pTrack).openAudioSource(config); if (!m_pAudioSource) { m_readableFrameIndexRange = mixxx::IndexRange(); - // Must unlock before emitting to avoid deadlock - kLogger.debug() << m_group << "loadTrack() load failed for\"" - << filename << "\", file invalid, unlocked reader lock"; + kLogger.warning() + << m_group + << "Failed to open file" + << filename; m_pReaderStatusFIFO->writeBlocking(&update, 1); emit(trackLoadFailed( pTrack, QString("The file '%1' could not be loaded.").arg(filename))); @@ -189,7 +180,6 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Clear the chunks to read list. CachingReaderChunkReadRequest request; while (m_pChunkReadRequestFIFO->read(&request, 1) == 1) { - kLogger.debug() << "Cancelling read request for " << request.chunk->getIndex(); update.init(CHUNK_READ_INVALID, request.chunk); m_pReaderStatusFIFO->writeBlocking(&update, 1); } From 8ba0de6a1eace46980b3b0c5bdd5b9f1643b26c0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 21:21:43 +0200 Subject: [PATCH 020/103] Reduce number of allocated chunks and leave a note --- src/engine/cachingreader.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 16caf31f1d94..a294512e4980 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -23,20 +23,19 @@ const SINT kDefaultHintFrames = 1024; // With CachingReaderChunk::kFrames = 8192 each chunk consumes // 8192 frames * 2 channels/frame * 4-bytes per sample = 65 kB. // -// NOTE(2019-09-04): https://bugs.launchpad.net/mixxx/+bug/1842679 -// According to the linked bug report reserving only 80 chunks in -// version 2.2.2 for doesn't seem to be sufficient to prevent running -// out of free chunks. Therefore we increased the number of cached -// chunks to 256: -// // 80 chunks -> 5120 KB = 5 MB -// 256 chunks -> 16384 KB = 16 MB +// +// Each deck (including sample decks) will use their own CachingReader. +// Consequently the total memory required for all allocated chunks depends +// on the number of decks. The amount of memory reserved for a single +// CachingReader must be multiplied by the number of decks to calculate +// the total amount! // // NOTE(uklotzde, 2019-09-05): Reduce this number to just few chunks // (kNumberOfCachedChunksInMemory = 1, 2, 3, ...) for testing purposes // to verify that the MRU/LRU cache works as expected. Even though // massive drop outs are expected to occur Mixxx should run reliably! -const SINT kNumberOfCachedChunksInMemory = 256; +const SINT kNumberOfCachedChunksInMemory = 80; } // anonymous namespace From 56625eb9e223bf5e0da8fc4d6a523f01c0e6ca88 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 21:31:06 +0200 Subject: [PATCH 021/103] Adjust logs in CachingReader --- src/engine/cachingreader.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index a294512e4980..098b813289e3 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -130,7 +130,6 @@ CachingReaderChunkForOwner* CachingReader::allocateChunk(SINT chunkIndex) { CachingReaderChunkForOwner* pChunk = m_freeChunks.takeFirst(); pChunk->init(chunkIndex); - //kLogger.debug() << "Allocating chunk" << pChunk << pChunk->getIndex(); m_allocatedCachingReaderChunks.insert(chunkIndex, pChunk); return pChunk; @@ -237,9 +236,10 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, // Refuse to read from an invalid number of samples (numSamples % CachingReaderChunk::kChannels == 0) && (numSamples >= 0)) { kLogger.critical() - << "read() invalid arguments:" + << "Invalid arguments for read():" << "startSample =" << startSample - << "numSamples =" << numSamples; + << "numSamples =" << numSamples + << "reverse =" << reverse; return ReadResult::UNAVAILABLE; } VERIFY_OR_DEBUG_ASSERT(buffer) { @@ -290,10 +290,12 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, remainingFrameIndexRange.start(), m_readableFrameIndexRange.start()); DEBUG_ASSERT(prerollFrameIndexRange.length() <= remainingFrameIndexRange.length()); - kLogger.debug() - << "Prepending" - << prerollFrameIndexRange.length() - << "frames of silence"; + if (kLogger.traceEnabled()) { + kLogger.trace() + << "Prepending" + << prerollFrameIndexRange.length() + << "frames of silence"; + } const SINT prerollFrames = prerollFrameIndexRange.length(); const SINT prerollSamples = CachingReaderChunk::frames2samples(prerollFrames); DEBUG_ASSERT(samplesRemaining >= prerollSamples); From 77b9013401bfbf9f302601f47c3c97709b119690 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 22:34:07 +0200 Subject: [PATCH 022/103] Limit the number of pending, in-flight read request (FIFO) --- src/engine/cachingreader.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 098b813289e3..54d98cd9a731 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -43,7 +43,19 @@ const SINT kNumberOfCachedChunksInMemory = 80; CachingReader::CachingReader(QString group, UserSettingsPointer config) : m_pConfig(config), - m_chunkReadRequestFIFO(kNumberOfCachedChunksInMemory), + // Limit the number of in-flight requests to the worker. This should + // prevent to overload the worker when it is not able to fetch those + // requests from the FIFO timely. Otherwise outdated requests pile up + // in the FIFO and it would take a long time to process them, just to + // discard the results that most likely have already become obsolete. + // TODO(XXX): Ideally the request FIFO would be implemented as a ring + // buffer, where new requests replace old requests when full. Those + // old requests need to be returned immediately to the CachingReader + // that must take ownership and free them!!! + m_chunkReadRequestFIFO(kNumberOfCachedChunksInMemory / 4), + // The capacity of the back channel must be equal to the number of + // allocated chunks, because the worker use writeBlocking(). Otherwise + // the worker could get stuck in a hot loop!!! m_readerStatusFIFO(kNumberOfCachedChunksInMemory), m_readerStatus(INVALID), m_mruCachingReaderChunk(nullptr), From 69d2414cb9a3bbbacd2556c4a7e941600f621343 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Sep 2019 22:39:29 +0200 Subject: [PATCH 023/103] Fix blocking write for FIFO --- src/util/fifo.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/util/fifo.h b/src/util/fifo.h index 3ffd03e7663e..1355430ddd77 100644 --- a/src/util/fifo.h +++ b/src/util/fifo.h @@ -43,10 +43,8 @@ class FIFO { } void writeBlocking(const DataType* pData, int count) { int written = 0; - while (written != count) { - int i = write(pData, count); - pData += i; - written += i; + while (written < count) { + written += write(pData + written, count - written); } } int aquireWriteRegions(int count, From 894020d418581fdd41422adad25bb17bd6cf173a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Sep 2019 18:31:43 +0200 Subject: [PATCH 024/103] Disable modplug feature by default --- build/features.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build/features.py b/build/features.py index 9321bbca1d36..141f28408100 100644 --- a/build/features.py +++ b/build/features.py @@ -441,8 +441,13 @@ class ModPlug(Feature): def description(self): return "Modplug module decoder plugin" + # NOTE(2019-09-07, uklotzde) + # Modplug support is disabled by default, even if libmodplug is + # available on many platforms. Instead it is recommend to enable + # this feature explicitly for the release builds if available, + # e.g. on Linux, FreeBSD, macOS, ... def default(self, build): - return 1 if build.platform_is_linux else 0 + return 0 def enabled(self, build): build.flags['modplug'] = util.get_flags(build.env, 'modplug', self.default(build)) From 41a19f8f9e1086f52ce8cbc383e1666d5f4c456d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 8 Sep 2019 00:18:29 +0200 Subject: [PATCH 025/103] Enable modplug feature implicitly if available --- build/features.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/build/features.py b/build/features.py index 141f28408100..da5091ed4c4e 100644 --- a/build/features.py +++ b/build/features.py @@ -441,16 +441,12 @@ class ModPlug(Feature): def description(self): return "Modplug module decoder plugin" - # NOTE(2019-09-07, uklotzde) - # Modplug support is disabled by default, even if libmodplug is - # available on many platforms. Instead it is recommend to enable - # this feature explicitly for the release builds if available, - # e.g. on Linux, FreeBSD, macOS, ... - def default(self, build): - return 0 - def enabled(self, build): - build.flags['modplug'] = util.get_flags(build.env, 'modplug', self.default(build)) + # Default to enabled on but only throw an error if it was explicitly + # requested and is not available. + if 'modplug' in build.flags: + return int(build.flags['modplug']) > 0 + build.flags['modplug'] = util.get_flags(build.env, 'modplug', 1) if int(build.flags['modplug']): return True return False @@ -458,22 +454,30 @@ def enabled(self, build): def add_options(self, build, vars): vars.Add('modplug', 'Set to 1 to enable libmodplug based module tracker support.', - self.default(build)) + 1) def configure(self, build, conf): if not self.enabled(build): return - build.env.Append(CPPDEFINES='__MODPLUG__') + # Only block the configure if modplug was explicitly requested. + explicit = 'modplug' in SCons.ARGUMENTS - have_modplug_h = conf.CheckHeader('libmodplug/modplug.h') - have_modplug = conf.CheckLib(['modplug', 'libmodplug'], autoadd=True) + if not conf.CheckHeader('libmodplug/modplug.h'): + if explicit: + raise Exception('Could not find libmodplug development headers.') + else: + build.flags['modplug'] = 0 + return - if not have_modplug_h: - raise Exception('Could not find libmodplug development headers.') + if not conf.CheckLib(['modplug', 'libmodplug'], autoadd=True): + if explicit: + raise Exception('Could not find libmodplug shared library.') + else: + build.flags['modplug'] = 0 + return - if not have_modplug: - raise Exception('Could not find libmodplug shared library.') + build.env.Append(CPPDEFINES='__MODPLUG__') def sources(self, build): depends.Qt.uic(build)('preferences/dialog/dlgprefmodplugdlg.ui') From 9d6b1dea7a64b606f4be6a81d3a1f915c2b9360b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 8 Sep 2019 13:16:54 +0200 Subject: [PATCH 026/103] Remove "QObject::disconnect: Unexpected null parameter" warning and avoid unecessary cue lookups --- src/engine/cuecontrol.cpp | 58 +++++++++++++++++++-------------------- src/engine/cuecontrol.h | 4 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/engine/cuecontrol.cpp b/src/engine/cuecontrol.cpp index 74be879c9efe..0dd730b304ee 100644 --- a/src/engine/cuecontrol.cpp +++ b/src/engine/cuecontrol.cpp @@ -159,30 +159,26 @@ void CueControl::createControls() { } } -void CueControl::attachCue(CuePointer pCue, int hotCue) { - HotcueControl* pControl = m_hotcueControls.value(hotCue, NULL); - if (pControl == NULL) { +void CueControl::attachCue(CuePointer pCue, HotcueControl* pControl) { + VERIFY_OR_DEBUG_ASSERT(pControl) { return; } - if (pControl->getCue() != NULL) { - detachCue(pControl->getHotcueNumber()); - } + detachCue(pControl); connect(pCue.get(), SIGNAL(updated()), this, SLOT(cueUpdated()), Qt::DirectConnection); pControl->setCue(pCue); - } -void CueControl::detachCue(int hotCue) { - HotcueControl* pControl = m_hotcueControls.value(hotCue, NULL); - if (pControl == NULL) { +void CueControl::detachCue(HotcueControl* pControl) { + VERIFY_OR_DEBUG_ASSERT(pControl) { return; } CuePointer pCue(pControl->getCue()); - if (!pCue) + if (!pCue) { return; + } disconnect(pCue.get(), 0, this, 0); pControl->resetCue(); } @@ -191,8 +187,9 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { QMutexLocker lock(&m_mutex); if (m_pLoadedTrack) { disconnect(m_pLoadedTrack.get(), 0, this, 0); - for (int i = 0; i < m_iNumHotCues; ++i) { - detachCue(i); + for (int hotCue = 0; hotCue < m_iNumHotCues; ++hotCue) { + HotcueControl* pControl = m_hotcueControls.value(hotCue, nullptr); + detachCue(pControl); } m_pCueIndicator->setBlinkValue(ControlIndicator::OFF); @@ -220,7 +217,8 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { } int hotcue = pCue->getHotCue(); if (hotcue != -1) { - attachCue(pCue, hotcue); + HotcueControl* pControl = m_hotcueControls.value(hotcue, nullptr); + attachCue(pCue, pControl); } } double cuePoint; @@ -278,10 +276,10 @@ void CueControl::trackCuesUpdated() { int hotcue = pCue->getHotCue(); if (hotcue != -1) { - HotcueControl* pControl = m_hotcueControls.value(hotcue, NULL); + HotcueControl* pControl = m_hotcueControls.value(hotcue, nullptr); // Cue's hotcue doesn't have a hotcue control. - if (pControl == NULL) { + if (!pControl) { continue; } @@ -289,11 +287,8 @@ void CueControl::trackCuesUpdated() { // If the old hotcue is different than this one. if (pOldCue != pCue) { - // If the old hotcue exists, detach it - if (pOldCue) { - detachCue(hotcue); - } - attachCue(pCue, hotcue); + // old cue is detached if required + attachCue(pCue, pControl); } else { // If the old hotcue is the same, then we only need to update pControl->setPosition(pCue->getPosition()); @@ -304,9 +299,10 @@ void CueControl::trackCuesUpdated() { } // Detach all hotcues that are no longer present - for (int i = 0; i < m_iNumHotCues; ++i) { - if (!active_hotcues.contains(i)) { - detachCue(i); + for (int hotCue = 0; hotCue < m_iNumHotCues; ++hotCue) { + if (!active_hotcues.contains(hotCue)) { + HotcueControl* pControl = m_hotcueControls.value(hotCue, nullptr); + detachCue(pControl); } } } @@ -338,7 +334,7 @@ void CueControl::hotcueSet(HotcueControl* pControl, double v) { pCue->setLabel(""); pCue->setType(Cue::CUE); // TODO(XXX) deal with spurious signals - attachCue(pCue, hotcue); + attachCue(pCue, pControl); // If quantize is enabled and we are not playing, jump to the cue point // since it's not necessarily where we currently are. TODO(XXX) is this @@ -456,11 +452,12 @@ void CueControl::hotcueActivate(HotcueControl* pControl, double v) { } } } else { + // The cue is non-existent ... if (v) { - // just in case + // create set it to the current position hotcueSet(pControl, v); } else if (m_iCurrentlyPreviewingHotcues) { - // The cue is non-existent, yet we got a release for it and are + // yet we got a release for it and are // currently previewing a hotcue. This is indicative of a corner // case where the cue was detached while we were pressing it. Let // hotcueActivatePreview handle it. @@ -520,7 +517,10 @@ void CueControl::hotcueClear(HotcueControl* pControl, double v) { } CuePointer pCue(pControl->getCue()); - detachCue(pControl->getHotcueNumber()); + if (!pCue) { + return; + } + detachCue(pControl); m_pLoadedTrack->removeCue(pCue); } @@ -534,7 +534,7 @@ void CueControl::hotcuePositionChanged(HotcueControl* pControl, double newPositi // Setting the position to -1 is the same as calling hotcue_x_clear if (newPosition == -1) { pCue->setHotCue(-1); - detachCue(pControl->getHotcueNumber()); + detachCue(pControl); } else if (newPosition > 0 && newPosition < m_pTrackSamples->get()) { pCue->setPosition(newPosition); } diff --git a/src/engine/cuecontrol.h b/src/engine/cuecontrol.h index c9d82c06f846..014929e4169f 100644 --- a/src/engine/cuecontrol.h +++ b/src/engine/cuecontrol.h @@ -137,8 +137,8 @@ class CueControl : public EngineControl { // These methods are not thread safe, only call them when the lock is held. void createControls(); - void attachCue(CuePointer pCue, int hotcueNumber); - void detachCue(int hotcueNumber); + void attachCue(CuePointer pCue, HotcueControl* pControl); + void detachCue(HotcueControl* pControl); TrackAt getTrackAt() const; bool m_bPreviewing; From 799dd681272d232187b50bced7184d5194904c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sun, 8 Sep 2019 23:29:10 +0200 Subject: [PATCH 027/103] Adopt the first 0 hotcue as The Cue, fixing lp:1843168 --- src/library/dlgtrackinfo.cpp | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index c0db2d815fec..56fb01bcee77 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -305,10 +305,7 @@ void DlgTrackInfo::populateCues(TrackPointer pTrack) { // them to the user as 1-indexex. Add 1 here. rryan 9/2010 int iHotcue = pCue->getHotCue() + 1; QString hotcue = ""; - if (iHotcue != -1) { - hotcue = QString("%1").arg(iHotcue); - } - + hotcue = QString("%1").arg(iHotcue); int position = pCue->getPosition(); double totalSeconds; if (position == -1) @@ -377,6 +374,7 @@ void DlgTrackInfo::saveTrack() { m_pLoadedTrack->setKeys(m_keysClone); + bool theCueFound = false; QSet updatedRows; for (int row = 0; row < cueTable->rowCount(); ++row) { QTableWidgetItem* rowItem = cueTable->item(row, 0); @@ -395,13 +393,25 @@ void DlgTrackInfo::saveTrack() { updatedRows.insert(oldRow); QVariant vHotcue = hotcueItem->data(Qt::DisplayRole); - if (vHotcue.canConvert()) { - int iTableHotcue = vHotcue.toInt(); - // The GUI shows hotcues as 1-indexed, but they are actually - // 0-indexed, so subtract 1 - pCue->setHotCue(iTableHotcue - 1); + bool ok; + + + int iTableHotcue = vHotcue.toInt(&ok); + if (ok) { + if (iTableHotcue == 0 && !theCueFound) { + // adopt the first 0 hot cue as the cue + pCue->setHotCue(-1); + pCue->setType(Cue::LOAD); + theCueFound = true; + } else { + // The GUI shows hotcues as 1-indexed, but they are actually + // 0-indexed, so subtract 1 + pCue->setHotCue(iTableHotcue - 1); + pCue->setType(Cue::CUE); + } } else { pCue->setHotCue(-1); + pCue->setType(Cue::CUE); } QString label = labelItem->data(Qt::DisplayRole).toString(); From ec2295ade67797c319885e699f2d0192e4ccddbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 10 Sep 2019 20:36:15 +0200 Subject: [PATCH 028/103] Renamed theCueFound to loadCueFound --- src/library/dlgtrackinfo.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index 56fb01bcee77..4b637e5a5af8 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -374,7 +374,7 @@ void DlgTrackInfo::saveTrack() { m_pLoadedTrack->setKeys(m_keysClone); - bool theCueFound = false; + bool loadCueFound = false; QSet updatedRows; for (int row = 0; row < cueTable->rowCount(); ++row) { QTableWidgetItem* rowItem = cueTable->item(row, 0); @@ -398,11 +398,11 @@ void DlgTrackInfo::saveTrack() { int iTableHotcue = vHotcue.toInt(&ok); if (ok) { - if (iTableHotcue == 0 && !theCueFound) { + if (iTableHotcue == 0 && !loadCueFound) { // adopt the first 0 hot cue as the cue pCue->setHotCue(-1); pCue->setType(Cue::LOAD); - theCueFound = true; + loadCueFound = true; } else { // The GUI shows hotcues as 1-indexed, but they are actually // 0-indexed, so subtract 1 From dcd41780ffa53b87f50ac9489a8bbcaf82dce066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 10 Sep 2019 20:48:50 +0200 Subject: [PATCH 029/103] Use HotCue -1 as default, because HotCue 0 is the same as The Cue --- src/library/dlgtrackinfo.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index 4b637e5a5af8..5645402a02e7 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -374,7 +374,7 @@ void DlgTrackInfo::saveTrack() { m_pLoadedTrack->setKeys(m_keysClone); - bool loadCueFound = false; + bool loadCueFound = false; // TODO: Rename with Cue::LOAD QSet updatedRows; for (int row = 0; row < cueTable->rowCount(); ++row) { QTableWidgetItem* rowItem = cueTable->item(row, 0); @@ -399,7 +399,7 @@ void DlgTrackInfo::saveTrack() { int iTableHotcue = vHotcue.toInt(&ok); if (ok) { if (iTableHotcue == 0 && !loadCueFound) { - // adopt the first 0 hot cue as the cue + // adopt the first HotCue 0 as LOAD Cue = THE CUE pCue->setHotCue(-1); pCue->setType(Cue::LOAD); loadCueFound = true; @@ -410,7 +410,9 @@ void DlgTrackInfo::saveTrack() { pCue->setType(Cue::CUE); } } else { - pCue->setHotCue(-1); + // Default to HotCue -1, a valid Cue point without a hot cue button assigned. + iTableHotcue = -1; + pCue->setHotCue(iTableHotcue - 1); pCue->setType(Cue::CUE); } From 460e46d07862ae7d56c0b2b5ab134569c84d2f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 10 Sep 2019 20:52:00 +0200 Subject: [PATCH 030/103] Improve comments --- src/engine/cuecontrol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/cuecontrol.cpp b/src/engine/cuecontrol.cpp index 0dd730b304ee..401699c8606b 100644 --- a/src/engine/cuecontrol.cpp +++ b/src/engine/cuecontrol.cpp @@ -454,7 +454,7 @@ void CueControl::hotcueActivate(HotcueControl* pControl, double v) { } else { // The cue is non-existent ... if (v) { - // create set it to the current position + // set it to the current position hotcueSet(pControl, v); } else if (m_iCurrentlyPreviewingHotcues) { // yet we got a release for it and are From c1e7a0ce08c048f84e74747b53be150759a5df61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 10 Sep 2019 23:10:42 +0200 Subject: [PATCH 031/103] Simplify the use of m_hotcueControls --- src/engine/cuecontrol.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/engine/cuecontrol.cpp b/src/engine/cuecontrol.cpp index 401699c8606b..1f33d9e3c168 100644 --- a/src/engine/cuecontrol.cpp +++ b/src/engine/cuecontrol.cpp @@ -187,8 +187,7 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { QMutexLocker lock(&m_mutex); if (m_pLoadedTrack) { disconnect(m_pLoadedTrack.get(), 0, this, 0); - for (int hotCue = 0; hotCue < m_iNumHotCues; ++hotCue) { - HotcueControl* pControl = m_hotcueControls.value(hotCue, nullptr); + for (const auto& pControl: m_hotcueControls) { detachCue(pControl); } @@ -216,8 +215,8 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { pLoadCue = pCue; } int hotcue = pCue->getHotCue(); - if (hotcue != -1) { - HotcueControl* pControl = m_hotcueControls.value(hotcue, nullptr); + HotcueControl* pControl = m_hotcueControls.value(hotcue); + if (pControl) { attachCue(pCue, pControl); } } @@ -275,13 +274,9 @@ void CueControl::trackCuesUpdated() { continue; int hotcue = pCue->getHotCue(); - if (hotcue != -1) { - HotcueControl* pControl = m_hotcueControls.value(hotcue, nullptr); - - // Cue's hotcue doesn't have a hotcue control. - if (!pControl) { - continue; - } + HotcueControl* pControl = m_hotcueControls.value(hotcue); + if (pControl) { + // Note: The Cue's hotcue doesn't have a hotcue control. CuePointer pOldCue(pControl->getCue()); @@ -301,7 +296,7 @@ void CueControl::trackCuesUpdated() { // Detach all hotcues that are no longer present for (int hotCue = 0; hotCue < m_iNumHotCues; ++hotCue) { if (!active_hotcues.contains(hotCue)) { - HotcueControl* pControl = m_hotcueControls.value(hotCue, nullptr); + HotcueControl* pControl = m_hotcueControls.at(hotCue); detachCue(pControl); } } @@ -533,7 +528,6 @@ void CueControl::hotcuePositionChanged(HotcueControl* pControl, double newPositi if (pCue) { // Setting the position to -1 is the same as calling hotcue_x_clear if (newPosition == -1) { - pCue->setHotCue(-1); detachCue(pControl); } else if (newPosition > 0 && newPosition < m_pTrackSamples->get()) { pCue->setPosition(newPosition); From 0155f11673cec3719122e284ce3f155996933057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Tue, 10 Sep 2019 23:30:27 +0200 Subject: [PATCH 032/103] Update m_pCuePoint as well when saving --- src/engine/cuecontrol.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/engine/cuecontrol.cpp b/src/engine/cuecontrol.cpp index 1f33d9e3c168..b70fa5ae96bf 100644 --- a/src/engine/cuecontrol.cpp +++ b/src/engine/cuecontrol.cpp @@ -265,12 +265,20 @@ void CueControl::trackCuesUpdated() { if (!m_pLoadedTrack) return; + bool loadCueFound = false; const QList cuePoints(m_pLoadedTrack->getCuePoints()); QListIterator it(cuePoints); while (it.hasNext()) { CuePointer pCue(it.next()); - if (pCue->getType() != Cue::CUE && pCue->getType() != Cue::LOAD) + if (pCue->getType() == Cue::LOAD) { + loadCueFound = true; + m_pCuePoint->set(pCue->getPosition()); + continue; + } + + + if (pCue->getType() != Cue::CUE) continue; int hotcue = pCue->getHotCue(); @@ -293,6 +301,10 @@ void CueControl::trackCuesUpdated() { } } + if (!loadCueFound) { + m_pCuePoint->set(-1); + } + // Detach all hotcues that are no longer present for (int hotCue = 0; hotCue < m_iNumHotCues; ++hotCue) { if (!active_hotcues.contains(hotCue)) { From aa9fbd711e88c2544c91267faa84bbc1eae7bdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Wed, 11 Sep 2019 19:57:58 +0200 Subject: [PATCH 033/103] remove redundant virtual keywords --- src/engine/cuecontrol.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/cuecontrol.h b/src/engine/cuecontrol.h index 014929e4169f..8530accd778a 100644 --- a/src/engine/cuecontrol.h +++ b/src/engine/cuecontrol.h @@ -22,7 +22,7 @@ class HotcueControl : public QObject { Q_OBJECT public: HotcueControl(QString group, int hotcueNumber); - virtual ~HotcueControl(); + ~HotcueControl() override; inline int getHotcueNumber() { return m_iHotcueNumber; } inline CuePointer getCue() { return m_pCue; } @@ -96,7 +96,7 @@ class CueControl : public EngineControl { UserSettingsPointer pConfig); ~CueControl() override; - virtual void hintReader(HintVector* pHintList) override; + void hintReader(HintVector* pHintList) override; bool updateIndicatorsAndModifyPlay(bool newPlay, bool playPossible); void updateIndicators(); void resetIndicators(); From 3c4558928e0f940c00c6f33819946d84da69b303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Fri, 13 Sep 2019 21:54:01 +0200 Subject: [PATCH 034/103] store cue points <= 0. Fixing lp1843922 --- src/track/track.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/track/track.cpp b/src/track/track.cpp index 3c16f82a9ad3..b514ce505570 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -757,7 +757,7 @@ void Track::setCuePoint(double cue) { break; } } - if (cue > 0) { + if (cue != -1) { if (!pLoadCue) { pLoadCue = CuePointer(new Cue(m_record.getId())); pLoadCue->setType(Cue::LOAD); From c1fd96e882866199950dae4ec1d59728cb58a355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Fri, 13 Sep 2019 22:53:21 +0200 Subject: [PATCH 035/103] Sync library table cue point with cue table cue point --- src/engine/cuecontrol.cpp | 41 +++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/engine/cuecontrol.cpp b/src/engine/cuecontrol.cpp index b70fa5ae96bf..9cc947047be7 100644 --- a/src/engine/cuecontrol.cpp +++ b/src/engine/cuecontrol.cpp @@ -207,35 +207,38 @@ void CueControl::trackLoaded(TrackPointer pNewTrack) { CuePointer pLoadCue; for (const CuePointer& pCue: m_pLoadedTrack->getCuePoints()) { - if (pCue->getType() == Cue::CUE) { - continue; // skip - } if (pCue->getType() == Cue::LOAD) { DEBUG_ASSERT(!pLoadCue); pLoadCue = pCue; } - int hotcue = pCue->getHotCue(); - HotcueControl* pControl = m_hotcueControls.value(hotcue); - if (pControl) { - attachCue(pCue, pControl); - } } + + // Need to unlock before emitting any signals to prevent deadlock. + lock.unlock(); + // Use pNewTrack from now, because m_pLoadedTrack might have been reset + // immediately after leaving the locking scope! + + + // Because of legacy, we store the (load) cue point twice and need to + // sync both values. + // The Cue::LOAD from getCuePoints() has the priority double cuePoint; if (pLoadCue) { cuePoint = pLoadCue->getPosition(); + // adjust the track cue accordingly + pNewTrack->setCuePoint(cuePoint); } else { // If no load cue point is stored, read from track - cuePoint = m_pLoadedTrack->getCuePoint(); + // Note: This is 0:00 for new tracks + cuePoint = pNewTrack->getCuePoint(); + // Than add the load cue to the list of cue + CuePointer pCue(pNewTrack->createAndAddCue()); + pCue->setPosition(cuePoint); + pCue->setHotCue(-1); + pCue->setType(Cue::LOAD); } m_pCuePoint->set(cuePoint); - // Need to unlock before emitting any signals to prevent deadlock. - lock.unlock(); - - // Use pNewTrack here, because m_pLoadedTrack might have been reset - // immediately after leaving the locking scope! - pNewTrack->setCuePoint(cuePoint); - // If cue recall is ON in the prefs, then we're supposed to seek to the cue // point on song load. Note that [Controls],cueRecall == 0 corresponds to "ON", not OFF. bool cueRecall = (getConfig()->getValue( @@ -271,15 +274,15 @@ void CueControl::trackCuesUpdated() { while (it.hasNext()) { CuePointer pCue(it.next()); - if (pCue->getType() == Cue::LOAD) { + if (!loadCueFound && pCue->getType() == Cue::LOAD) { loadCueFound = true; m_pCuePoint->set(pCue->getPosition()); continue; } - - if (pCue->getType() != Cue::CUE) + if (pCue->getType() != Cue::CUE) { continue; + } int hotcue = pCue->getHotCue(); HotcueControl* pControl = m_hotcueControls.value(hotcue); From 0facb16c75011c496602c6d5007b50c25014511a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Sep 2019 10:57:25 +0200 Subject: [PATCH 036/103] Delete empty lines to improve readability --- src/library/dlgtrackinfo.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/library/dlgtrackinfo.cpp b/src/library/dlgtrackinfo.cpp index 5645402a02e7..aa17c502ceac 100644 --- a/src/library/dlgtrackinfo.cpp +++ b/src/library/dlgtrackinfo.cpp @@ -389,13 +389,10 @@ void DlgTrackInfo::saveTrack() { if (!pCue) { continue; } - updatedRows.insert(oldRow); QVariant vHotcue = hotcueItem->data(Qt::DisplayRole); bool ok; - - int iTableHotcue = vHotcue.toInt(&ok); if (ok) { if (iTableHotcue == 0 && !loadCueFound) { From dc891b66a88a2e1ca26110d6fb3579bd907457c0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 13:48:08 +0200 Subject: [PATCH 037/103] Handle an edge case when evicting cached tracks --- src/track/globaltrackcache.cpp | 20 ++++++++++---------- src/track/globaltrackcache.h | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index b4d7b3302d76..1a205a658237 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -631,7 +631,7 @@ void GlobalTrackCache::evictAndSave( return; } - DEBUG_ASSERT(isEvicted(cacheEntryPtr->getPlainPtr())); + DEBUG_ASSERT(!isCached(cacheEntryPtr->getPlainPtr())); m_pSaver->saveCachedTrack(cacheEntryPtr->getPlainPtr()); // here the cacheEntryPtr goes out of scope, the cache is deleted @@ -653,8 +653,8 @@ bool GlobalTrackCache::evict(Track* plainPtr) { } if (trackRef.hasId()) { const auto trackById = m_tracksById.find(trackRef.getId()); - if (trackById != m_tracksById.end()) { - DEBUG_ASSERT(trackById->second->getPlainPtr() == plainPtr); + if (trackById != m_tracksById.end() && + trackById->second->getPlainPtr() == plainPtr) { m_tracksById.erase(trackById); evicted = true; } @@ -662,14 +662,14 @@ bool GlobalTrackCache::evict(Track* plainPtr) { if (trackRef.hasCanonicalLocation()) { const auto trackByCanonicalLocation( m_tracksByCanonicalLocation.find(trackRef.getCanonicalLocation())); - if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation) { - DEBUG_ASSERT(trackByCanonicalLocation->second->getPlainPtr() == plainPtr); + if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation && + trackByCanonicalLocation->second->getPlainPtr() == plainPtr) { m_tracksByCanonicalLocation.erase( trackByCanonicalLocation); evicted = true; } } - DEBUG_ASSERT(isEvicted(plainPtr)); + DEBUG_ASSERT(!isCached(plainPtr)); // Don't erase the pointer from m_cachedTracks here, because // this function is invoked from 2 different contexts. The // caller is responsible for doing this. Until then the cache @@ -677,16 +677,16 @@ bool GlobalTrackCache::evict(Track* plainPtr) { return evicted; } -bool GlobalTrackCache::isEvicted(Track* plainPtr) const { +bool GlobalTrackCache::isCached(Track* plainPtr) const { for (auto&& entry: m_tracksById) { if (entry.second->getPlainPtr() == plainPtr) { - return false; + return true; } } for (auto&& entry: m_tracksByCanonicalLocation) { if (entry.second->getPlainPtr() == plainPtr) { - return false; + return true; } } - return true; + return false; } diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 22fc016348ee..f38d211569cf 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -221,7 +221,7 @@ private slots: void purgeTrackId(TrackId trackId); bool evict(Track* plainPtr); - bool isEvicted(Track* plainPtr) const; + bool isCached(Track* plainPtr) const; bool isEmpty() const; From 427cf4a8256869f2d85d66b88d98e208822bb2f0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 14:02:53 +0200 Subject: [PATCH 038/103] Delete obsolete comment --- src/track/globaltrackcache.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 1a205a658237..5d9397fa0319 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -670,10 +670,6 @@ bool GlobalTrackCache::evict(Track* plainPtr) { } } DEBUG_ASSERT(!isCached(plainPtr)); - // Don't erase the pointer from m_cachedTracks here, because - // this function is invoked from 2 different contexts. The - // caller is responsible for doing this. Until then the cache - // is inconsistent and verifyConsitency() is expected to fail. return evicted; } From cb3fb2176c25ae00e50c18ceaa56beacba9e4388 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 14:03:35 +0200 Subject: [PATCH 039/103] Reword comment --- src/track/globaltrackcache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 5d9397fa0319..69da9f9caf28 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -520,7 +520,7 @@ void GlobalTrackCache::resolve( // and will be deleted later within the event loop. But this // function might be called from any thread, even from worker // threads without an event loop. We need to move the newly - // created object to the target thread. + // created object to the main thread. deletingPtr->moveToThread(QApplication::instance()->thread()); auto cacheEntryPtr = std::make_shared( From b3eefd59f439162e56db5dcf446cca3daf7ad223 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 14:48:58 +0200 Subject: [PATCH 040/103] Replace getter/setter with dedicated functions --- src/track/globaltrackcache.cpp | 10 +++++----- src/track/globaltrackcache.h | 15 +++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 69da9f9caf28..b13b7f0a4e34 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -413,7 +413,7 @@ TrackPointer GlobalTrackCache::lookupByRef( TrackPointer GlobalTrackCache::revive( GlobalTrackCacheEntryPointer entryPtr) { - TrackPointer savingPtr = entryPtr->getSavingWeakPtr().lock(); + TrackPointer savingPtr = entryPtr->lock(); if (savingPtr) { if (traceLogEnabled()) { kLogger.trace() @@ -433,11 +433,11 @@ TrackPointer GlobalTrackCache::revive( << "Reviving zombie track" << entryPtr->getPlainPtr(); } - DEBUG_ASSERT(entryPtr->getSavingWeakPtr().expired()); + DEBUG_ASSERT(entryPtr->expired()); savingPtr = TrackPointer(entryPtr->getPlainPtr(), EvictAndSaveFunctor(entryPtr)); - entryPtr->setSavingWeakPtr(savingPtr); + entryPtr->init(savingPtr); return savingPtr; } @@ -528,7 +528,7 @@ void GlobalTrackCache::resolve( auto savingPtr = TrackPointer( cacheEntryPtr->getPlainPtr(), EvictAndSaveFunctor(cacheEntryPtr)); - cacheEntryPtr->setSavingWeakPtr(savingPtr); + cacheEntryPtr->init(savingPtr); if (debugLogEnabled()) { kLogger.debug() @@ -608,7 +608,7 @@ void GlobalTrackCache::evictAndSave( GlobalTrackCacheLocker cacheLocker; - if (!cacheEntryPtr->getSavingWeakPtr().expired()) { + if (!cacheEntryPtr->expired()) { // We have handed out (revived) this track again after our reference count // drops to zero and before acquire the lock at the beginning of this function if (debugLogEnabled()) { diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index f38d211569cf..e088484d661a 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -46,14 +46,21 @@ class GlobalTrackCacheEntry final { GlobalTrackCacheEntry(const GlobalTrackCacheEntry& other) = delete; + void init(TrackWeakPointer savingWeakPtr) { + // Uninitialized or expired + DEBUG_ASSERT(!m_savingWeakPtr.lock()); + m_savingWeakPtr = std::move(savingWeakPtr); + } + Track* getPlainPtr() const { return m_deletingPtr.get(); } - const TrackWeakPointer& getSavingWeakPtr() const { - return m_savingWeakPtr; + + TrackPointer lock() const { + return m_savingWeakPtr.lock(); } - void setSavingWeakPtr(TrackWeakPointer savingWeakPtr) { - m_savingWeakPtr = std::move(savingWeakPtr); + bool expired() const { + return m_savingWeakPtr.expired(); } private: From 55703a4a07aaef241927da7cd28390c88b78181a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 22:48:35 +0200 Subject: [PATCH 041/103] Block track signals in cache instead of library --- src/library/library.cpp | 5 +---- src/track/globaltrackcache.cpp | 25 ++++++++++++++++++++----- src/track/globaltrackcache.h | 4 +++- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/library/library.cpp b/src/library/library.cpp index e223fa9ba096..6a7c11c6ef75 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -442,10 +442,7 @@ void Library::saveCachedTrack(Track* pTrack) noexcept { // It can produce dangerous signal loops if the track is still // sending signals while being saved! // See: https://bugs.launchpad.net/mixxx/+bug/1365708 - // NOTE(uklotzde, 2018-02-03): Simply disconnecting all receivers - // doesn't seem to work reliably. Emitting the clean() signal from - // a track that is about to deleted may cause access violations!! - pTrack->blockSignals(true); + DEBUG_ASSERT(pTrack->signalsBlocked()); // The metadata must be exported while the cache is locked to // ensure that we have exclusive (write) access on the file diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index b13b7f0a4e34..5d1dfdbf26a1 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -74,6 +74,7 @@ void deleteTrack(Track* plainPtr) { << "Deleting" << plainPtr; } + DEBUG_ASSERT(plainPtr->signalsBlocked()); plainPtr->deleteLater(); } @@ -321,6 +322,18 @@ void GlobalTrackCache::relocateTracks( m_tracksByCanonicalLocation = std::move(relocatedTracksByCanonicalLocation); } +void GlobalTrackCache::saveEvictedTrack(Track* plainPtr) const { + DEBUG_ASSERT(plainPtr); + // Disconnect all receivers and block signals before saving the + // track. + // NOTE(uklotzde, 2018-02-03): Simply disconnecting all receivers + // doesn't seem to work reliably. Emitting the clean() signal from + // a track that is about to deleted may cause access violations!! + plainPtr->disconnect(); + plainPtr->blockSignals(true); + m_pSaver->saveCachedTrack(plainPtr); +} + void GlobalTrackCache::deactivate() { // Ideally the cache should be empty when destroyed. // But since this is difficult to achieve all remaining @@ -331,7 +344,7 @@ void GlobalTrackCache::deactivate() { auto i = m_tracksById.begin(); while (i != m_tracksById.end()) { Track* plainPtr= i->second->getPlainPtr(); - m_pSaver->saveCachedTrack(plainPtr); + saveEvictedTrack(plainPtr); m_tracksByCanonicalLocation.erase(plainPtr->getCanonicalLocation()); i = m_tracksById.erase(i); } @@ -339,7 +352,7 @@ void GlobalTrackCache::deactivate() { auto j = m_tracksByCanonicalLocation.begin(); while (j != m_tracksByCanonicalLocation.end()) { Track* plainPtr= j->second->getPlainPtr(); - m_pSaver->saveCachedTrack(plainPtr); + saveEvictedTrack(plainPtr); j = m_tracksByCanonicalLocation.erase(j); } @@ -420,6 +433,7 @@ TrackPointer GlobalTrackCache::revive( << "Found alive track" << entryPtr->getPlainPtr(); } + DEBUG_ASSERT(!savingPtr->signalsBlocked()); return savingPtr; } @@ -438,6 +452,7 @@ TrackPointer GlobalTrackCache::revive( savingPtr = TrackPointer(entryPtr->getPlainPtr(), EvictAndSaveFunctor(entryPtr)); entryPtr->init(savingPtr); + DEBUG_ASSERT(!savingPtr->signalsBlocked()); return savingPtr; } @@ -632,10 +647,10 @@ void GlobalTrackCache::evictAndSave( } DEBUG_ASSERT(!isCached(cacheEntryPtr->getPlainPtr())); - m_pSaver->saveCachedTrack(cacheEntryPtr->getPlainPtr()); + saveEvictedTrack(cacheEntryPtr->getPlainPtr()); - // here the cacheEntryPtr goes out of scope, the cache is deleted - // including the owned track + // here the cacheEntryPtr goes out of scope, the cache entry is + // deleted including the owned track } bool GlobalTrackCache::evict(Track* plainPtr) { diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index e088484d661a..87c69f639fc8 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -172,7 +172,7 @@ class GlobalTrackCacheResolver final: public GlobalTrackCacheLocker { class /*interface*/ GlobalTrackCacheSaver { private: friend class GlobalTrackCache; - virtual void saveCachedTrack(Track* pTrack) noexcept = 0; + virtual void saveCachedTrack(Track* plainPtr) noexcept = 0; protected: virtual ~GlobalTrackCacheSaver() {} @@ -234,6 +234,8 @@ private slots: void deactivate(); + void saveEvictedTrack(Track* plainPtr) const; + // Managed by GlobalTrackCacheLocker mutable QMutex m_mutex; From 0554943a078c67ff78c540db4b886b1b468997d3 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 22:50:52 +0200 Subject: [PATCH 042/103] Ensure that cache items are moved and not copied --- src/track/globaltrackcache.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 87c69f639fc8..6f3197eee62f 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -43,8 +43,8 @@ class GlobalTrackCacheEntry final { std::unique_ptr deletingPtr) : m_deletingPtr(std::move(deletingPtr)) { } - GlobalTrackCacheEntry(const GlobalTrackCacheEntry& other) = delete; + GlobalTrackCacheEntry(GlobalTrackCacheEntry&&) = default; void init(TrackWeakPointer savingWeakPtr) { // Uninitialized or expired From 3ebfee823040b2c153fb048b773a6a6bbb392904 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Sep 2019 19:55:54 +0200 Subject: [PATCH 043/103] Verify that the evict-and-save functor is only executed once --- src/track/globaltrackcache.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 5d1dfdbf26a1..b39326c3f844 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -36,17 +36,22 @@ TrackRef createTrackRef(const Track& track) { class EvictAndSaveFunctor { public: - explicit EvictAndSaveFunctor(GlobalTrackCacheEntryPointer cacheEntryPtr) + explicit EvictAndSaveFunctor( + GlobalTrackCacheEntryPointer cacheEntryPtr) : m_cacheEntryPtr(std::move(cacheEntryPtr)) { } void operator()(Track* plainPtr) { + Q_UNUSED(plainPtr); // only used in DEBUG_ASSERT + DEBUG_ASSERT(m_cacheEntryPtr); DEBUG_ASSERT(plainPtr == m_cacheEntryPtr->getPlainPtr()); // Here we move m_cacheEntryPtr and the owned track out of the // functor and the owning reference counting object. // This is required to break a cycle reference from the weak pointer // inside the cache entry to the same reference counting object. GlobalTrackCache::evictAndSaveCachedTrack(std::move(m_cacheEntryPtr)); + // Verify that this functor is only invoked once + DEBUG_ASSERT(!m_cacheEntryPtr); } const GlobalTrackCacheEntryPointer& getCacheEntryPointer() const { From 8266455898b3b58358ce51a9505ef282c00e106f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 22:51:35 +0200 Subject: [PATCH 044/103] Move code block --- src/track/globaltrackcache.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index b39326c3f844..3fa87da5998a 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -536,12 +536,6 @@ void GlobalTrackCache::resolve( std::move(pSecurityToken), std::move(trackId)), deleteTrack); - // Track objects live together with the cache on the main thread - // and will be deleted later within the event loop. But this - // function might be called from any thread, even from worker - // threads without an event loop. We need to move the newly - // created object to the main thread. - deletingPtr->moveToThread(QApplication::instance()->thread()); auto cacheEntryPtr = std::make_shared( std::move(deletingPtr)); @@ -573,6 +567,14 @@ void GlobalTrackCache::resolve( trackRef.getCanonicalLocation(), cacheEntryPtr)); } + + // Track objects live together with the cache on the main thread + // and will be deleted later within the event loop. But this + // function might be called from any thread, even from worker + // threads without an event loop. We need to move the newly + // created object to the main thread. + savingPtr->moveToThread(QApplication::instance()->thread()); + pCacheResolver->initLookupResult( GlobalTrackCacheLookupResult::MISS, std::move(savingPtr), From 2b1d429d357df480730eb02f1683df4bb0a80842 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 22:52:25 +0200 Subject: [PATCH 045/103] Balance load in test threads ...to avoid that more track objects are created than destroyed. --- src/test/globaltrackcache_test.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 6630c22f676e..6585130d853e 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -156,9 +156,13 @@ TEST_F(GlobalTrackCacheTest, concurrentDelete) { // lp1744550: Accessing the track from multiple threads is // required to cause a SIGSEGV - track->setArtist(track->getTitle()); + track->setArtist(QString("Artist %1").arg(QString::number(i))); m_recentTrackPtr = std::move(track); + + // Lookup the track again + track = GlobalTrackCacheLocker().lookupTrackById(trackId); + EXPECT_TRUE(static_cast(track)); } m_recentTrackPtr.reset(); From 540c933aa9bfc500516d6ff57786dc3361d3b437 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Sep 2019 22:53:28 +0200 Subject: [PATCH 046/103] Fix memory leak in test --- src/test/globaltrackcache_test.cpp | 19 +++++++++++++------ src/track/globaltrackcache.cpp | 13 +++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 6585130d853e..4d05069ed011 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -27,8 +27,7 @@ class TrackTitleThread: public QThread { void run() override { int loopCount = 0; - while (!m_stop.load()) { - m_recentTrackPtr.reset(); + while (!(m_stop.load() && GlobalTrackCacheLocker().isEmpty())) { const TrackId trackId(loopCount % 2); auto track = GlobalTrackCacheLocker().lookupTrackById(trackId); if (track) { @@ -46,7 +45,7 @@ class TrackTitleThread: public QThread { m_recentTrackPtr = std::move(track); ++loopCount; } - m_recentTrackPtr.reset(); + DEBUG_ASSERT(!m_recentTrackPtr); qDebug() << "Finished" << loopCount << " thread loops"; } @@ -163,16 +162,20 @@ TEST_F(GlobalTrackCacheTest, concurrentDelete) { // Lookup the track again track = GlobalTrackCacheLocker().lookupTrackById(trackId); EXPECT_TRUE(static_cast(track)); + + // Ensure that track objects are evicted and deleted + QCoreApplication::processEvents(); } m_recentTrackPtr.reset(); workerThread.stop(); - workerThread.wait(); // Ensure that all track objects have been deleted - QCoreApplication::processEvents(); + while (!GlobalTrackCacheLocker().isEmpty()) { + QCoreApplication::processEvents(); + } - EXPECT_TRUE(GlobalTrackCacheLocker().isEmpty()); + workerThread.wait(); } TEST_F(GlobalTrackCacheTest, evictWhileMoving) { @@ -188,4 +191,8 @@ TEST_F(GlobalTrackCacheTest, evictWhileMoving) { EXPECT_TRUE(static_cast(track1)); EXPECT_FALSE(static_cast(track2)); + + track1.reset(); + + EXPECT_TRUE(GlobalTrackCacheLocker().isEmpty()); } diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 3fa87da5998a..194c2961d761 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -80,7 +80,20 @@ void deleteTrack(Track* plainPtr) { << plainPtr; } DEBUG_ASSERT(plainPtr->signalsBlocked()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + if (plainPtr->thread()->loopLevel() > 0) { + plainPtr->deleteLater(); + } else { + // Delete track directly if no event loop is running. + // Otherwise no track objects would be deleted during + // a unit test that doesn't start an event loop. Invoking + // QCoreApplication::processEvents() periodically is not + // sufficient! + delete plainPtr; + } +#else plainPtr->deleteLater(); +#endif } } // anonymous namespace From 1181897a12178c766db5f259462592083f2ac1ce Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Sep 2019 00:04:44 +0200 Subject: [PATCH 047/103] Fix crash during tests --- src/test/librarytest.h | 29 ++++++++++++++--------------- src/track/globaltrackcache.cpp | 5 +++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/test/librarytest.h b/src/test/librarytest.h index 7cf4459a2100..c94877f89bed 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -1,5 +1,6 @@ -#ifndef LIBRARYTEST_H -#define LIBRARYTEST_H +#pragma once + +#include #include "test/mixxxtest.h" @@ -9,17 +10,15 @@ #include "util/db/dbconnectionpooled.h" #include "track/globaltrackcache.h" -namespace { - const bool kInMemoryDbConnection = true; -} // anonymous namespace +const bool kInMemoryDbConnection = true; class LibraryTest : public MixxxTest, public virtual /*implements*/ GlobalTrackCacheSaver { public: void saveCachedTrack(Track* pTrack) noexcept override { - m_trackCollection.exportTrackMetadata(pTrack); - m_trackCollection.saveTrack(pTrack); + m_trackCollection->exportTrackMetadata(pTrack); + m_trackCollection->saveTrack(pTrack); } protected: @@ -27,14 +26,17 @@ class LibraryTest : public MixxxTest, : m_mixxxDb(config(), kInMemoryDbConnection), m_dbConnectionPooler(m_mixxxDb.connectionPool()), m_dbConnection(mixxx::DbConnectionPooled(m_mixxxDb.connectionPool())), - m_trackCollection(config()) { + m_trackCollection(std::make_unique(config())) { MixxxDb::initDatabaseSchema(m_dbConnection); - m_trackCollection.connectDatabase(m_dbConnection); + m_trackCollection->connectDatabase(m_dbConnection); GlobalTrackCache::createInstance(this); } ~LibraryTest() override { + m_trackCollection->disconnectDatabase(); + m_trackCollection.reset(); + // With the track collection all remaining track references + // should have been dropped before destroying the cache. GlobalTrackCache::destroyInstance(); - m_trackCollection.disconnectDatabase(); } mixxx::DbConnectionPoolPtr dbConnectionPool() const { @@ -46,15 +48,12 @@ class LibraryTest : public MixxxTest, } TrackCollection* collection() { - return &m_trackCollection; + return m_trackCollection.get(); } private: const MixxxDb m_mixxxDb; const mixxx::DbConnectionPooler m_dbConnectionPooler; QSqlDatabase m_dbConnection; - TrackCollection m_trackCollection; + std::unique_ptr m_trackCollection; }; - - -#endif /* LIBRARYTEST_H */ diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 194c2961d761..e1cf1d2cc328 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -236,6 +236,11 @@ void GlobalTrackCache::createInstance(GlobalTrackCacheSaver* pDeleter) { //static void GlobalTrackCache::destroyInstance() { DEBUG_ASSERT(s_pInstance); + // Processing all pending events is required to evict all + // remaining references from the cache. + QCoreApplication::processEvents(); + // Now the cache should be empty + DEBUG_ASSERT(GlobalTrackCacheLocker().isEmpty()); GlobalTrackCache* pInstance = s_pInstance; // Reset the static/global pointer before entering the destructor s_pInstance = nullptr; From b6056bdb69f1ddc9a4ecb9b52381535dcd7da6dc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Sep 2019 00:11:46 +0200 Subject: [PATCH 048/103] Explain how the test is intended to work --- src/test/globaltrackcache_test.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 4d05069ed011..4fd7926f8d56 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -28,6 +28,10 @@ class TrackTitleThread: public QThread { void run() override { int loopCount = 0; while (!(m_stop.load() && GlobalTrackCacheLocker().isEmpty())) { + // Drop the previous reference to avoid resolving the + // same track twice + m_recentTrackPtr.reset(); + // Try to resolve the next track by guessing the id const TrackId trackId(loopCount % 2); auto track = GlobalTrackCacheLocker().lookupTrackById(trackId); if (track) { @@ -42,10 +46,13 @@ class TrackTitleThread: public QThread { } ASSERT_TRUE(track->isDirty()); } + // Replace the current reference with this one and keep it alive + // until the next loop cycle m_recentTrackPtr = std::move(track); ++loopCount; } - DEBUG_ASSERT(!m_recentTrackPtr); + // If the cache is empty all references must have been dropped + ASSERT_TRUE(!m_recentTrackPtr); qDebug() << "Finished" << loopCount << " thread loops"; } From 1d9953a78446d49a83bbaf10bcde8be08043e1ee Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Sep 2019 00:12:22 +0200 Subject: [PATCH 049/103] Rename class member --- src/test/librarytest.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/librarytest.h b/src/test/librarytest.h index c94877f89bed..cb3622f7927b 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -26,14 +26,14 @@ class LibraryTest : public MixxxTest, : m_mixxxDb(config(), kInMemoryDbConnection), m_dbConnectionPooler(m_mixxxDb.connectionPool()), m_dbConnection(mixxx::DbConnectionPooled(m_mixxxDb.connectionPool())), - m_trackCollection(std::make_unique(config())) { + m_pTrackCollection(std::make_unique(config())) { MixxxDb::initDatabaseSchema(m_dbConnection); - m_trackCollection->connectDatabase(m_dbConnection); + m_pTrackCollection->connectDatabase(m_dbConnection); GlobalTrackCache::createInstance(this); } ~LibraryTest() override { - m_trackCollection->disconnectDatabase(); - m_trackCollection.reset(); + m_pTrackCollection->disconnectDatabase(); + m_pTrackCollection.reset(); // With the track collection all remaining track references // should have been dropped before destroying the cache. GlobalTrackCache::destroyInstance(); @@ -48,12 +48,12 @@ class LibraryTest : public MixxxTest, } TrackCollection* collection() { - return m_trackCollection.get(); + return m_pTrackCollection.get(); } private: const MixxxDb m_mixxxDb; const mixxx::DbConnectionPooler m_dbConnectionPooler; QSqlDatabase m_dbConnection; - std::unique_ptr m_trackCollection; + std::unique_ptr m_pTrackCollection; }; From bf0fe4c45d7db9f12d261b31f80fef2b7cfb6e49 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Sep 2019 00:12:46 +0200 Subject: [PATCH 050/103] Rename method and parameter --- src/library/library.cpp | 2 +- src/library/library.h | 2 +- src/test/globaltrackcache_test.cpp | 2 +- src/test/librarytest.h | 6 +++--- src/track/globaltrackcache.cpp | 10 +++++----- src/track/globaltrackcache.h | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/library/library.cpp b/src/library/library.cpp index 6a7c11c6ef75..12787fa5b90d 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -438,7 +438,7 @@ void Library::setEditMedatataSelectedClick(bool enabled) { emit(setSelectedClick(enabled)); } -void Library::saveCachedTrack(Track* pTrack) noexcept { +void Library::saveEvictedTrack(Track* pTrack) noexcept { // It can produce dangerous signal loops if the track is still // sending signals while being saved! // See: https://bugs.launchpad.net/mixxx/+bug/1365708 diff --git a/src/library/library.h b/src/library/library.h index c699be46074d..2c1442e94eea 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -126,7 +126,7 @@ class Library: public QObject, private: // Callback for GlobalTrackCache - void saveCachedTrack(Track* pTrack) noexcept override; + void saveEvictedTrack(Track* pTrack) noexcept override; const UserSettingsPointer m_pConfig; diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 4fd7926f8d56..87cad6f912c4 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -66,7 +66,7 @@ class TrackTitleThread: public QThread { class GlobalTrackCacheTest: public MixxxTest, public virtual GlobalTrackCacheSaver { public: - void saveCachedTrack(Track* pTrack) noexcept override { + void saveEvictedTrack(Track* pTrack) noexcept override { ASSERT_FALSE(pTrack == nullptr); } diff --git a/src/test/librarytest.h b/src/test/librarytest.h index cb3622f7927b..6fef9112b206 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -16,9 +16,9 @@ class LibraryTest : public MixxxTest, public virtual /*implements*/ GlobalTrackCacheSaver { public: - void saveCachedTrack(Track* pTrack) noexcept override { - m_trackCollection->exportTrackMetadata(pTrack); - m_trackCollection->saveTrack(pTrack); + void saveEvictedTrack(Track* pTrack) noexcept override { + m_pTrackCollection->exportTrackMetadata(pTrack); + m_pTrackCollection->saveTrack(pTrack); } protected: diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index e1cf1d2cc328..5716f93da36e 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -345,16 +345,16 @@ void GlobalTrackCache::relocateTracks( m_tracksByCanonicalLocation = std::move(relocatedTracksByCanonicalLocation); } -void GlobalTrackCache::saveEvictedTrack(Track* plainPtr) const { - DEBUG_ASSERT(plainPtr); +void GlobalTrackCache::saveEvictedTrack(Track* pEvictedTrack) const { + DEBUG_ASSERT(pEvictedTrack); // Disconnect all receivers and block signals before saving the // track. // NOTE(uklotzde, 2018-02-03): Simply disconnecting all receivers // doesn't seem to work reliably. Emitting the clean() signal from // a track that is about to deleted may cause access violations!! - plainPtr->disconnect(); - plainPtr->blockSignals(true); - m_pSaver->saveCachedTrack(plainPtr); + pEvictedTrack->disconnect(); + pEvictedTrack->blockSignals(true); + m_pSaver->saveEvictedTrack(pEvictedTrack); } void GlobalTrackCache::deactivate() { diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 6f3197eee62f..5d5ac536dca5 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -172,7 +172,7 @@ class GlobalTrackCacheResolver final: public GlobalTrackCacheLocker { class /*interface*/ GlobalTrackCacheSaver { private: friend class GlobalTrackCache; - virtual void saveCachedTrack(Track* plainPtr) noexcept = 0; + virtual void saveEvictedTrack(Track* pEvictedTrack) noexcept = 0; protected: virtual ~GlobalTrackCacheSaver() {} @@ -234,7 +234,7 @@ private slots: void deactivate(); - void saveEvictedTrack(Track* plainPtr) const; + void saveEvictedTrack(Track* pEvictedTrack) const; // Managed by GlobalTrackCacheLocker mutable QMutex m_mutex; From ecbf655c5e13351e363e16022653ee496b4d80e1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Sep 2019 01:12:30 +0200 Subject: [PATCH 051/103] Use a custom function for deleting Track objects during tests --- src/test/globaltrackcache_test.cpp | 8 +++- src/test/librarytest.cpp | 36 ++++++++++++++ src/test/librarytest.h | 25 ++-------- src/track/globaltrackcache.cpp | 75 ++++++++++++++---------------- src/track/globaltrackcache.h | 29 ++++++++++-- 5 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 src/test/librarytest.cpp diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 87cad6f912c4..36c6862f8550 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -62,6 +62,12 @@ class TrackTitleThread: public QThread { std::atomic m_stop; }; +void deleteTrack(Track* pTrack) { + // Delete track objects directly in unit tests with + // no main event loop + delete pTrack; +}; + } // anonymous namespace class GlobalTrackCacheTest: public MixxxTest, public virtual GlobalTrackCacheSaver { @@ -72,7 +78,7 @@ class GlobalTrackCacheTest: public MixxxTest, public virtual GlobalTrackCacheSav protected: GlobalTrackCacheTest() { - GlobalTrackCache::createInstance(this); + GlobalTrackCache::createInstance(this, deleteTrack); } ~GlobalTrackCacheTest() { GlobalTrackCache::destroyInstance(); diff --git a/src/test/librarytest.cpp b/src/test/librarytest.cpp new file mode 100644 index 000000000000..8451597a5268 --- /dev/null +++ b/src/test/librarytest.cpp @@ -0,0 +1,36 @@ +#include "test/librarytest.h" + +namespace { + +const bool kInMemoryDbConnection = true; + +void deleteTrack(Track* pTrack) { + // Delete track objects directly in unit tests with + // no main event loop + delete pTrack; +}; + +} + +LibraryTest::LibraryTest() + : m_mixxxDb(config(), kInMemoryDbConnection), + m_dbConnectionPooler(m_mixxxDb.connectionPool()), + m_dbConnection(mixxx::DbConnectionPooled(m_mixxxDb.connectionPool())), + m_pTrackCollection(std::make_unique(config())) { + MixxxDb::initDatabaseSchema(m_dbConnection); + m_pTrackCollection->connectDatabase(m_dbConnection); + GlobalTrackCache::createInstance(this, deleteTrack); +} + +LibraryTest::~LibraryTest() { + m_pTrackCollection->disconnectDatabase(); + m_pTrackCollection.reset(); + // With the track collection all remaining track references + // should have been dropped before destroying the cache. + GlobalTrackCache::destroyInstance(); +} + +void LibraryTest::saveEvictedTrack(Track* pTrack) noexcept { + m_pTrackCollection->exportTrackMetadata(pTrack); + m_pTrackCollection->saveTrack(pTrack); +} diff --git a/src/test/librarytest.h b/src/test/librarytest.h index 6fef9112b206..e97fcbe020e2 100644 --- a/src/test/librarytest.h +++ b/src/test/librarytest.h @@ -10,34 +10,15 @@ #include "util/db/dbconnectionpooled.h" #include "track/globaltrackcache.h" -const bool kInMemoryDbConnection = true; - class LibraryTest : public MixxxTest, public virtual /*implements*/ GlobalTrackCacheSaver { public: - void saveEvictedTrack(Track* pTrack) noexcept override { - m_pTrackCollection->exportTrackMetadata(pTrack); - m_pTrackCollection->saveTrack(pTrack); - } + void saveEvictedTrack(Track* pTrack) noexcept override; protected: - LibraryTest() - : m_mixxxDb(config(), kInMemoryDbConnection), - m_dbConnectionPooler(m_mixxxDb.connectionPool()), - m_dbConnection(mixxx::DbConnectionPooled(m_mixxxDb.connectionPool())), - m_pTrackCollection(std::make_unique(config())) { - MixxxDb::initDatabaseSchema(m_dbConnection); - m_pTrackCollection->connectDatabase(m_dbConnection); - GlobalTrackCache::createInstance(this); - } - ~LibraryTest() override { - m_pTrackCollection->disconnectDatabase(); - m_pTrackCollection.reset(); - // With the track collection all remaining track references - // should have been dropped before destroying the cache. - GlobalTrackCache::destroyInstance(); - } + LibraryTest(); + ~LibraryTest() override; mixxx::DbConnectionPoolPtr dbConnectionPool() const { return m_mixxxDb.connectionPool(); diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 5716f93da36e..13cf65b13e6d 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -62,40 +62,6 @@ class EvictAndSaveFunctor { GlobalTrackCacheEntryPointer m_cacheEntryPtr; }; - -void deleteTrack(Track* plainPtr) { - DEBUG_ASSERT(plainPtr); - - // We safely delete the object via the Qt event queue instead - // of using operator delete! Otherwise the deleted track object - // might be accessed when processing cross-thread signals that - // are delayed within a queued connection and may arrive after - // the object has already been deleted. - if (traceLogEnabled()) { - plainPtr->dumpObjectInfo(); - } - if (debugLogEnabled()) { - kLogger.debug() - << "Deleting" - << plainPtr; - } - DEBUG_ASSERT(plainPtr->signalsBlocked()); -#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) - if (plainPtr->thread()->loopLevel() > 0) { - plainPtr->deleteLater(); - } else { - // Delete track directly if no event loop is running. - // Otherwise no track objects would be deleted during - // a unit test that doesn't start an event loop. Invoking - // QCoreApplication::processEvents() periodically is not - // sufficient! - delete plainPtr; - } -#else - plainPtr->deleteLater(); -#endif -} - } // anonymous namespace GlobalTrackCacheLocker::GlobalTrackCacheLocker() @@ -228,9 +194,11 @@ void GlobalTrackCacheResolver::initTrackIdAndUnlockCache(TrackId trackId) { } //static -void GlobalTrackCache::createInstance(GlobalTrackCacheSaver* pDeleter) { +void GlobalTrackCache::createInstance( + GlobalTrackCacheSaver* pSaver, + deleteTrackFn deleteTrack) { DEBUG_ASSERT(!s_pInstance); - s_pInstance = new GlobalTrackCache(pDeleter); + s_pInstance = new GlobalTrackCache(pSaver, deleteTrack); } //static @@ -248,6 +216,32 @@ void GlobalTrackCache::destroyInstance() { pInstance->deleteLater(); } +void GlobalTrackCacheEntry::TrackDeleter::operator()(Track* pTrack) const { + DEBUG_ASSERT(pTrack); + + // We safely delete the object via the Qt event queue instead + // of using operator delete! Otherwise the deleted track object + // might be accessed when processing cross-thread signals that + // are delayed within a queued connection and may arrive after + // the object has already been deleted. + if (traceLogEnabled()) { + pTrack->dumpObjectInfo(); + } + if (debugLogEnabled()) { + kLogger.debug() + << "Deleting" + << pTrack; + } + + if (m_deleteTrack) { + // Custom delete function + (*m_deleteTrack)(pTrack); + } else { + // Default delete function + pTrack->deleteLater(); + } +} + //static void GlobalTrackCache::evictAndSaveCachedTrack(GlobalTrackCacheEntryPointer cacheEntryPtr) { // Any access to plainPtr before a validity check inside the @@ -276,9 +270,12 @@ void GlobalTrackCache::evictAndSaveCachedTrack(GlobalTrackCacheEntryPointer cach } } -GlobalTrackCache::GlobalTrackCache(GlobalTrackCacheSaver* pSaver) +GlobalTrackCache::GlobalTrackCache( + GlobalTrackCacheSaver* pSaver, + deleteTrackFn deleteTrack) : m_mutex(QMutex::Recursive), m_pSaver(pSaver), + m_deleteTrack(deleteTrack), m_tracksById(kUnorderedCollectionMinCapacity, DbId::hash_fun) { DEBUG_ASSERT(m_pSaver); qRegisterMetaType("GlobalTrackCacheEntryPointer"); @@ -548,12 +545,12 @@ void GlobalTrackCache::resolve( << "Cache miss - allocating track" << trackRef; } - auto deletingPtr = std::unique_ptr( + auto deletingPtr = std::unique_ptr( new Track( std::move(fileInfo), std::move(pSecurityToken), std::move(trackId)), - deleteTrack); + GlobalTrackCacheEntry::TrackDeleter(m_deleteTrack)); auto cacheEntryPtr = std::make_shared( std::move(deletingPtr)); diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 5d5ac536dca5..6f04f83393a1 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -32,6 +32,8 @@ class /*interface*/ GlobalTrackCacheRelocator { virtual ~GlobalTrackCacheRelocator() {} }; +typedef void (*deleteTrackFn)(Track*); + class GlobalTrackCacheEntry final { // We need to hold two shared pointers, the deletingPtr is // responsible for the lifetime of the Track object itselfe. @@ -39,8 +41,20 @@ class GlobalTrackCacheEntry final { // is not longer referenced, the track is saved and evicted // from the cache. public: + class TrackDeleter { + public: + explicit TrackDeleter(deleteTrackFn deleteTrack = nullptr) + : m_deleteTrack(deleteTrack) { + } + + void operator()(Track* pTrack) const; + + private: + deleteTrackFn m_deleteTrack; + }; + explicit GlobalTrackCacheEntry( - std::unique_ptr deletingPtr) + std::unique_ptr deletingPtr) : m_deletingPtr(std::move(deletingPtr)) { } GlobalTrackCacheEntry(const GlobalTrackCacheEntry& other) = delete; @@ -64,7 +78,7 @@ class GlobalTrackCacheEntry final { } private: - std::unique_ptr m_deletingPtr; + std::unique_ptr m_deletingPtr; TrackWeakPointer m_savingWeakPtr; }; @@ -182,7 +196,10 @@ class GlobalTrackCache : public QObject { Q_OBJECT public: - static void createInstance(GlobalTrackCacheSaver* pDeleter); + static void createInstance( + GlobalTrackCacheSaver* pSaver, + // A custom deleter is only needed for tests without an event loop! + deleteTrackFn deleteTrack = nullptr); // NOTE(uklotzde, 2018-02-20): We decided not to destroy the singular // instance during shutdown, because we are not able to guarantee that // all track references have been released before. Instead the singular @@ -201,7 +218,9 @@ private slots: friend class GlobalTrackCacheLocker; friend class GlobalTrackCacheResolver; - explicit GlobalTrackCache(GlobalTrackCacheSaver* pDeleter); + GlobalTrackCache( + GlobalTrackCacheSaver* pSaver, + deleteTrackFn deleteTrack); ~GlobalTrackCache(); void relocateTracks( @@ -241,6 +260,8 @@ private slots: GlobalTrackCacheSaver* m_pSaver; + deleteTrackFn m_deleteTrack; + // This caches the unsaved Tracks by ID typedef std::unordered_map TracksById; TracksById m_tracksById; From d29a78fc8bdd4a33879aaafba8d698ba3811145a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Sep 2019 14:24:59 +0200 Subject: [PATCH 052/103] Fix reset of ReplayGain peak --- src/track/replaygain.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/track/replaygain.h b/src/track/replaygain.h index ae7c89e02135..ca17df25a810 100644 --- a/src/track/replaygain.h +++ b/src/track/replaygain.h @@ -82,7 +82,7 @@ class ReplayGain final { m_peak = peak; } void resetPeak() { - m_peak = CSAMPLE_PEAK; + m_peak = kPeakUndefined; } // Parsing and formatting of peak amplitude values according to From 36e6a2d6e908ff4efa5f2c1cdec5a6fc665d52a4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Sep 2019 15:04:11 +0200 Subject: [PATCH 053/103] Add replay gain tests --- src/test/replaygaintest.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/replaygaintest.cpp b/src/test/replaygaintest.cpp index 47df5d045ef5..eeef1ba65ea3 100644 --- a/src/test/replaygaintest.cpp +++ b/src/test/replaygaintest.cpp @@ -51,6 +51,32 @@ class ReplayGainTest : public testing::Test { } }; +TEST_F(ReplayGainTest, hasRatio) { + mixxx::ReplayGain replayGain; + EXPECT_FALSE(replayGain.hasRatio()); + replayGain.setRatio(mixxx::ReplayGain::kRatioUndefined); + EXPECT_FALSE(replayGain.hasRatio()); + replayGain.setRatio(mixxx::ReplayGain::kRatioMin); // exclusive + EXPECT_FALSE(replayGain.hasRatio()); + replayGain.setRatio(mixxx::ReplayGain::kRatio0dB); + EXPECT_TRUE(replayGain.hasRatio()); + replayGain.resetRatio(); + EXPECT_FALSE(replayGain.hasRatio()); +} + +TEST_F(ReplayGainTest, hasPeak) { + mixxx::ReplayGain replayGain; + EXPECT_FALSE(replayGain.hasPeak()); + replayGain.setPeak(mixxx::ReplayGain::kPeakUndefined); + EXPECT_FALSE(replayGain.hasPeak()); + replayGain.setPeak(mixxx::ReplayGain::kPeakMin); + EXPECT_TRUE(replayGain.hasPeak()); + replayGain.setPeak(mixxx::ReplayGain::kPeakClip); + EXPECT_TRUE(replayGain.hasPeak()); + replayGain.resetPeak(); + EXPECT_FALSE(replayGain.hasPeak()); +} + TEST_F(ReplayGainTest, RatioFromString0dB) { ratioFromString("0 dB", true, mixxx::ReplayGain::kRatio0dB); ratioFromString("0.0dB", true, mixxx::ReplayGain::kRatio0dB); From b57be5afbb16983a5f4edf64f2d8566c1f66bd44 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Sep 2019 21:41:45 +0200 Subject: [PATCH 054/103] Explain how the test works --- src/test/globaltrackcache_test.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/globaltrackcache_test.cpp b/src/test/globaltrackcache_test.cpp index 36c6862f8550..6fadb68a73ef 100644 --- a/src/test/globaltrackcache_test.cpp +++ b/src/test/globaltrackcache_test.cpp @@ -51,7 +51,11 @@ class TrackTitleThread: public QThread { m_recentTrackPtr = std::move(track); ++loopCount; } - // If the cache is empty all references must have been dropped + // If the cache is empty all references must have been dropped. + // Why? m_recentTrackPtr is only valid if a pointer has been found + // in the cache during the previous cycle, i.e. the cache could not + // have been empty. In this case at least another loop cycle follow, + // and so on... ASSERT_TRUE(!m_recentTrackPtr); qDebug() << "Finished" << loopCount << " thread loops"; } From 29167403b0e930dde61c2cbe99eab20ad5d2b833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 28 Sep 2019 21:44:02 +0200 Subject: [PATCH 055/103] Add new functions resolveTrackIdsFromUrls() and resolveTrackIdsFromLocations() to remove duplicates --- src/library/analysisfeature.cpp | 7 ++---- src/library/autodj/autodjfeature.cpp | 12 ++--------- src/library/crate/cratefeature.cpp | 12 ++--------- src/library/crate/cratetablemodel.cpp | 11 ++-------- src/library/librarytablemodel.cpp | 9 ++------ src/library/mixxxlibraryfeature.cpp | 6 ++---- src/library/playlistfeature.cpp | 13 ++--------- src/library/playlisttablemodel.cpp | 13 +++-------- src/library/trackcollection.cpp | 31 +++++++++++++++++++++++++++ src/library/trackcollection.h | 7 +++++- 10 files changed, 54 insertions(+), 67 deletions(-) diff --git a/src/library/analysisfeature.cpp b/src/library/analysisfeature.cpp index 058b7bc23e83..d5d9a5c817f0 100644 --- a/src/library/analysisfeature.cpp +++ b/src/library/analysisfeature.cpp @@ -193,11 +193,8 @@ void AnalysisFeature::cleanupAnalyzer() { } bool AnalysisFeature::dropAccept(QList urls, QObject* pSource) { - Q_UNUSED(pSource); - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - QList trackIds = m_pTrackCollection->resolveTrackIds(files, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls, + !pSource); analyzeTracks(trackIds); return trackIds.size() > 0; } diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 1d4eed271faf..a378ab15c067 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -138,21 +138,13 @@ void AutoDJFeature::activate() { } bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - if (!files.size()) { - return false; - } - // If a track is dropped onto the Auto DJ tree node, but the track isn't in the // library, then add the track to the library before adding it to the // Auto DJ playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; - if (pSource == nullptr) { - options |= TrackDAO::ResolveTrackIdOption::AddMissing; - } - QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls, + !pSource); if (!trackIds.size()) { return false; } diff --git a/src/library/crate/cratefeature.cpp b/src/library/crate/cratefeature.cpp index 14d375b6e9be..073fd6ca75ed 100644 --- a/src/library/crate/cratefeature.cpp +++ b/src/library/crate/cratefeature.cpp @@ -203,21 +203,13 @@ bool CrateFeature::dropAcceptChild(const QModelIndex& index, QList urls, VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) { return false; } - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - if (!files.size()) { - return false; - } - // If a track is dropped onto a crate's name, but the track isn't in the // library, then add the track to the library before adding it to the // playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; - if (pSource == nullptr) { - options |= TrackDAO::ResolveTrackIdOption::AddMissing; - } - QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls, + !pSource); if (!trackIds.size()) { return false; } diff --git a/src/library/crate/cratetablemodel.cpp b/src/library/crate/cratetablemodel.cpp index 05b594632874..94fe753c4894 100644 --- a/src/library/crate/cratetablemodel.cpp +++ b/src/library/crate/cratetablemodel.cpp @@ -146,15 +146,8 @@ int CrateTableModel::addTracks(const QModelIndex& index, Q_UNUSED(index); // If a track is dropped but it isn't in the library, then add it because // the user probably dropped a file from outside Mixxx into this crate. - QList fileInfoList; - foreach(QString fileLocation, locations) { - QFileInfo fileInfo(fileLocation); - fileInfoList.append(fileInfo); - } - - QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromLocations( + locations); if (m_pTrackCollection->addCrateTracks(m_selectedCrate, trackIds)) { select(); return trackIds.size(); diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index e3c2059edf16..22342f7962f5 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -61,13 +61,8 @@ void LibraryTableModel::setTableModel(int id) { int LibraryTableModel::addTracks(const QModelIndex& index, const QList& locations) { Q_UNUSED(index); - QList fileInfoList; - foreach (QString fileLocation, locations) { - fileInfoList.append(QFileInfo(fileLocation)); - } - QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromLocations( + locations); select(); return trackIds.size(); } diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index cf38c3a8c3b3..b8fde12d239f 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -181,10 +181,8 @@ bool MixxxLibraryFeature::dropAccept(QList urls, QObject* pSource) { if (pSource) { return false; } else { - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - QList trackIds = m_pTrackCollection->resolveTrackIds(files, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls( + urls, true); return trackIds.size() > 0; } } diff --git a/src/library/playlistfeature.cpp b/src/library/playlistfeature.cpp index 5b284030a6c4..e8253266dad2 100644 --- a/src/library/playlistfeature.cpp +++ b/src/library/playlistfeature.cpp @@ -106,22 +106,13 @@ bool PlaylistFeature::dropAcceptChild(const QModelIndex& index, QList urls VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) { return false; } - - QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - if (!files.size()) { - return false; - } - // If a track is dropped onto a playlist's name, but the track isn't in the // library, then add the track to the library before adding it to the // playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB - TrackDAO::ResolveTrackIdOptions options = TrackDAO::ResolveTrackIdOption::UnhideHidden; - if (pSource == nullptr) { - options |= TrackDAO::ResolveTrackIdOption::AddMissing; - } - QList trackIds = m_pTrackCollection->resolveTrackIds(files, options); + QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls, + !pSource); if (!trackIds.size()) { return false; } diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 1abe0b666ee7..d6ac44e729dd 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -78,6 +78,9 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, return 0; } + QList trackIds = m_pTrackCollection->resolveTrackIdsFromLocations( + locations); + const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); int position = index.sibling(index.row(), positionColumn).data().toInt(); @@ -86,16 +89,6 @@ int PlaylistTableModel::addTracks(const QModelIndex& index, position = rowCount() + 1; } - QList fileInfoList; - foreach (QString fileLocation, locations) { - QFileInfo fileInfo(fileLocation); - fileInfoList.append(fileInfo); - } - - QList trackIds = m_pTrackCollection->resolveTrackIds(fileInfoList, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); - int tracksAdded = m_pTrackCollection->getPlaylistDAO().insertTracksIntoPlaylist( trackIds, m_iPlaylistId, position); diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index f2c41811765a..cafc9e3c0516 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -9,6 +9,7 @@ #include "util/db/sqltransaction.h" #include "util/assert.h" +#include "util/dnd.h" namespace { @@ -96,6 +97,36 @@ QList TrackCollection::resolveTrackIds( return trackIds; } +QList TrackCollection::resolveTrackIdsFromUrls( + const QList& urls, bool addMissing) { + QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); + if (!files.size()) { + return QList(); + } + + TrackDAO::ResolveTrackIdOptions options = + TrackDAO::ResolveTrackIdOption::UnhideHidden; + if (addMissing) { + options |= TrackDAO::ResolveTrackIdOption::AddMissing; + } + return resolveTrackIds(files, options); +} + +QList TrackCollection::resolveTrackIdsFromLocations( + const QList& locations) { + QList fileInfoList; + foreach(QString fileLocation, locations) { + QFileInfo fileInfo(fileLocation); + fileInfoList.append(fileInfo); + } + return resolveTrackIds(fileInfoList, + TrackDAO::ResolveTrackIdOption::UnhideHidden + | TrackDAO::ResolveTrackIdOption::AddMissing); +} + +QList resolveTrackIdsFromUrls(const QList& urls, + TrackDAO::ResolveTrackIdOptions options); + bool TrackCollection::hideTracks(const QList& trackIds) { DEBUG_ASSERT(QApplication::instance()->thread() == QThread::currentThread()); diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index 21f18e103550..8afd7aded389 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -68,7 +68,12 @@ class TrackCollection : public QObject, // This function returns a track ID of all file in the list not already visible, // it adds and unhides the tracks as well. - QList resolveTrackIds(const QList& files, TrackDAO::ResolveTrackIdOptions options); + QList resolveTrackIds(const QList &files, + TrackDAO::ResolveTrackIdOptions options); + QList resolveTrackIdsFromUrls(const QList& urls, + bool addMissing); + QList resolveTrackIdsFromLocations( + const QList& locations); bool hideTracks(const QList& trackIds); bool unhideTracks(const QList& trackIds); From 83ea2caa985a496deb8791a7efdd34cb25e9340c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Sep 2019 22:40:15 +0200 Subject: [PATCH 056/103] Explain race conditions during evict and verify assumptions --- src/track/globaltrackcache.cpp | 40 ++++++++++++++++++++++------------ src/track/globaltrackcache.h | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 13cf65b13e6d..30028ad1f54d 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -656,13 +656,13 @@ void GlobalTrackCache::evictAndSave( return; } - if (!evict(cacheEntryPtr->getPlainPtr())) { - // A scond deleter has already evict the track from cache after our - // reference count drops to zero and before acquire the lock at the + if (!tryEvict(cacheEntryPtr->getPlainPtr())) { + // A second deleter has already evicted the track from cache after our + // reference count drops to zero and before acquiring the lock at the // beginning of this function if (debugLogEnabled()) { kLogger.debug() - << "Skip to save an already evicted track" + << "Skip to save an already evicted track again" << cacheEntryPtr->getPlainPtr(); } return; @@ -675,12 +675,17 @@ void GlobalTrackCache::evictAndSave( // deleted including the owned track } -bool GlobalTrackCache::evict(Track* plainPtr) { +bool GlobalTrackCache::tryEvict(Track* plainPtr) { DEBUG_ASSERT(plainPtr); // Make the cached track object invisible to avoid reusing // it before starting to save it. This is achieved by // removing it from both cache indices. + // Due to expected race conditions pointers might be evicted + // multiple times. Therefore we need to check the stored + // pointers to avoid evicting a new cached track object instead + // of the given plainPtr!! bool evicted = false; + bool notEvicted = false; const auto trackRef = createTrackRef(*plainPtr); if (debugLogEnabled()) { kLogger.debug() @@ -690,23 +695,30 @@ bool GlobalTrackCache::evict(Track* plainPtr) { } if (trackRef.hasId()) { const auto trackById = m_tracksById.find(trackRef.getId()); - if (trackById != m_tracksById.end() && - trackById->second->getPlainPtr() == plainPtr) { - m_tracksById.erase(trackById); - evicted = true; + if (trackById != m_tracksById.end()) { + if (trackById->second->getPlainPtr() == plainPtr) { + m_tracksById.erase(trackById); + evicted = true; + } else { + notEvicted = true; + } } } if (trackRef.hasCanonicalLocation()) { const auto trackByCanonicalLocation( m_tracksByCanonicalLocation.find(trackRef.getCanonicalLocation())); - if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation && - trackByCanonicalLocation->second->getPlainPtr() == plainPtr) { - m_tracksByCanonicalLocation.erase( - trackByCanonicalLocation); - evicted = true; + if (m_tracksByCanonicalLocation.end() != trackByCanonicalLocation) { + if (trackByCanonicalLocation->second->getPlainPtr() == plainPtr) { + m_tracksByCanonicalLocation.erase( + trackByCanonicalLocation); + evicted = true; + } else { + notEvicted = true; + } } } DEBUG_ASSERT(!isCached(plainPtr)); + DEBUG_ASSERT(!(evicted && notEvicted)); return evicted; } diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 6f04f83393a1..860ee39149f5 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -246,7 +246,7 @@ private slots: void purgeTrackId(TrackId trackId); - bool evict(Track* plainPtr); + bool tryEvict(Track* plainPtr); bool isCached(Track* plainPtr) const; bool isEmpty() const; From ea855d60db8767efe57c8ee8352e755f6c64e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 28 Sep 2019 22:49:27 +0200 Subject: [PATCH 057/103] rename options -> flags --- src/library/dao/trackdao.cpp | 4 ++-- src/library/dao/trackdao.h | 9 +++++---- src/library/trackcollection.cpp | 20 ++++++++++---------- src/library/trackcollection.h | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 76362dee0e04..b29656208f8e 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -136,7 +136,7 @@ TrackId TrackDAO::getTrackId(const QString& absoluteFilePath) { } QList TrackDAO::resolveTrackIds(const QList& files, - ResolveTrackIdOptions options) { + ResolveTrackIdFlags flags) { QList trackIds; trackIds.reserve(files.size()); @@ -164,7 +164,7 @@ QList TrackDAO::resolveTrackIds(const QList& files, LOG_FAILED_QUERY(query); } - if (options & ResolveTrackIdOption::AddMissing) { + if (flags & ResolveTrackIdFlag::AddMissing) { // Prepare to add tracks to the database. // This also begins an SQL transaction. addTracksPrepare(); diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index b007dbd9e4c0..7632a0468f31 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -25,12 +25,12 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC Q_OBJECT public: - enum class ResolveTrackIdOption : int { + enum class ResolveTrackIdFlag : int { ResolveOnly = 0, UnhideHidden = 1, AddMissing = 2 }; - Q_DECLARE_FLAGS(ResolveTrackIdOptions, ResolveTrackIdOption) + Q_DECLARE_FLAGS(ResolveTrackIdFlags, ResolveTrackIdFlag) // The 'config object' is necessary because users decide ID3 tags get // synchronized on track metadata change @@ -48,7 +48,8 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC void finish(); TrackId getTrackId(const QString& absoluteFilePath); - QList resolveTrackIds(const QList& files, ResolveTrackIdOptions options); + QList resolveTrackIds(const QList &files, + ResolveTrackIdFlags flags); QList getTrackIds(const QDir& dir); // WARNING: Only call this from the main thread instance of TrackDAO. @@ -166,6 +167,6 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC }; -Q_DECLARE_OPERATORS_FOR_FLAGS(TrackDAO::ResolveTrackIdOptions) +Q_DECLARE_OPERATORS_FOR_FLAGS(TrackDAO::ResolveTrackIdFlags) #endif //TRACKDAO_H diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index cafc9e3c0516..a8b029899a79 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -89,9 +89,9 @@ void TrackCollection::relocateDirectory(QString oldDir, QString newDir) { } QList TrackCollection::resolveTrackIds( - const QList& files, TrackDAO::ResolveTrackIdOptions options) { - QList trackIds = m_trackDao.resolveTrackIds(files, options); - if (options & TrackDAO::ResolveTrackIdOption::UnhideHidden) { + const QList& files, TrackDAO::ResolveTrackIdFlags flags) { + QList trackIds = m_trackDao.resolveTrackIds(files, flags); + if (flags & TrackDAO::ResolveTrackIdFlag::UnhideHidden) { unhideTracks(trackIds); } return trackIds; @@ -104,12 +104,12 @@ QList TrackCollection::resolveTrackIdsFromUrls( return QList(); } - TrackDAO::ResolveTrackIdOptions options = - TrackDAO::ResolveTrackIdOption::UnhideHidden; + TrackDAO::ResolveTrackIdFlags flags = + TrackDAO::ResolveTrackIdFlag::UnhideHidden; if (addMissing) { - options |= TrackDAO::ResolveTrackIdOption::AddMissing; + flags |= TrackDAO::ResolveTrackIdFlag::AddMissing; } - return resolveTrackIds(files, options); + return resolveTrackIds(files, flags); } QList TrackCollection::resolveTrackIdsFromLocations( @@ -120,12 +120,12 @@ QList TrackCollection::resolveTrackIdsFromLocations( fileInfoList.append(fileInfo); } return resolveTrackIds(fileInfoList, - TrackDAO::ResolveTrackIdOption::UnhideHidden - | TrackDAO::ResolveTrackIdOption::AddMissing); + TrackDAO::ResolveTrackIdFlag::UnhideHidden + | TrackDAO::ResolveTrackIdFlag::AddMissing); } QList resolveTrackIdsFromUrls(const QList& urls, - TrackDAO::ResolveTrackIdOptions options); + TrackDAO::ResolveTrackIdFlags flags); bool TrackCollection::hideTracks(const QList& trackIds) { DEBUG_ASSERT(QApplication::instance()->thread() == QThread::currentThread()); diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index 8afd7aded389..3f9e36f9bd95 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -69,7 +69,7 @@ class TrackCollection : public QObject, // This function returns a track ID of all file in the list not already visible, // it adds and unhides the tracks as well. QList resolveTrackIds(const QList &files, - TrackDAO::ResolveTrackIdOptions options); + TrackDAO::ResolveTrackIdFlags flags); QList resolveTrackIdsFromUrls(const QList& urls, bool addMissing); QList resolveTrackIdsFromLocations( From ad73dbafd53c11b928efeae7446780276e4bfb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 28 Sep 2019 22:52:56 +0200 Subject: [PATCH 058/103] predefere isEmpty() over !size() --- src/library/trackcollection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index a8b029899a79..0db796a9928b 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -100,7 +100,7 @@ QList TrackCollection::resolveTrackIds( QList TrackCollection::resolveTrackIdsFromUrls( const QList& urls, bool addMissing) { QList files = DragAndDropHelper::supportedTracksFromUrls(urls, false, true); - if (!files.size()) { + if (files.isEmpty()) { return QList(); } From ec0b35478434a053bafcaa21fb159b0c415ba069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 28 Sep 2019 23:13:03 +0200 Subject: [PATCH 059/103] rename onHideTracks to hideTracks --- src/library/dao/trackdao.cpp | 2 +- src/library/dao/trackdao.h | 2 +- src/library/trackcollection.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index b29656208f8e..9359506d90a9 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -744,7 +744,7 @@ TrackPointer TrackDAO::addSingleTrack(const QFileInfo& fileInfo, bool unremove) return pTrack; } -bool TrackDAO::onHidingTracks( +bool TrackDAO::hideTracks( const QList& trackIds) { QStringList idList; for (const auto& trackId: trackIds) { diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 7632a0468f31..dcb0fefaac3b 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -65,7 +65,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC TrackId addTracksAddTrack(const TrackPointer& pTrack, bool unremove); void addTracksFinish(bool rollback = false); - bool onHidingTracks( + bool hideTracks( const QList& trackIds); void afterHidingTracks( const QList& trackIds); diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index 0db796a9928b..35af06d177f9 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -171,7 +171,7 @@ bool TrackCollection::hideTracks(const QList& trackIds) { VERIFY_OR_DEBUG_ASSERT(transaction) { return false; } - VERIFY_OR_DEBUG_ASSERT(m_trackDao.onHidingTracks(trackIds)) { + VERIFY_OR_DEBUG_ASSERT(m_trackDao.hideTracks(trackIds)) { return false; } VERIFY_OR_DEBUG_ASSERT(transaction.commit()) { From 81304d28cb664f7927e2e87513f22b981d396c5c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Sep 2019 23:58:07 +0200 Subject: [PATCH 060/103] Rename function pointer type and values --- src/track/globaltrackcache.cpp | 14 +++++++------- src/track/globaltrackcache.h | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 30028ad1f54d..46ea59254cd0 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -196,9 +196,9 @@ void GlobalTrackCacheResolver::initTrackIdAndUnlockCache(TrackId trackId) { //static void GlobalTrackCache::createInstance( GlobalTrackCacheSaver* pSaver, - deleteTrackFn deleteTrack) { + deleteTrackFn_t deleteTrackFn) { DEBUG_ASSERT(!s_pInstance); - s_pInstance = new GlobalTrackCache(pSaver, deleteTrack); + s_pInstance = new GlobalTrackCache(pSaver, deleteTrackFn); } //static @@ -233,9 +233,9 @@ void GlobalTrackCacheEntry::TrackDeleter::operator()(Track* pTrack) const { << pTrack; } - if (m_deleteTrack) { + if (m_deleteTrackFn) { // Custom delete function - (*m_deleteTrack)(pTrack); + (*m_deleteTrackFn)(pTrack); } else { // Default delete function pTrack->deleteLater(); @@ -272,10 +272,10 @@ void GlobalTrackCache::evictAndSaveCachedTrack(GlobalTrackCacheEntryPointer cach GlobalTrackCache::GlobalTrackCache( GlobalTrackCacheSaver* pSaver, - deleteTrackFn deleteTrack) + deleteTrackFn_t deleteTrackFn) : m_mutex(QMutex::Recursive), m_pSaver(pSaver), - m_deleteTrack(deleteTrack), + m_deleteTrackFn(deleteTrackFn), m_tracksById(kUnorderedCollectionMinCapacity, DbId::hash_fun) { DEBUG_ASSERT(m_pSaver); qRegisterMetaType("GlobalTrackCacheEntryPointer"); @@ -550,7 +550,7 @@ void GlobalTrackCache::resolve( std::move(fileInfo), std::move(pSecurityToken), std::move(trackId)), - GlobalTrackCacheEntry::TrackDeleter(m_deleteTrack)); + GlobalTrackCacheEntry::TrackDeleter(m_deleteTrackFn)); auto cacheEntryPtr = std::make_shared( std::move(deletingPtr)); diff --git a/src/track/globaltrackcache.h b/src/track/globaltrackcache.h index 860ee39149f5..d0e575648864 100644 --- a/src/track/globaltrackcache.h +++ b/src/track/globaltrackcache.h @@ -32,7 +32,7 @@ class /*interface*/ GlobalTrackCacheRelocator { virtual ~GlobalTrackCacheRelocator() {} }; -typedef void (*deleteTrackFn)(Track*); +typedef void (*deleteTrackFn_t)(Track*); class GlobalTrackCacheEntry final { // We need to hold two shared pointers, the deletingPtr is @@ -43,14 +43,14 @@ class GlobalTrackCacheEntry final { public: class TrackDeleter { public: - explicit TrackDeleter(deleteTrackFn deleteTrack = nullptr) - : m_deleteTrack(deleteTrack) { + explicit TrackDeleter(deleteTrackFn_t deleteTrackFn = nullptr) + : m_deleteTrackFn(deleteTrackFn) { } void operator()(Track* pTrack) const; private: - deleteTrackFn m_deleteTrack; + deleteTrackFn_t m_deleteTrackFn; }; explicit GlobalTrackCacheEntry( @@ -199,7 +199,7 @@ class GlobalTrackCache : public QObject { static void createInstance( GlobalTrackCacheSaver* pSaver, // A custom deleter is only needed for tests without an event loop! - deleteTrackFn deleteTrack = nullptr); + deleteTrackFn_t deleteTrackFn = nullptr); // NOTE(uklotzde, 2018-02-20): We decided not to destroy the singular // instance during shutdown, because we are not able to guarantee that // all track references have been released before. Instead the singular @@ -220,7 +220,7 @@ private slots: GlobalTrackCache( GlobalTrackCacheSaver* pSaver, - deleteTrackFn deleteTrack); + deleteTrackFn_t deleteTrackFn); ~GlobalTrackCache(); void relocateTracks( @@ -260,7 +260,7 @@ private slots: GlobalTrackCacheSaver* m_pSaver; - deleteTrackFn m_deleteTrack; + deleteTrackFn_t m_deleteTrackFn; // This caches the unsaved Tracks by ID typedef std::unordered_map TracksById; From 95dde5d5d97fefa6f2954b34d677eb99dfa0e8ba Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 16 Aug 2019 22:46:19 +0200 Subject: [PATCH 061/103] Only highlight crates/playlists of a single selected track --- src/widget/wtracktableview.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index dee54e156c09..81d022e7911c 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -209,20 +209,23 @@ void WTrackTableView::slotGuiTick50ms(double /*unused*/) { mixxx::Duration timeDelta = mixxx::Time::elapsed() - m_lastUserAction; if (m_loadCachedOnly && timeDelta > mixxx::Duration::fromMillis(100)) { - // Show the currently selected track in the large cover art view. Doing - // this in selectionChanged slows down scrolling performance so we wait - // until the user has stopped interacting first. + // Show the currently selected track in the large cover art view and + // hightlights crate and playlists. Doing this in selectionChanged + // slows down scrolling performance so we wait until the user has + // stopped interacting first. if (m_selectionChangedSinceLastGuiTick) { const QModelIndexList indices = selectionModel()->selectedRows(); - if (indices.size() > 0 && indices.last().isValid()) { + if (indices.size() == 1 && indices.first().isValid()) { + // A single track has been selected TrackModel* trackModel = getTrackModel(); if (trackModel) { - TrackPointer pTrack = trackModel->getTrack(indices.last()); + TrackPointer pTrack = trackModel->getTrack(indices.first()); if (pTrack) { emit(trackSelected(pTrack)); } } } else { + // None or multiple tracks have been selected emit(trackSelected(TrackPointer())); } m_selectionChangedSinceLastGuiTick = false; From 313e0c1402c3ffca63567e5eae1c645a00347c88 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 29 Sep 2019 13:05:14 +0200 Subject: [PATCH 062/103] Fix warning --- src/track/globaltrackcache.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/track/globaltrackcache.cpp b/src/track/globaltrackcache.cpp index 46ea59254cd0..132c5f65ea69 100644 --- a/src/track/globaltrackcache.cpp +++ b/src/track/globaltrackcache.cpp @@ -718,6 +718,7 @@ bool GlobalTrackCache::tryEvict(Track* plainPtr) { } } DEBUG_ASSERT(!isCached(plainPtr)); + Q_UNUSED(notEvicted); // only used in debug assertion DEBUG_ASSERT(!(evicted && notEvicted)); return evicted; } From a7a1344565cca9ff185213b75cb713587b143f7c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 29 Sep 2019 15:37:03 +0200 Subject: [PATCH 063/103] Fix race condition(s) while unloading/loading tracks Pending read requests are discarded when unloading a track. But their response might arrive when the new track has already been loaded. This would reset the readable frame index range to empty and the new track plays with silence. Also, the FIFO between the worker and the reader needs to be drained entirely when unloading a track. No new read requests must be accepted until the new track has been loaded. --- src/engine/cachingreader.cpp | 52 ++++++++++++++++------- src/engine/cachingreaderworker.cpp | 68 ++++++++++++++++++------------ src/engine/cachingreaderworker.h | 30 +++++++++++-- 3 files changed, 104 insertions(+), 46 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 54d98cd9a731..2aa46f52afb5 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -203,13 +203,35 @@ CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex void CachingReader::newTrack(TrackPointer pTrack) { m_worker.newTrack(pTrack); m_worker.workReady(); + // Don't accept any new read requests until the current + // track has been unloaded and the new track has been + // loaded! + m_readerStatus = INVALID; + // Free all chunks with sample data from the current track + freeAllChunks(); } void CachingReader::process() { ReaderStatusUpdate update; + bool reloaded = false; while (m_readerStatusFIFO.read(&update, 1) == 1) { auto pChunk = update.takeFromWorker(); if (pChunk) { + // Response to a read request + DEBUG_ASSERT( + update.status == CHUNK_READ_SUCCESS || + update.status == CHUNK_READ_EOF || + update.status == CHUNK_READ_INVALID || + update.status == CHUNK_READ_DISCARDED); + if (reloaded || m_readerStatus == INVALID) { + // All chunks are freed when loading/unloading a track! + DEBUG_ASSERT(!m_mruCachingReaderChunk); + DEBUG_ASSERT(!m_lruCachingReaderChunk); + // Discard all pending read requests for the previous track + // that arrived while the worker was unloading that track! + freeChunk(pChunk); + continue; + } if (update.status == CHUNK_READ_SUCCESS) { // Insert or freshen the chunk in the MRU/LRU list after // obtaining ownership from the worker. @@ -218,24 +240,22 @@ void CachingReader::process() { // Discard chunks that don't carry any data freeChunk(pChunk); } - } - if (update.status == TRACK_NOT_LOADED) { - m_readerStatus = update.status; - } else if (update.status == TRACK_LOADED) { - m_readerStatus = update.status; - // Reset the max. readable frame index - m_readableFrameIndexRange = update.readableFrameIndexRange(); - // Free all chunks with sample data from a previous track - freeAllChunks(); - } - if (m_readerStatus == TRACK_LOADED) { - // Adjust the readable frame index range after loading or reading - m_readableFrameIndexRange = intersect( - m_readableFrameIndexRange, - update.readableFrameIndexRange()); + // Adjust the readable frame index range (if available) + if (update.status != CHUNK_READ_DISCARDED) { + m_readableFrameIndexRange = intersect( + m_readableFrameIndexRange, + update.readableFrameIndexRange()); + } } else { + DEBUG_ASSERT( + update.status == TRACK_LOADED || + update.status == TRACK_NOT_LOADED); + DEBUG_ASSERT(!m_mruCachingReaderChunk); + DEBUG_ASSERT(!m_lruCachingReaderChunk); + m_readerStatus = update.status; // Reset the readable frame index range - m_readableFrameIndexRange = mixxx::IndexRange(); + m_readableFrameIndexRange = update.readableFrameIndexRange(); + reloaded = true; } } } diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index 955267563cf8..dae4f303f3e2 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -122,19 +122,26 @@ void CachingReaderWorker::run() { } void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { - ReaderStatusUpdate update; - update.init(TRACK_NOT_LOADED); + // Discard all pending read requests + CachingReaderChunkReadRequest request; + while (m_pChunkReadRequestFIFO->read(&request, 1) == 1) { + const auto update = ReaderStatusUpdate::readDiscarded(request.chunk); + m_pReaderStatusFIFO->writeBlocking(&update, 1); + } + + // Unload the track + m_readableFrameIndexRange = mixxx::IndexRange(); + m_pAudioSource.reset(); // Close open file handles if (!pTrack) { - // Unload track - m_pAudioSource.reset(); // Close open file handles - m_readableFrameIndexRange = mixxx::IndexRange(); + // If no new track is available then we are done + const auto update = ReaderStatusUpdate::trackNotLoaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); return; } // Emit that a new track is loading, stops the current track - emit(trackLoading()); + emit trackLoading(); QString filename = pTrack->getLocation(); if (filename.isEmpty() || !pTrack->exists()) { @@ -142,10 +149,11 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { << m_group << "File not found" << filename; + const auto update = ReaderStatusUpdate::trackNotLoaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); - emit(trackLoadFailed( + emit trackLoadFailed( pTrack, QString("The file '%1' could not be found.") - .arg(QDir::toNativeSeparators(filename)))); + .arg(QDir::toNativeSeparators(filename))); return; } @@ -153,42 +161,50 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { config.setChannelCount(CachingReaderChunk::kChannels); m_pAudioSource = SoundSourceProxy(pTrack).openAudioSource(config); if (!m_pAudioSource) { - m_readableFrameIndexRange = mixxx::IndexRange(); kLogger.warning() << m_group << "Failed to open file" << filename; + const auto update = ReaderStatusUpdate::trackNotLoaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); - emit(trackLoadFailed( - pTrack, QString("The file '%1' could not be loaded.").arg(filename))); + emit trackLoadFailed( + pTrack, QString("The file '%1' could not be loaded").arg(filename)); return; } - const SINT tempReadBufferSize = m_pAudioSource->frames2samples(CachingReaderChunk::kFrames); - if (m_tempReadBuffer.size() != tempReadBufferSize) { - mixxx::SampleBuffer(tempReadBufferSize).swap(m_tempReadBuffer); - } - // Initially assume that the complete content offered by audio source // is available for reading. Later if read errors occur this value will // be decreased to avoid repeated reading of corrupt audio data. m_readableFrameIndexRange = m_pAudioSource->frameIndexRange(); - - update.init(TRACK_LOADED, nullptr, m_pAudioSource->frameIndexRange()); - m_pReaderStatusFIFO->writeBlocking(&update, 1); - - // Clear the chunks to read list. - CachingReaderChunkReadRequest request; - while (m_pChunkReadRequestFIFO->read(&request, 1) == 1) { - update.init(CHUNK_READ_INVALID, request.chunk); + if (m_readableFrameIndexRange.empty()) { + m_pAudioSource.reset(); // Close open file handles + kLogger.warning() + << m_group + << "Failed to open empty file" + << filename; + const auto update = ReaderStatusUpdate::trackNotLoaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); + emit trackLoadFailed( + pTrack, QString("The file '%1' is empty and could not be loaded").arg(filename)); + return; } + // Adjust the internal buffer + const SINT tempReadBufferSize = + m_pAudioSource->frames2samples(CachingReaderChunk::kFrames); + if (m_tempReadBuffer.size() != tempReadBufferSize) { + mixxx::SampleBuffer(tempReadBufferSize).swap(m_tempReadBuffer); + } + + const auto update = + ReaderStatusUpdate::trackLoaded(m_readableFrameIndexRange); + m_pReaderStatusFIFO->writeBlocking(&update, 1); + // Emit that the track is loaded. const SINT sampleCount = CachingReaderChunk::frames2samples( - m_pAudioSource->frameLength()); - emit(trackLoaded(pTrack, m_pAudioSource->sampleRate(), sampleCount)); + m_readableFrameIndexRange.length()); + emit trackLoaded(pTrack, m_pAudioSource->sampleRate(), sampleCount); } void CachingReaderWorker::quitWait() { diff --git a/src/engine/cachingreaderworker.h b/src/engine/cachingreaderworker.h index eab3c4a8c980..5042a9ac9852 100644 --- a/src/engine/cachingreaderworker.h +++ b/src/engine/cachingreaderworker.h @@ -31,7 +31,8 @@ enum ReaderStatus { TRACK_LOADED, CHUNK_READ_SUCCESS, CHUNK_READ_EOF, - CHUNK_READ_INVALID + CHUNK_READ_INVALID, + CHUNK_READ_DISCARDED, // response without frame index range! }; // POD with trivial ctor/dtor/copy for passing through FIFO @@ -45,15 +46,36 @@ typedef struct ReaderStatusUpdate { ReaderStatus status; void init( - ReaderStatus statusArg = INVALID, - CachingReaderChunk* chunkArg = nullptr, - const mixxx::IndexRange& readableFrameIndexRangeArg = mixxx::IndexRange()) { + ReaderStatus statusArg, + CachingReaderChunk* chunkArg, + const mixxx::IndexRange& readableFrameIndexRangeArg) { status = statusArg; chunk = chunkArg; readableFrameIndexRangeStart = readableFrameIndexRangeArg.start(); readableFrameIndexRangeEnd = readableFrameIndexRangeArg.end(); } + static ReaderStatusUpdate readDiscarded( + CachingReaderChunk* chunk) { + ReaderStatusUpdate update; + update.init(CHUNK_READ_DISCARDED, chunk, mixxx::IndexRange()); + return update; + } + + static ReaderStatusUpdate trackLoaded( + const mixxx::IndexRange& readableFrameIndexRange) { + DEBUG_ASSERT(!readableFrameIndexRange.empty()); + ReaderStatusUpdate update; + update.init(TRACK_LOADED, nullptr, readableFrameIndexRange); + return update; + } + + static ReaderStatusUpdate trackNotLoaded() { + ReaderStatusUpdate update; + update.init(TRACK_NOT_LOADED, nullptr, mixxx::IndexRange()); + return update; + } + CachingReaderChunkForOwner* takeFromWorker() { CachingReaderChunkForOwner* pChunk = nullptr; if (chunk) { From dc1302a307a4bb6e3d8fece0d538c52aff788b53 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 29 Sep 2019 20:46:34 +0200 Subject: [PATCH 064/103] Simplify and secure state management of CachingReader --- src/engine/cachingreader.cpp | 35 +++++++++++++++++--------------- src/engine/cachingreader.h | 9 ++++++-- src/engine/cachingreaderworker.h | 5 ++--- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 2aa46f52afb5..22eab749ef87 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -56,12 +56,12 @@ CachingReader::CachingReader(QString group, // The capacity of the back channel must be equal to the number of // allocated chunks, because the worker use writeBlocking(). Otherwise // the worker could get stuck in a hot loop!!! - m_readerStatusFIFO(kNumberOfCachedChunksInMemory), - m_readerStatus(INVALID), + m_stateFIFO(kNumberOfCachedChunksInMemory), + m_state(State::Idle), m_mruCachingReaderChunk(nullptr), m_lruCachingReaderChunk(nullptr), m_sampleBuffer(CachingReaderChunk::kSamples * kNumberOfCachedChunksInMemory), - m_worker(group, &m_chunkReadRequestFIFO, &m_readerStatusFIFO) { + m_worker(group, &m_chunkReadRequestFIFO, &m_stateFIFO) { m_allocatedCachingReaderChunks.reserve(kNumberOfCachedChunksInMemory); // Divide up the allocated raw memory buffer into total_chunks @@ -206,15 +206,15 @@ void CachingReader::newTrack(TrackPointer pTrack) { // Don't accept any new read requests until the current // track has been unloaded and the new track has been // loaded! - m_readerStatus = INVALID; + m_state = State::TrackLoading; // Free all chunks with sample data from the current track freeAllChunks(); } void CachingReader::process() { ReaderStatusUpdate update; - bool reloaded = false; - while (m_readerStatusFIFO.read(&update, 1) == 1) { + while (m_stateFIFO.read(&update, 1) == 1) { + DEBUG_ASSERT(m_state != State::Idle); auto pChunk = update.takeFromWorker(); if (pChunk) { // Response to a read request @@ -223,15 +223,17 @@ void CachingReader::process() { update.status == CHUNK_READ_EOF || update.status == CHUNK_READ_INVALID || update.status == CHUNK_READ_DISCARDED); - if (reloaded || m_readerStatus == INVALID) { - // All chunks are freed when loading/unloading a track! + if (m_state == State::TrackLoading) { + // All chunks have been freed before loading the next track! DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); // Discard all pending read requests for the previous track - // that arrived while the worker was unloading that track! + // that are discarded by the worker before loading the next + // track. freeChunk(pChunk); continue; } + DEBUG_ASSERT(m_state == State::TrackLoaded); if (update.status == CHUNK_READ_SUCCESS) { // Insert or freshen the chunk in the MRU/LRU list after // obtaining ownership from the worker. @@ -247,15 +249,16 @@ void CachingReader::process() { update.readableFrameIndexRange()); } } else { - DEBUG_ASSERT( - update.status == TRACK_LOADED || - update.status == TRACK_NOT_LOADED); DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); - m_readerStatus = update.status; + if (update.status == TRACK_LOADED) { + m_state = State::TrackLoaded; + } else { + DEBUG_ASSERT(update.status == TRACK_UNLOADED); + m_state = State::Idle; + } // Reset the readable frame index range m_readableFrameIndexRange = update.readableFrameIndexRange(); - reloaded = true; } } } @@ -279,7 +282,7 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, } // If no track is loaded, don't do anything. - if (m_readerStatus != TRACK_LOADED) { + if (m_state != State::TrackLoaded) { return ReadResult::UNAVAILABLE; } @@ -476,7 +479,7 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, void CachingReader::hintAndMaybeWake(const HintVector& hintList) { // If no file is loaded, skip. - if (m_readerStatus != TRACK_LOADED) { + if (m_state != State::TrackLoaded) { return; } diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index e148cd637e1f..1e9ec831fbfd 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -124,7 +124,7 @@ class CachingReader : public QObject { // Thread-safe FIFOs for communication between the engine callback and // reader thread. FIFO m_chunkReadRequestFIFO; - FIFO m_readerStatusFIFO; + FIFO m_stateFIFO; // Looks for the provided chunk number in the index of in-memory chunks and // returns it if it is present. If not, returns nullptr. If it is present then @@ -151,7 +151,12 @@ class CachingReader : public QObject { // Gets a chunk from the free list, frees the LRU CachingReaderChunk if none available. CachingReaderChunkForOwner* allocateChunkExpireLRU(SINT chunkIndex); - ReaderStatus m_readerStatus; + enum class State { + Idle, + TrackLoading, + TrackLoaded, + }; + State m_state; // Keeps track of all CachingReaderChunks we've allocated. QVector m_chunks; diff --git a/src/engine/cachingreaderworker.h b/src/engine/cachingreaderworker.h index 5042a9ac9852..a4c05a2b2376 100644 --- a/src/engine/cachingreaderworker.h +++ b/src/engine/cachingreaderworker.h @@ -26,9 +26,8 @@ typedef struct CachingReaderChunkReadRequest { } CachingReaderChunkReadRequest; enum ReaderStatus { - INVALID, - TRACK_NOT_LOADED, TRACK_LOADED, + TRACK_UNLOADED, CHUNK_READ_SUCCESS, CHUNK_READ_EOF, CHUNK_READ_INVALID, @@ -72,7 +71,7 @@ typedef struct ReaderStatusUpdate { static ReaderStatusUpdate trackNotLoaded() { ReaderStatusUpdate update; - update.init(TRACK_NOT_LOADED, nullptr, mixxx::IndexRange()); + update.init(TRACK_UNLOADED, nullptr, mixxx::IndexRange()); return update; } From e0b94c37cb596e1513984e49f0fe25b01d0ee320 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 30 Sep 2019 00:18:19 +0200 Subject: [PATCH 065/103] Keep the ordering of operations and explain why --- src/engine/cachingreader.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 22eab749ef87..57661cc10d1a 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -201,13 +201,17 @@ CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex } void CachingReader::newTrack(TrackPointer pTrack) { + // Feed the track to the worker as soon as possible + // to get ready while the reader switches its internal + // state. There are no race conditions, because the + // reader polls the worker. m_worker.newTrack(pTrack); m_worker.workReady(); // Don't accept any new read requests until the current // track has been unloaded and the new track has been - // loaded! + // loaded. m_state = State::TrackLoading; - // Free all chunks with sample data from the current track + // Free all chunks with sample data from the current track. freeAllChunks(); } From 89b5b4063c28fa278656873407483f476a4a101b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 30 Sep 2019 00:20:30 +0200 Subject: [PATCH 066/103] Reword comment --- src/engine/cachingreader.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 57661cc10d1a..1551a8bf9b8b 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -231,9 +231,8 @@ void CachingReader::process() { // All chunks have been freed before loading the next track! DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); - // Discard all pending read requests for the previous track - // that are discarded by the worker before loading the next - // track. + // Discard all results from pending read requests for the + // previous track before the next track has been loaded. freeChunk(pChunk); continue; } From 16655dca7917a7998c2e0320df5a13c9e06b7a1c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 30 Sep 2019 00:24:19 +0200 Subject: [PATCH 067/103] Add comments two if/else branches --- src/engine/cachingreader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 1551a8bf9b8b..10ee36fbf0a7 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -221,7 +221,7 @@ void CachingReader::process() { DEBUG_ASSERT(m_state != State::Idle); auto pChunk = update.takeFromWorker(); if (pChunk) { - // Response to a read request + // Result of a read request (with a chunk) DEBUG_ASSERT( update.status == CHUNK_READ_SUCCESS || update.status == CHUNK_READ_EOF || @@ -252,6 +252,7 @@ void CachingReader::process() { update.readableFrameIndexRange()); } } else { + // State update (without a chunk) DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); if (update.status == TRACK_LOADED) { From 725a15a226e0ee2909b30209bc9b8b6da55dba2e Mon Sep 17 00:00:00 2001 From: Be Date: Sun, 29 Sep 2019 21:17:40 -0500 Subject: [PATCH 068/103] track device name, ALSA device, and PortAudio device index separately This allows for more reliable identification of hardware devices from the configuration file so users do not need to reconfigure their sound hardware as often as before. --- src/mixxxapplication.cpp | 3 + src/preferences/dialog/dlgprefsound.cpp | 5 +- src/preferences/dialog/dlgprefsounditem.cpp | 76 +++++++------- src/preferences/dialog/dlgprefsounditem.h | 4 +- src/soundio/sounddevice.cpp | 7 +- src/soundio/sounddevice.h | 8 +- src/soundio/sounddevicenetwork.cpp | 8 +- src/soundio/sounddevicenotfound.h | 6 +- src/soundio/sounddeviceportaudio.cpp | 68 +++++++----- src/soundio/sounddeviceportaudio.h | 4 +- src/soundio/soundmanager.cpp | 46 ++++++--- src/soundio/soundmanagerconfig.cpp | 108 ++++++++++++++++---- src/soundio/soundmanagerconfig.h | 19 ++-- src/soundio/soundmanagerutil.h | 34 ++++++ 14 files changed, 265 insertions(+), 131 deletions(-) diff --git a/src/mixxxapplication.cpp b/src/mixxxapplication.cpp index 4b0ea8b3538f..2b31c3df7df1 100644 --- a/src/mixxxapplication.cpp +++ b/src/mixxxapplication.cpp @@ -6,6 +6,7 @@ #include "library/crate/crateid.h" #include "control/controlproxy.h" #include "mixxx.h" +#include "soundio/soundmanagerutil.h" // When linking Qt statically on Windows we have to Q_IMPORT_PLUGIN all the // plugins we link in build/depends.py. @@ -65,6 +66,8 @@ void MixxxApplication::registerMetaTypes() { qRegisterMetaType("mixxx::ReplayGain"); qRegisterMetaType("mixxx::Bpm"); qRegisterMetaType("mixxx::Duration"); + qRegisterMetaType("SoundDeviceId"); + QMetaType::registerComparators(); } // Macs do not have touchscreens diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index e20a567dc091..6b0972e4e243 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -38,7 +38,8 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, m_settingsModified(false), m_bLatencyChanged(false), m_bSkipConfigClear(true), - m_loading(false) { + m_loading(false), + m_config(pSoundManager) { setupUi(this); connect(m_pSoundManager, SIGNAL(devicesUpdated()), @@ -590,7 +591,7 @@ void DlgPrefSound::queryClicked() { * Slot called when the "Reset to Defaults" button is clicked. */ void DlgPrefSound::slotResetToDefaults() { - SoundManagerConfig newConfig; + SoundManagerConfig newConfig(m_pSoundManager); newConfig.loadDefaults(m_pSoundManager, SoundManagerConfig::ALL); loadSettings(newConfig); keylockComboBox->setCurrentIndex(EngineBuffer::RUBBERBAND); diff --git a/src/preferences/dialog/dlgprefsounditem.cpp b/src/preferences/dialog/dlgprefsounditem.cpp index cf782731aec7..b77e79f04896 100644 --- a/src/preferences/dialog/dlgprefsounditem.cpp +++ b/src/preferences/dialog/dlgprefsounditem.cpp @@ -58,7 +58,7 @@ DlgPrefSoundItem::~DlgPrefSoundItem() { */ void DlgPrefSoundItem::refreshDevices(const QList& devices) { m_devices = devices; - QString oldDev = deviceComboBox->itemData(deviceComboBox->currentIndex()).toString(); + SoundDeviceId oldDev = deviceComboBox->itemData(deviceComboBox->currentIndex()).value(); deviceComboBox->setCurrentIndex(0); // not using combobox->clear means we can leave in "None" so it // doesn't flicker when you switch APIs... cleaner Mixxx :) bkgood @@ -67,9 +67,9 @@ void DlgPrefSoundItem::refreshDevices(const QList& devices) } for (const auto& pDevice: qAsConst(m_devices)) { if (!hasSufficientChannels(*pDevice)) continue; - deviceComboBox->addItem(pDevice->getDisplayName(), pDevice->getInternalName()); + deviceComboBox->addItem(pDevice->getDisplayName(), QVariant::fromValue(pDevice->getDeviceId())); } - int newIndex = deviceComboBox->findData(oldDev); + int newIndex = deviceComboBox->findData(QVariant::fromValue(oldDev)); if (newIndex != -1) { deviceComboBox->setCurrentIndex(newIndex); } @@ -81,13 +81,13 @@ void DlgPrefSoundItem::refreshDevices(const QList& devices) */ void DlgPrefSoundItem::deviceChanged(int index) { channelComboBox->clear(); - QString selection = deviceComboBox->itemData(index).toString(); + SoundDeviceId selection = deviceComboBox->itemData(index).value(); unsigned int numChannels = 0; - if (selection == "None") { + if (selection.name == "None") { goto emitAndReturn; } else { for (const auto& pDevice: qAsConst(m_devices)) { - if (pDevice->getInternalName() == selection) { + if (pDevice->getDeviceId() == selection) { if (m_isInput) { numChannels = pDevice->getNumInputChannels(); } else { @@ -148,35 +148,31 @@ void DlgPrefSoundItem::channelChanged() { */ void DlgPrefSoundItem::loadPath(const SoundManagerConfig &config) { if (m_isInput) { - QMultiHash inputs(config.getInputs()); - foreach (QString devName, inputs.uniqueKeys()) { - foreach (AudioInput in, inputs.values(devName)) { - if (in.getType() == m_type && in.getIndex() == m_index) { - setDevice(devName); - setChannel(in.getChannelGroup().getChannelBase(), - in.getChannelGroup().getChannelCount()); - return; // we're just using the first one found, leave - // multiples to a more advanced dialog -- bkgood - } + QMultiHash inputs(config.getInputs()); + QHashIterator it(inputs); + while (it.hasNext()) { + it.next(); + if (it.value().getType() == m_type && it.value().getIndex() == m_index) { + setDevice(it.key()); + setChannel(it.value().getChannelGroup().getChannelBase(), + it.value().getChannelGroup().getChannelCount()); + return; } } } else { - QMultiHash outputs(config.getOutputs()); - foreach (QString devName, outputs.uniqueKeys()) { - foreach (AudioOutput out, outputs.values(devName)) { - if (out.getType() == m_type && out.getIndex() == m_index) { - setDevice(devName); - setChannel(out.getChannelGroup().getChannelBase(), - out.getChannelGroup().getChannelCount()); - return; // we're just using the first one found, leave - // multiples to a more advanced dialog -- bkgood - } + QMultiHash outputs(config.getOutputs()); + QHashIterator it(outputs); + while (it.hasNext()) { + it.next(); + if (it.value().getType() == m_type && it.value().getIndex() == m_index) { + setDevice(it.key()); + setChannel(it.value().getChannelGroup().getChannelBase(), + it.value().getChannelGroup().getChannelCount()); + return; } } } - // if we've gotten here without returning, we didn't find a path applicable - // to us so set some defaults -- bkgood - setDevice("None"); // this will blank the channel combo box + setDevice(SoundDeviceId()); // this will blank the channel combo box } /** @@ -201,11 +197,11 @@ void DlgPrefSoundItem::writePath(SoundManagerConfig* config) const { if (m_isInput) { config->addInput( - pDevice->getInternalName(), + pDevice->getDeviceId(), AudioInput(m_type, channelBase, channelCount, m_index)); } else { config->addOutput( - pDevice->getInternalName(), + pDevice->getDeviceId(), AudioOutput(m_type, channelBase, channelCount, m_index)); } } @@ -214,7 +210,7 @@ void DlgPrefSoundItem::writePath(SoundManagerConfig* config) const { * Slot called to tell the Item to save its selections for later use. */ void DlgPrefSoundItem::save() { - m_savedDevice = deviceComboBox->itemData(deviceComboBox->currentIndex()).toString(); + m_savedDevice = deviceComboBox->itemData(deviceComboBox->currentIndex()).value(); m_savedChannel = channelComboBox->itemData(channelComboBox->currentIndex()).toPoint(); } @@ -222,7 +218,7 @@ void DlgPrefSoundItem::save() { * Slot called to reload Item with previously saved settings. */ void DlgPrefSoundItem::reload() { - int newDevice = deviceComboBox->findData(m_savedDevice); + int newDevice = deviceComboBox->findData(QVariant::fromValue(m_savedDevice)); if (newDevice > -1) { deviceComboBox->setCurrentIndex(newDevice); } @@ -237,14 +233,13 @@ void DlgPrefSoundItem::reload() { * @returns pointer to SoundDevice, or NULL if the "None" option is selected. */ SoundDevicePointer DlgPrefSoundItem::getDevice() const { - QString selection = deviceComboBox->itemData(deviceComboBox->currentIndex()).toString(); - if (selection == "None") { + SoundDeviceId selection = deviceComboBox->itemData(deviceComboBox->currentIndex()).value(); + if (selection.name == "None") { return SoundDevicePointer(); } for (const auto& pDevice: qAsConst(m_devices)) { - qDebug() << "1" << pDevice->getDisplayName(); - qDebug() << "2" << pDevice->getInternalName(); - if (selection == pDevice->getInternalName()) { + if (selection == pDevice->getDeviceId()) { + //qDebug() << "DlgPrefSoundItem::getDevice" << pDevice->getDeviceId().debugName(); return pDevice; } } @@ -257,8 +252,9 @@ SoundDevicePointer DlgPrefSoundItem::getDevice() const { * Selects a device in the device combo box given a SoundDevice * internal name, or selects "None" if the device isn't found. */ -void DlgPrefSoundItem::setDevice(const QString &deviceName) { - int index = deviceComboBox->findData(deviceName); +void DlgPrefSoundItem::setDevice(const SoundDeviceId& device) { + int index = deviceComboBox->findData(QVariant::fromValue(device)); + //qDebug() << "DlgPrefSoundItem::setDevice" << device.debugName(); if (index != -1) { m_inhibitSettingChanged = true; deviceComboBox->setCurrentIndex(index); diff --git a/src/preferences/dialog/dlgprefsounditem.h b/src/preferences/dialog/dlgprefsounditem.h index 85e019795292..e5a6d05722a0 100644 --- a/src/preferences/dialog/dlgprefsounditem.h +++ b/src/preferences/dialog/dlgprefsounditem.h @@ -54,7 +54,7 @@ class DlgPrefSoundItem : public QWidget, public Ui::DlgPrefSoundItem { private: SoundDevicePointer getDevice() const; // if this returns NULL, we don't have a valid AudioPath - void setDevice(const QString &deviceName); + void setDevice(const SoundDeviceId& device); void setChannel(unsigned int channelBase, unsigned int channels); int hasSufficientChannels(const SoundDevice& device) const; @@ -62,7 +62,7 @@ class DlgPrefSoundItem : public QWidget, public Ui::DlgPrefSoundItem { unsigned int m_index; QList m_devices; bool m_isInput; - QString m_savedDevice; + SoundDeviceId m_savedDevice; // Because QVariant supports QPoint natively we use a QPoint to store the // channel info. x is the channel base and y is the channel count. QPoint m_savedChannel; diff --git a/src/soundio/sounddevice.cpp b/src/soundio/sounddevice.cpp index d3d98f791212..95dd71fbb45f 100644 --- a/src/soundio/sounddevice.cpp +++ b/src/soundio/sounddevice.cpp @@ -29,7 +29,6 @@ SoundDevice::SoundDevice(UserSettingsPointer config, SoundManager* sm) : m_pConfig(config), m_pSoundManager(sm), - m_strInternalName("Unknown Soundcard"), m_strDisplayName("Unknown Soundcard"), m_iNumOutputChannels(2), m_iNumInputChannels(2), @@ -104,11 +103,7 @@ void SoundDevice::clearInputs() { } bool SoundDevice::operator==(const SoundDevice &other) const { - return this->getInternalName() == other.getInternalName(); -} - -bool SoundDevice::operator==(const QString &other) const { - return getInternalName() == other; + return m_deviceId == other.getDeviceId(); } void SoundDevice::composeOutputBuffer(CSAMPLE* outputBuffer, diff --git a/src/soundio/sounddevice.h b/src/soundio/sounddevice.h index 4db9a349ca9b..1d6949255f18 100644 --- a/src/soundio/sounddevice.h +++ b/src/soundio/sounddevice.h @@ -25,6 +25,7 @@ #include "preferences/usersettings.h" #include "soundio/sounddeviceerror.h" #include "soundio/sounddevice.h" +#include "soundio/soundmanagerutil.h" class SoundDevice; class SoundManager; @@ -40,8 +41,8 @@ class SoundDevice { SoundDevice(UserSettingsPointer config, SoundManager* sm); virtual ~SoundDevice(); - inline const QString& getInternalName() const { - return m_strInternalName; + inline const SoundDeviceId& getDeviceId() const { + return m_deviceId; } inline const QString& getDisplayName() const { return m_strDisplayName; @@ -88,11 +89,10 @@ class SoundDevice { void clearInputBuffer(const SINT framesToPush, const SINT framesWriteOffset); + SoundDeviceId m_deviceId; UserSettingsPointer m_pConfig; // Pointer to the SoundManager object which we'll request audio from. SoundManager* m_pSoundManager; - // The name of the soundcard, used internally (may include the device ID) - QString m_strInternalName; // The name of the soundcard, as displayed to the user QString m_strDisplayName; // The number of output channels that the soundcard has diff --git a/src/soundio/sounddevicenetwork.cpp b/src/soundio/sounddevicenetwork.cpp index 621a4f86ac13..6f677c7af4be 100644 --- a/src/soundio/sounddevicenetwork.cpp +++ b/src/soundio/sounddevicenetwork.cpp @@ -47,7 +47,7 @@ SoundDeviceNetwork::SoundDeviceNetwork(UserSettingsPointer config, // Setting parent class members: m_hostAPI = "Network stream"; m_dSampleRate = 44100.0; - m_strInternalName = kNetworkDeviceInternalName; + m_deviceId.name = kNetworkDeviceInternalName; m_strDisplayName = QObject::tr("Network stream"); m_iNumInputChannels = pNetworkStream->getNumInputChannels(); m_iNumOutputChannels = pNetworkStream->getNumOutputChannels(); @@ -61,7 +61,7 @@ SoundDeviceNetwork::~SoundDeviceNetwork() { SoundDeviceError SoundDeviceNetwork::open(bool isClkRefDevice, int syncBuffers) { Q_UNUSED(syncBuffers); - kLogger.debug() << "open:" << getInternalName(); + kLogger.debug() << "open:" << m_deviceId.name; // Sample rate if (m_dSampleRate <= 0) { @@ -414,7 +414,7 @@ void SoundDeviceNetwork::callbackProcessClkRef() { updateCallbackEntryToDacTime(); Trace trace("SoundDeviceNetwork::callbackProcessClkRef %1", - getInternalName()); + m_deviceId.name); if (!m_denormals) { @@ -453,7 +453,7 @@ void SoundDeviceNetwork::callbackProcessClkRef() { { ScopedTimer t("SoundDevicePortAudio::callbackProcess prepare %1", - getInternalName()); + m_deviceId.name); m_pSoundManager->onDeviceOutputCallback(m_framesPerBuffer); } diff --git a/src/soundio/sounddevicenotfound.h b/src/soundio/sounddevicenotfound.h index 5b3527f7d06c..00677a4c2c7c 100644 --- a/src/soundio/sounddevicenotfound.h +++ b/src/soundio/sounddevicenotfound.h @@ -15,10 +15,10 @@ class EngineNetworkStream; class SoundDeviceNotFound : public SoundDevice { public: - SoundDeviceNotFound(QString internalName) + SoundDeviceNotFound(QString name) : SoundDevice(UserSettingsPointer(), nullptr) { - m_strInternalName = internalName; - m_strDisplayName = internalName; + m_deviceId.name = name; + m_strDisplayName = name; } SoundDeviceError open(bool isClkRefDevice, int syncBuffers) override { diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index c502b58c73c1..40d0bf36fdbd 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #ifdef __LINUX__ @@ -90,10 +91,10 @@ int paV19CallbackClkRef(const void *inputBuffer, void *outputBuffer, SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, SoundManager* sm, const PaDeviceInfo* deviceInfo, - unsigned int devIndex) + unsigned int devIndex, + QHash apiIndexToTypeId) : SoundDevice(config, sm), m_pStream(NULL), - m_devId(devIndex), m_deviceInfo(deviceInfo), m_outputFifo(NULL), m_inputFifo(NULL), @@ -107,7 +108,26 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, // Setting parent class members: m_hostAPI = Pa_GetHostApiInfo(deviceInfo->hostApi)->name; m_dSampleRate = deviceInfo->defaultSampleRate; - m_strInternalName = deviceInfo->name; + if (apiIndexToTypeId.value(deviceInfo->hostApi) == paALSA) { + // PortAudio gives the device name including the ALSA hw device. The + // ALSA hw device is an only somewhat reliable identifier; it may change + // when an audio interface is unplugged or Linux is restarted. Separating + // the name from the hw device allows for making the use of both pieces + // of information in SoundManagerConfig::readFromDisk to minimize how + // often users need to reconfigure their sound hardware. + QRegularExpression alsaHwDeviceRegex("(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)"); + QRegularExpressionMatch match = alsaHwDeviceRegex.match(deviceInfo->name); + if (match.hasMatch()) { + m_deviceId.name = match.captured(1); + m_deviceId.alsaDeviceName = match.captured(3); + } else { + // Special ALSA devices like "default" and "pulse" do not match the regex + m_deviceId.name = deviceInfo->name; + } + } else { + m_deviceId.name = deviceInfo->name; + } + m_deviceId.portAudioIndex = devIndex; m_strDisplayName = QString::fromLocal8Bit(deviceInfo->name); m_iNumInputChannels = m_deviceInfo->maxInputChannels; m_iNumOutputChannels = m_deviceInfo->maxOutputChannels; @@ -133,7 +153,7 @@ SoundDevicePortAudio::~SoundDevicePortAudio() { } SoundDeviceError SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffers) { - qDebug() << "SoundDevicePortAudio::open()" << getInternalName(); + qDebug() << "SoundDevicePortAudio::open()" << m_deviceId.debugName(); PaError err; if (m_audioOutputs.empty() && m_audioInputs.empty()) { @@ -226,17 +246,17 @@ SoundDeviceError SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffers } //Fill out the rest of the info. - m_outputParams.device = m_devId; + m_outputParams.device = m_deviceId.portAudioIndex; m_outputParams.sampleFormat = paFloat32; m_outputParams.suggestedLatency = bufferMSec / 1000.0; m_outputParams.hostApiSpecificStreamInfo = NULL; - m_inputParams.device = m_devId; + m_inputParams.device = m_deviceId.portAudioIndex; m_inputParams.sampleFormat = paFloat32; m_inputParams.suggestedLatency = bufferMSec / 1000.0; m_inputParams.hostApiSpecificStreamInfo = NULL; - qDebug() << "Opening stream with id" << m_devId; + qDebug() << "Opening stream with id" << m_deviceId.portAudioIndex; m_lastCallbackEntrytoDacSecs = bufferMSec / 1000.0; @@ -339,7 +359,7 @@ SoundDeviceError SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffers err = Pa_CloseStream(pStream); if (err != paNoError) { qWarning() << "PortAudio: Close stream error:" - << Pa_GetErrorText(err) << getInternalName(); + << Pa_GetErrorText(err) << m_deviceId.debugName(); } return SOUNDDEVICE_ERROR_ERR; } else { @@ -371,7 +391,7 @@ bool SoundDevicePortAudio::isOpen() const { } SoundDeviceError SoundDevicePortAudio::close() { - //qDebug() << "SoundDevicePortAudio::close()" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::close()" << m_deviceId.debugName(); PaStream* pStream = m_pStream; m_pStream = NULL; if (pStream) { @@ -385,7 +405,7 @@ SoundDeviceError SoundDevicePortAudio::close() { // Real PaErrors are always negative. if (err < 0) { qWarning() << "PortAudio: Stream already stopped:" - << Pa_GetErrorText(err) << getInternalName(); + << Pa_GetErrorText(err) << m_deviceId.debugName(); return SOUNDDEVICE_ERROR_ERR; } @@ -401,7 +421,7 @@ SoundDeviceError SoundDevicePortAudio::close() { if (err != paNoError) { qWarning() << "PortAudio: Stop stream error:" - << Pa_GetErrorText(err) << getInternalName(); + << Pa_GetErrorText(err) << m_deviceId.debugName(); return SOUNDDEVICE_ERROR_ERR; } @@ -409,7 +429,7 @@ SoundDeviceError SoundDevicePortAudio::close() { err = Pa_CloseStream(pStream); if (err != paNoError) { qWarning() << "PortAudio: Close stream error:" - << Pa_GetErrorText(err) << getInternalName(); + << Pa_GetErrorText(err) << m_deviceId.debugName(); return SOUNDDEVICE_ERROR_ERR; } @@ -472,7 +492,7 @@ void SoundDevicePortAudio::readProcess() { size1 / m_inputParams.channelCount); CSAMPLE* lastFrame = &dataPtr1[size1 - m_inputParams.channelCount]; if (err == paInputOverflowed) { - //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId.debugName(); m_pSoundManager->underflowHappened(12); } if (size2 > 0) { @@ -480,7 +500,7 @@ void SoundDevicePortAudio::readProcess() { size2 / m_inputParams.channelCount); lastFrame = &dataPtr2[size2 - m_inputParams.channelCount]; if (err == paInputOverflowed) { - //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId.debugName(); m_pSoundManager->underflowHappened(13); } } @@ -496,7 +516,7 @@ void SoundDevicePortAudio::readProcess() { if (err == paInputOverflowed) { //qDebug() // << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" - // << getInternalName(); + // << m_deviceId.debugName(); m_pSoundManager->underflowHappened(14); } } else { @@ -651,14 +671,14 @@ void SoundDevicePortAudio::writeProcess() { PaError err = Pa_WriteStream(pStream, dataPtr1, size1 / m_outputParams.channelCount); if (err == paOutputUnderflowed) { - //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_ReadStream paOutputUnderflowed" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_ReadStream paOutputUnderflowed" << m_deviceId.debugName(); m_pSoundManager->underflowHappened(19); } if (size2 > 0) { PaError err = Pa_WriteStream(pStream, dataPtr2, size2 / m_outputParams.channelCount); if (err == paOutputUnderflowed) { - //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_WriteStream paOutputUnderflowed" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_WriteStream paOutputUnderflowed" << m_deviceId.debugName(); m_pSoundManager->underflowHappened(20); } } @@ -674,7 +694,7 @@ int SoundDevicePortAudio::callbackProcessDrift( PaStreamCallbackFlags statusFlags) { Q_UNUSED(timeInfo); Trace trace("SoundDevicePortAudio::callbackProcessDrift %1", - getInternalName()); + m_deviceId.debugName()); if (statusFlags & (paOutputUnderflow | paInputOverflow)) { m_pSoundManager->underflowHappened(7); @@ -806,7 +826,7 @@ int SoundDevicePortAudio::callbackProcess(const SINT framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags) { Q_UNUSED(timeInfo); - Trace trace("SoundDevicePortAudio::callbackProcess %1", getInternalName()); + Trace trace("SoundDevicePortAudio::callbackProcess %1", m_deviceId.debugName()); if (statusFlags & (paOutputUnderflow | paInputOverflow)) { m_pSoundManager->underflowHappened(1); @@ -861,9 +881,9 @@ int SoundDevicePortAudio::callbackProcessClkRef( updateCallbackEntryToDacTime(timeInfo); Trace trace("SoundDevicePortAudio::callbackProcessClkRef %1", - getInternalName()); + m_deviceId.debugName()); - //qDebug() << "SoundDevicePortAudio::callbackProcess:" << getInternalName(); + //qDebug() << "SoundDevicePortAudio::callbackProcess:" << m_deviceId.debugName(); // Turn on TimeCritical priority for the callback thread. If we are running // in Linux userland, for example, this will have no effect. if (!m_bSetThreadPriority) { @@ -928,7 +948,7 @@ int SoundDevicePortAudio::callbackProcessClkRef( // Send audio from the soundcard's input off to the SoundManager... if (in) { ScopedTimer t("SoundDevicePortAudio::callbackProcess input %1", - getInternalName()); + m_deviceId.debugName()); composeInputBuffer(in, framesPerBuffer, 0, m_inputParams.channelCount); m_pSoundManager->pushInputBuffers(m_audioInputs, m_framesPerBuffer); } @@ -937,13 +957,13 @@ int SoundDevicePortAudio::callbackProcessClkRef( { ScopedTimer t("SoundDevicePortAudio::callbackProcess prepare %1", - getInternalName()); + m_deviceId.debugName()); m_pSoundManager->onDeviceOutputCallback(framesPerBuffer); } if (out) { ScopedTimer t("SoundDevicePortAudio::callbackProcess output %1", - getInternalName()); + m_deviceId.debugName()); if (m_outputParams.channelCount <= 0) { qWarning() diff --git a/src/soundio/sounddeviceportaudio.h b/src/soundio/sounddeviceportaudio.h index 169c724923d4..95ea7f9223ba 100644 --- a/src/soundio/sounddeviceportaudio.h +++ b/src/soundio/sounddeviceportaudio.h @@ -41,7 +41,7 @@ class SoundDevicePortAudio : public SoundDevice { public: SoundDevicePortAudio(UserSettingsPointer config, SoundManager* sm, const PaDeviceInfo* deviceInfo, - unsigned int devIndex); + unsigned int devIndex, QHash apiIndexToTypeId); ~SoundDevicePortAudio() override; SoundDeviceError open(bool isClkRefDevice, int syncBuffers) override; @@ -79,8 +79,6 @@ class SoundDevicePortAudio : public SoundDevice { // PortAudio stream for this device. PaStream* volatile m_pStream; - // PortAudio device index for this device. - PaDeviceIndex m_devId; // Struct containing information about this device. Don't free() it, it // belongs to PortAudio. const PaDeviceInfo* m_deviceInfo; diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index dc2d55b0ecb5..aea7413c2eb3 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -44,6 +44,7 @@ #include "vinylcontrol/defs_vinylcontrol.h" #ifdef __PORTAUDIO__ +#include typedef PaError (*SetJackClientName)(const char *name); #endif @@ -70,6 +71,7 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, m_paInitialized(false), m_jackSampleRate(-1), #endif + m_config(this), m_pErrorDevice(NULL), m_underflowHappened(0) { // TODO(xxx) some of these ControlObject are not needed by soundmanager, or are unused here. @@ -139,10 +141,14 @@ QList SoundManager::getDeviceList( for (const auto& pDevice: m_devices) { // Skip devices that don't match the API, don't have input channels when // we want input devices, or don't have output channels when we want - // output devices. + // output devices. If searching for both input and output devices, + // make sure to include any devices that have >0 channels. + bool hasOutputs = pDevice->getNumOutputChannels() >= 0; + bool hasInputs = pDevice->getNumInputChannels() >= 0; if (pDevice->getHostAPI() != filterAPI || - (bOutputDevices && pDevice->getNumOutputChannels() <= 0) || - (bInputDevices && pDevice->getNumInputChannels() <= 0)) { + (bOutputDevices && !bInputDevices && !hasOutputs) || + (bInputDevices && !bOutputDevices && !hasInputs) || + (!hasInputs && !hasOutputs)) { continue; } filteredDeviceList.push_back(pDevice); @@ -293,6 +299,18 @@ void SoundManager::queryDevicesPortaudio() { return; } + // PaDeviceInfo structs have a PaHostApiIndex member, but PortAudio + // unfortunately provides no good way to associate this with a persistent, + // unique identifier for the API. So, build a QHash to do that and pass + // it to the SoundDevicePortAudio constructor. + QHash paApiIndexToTypeId; + for (PaHostApiIndex i = 0; i < Pa_GetHostApiCount(); i++) { + const PaHostApiInfo* api = Pa_GetHostApiInfo(i); + if (api && QString(api->name) != "skeleton implementation") { + paApiIndexToTypeId.insert(i, api->type); + } + } + const PaDeviceInfo* deviceInfo; for (int i = 0; i < iNumDevices; i++) { deviceInfo = Pa_GetDeviceInfo(i); @@ -312,7 +330,7 @@ void SoundManager::queryDevicesPortaudio() { double defaultSampleRate */ auto currentDevice = SoundDevicePointer(new SoundDevicePortAudio( - m_pConfig, this, deviceInfo, i)); + m_pConfig, this, deviceInfo, i, paApiIndexToTypeId)); m_devices.push_back(currentDevice); if (!strcmp(Pa_GetHostApiInfo(deviceInfo->hostApi)->name, MIXXX_PORTAUDIO_JACK_STRING)) { @@ -366,7 +384,7 @@ SoundDeviceError SoundManager::setupDevices() { // load with all configured devices. // all found devices are removed below - QSet devicesNotFound = m_config.getDevices(); + QSet devicesNotFound = m_config.getDevices(); // pair is isInput, isOutput QList toOpen; @@ -378,7 +396,7 @@ SoundDeviceError SoundManager::setupDevices() { pDevice->clearOutputs(); m_pErrorDevice = pDevice; for (const auto& in: - m_config.getInputs().values(pDevice->getInternalName())) { + m_config.getInputs().values(pDevice->getDeviceId())) { mode.isInput = true; // TODO(bkgood) look into allocating this with the frames per // buffer value from SMConfig @@ -401,10 +419,10 @@ SoundDeviceError SoundManager::setupDevices() { } } QList outputs = - m_config.getOutputs().values(pDevice->getInternalName()); + m_config.getOutputs().values(pDevice->getDeviceId()); // Statically connect the Network Device to the Sidechain - if (pDevice->getInternalName() == kNetworkDeviceInternalName) { + if (pDevice->getDeviceId().name == kNetworkDeviceInternalName) { AudioOutput out(AudioPath::RECORD_BROADCAST, 0, 2, 0); outputs.append(out); if (m_config.getForceNetworkClock()) { @@ -414,7 +432,7 @@ SoundDeviceError SoundManager::setupDevices() { for (const auto& out: qAsConst(outputs)) { mode.isOutput = true; - if (pDevice->getInternalName() != kNetworkDeviceInternalName) { + if (pDevice->getDeviceId().name != kNetworkDeviceInternalName) { haveOutput = true; } // following keeps us from asking for a channel buffer EngineMaster @@ -476,7 +494,7 @@ SoundDeviceError SoundManager::setupDevices() { } err = pDevice->open(pNewMasterClockRef == pDevice, syncBuffers); if (err != SOUNDDEVICE_ERROR_OK) goto closeAndError; - devicesNotFound.remove(pDevice->getInternalName()); + devicesNotFound.remove(pDevice->getDeviceId()); if (mode.isOutput) { ++outputDevicesOpened; } @@ -494,8 +512,8 @@ SoundDeviceError SoundManager::setupDevices() { qDebug() << outputDevicesOpened << "output sound devices opened"; qDebug() << inputDevicesOpened << "input sound devices opened"; - for (const auto& deviceName: devicesNotFound) { - qWarning() << deviceName << "not found"; + for (const auto& device: devicesNotFound) { + qWarning() << device.debugName() << "not found"; } m_pControlObjectSoundStatusCO->set( @@ -508,7 +526,7 @@ SoundDeviceError SoundManager::setupDevices() { return SOUNDDEVICE_ERROR_OK; } m_pErrorDevice = SoundDevicePointer( - new SoundDeviceNotFound(*devicesNotFound.constBegin())); + new SoundDeviceNotFound(devicesNotFound.constBegin()->name)); return SOUNDDEVICE_ERROR_DEVICE_COUNT; closeAndError: @@ -578,7 +596,7 @@ SoundDeviceError SoundManager::setConfig(SoundManagerConfig config) { } void SoundManager::checkConfig() { - if (!m_config.checkAPI(*this)) { + if (!m_config.checkAPI()) { m_config.setAPI(SoundManagerConfig::kDefaultAPI); m_config.loadDefaults(this, SoundManagerConfig::API | SoundManagerConfig::DEVICES); } diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index 402a3120368e..2c12c48139ed 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -33,7 +33,7 @@ const int SoundManagerConfig::kDefaultAudioBufferSizeIndex = 5; const int SoundManagerConfig::kDefaultSyncBuffers = 2; -SoundManagerConfig::SoundManagerConfig() +SoundManagerConfig::SoundManagerConfig(SoundManager* pSoundManager) : m_api("None"), m_sampleRate(kFallbackSampleRate), m_deckCount(kDefaultDeckCount), @@ -41,7 +41,8 @@ SoundManagerConfig::SoundManagerConfig() m_syncBuffers(2), m_forceNetworkClock(false), m_iNumMicInputs(0), - m_bExternalRecordBroadcastConnected(false) { + m_bExternalRecordBroadcastConnected(false), + m_pSoundManager(pSoundManager) { m_configFile = QFileInfo(QDir(CmdlineArgs::Instance().getSettingsPath()).filePath(SOUNDMANAGERCONFIG_FILENAME)); } @@ -80,11 +81,68 @@ bool SoundManagerConfig::readFromDisk() { clearOutputs(); clearInputs(); QDomNodeList devElements(rootElement.elementsByTagName("SoundDevice")); + + VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) { + return false; + } + QList soundDevices = m_pSoundManager->getDeviceList(m_api, true, true); + for (int i = 0; i < devElements.count(); ++i) { QDomElement devElement(devElements.at(i).toElement()); if (devElement.isNull()) continue; - QString device(devElement.attribute("name")); - if (device.isEmpty()) continue; + SoundDeviceId deviceIdFromFile; + deviceIdFromFile.name = devElement.attribute("name"); + if (deviceIdFromFile.name.isEmpty()) { + continue; + } + deviceIdFromFile.alsaDeviceName = devElement.attribute("alsaDeviceName"); + deviceIdFromFile.portAudioIndex = devElement.attribute("portAudioIndex").toInt(); + + int devicesMatchingByName = 0; + for (const auto& soundDevice : soundDevices) { + SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); + if (hardwareDeviceId.name == deviceIdFromFile.name) { + devicesMatchingByName++; + } + } + + if (devicesMatchingByName == 0) { + continue; + } else if (devicesMatchingByName == 1) { + // There is only one device with this name, so it is unambiguous + // which it is. Neither the alsaDeviceName nor portAudioIndex are + // very reliable as persistent identifiers across restarts of Mixxx. + // Set deviceIdFromFile's alsaDeviceName and portAudioIndex to match + // the hardwareDeviceId so operator== works for SoundDeviceId. + for (const auto& soundDevice : soundDevices) { + SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); + if (hardwareDeviceId.name == deviceIdFromFile.name) { + deviceIdFromFile.alsaDeviceName = hardwareDeviceId.alsaDeviceName; + deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; + } + } + } else { + // It is not clear which hardwareDeviceId corresponds to the device + // listed in the configuration file using only the name. + if (!deviceIdFromFile.alsaDeviceName.isEmpty()) { + // If using ALSA, attempt to match based on the ALSA device name. + // This is reliable between restarts of Mixxx until the user + // unplugs an audio interface or restarts Linux. + // NOTE(Be): I am not sure if there is a way to assign a + // persistent ALSA device name across restarts of Linux for + // multiple devices with the same name. This might be possible + // somehow with a udev rule matching device serial numbers, but + // I have not tested this. + for (const auto& soundDevice : soundDevices) { + SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); + if (hardwareDeviceId.name == deviceIdFromFile.name + && hardwareDeviceId.alsaDeviceName == deviceIdFromFile.alsaDeviceName) { + deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; + } + } + } + } + QDomNodeList outElements(devElement.elementsByTagName("output")); QDomNodeList inElements(devElement.elementsByTagName("input")); for (int j = 0; j < outElements.count(); ++j) { @@ -93,7 +151,7 @@ bool SoundManagerConfig::readFromDisk() { AudioOutput out(AudioOutput::fromXML(outElement)); if (out.getType() == AudioPath::INVALID) continue; bool dupe(false); - foreach (AudioOutput otherOut, m_outputs) { + for (const AudioOutput& otherOut : m_outputs) { if (out == otherOut && out.getChannelGroup() == otherOut.getChannelGroup()) { dupe = true; @@ -101,7 +159,8 @@ bool SoundManagerConfig::readFromDisk() { } } if (dupe) continue; - addOutput(device, out); + + addOutput(deviceIdFromFile, out); } for (int j = 0; j < inElements.count(); ++j) { QDomElement inElement(inElements.at(j).toElement()); @@ -109,7 +168,7 @@ bool SoundManagerConfig::readFromDisk() { AudioInput in(AudioInput::fromXML(inElement)); if (in.getType() == AudioPath::INVALID) continue; bool dupe(false); - foreach (AudioInput otherIn, m_inputs) { + for (const AudioInput& otherIn : m_inputs) { if (in == otherIn && in.getChannelGroup() == otherIn.getChannelGroup()) { dupe = true; @@ -117,7 +176,7 @@ bool SoundManagerConfig::readFromDisk() { } } if (dupe) continue; - addInput(device, in); + addInput(deviceIdFromFile, in); } } return true; @@ -135,15 +194,19 @@ bool SoundManagerConfig::writeToDisk() const { docElement.setAttribute("deck_count", m_deckCount); doc.appendChild(docElement); - for (const auto& device: getDevices()) { + for (const auto& deviceId: getDevices()) { QDomElement devElement(doc.createElement("SoundDevice")); - devElement.setAttribute("name", device); - foreach (AudioInput in, m_inputs.values(device)) { + devElement.setAttribute("name", deviceId.name); + devElement.setAttribute("portAudioIndex", deviceId.portAudioIndex); + if (m_api == MIXXX_PORTAUDIO_ALSA_STRING) { + devElement.setAttribute("alsaDeviceName", deviceId.alsaDeviceName); + } + for (const AudioInput& in : m_inputs.values(deviceId)) { QDomElement inElement(doc.createElement("input")); in.toXML(&inElement); devElement.appendChild(inElement); } - foreach (AudioOutput out, m_outputs.values(device)) { + for (const AudioOutput& out : m_outputs.values(deviceId)) { QDomElement outElement(doc.createElement("output")); out.toXML(&outElement); devElement.appendChild(outElement); @@ -176,8 +239,11 @@ void SoundManagerConfig::setAPI(const QString &api) { * @returns false if the API is not found in SoundManager's list, otherwise * true */ -bool SoundManagerConfig::checkAPI(const SoundManager &soundManager) { - if (!soundManager.getHostAPIList().contains(m_api) && m_api != "None") { +bool SoundManagerConfig::checkAPI() { + VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) { + return false; + } + if (!m_pSoundManager->getHostAPIList().contains(m_api) && m_api != "None") { return false; } return true; @@ -303,11 +369,11 @@ void SoundManagerConfig::setAudioBufferSizeIndex(unsigned int sizeIndex) { m_audioBufferSizeIndex = sizeIndex != 0 ? math_min(sizeIndex, kMaxAudioBufferSizeIndex) : 1; } -void SoundManagerConfig::addOutput(const QString &device, const AudioOutput &out) { +void SoundManagerConfig::addOutput(const SoundDeviceId &device, const AudioOutput &out) { m_outputs.insert(device, out); } -void SoundManagerConfig::addInput(const QString &device, const AudioInput &in) { +void SoundManagerConfig::addInput(const SoundDeviceId &device, const AudioInput &in) { m_inputs.insert(device, in); if (in.getType() == AudioPath::MICROPHONE) { m_iNumMicInputs++; @@ -316,11 +382,11 @@ void SoundManagerConfig::addInput(const QString &device, const AudioInput &in) { } } -QMultiHash SoundManagerConfig::getOutputs() const { +QMultiHash SoundManagerConfig::getOutputs() const { return m_outputs; } -QMultiHash SoundManagerConfig::getInputs() const { +QMultiHash SoundManagerConfig::getInputs() const { return m_inputs; } @@ -391,7 +457,7 @@ void SoundManagerConfig::loadDefaults(SoundManager *soundManager, unsigned int f continue; } AudioOutput masterOut(AudioPath::MASTER, 0, 2, 0); - addOutput(pDevice->getInternalName(), masterOut); + addOutput(pDevice->getDeviceId(), masterOut); defaultSampleRate = pDevice->getDefaultSampleRate(); break; } @@ -416,8 +482,8 @@ void SoundManagerConfig::loadDefaults(SoundManager *soundManager, unsigned int f m_forceNetworkClock = false; } -QSet SoundManagerConfig::getDevices() const { - QSet devices; +QSet SoundManagerConfig::getDevices() const { + QSet devices; devices.reserve(m_outputs.size() + m_inputs.size()); for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { devices.insert(it.key()); diff --git a/src/soundio/soundmanagerconfig.h b/src/soundio/soundmanagerconfig.h index d8f6357cf6c5..83425183936c 100644 --- a/src/soundio/soundmanagerconfig.h +++ b/src/soundio/soundmanagerconfig.h @@ -31,6 +31,8 @@ class SoundManager; class SoundManagerConfig { public: + SoundManagerConfig(SoundManager* pSoundManager); + enum Defaults { API = (1 << 0), DEVICES = (1 << 1), @@ -51,7 +53,7 @@ class SoundManagerConfig { bool writeToDisk() const; QString getAPI() const; void setAPI(const QString &api); - bool checkAPI(const SoundManager &soundManager); + bool checkAPI(); unsigned int getSampleRate() const; void setSampleRate(unsigned int sampleRate); bool checkSampleRate(const SoundManager &soundManager); @@ -61,7 +63,7 @@ class SoundManagerConfig { unsigned int getDeckCount() const; void setDeckCount(unsigned int deckCount); void setCorrectDeckCount(int configuredDeckCount); - QSet getDevices() const; + QSet getDevices() const; unsigned int getAudioBufferSizeIndex() const; unsigned int getFramesPerBuffer() const; @@ -72,10 +74,10 @@ class SoundManagerConfig { void setSyncBuffers(unsigned int sampleRate); bool getForceNetworkClock() const; void setForceNetworkClock(bool force); - void addOutput(const QString &device, const AudioOutput &out); - void addInput(const QString &device, const AudioInput &in); - QMultiHash getOutputs() const; - QMultiHash getInputs() const; + void addOutput(const SoundDeviceId &device, const AudioOutput &out); + void addInput(const SoundDeviceId &device, const AudioInput &in); + QMultiHash getOutputs() const; + QMultiHash getInputs() const; void clearOutputs(); void clearInputs(); bool hasMicInputs(); @@ -95,9 +97,10 @@ class SoundManagerConfig { unsigned int m_audioBufferSizeIndex; unsigned int m_syncBuffers; bool m_forceNetworkClock; - QMultiHash m_outputs; - QMultiHash m_inputs; + QMultiHash m_outputs; + QMultiHash m_inputs; int m_iNumMicInputs; bool m_bExternalRecordBroadcastConnected; + SoundManager* m_pSoundManager; }; #endif diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index becf397955a5..fbefcdfdddd9 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -197,6 +197,40 @@ class AudioDestination { typedef AudioPath::AudioPathType AudioPathType; +class SoundDeviceId { + public: + QString name; + QString alsaDeviceName; + int portAudioIndex; + + SoundDeviceId() : name(""), alsaDeviceName(""), portAudioIndex(-1) {}; + + QString debugName() const { + if (alsaDeviceName.isEmpty()) { + return name + ", " + portAudioIndex; + } else { + return name + ", " + alsaDeviceName + ", " + QString::number(portAudioIndex); + } + } +}; + +inline bool operator==(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { + return lhs.name == rhs.name + && lhs.alsaDeviceName == rhs.alsaDeviceName + && lhs.portAudioIndex == rhs.portAudioIndex; +} + +// There is not really a use case for this, but it is required for QMetaType::registerComparators. +inline bool operator<(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { + return lhs.portAudioIndex < rhs.portAudioIndex; +} + +Q_DECLARE_METATYPE(SoundDeviceId); + +inline unsigned int qHash(const SoundDeviceId& id) { + return qHash(id.name) + qHash(id.alsaDeviceName) + id.portAudioIndex; +} + // globals for QHash unsigned int qHash(const ChannelGroup &group); unsigned int qHash(const AudioOutput &output); From 28603cfdca58533ec4ef0f243dd61f55fbe05c73 Mon Sep 17 00:00:00 2001 From: Be Date: Sun, 29 Sep 2019 21:50:48 -0500 Subject: [PATCH 069/103] make SoundDeviceId qDebuggable --- src/preferences/dialog/dlgprefsounditem.cpp | 4 ++-- src/soundio/sounddeviceportaudio.cpp | 24 ++++++++++----------- src/soundio/soundmanager.cpp | 2 +- src/soundio/soundmanagerutil.h | 8 +++++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/preferences/dialog/dlgprefsounditem.cpp b/src/preferences/dialog/dlgprefsounditem.cpp index b77e79f04896..662101bf3e1f 100644 --- a/src/preferences/dialog/dlgprefsounditem.cpp +++ b/src/preferences/dialog/dlgprefsounditem.cpp @@ -239,7 +239,7 @@ SoundDevicePointer DlgPrefSoundItem::getDevice() const { } for (const auto& pDevice: qAsConst(m_devices)) { if (selection == pDevice->getDeviceId()) { - //qDebug() << "DlgPrefSoundItem::getDevice" << pDevice->getDeviceId().debugName(); + //qDebug() << "DlgPrefSoundItem::getDevice" << pDevice->getDeviceId(); return pDevice; } } @@ -254,7 +254,7 @@ SoundDevicePointer DlgPrefSoundItem::getDevice() const { */ void DlgPrefSoundItem::setDevice(const SoundDeviceId& device) { int index = deviceComboBox->findData(QVariant::fromValue(device)); - //qDebug() << "DlgPrefSoundItem::setDevice" << device.debugName(); + //qDebug() << "DlgPrefSoundItem::setDevice" << device; if (index != -1) { m_inhibitSettingChanged = true; deviceComboBox->setCurrentIndex(index); diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index 40d0bf36fdbd..dd0429d3aae4 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -153,7 +153,7 @@ SoundDevicePortAudio::~SoundDevicePortAudio() { } SoundDeviceError SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffers) { - qDebug() << "SoundDevicePortAudio::open()" << m_deviceId.debugName(); + qDebug() << "SoundDevicePortAudio::open()" << m_deviceId; PaError err; if (m_audioOutputs.empty() && m_audioInputs.empty()) { @@ -359,7 +359,7 @@ SoundDeviceError SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffers err = Pa_CloseStream(pStream); if (err != paNoError) { qWarning() << "PortAudio: Close stream error:" - << Pa_GetErrorText(err) << m_deviceId.debugName(); + << Pa_GetErrorText(err) << m_deviceId; } return SOUNDDEVICE_ERROR_ERR; } else { @@ -391,7 +391,7 @@ bool SoundDevicePortAudio::isOpen() const { } SoundDeviceError SoundDevicePortAudio::close() { - //qDebug() << "SoundDevicePortAudio::close()" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::close()" << m_deviceId; PaStream* pStream = m_pStream; m_pStream = NULL; if (pStream) { @@ -405,7 +405,7 @@ SoundDeviceError SoundDevicePortAudio::close() { // Real PaErrors are always negative. if (err < 0) { qWarning() << "PortAudio: Stream already stopped:" - << Pa_GetErrorText(err) << m_deviceId.debugName(); + << Pa_GetErrorText(err) << m_deviceId; return SOUNDDEVICE_ERROR_ERR; } @@ -421,7 +421,7 @@ SoundDeviceError SoundDevicePortAudio::close() { if (err != paNoError) { qWarning() << "PortAudio: Stop stream error:" - << Pa_GetErrorText(err) << m_deviceId.debugName(); + << Pa_GetErrorText(err) << m_deviceId; return SOUNDDEVICE_ERROR_ERR; } @@ -429,7 +429,7 @@ SoundDeviceError SoundDevicePortAudio::close() { err = Pa_CloseStream(pStream); if (err != paNoError) { qWarning() << "PortAudio: Close stream error:" - << Pa_GetErrorText(err) << m_deviceId.debugName(); + << Pa_GetErrorText(err) << m_deviceId; return SOUNDDEVICE_ERROR_ERR; } @@ -492,7 +492,7 @@ void SoundDevicePortAudio::readProcess() { size1 / m_inputParams.channelCount); CSAMPLE* lastFrame = &dataPtr1[size1 - m_inputParams.channelCount]; if (err == paInputOverflowed) { - //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId; m_pSoundManager->underflowHappened(12); } if (size2 > 0) { @@ -500,7 +500,7 @@ void SoundDevicePortAudio::readProcess() { size2 / m_inputParams.channelCount); lastFrame = &dataPtr2[size2 - m_inputParams.channelCount]; if (err == paInputOverflowed) { - //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" << m_deviceId; m_pSoundManager->underflowHappened(13); } } @@ -516,7 +516,7 @@ void SoundDevicePortAudio::readProcess() { if (err == paInputOverflowed) { //qDebug() // << "SoundDevicePortAudio::readProcess() Pa_ReadStream paInputOverflowed" - // << m_deviceId.debugName(); + // << m_deviceId; m_pSoundManager->underflowHappened(14); } } else { @@ -671,14 +671,14 @@ void SoundDevicePortAudio::writeProcess() { PaError err = Pa_WriteStream(pStream, dataPtr1, size1 / m_outputParams.channelCount); if (err == paOutputUnderflowed) { - //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_ReadStream paOutputUnderflowed" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_ReadStream paOutputUnderflowed" << m_deviceId; m_pSoundManager->underflowHappened(19); } if (size2 > 0) { PaError err = Pa_WriteStream(pStream, dataPtr2, size2 / m_outputParams.channelCount); if (err == paOutputUnderflowed) { - //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_WriteStream paOutputUnderflowed" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::writeProcess() Pa_WriteStream paOutputUnderflowed" << m_deviceId; m_pSoundManager->underflowHappened(20); } } @@ -883,7 +883,7 @@ int SoundDevicePortAudio::callbackProcessClkRef( Trace trace("SoundDevicePortAudio::callbackProcessClkRef %1", m_deviceId.debugName()); - //qDebug() << "SoundDevicePortAudio::callbackProcess:" << m_deviceId.debugName(); + //qDebug() << "SoundDevicePortAudio::callbackProcess:" << m_deviceId; // Turn on TimeCritical priority for the callback thread. If we are running // in Linux userland, for example, this will have no effect. if (!m_bSetThreadPriority) { diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index aea7413c2eb3..5f51547005de 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -513,7 +513,7 @@ SoundDeviceError SoundManager::setupDevices() { qDebug() << outputDevicesOpened << "output sound devices opened"; qDebug() << inputDevicesOpened << "input sound devices opened"; for (const auto& device: devicesNotFound) { - qWarning() << device.debugName() << "not found"; + qWarning() << device << "not found"; } m_pControlObjectSoundStatusCO->set( diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index fbefcdfdddd9..dc7608c50a1a 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -203,8 +203,6 @@ class SoundDeviceId { QString alsaDeviceName; int portAudioIndex; - SoundDeviceId() : name(""), alsaDeviceName(""), portAudioIndex(-1) {}; - QString debugName() const { if (alsaDeviceName.isEmpty()) { return name + ", " + portAudioIndex; @@ -212,6 +210,8 @@ class SoundDeviceId { return name + ", " + alsaDeviceName + ", " + QString::number(portAudioIndex); } } + SoundDeviceId() : + name(""), alsaDeviceName(""), portAudioIndex(-1) {}; }; inline bool operator==(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { @@ -231,6 +231,10 @@ inline unsigned int qHash(const SoundDeviceId& id) { return qHash(id.name) + qHash(id.alsaDeviceName) + id.portAudioIndex; } +inline QDebug operator<<(QDebug dbg, const SoundDeviceId& soundDeviceId) { + return dbg << QString("SoundDeviceId(" + soundDeviceId.debugName() + ")"); +} + // globals for QHash unsigned int qHash(const ChannelGroup &group); unsigned int qHash(const AudioOutput &output); From a35a3923f2a9c60a1e7a44b6d2bee5216c3f9ce6 Mon Sep 17 00:00:00 2001 From: Be Date: Sun, 29 Sep 2019 22:49:56 -0500 Subject: [PATCH 070/103] alsaDeviceName -> alsaHwDevice It isn't really a name, just an ID that ALSA defines. --- src/soundio/sounddeviceportaudio.cpp | 2 +- src/soundio/soundmanagerconfig.cpp | 14 +++++++------- src/soundio/soundmanagerutil.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index dd0429d3aae4..261169c3d3a9 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -119,7 +119,7 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, QRegularExpressionMatch match = alsaHwDeviceRegex.match(deviceInfo->name); if (match.hasMatch()) { m_deviceId.name = match.captured(1); - m_deviceId.alsaDeviceName = match.captured(3); + m_deviceId.alsaHwDevice = match.captured(3); } else { // Special ALSA devices like "default" and "pulse" do not match the regex m_deviceId.name = deviceInfo->name; diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index 2c12c48139ed..a37c6f4b5d2d 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -95,7 +95,7 @@ bool SoundManagerConfig::readFromDisk() { if (deviceIdFromFile.name.isEmpty()) { continue; } - deviceIdFromFile.alsaDeviceName = devElement.attribute("alsaDeviceName"); + deviceIdFromFile.alsaHwDevice = devElement.attribute("alsaHwDevice"); deviceIdFromFile.portAudioIndex = devElement.attribute("portAudioIndex").toInt(); int devicesMatchingByName = 0; @@ -110,21 +110,21 @@ bool SoundManagerConfig::readFromDisk() { continue; } else if (devicesMatchingByName == 1) { // There is only one device with this name, so it is unambiguous - // which it is. Neither the alsaDeviceName nor portAudioIndex are + // which it is. Neither the alsaHwDevice nor portAudioIndex are // very reliable as persistent identifiers across restarts of Mixxx. - // Set deviceIdFromFile's alsaDeviceName and portAudioIndex to match + // Set deviceIdFromFile's alsaHwDevice and portAudioIndex to match // the hardwareDeviceId so operator== works for SoundDeviceId. for (const auto& soundDevice : soundDevices) { SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); if (hardwareDeviceId.name == deviceIdFromFile.name) { - deviceIdFromFile.alsaDeviceName = hardwareDeviceId.alsaDeviceName; + deviceIdFromFile.alsaHwDevice = hardwareDeviceId.alsaHwDevice; deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; } } } else { // It is not clear which hardwareDeviceId corresponds to the device // listed in the configuration file using only the name. - if (!deviceIdFromFile.alsaDeviceName.isEmpty()) { + if (!deviceIdFromFile.alsaHwDevice.isEmpty()) { // If using ALSA, attempt to match based on the ALSA device name. // This is reliable between restarts of Mixxx until the user // unplugs an audio interface or restarts Linux. @@ -136,7 +136,7 @@ bool SoundManagerConfig::readFromDisk() { for (const auto& soundDevice : soundDevices) { SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); if (hardwareDeviceId.name == deviceIdFromFile.name - && hardwareDeviceId.alsaDeviceName == deviceIdFromFile.alsaDeviceName) { + && hardwareDeviceId.alsaHwDevice == deviceIdFromFile.alsaHwDevice) { deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; } } @@ -199,7 +199,7 @@ bool SoundManagerConfig::writeToDisk() const { devElement.setAttribute("name", deviceId.name); devElement.setAttribute("portAudioIndex", deviceId.portAudioIndex); if (m_api == MIXXX_PORTAUDIO_ALSA_STRING) { - devElement.setAttribute("alsaDeviceName", deviceId.alsaDeviceName); + devElement.setAttribute("alsaHwDevice", deviceId.alsaHwDevice); } for (const AudioInput& in : m_inputs.values(deviceId)) { QDomElement inElement(doc.createElement("input")); diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index dc7608c50a1a..a8fb7190b17d 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -200,23 +200,23 @@ typedef AudioPath::AudioPathType AudioPathType; class SoundDeviceId { public: QString name; - QString alsaDeviceName; + QString alsaHwDevice; int portAudioIndex; QString debugName() const { - if (alsaDeviceName.isEmpty()) { + if (alsaHwDevice.isEmpty()) { return name + ", " + portAudioIndex; } else { - return name + ", " + alsaDeviceName + ", " + QString::number(portAudioIndex); + return name + ", " + alsaHwDevice + ", " + QString::number(portAudioIndex); } } SoundDeviceId() : - name(""), alsaDeviceName(""), portAudioIndex(-1) {}; + name(""), alsaHwDevice(""), portAudioIndex(-1) {}; }; inline bool operator==(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { return lhs.name == rhs.name - && lhs.alsaDeviceName == rhs.alsaDeviceName + && lhs.alsaHwDevice == rhs.alsaHwDevice && lhs.portAudioIndex == rhs.portAudioIndex; } @@ -228,7 +228,7 @@ inline bool operator<(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { Q_DECLARE_METATYPE(SoundDeviceId); inline unsigned int qHash(const SoundDeviceId& id) { - return qHash(id.name) + qHash(id.alsaDeviceName) + id.portAudioIndex; + return qHash(id.name) + qHash(id.alsaHwDevice) + id.portAudioIndex; } inline QDebug operator<<(QDebug dbg, const SoundDeviceId& soundDeviceId) { From 8ffb16e8e32a01d1da6f7b91260aa60e0a870fea Mon Sep 17 00:00:00 2001 From: Be Date: Sun, 29 Sep 2019 23:00:37 -0500 Subject: [PATCH 071/103] DlgPrefSound: don't reinitialize sound hardware when nothing changes --- src/preferences/dialog/dlgprefsound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 6b0972e4e243..799ce668bfce 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -205,11 +205,11 @@ void DlgPrefSound::slotUpdate() { // we change to this pane, we lose changed and unapplied settings // every time. There's no real way around this, just another argument // for a prefs rewrite -- bkgood - m_settingsModified = false; m_bSkipConfigClear = true; loadSettings(); checkLatencyCompensation(); m_bSkipConfigClear = false; + m_settingsModified = false; } /** From 9d0bc6561abe0ca119a4f941ee36e9a0e688bc7b Mon Sep 17 00:00:00 2001 From: Be Date: Mon, 30 Sep 2019 12:56:19 -0500 Subject: [PATCH 072/103] SoundDeviceId: add comments; code formatting --- src/soundio/soundmanagerutil.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index a8fb7190b17d..aa9e5c5af3fd 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -200,6 +200,8 @@ typedef AudioPath::AudioPathType AudioPathType; class SoundDeviceId { public: QString name; + // The "hw:X,Y" device name. Remains an empty string if not using ALSA + // or using a non-hw ALSA device such as "default" or "pulse". QString alsaHwDevice; int portAudioIndex; @@ -210,10 +212,16 @@ class SoundDeviceId { return name + ", " + alsaHwDevice + ", " + QString::number(portAudioIndex); } } - SoundDeviceId() : - name(""), alsaHwDevice(""), portAudioIndex(-1) {}; + + SoundDeviceId() + : name(""), + alsaHwDevice(""), + portAudioIndex(-1) {}; }; +// This must be registered with QMetaType::registerComparators for +// QVariant::operator== to use it, which is required for QComboBox::findData to +// work in DlgPrefSoundItem. inline bool operator==(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { return lhs.name == rhs.name && lhs.alsaHwDevice == rhs.alsaHwDevice From b115e441d384ad560bc88a88bf12a54c40f2b917 Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 09:25:03 -0500 Subject: [PATCH 073/103] DlgPrefSoundItem: use default-constructed SoundDeviceId for "None" --- src/preferences/dialog/dlgprefsounditem.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/preferences/dialog/dlgprefsounditem.cpp b/src/preferences/dialog/dlgprefsounditem.cpp index 662101bf3e1f..e1ecb798da74 100644 --- a/src/preferences/dialog/dlgprefsounditem.cpp +++ b/src/preferences/dialog/dlgprefsounditem.cpp @@ -40,7 +40,7 @@ DlgPrefSoundItem::DlgPrefSoundItem(QWidget* parent, AudioPathType type, setupUi(this); typeLabel->setText(AudioPath::getTrStringFromType(type, index)); - deviceComboBox->addItem(tr("None"), "None"); + deviceComboBox->addItem(tr("None"), QVariant::fromValue(SoundDeviceId())); connect(deviceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(deviceChanged(int))); connect(channelComboBox, SIGNAL(currentIndexChanged(int)), @@ -83,7 +83,7 @@ void DlgPrefSoundItem::deviceChanged(int index) { channelComboBox->clear(); SoundDeviceId selection = deviceComboBox->itemData(index).value(); unsigned int numChannels = 0; - if (selection.name == "None") { + if (selection == SoundDeviceId()) { goto emitAndReturn; } else { for (const auto& pDevice: qAsConst(m_devices)) { @@ -234,7 +234,7 @@ void DlgPrefSoundItem::reload() { */ SoundDevicePointer DlgPrefSoundItem::getDevice() const { SoundDeviceId selection = deviceComboBox->itemData(deviceComboBox->currentIndex()).value(); - if (selection.name == "None") { + if (selection == SoundDeviceId()) { return SoundDevicePointer(); } for (const auto& pDevice: qAsConst(m_devices)) { From 1ce84d78436ae6b73f7a161057aeb80182f00b28 Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 09:54:30 -0500 Subject: [PATCH 074/103] SoundDevicePortAudio: make ALSA hw device regex a constant --- src/soundio/sounddeviceportaudio.cpp | 5 +++-- src/soundio/sounddeviceportaudio.h | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index 261169c3d3a9..46de29a11eed 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -84,6 +84,8 @@ int paV19CallbackClkRef(const void *inputBuffer, void *outputBuffer, (const CSAMPLE*) inputBuffer, timeInfo, statusFlags); } +const QRegularExpression kAlsaHwDeviceRegex("(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)"); + } // anonymous namespace @@ -115,8 +117,7 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, // the name from the hw device allows for making the use of both pieces // of information in SoundManagerConfig::readFromDisk to minimize how // often users need to reconfigure their sound hardware. - QRegularExpression alsaHwDeviceRegex("(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)"); - QRegularExpressionMatch match = alsaHwDeviceRegex.match(deviceInfo->name); + QRegularExpressionMatch match = kAlsaHwDeviceRegex.match(deviceInfo->name); if (match.hasMatch()) { m_deviceId.name = match.captured(1); m_deviceId.alsaHwDevice = match.captured(3); diff --git a/src/soundio/sounddeviceportaudio.h b/src/soundio/sounddeviceportaudio.h index 95ea7f9223ba..91cdc22f0de2 100644 --- a/src/soundio/sounddeviceportaudio.h +++ b/src/soundio/sounddeviceportaudio.h @@ -102,7 +102,6 @@ class SoundDevicePortAudio : public SoundDevice { int m_invalidTimeInfoCount; PerformanceTimer m_clkRefTimer; PaTime m_lastCallbackEntrytoDacSecs; - }; #endif From 1e5d6a455524f8322eb2202aa1ee2c1a25b00ece Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 09:56:46 -0500 Subject: [PATCH 075/103] SoundManagerConfig: use named constants for XML attributes & elements This ensures the strings are the same in the readFromDisk and writeToDisk functions. --- src/soundio/soundmanagerconfig.cpp | 75 ++++++++++++++++++------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index a37c6f4b5d2d..9c2b8e2ae502 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -33,8 +33,26 @@ const int SoundManagerConfig::kDefaultAudioBufferSizeIndex = 5; const int SoundManagerConfig::kDefaultSyncBuffers = 2; +namespace { +const QString xmlRootElement = "SoundManagerConfig"; +const QString xmlAttributeApi = "api"; +const QString xmlAttributeSampleRate = "samplerate"; +const QString xmlAttributeBufferSize = "latency"; +const QString xmlAttributeSyncBuffers = "sync_buffers"; +const QString xmlAttributeForceNetworkClock = "force_network_clock"; +const QString xmlAttributeDeckCount = "deck_count"; + +const QString xmlElementSoundDevice = "SoundDevice"; +const QString xmlAttributeDeviceName = "name"; +const QString xmlAttributeAlsaHwDevice = "alsaHwDevice"; +const QString xmlAttributePortAudioIndex = "portAudioIndex"; + +const QString xmlElementOutput = "output"; +const QString xmlElementInput = "input"; +} + SoundManagerConfig::SoundManagerConfig(SoundManager* pSoundManager) - : m_api("None"), + : m_api(kDefaultAPI), m_sampleRate(kFallbackSampleRate), m_deckCount(kDefaultDeckCount), m_audioBufferSizeIndex(kDefaultAudioBufferSizeIndex), @@ -69,18 +87,18 @@ bool SoundManagerConfig::readFromDisk() { } file.close(); rootElement = doc.documentElement(); - setAPI(rootElement.attribute("api")); - setSampleRate(rootElement.attribute("samplerate", "0").toUInt()); + setAPI(rootElement.attribute(xmlAttributeApi)); + setSampleRate(rootElement.attribute(xmlAttributeSampleRate, "0").toUInt()); // audioBufferSizeIndex is refereed as "latency" in the config file - setAudioBufferSizeIndex(rootElement.attribute("latency", "0").toUInt()); - setSyncBuffers(rootElement.attribute("sync_buffers", "2").toUInt()); - setForceNetworkClock(rootElement.attribute("force_network_clock", + setAudioBufferSizeIndex(rootElement.attribute(xmlAttributeBufferSize, "0").toUInt()); + setSyncBuffers(rootElement.attribute(xmlAttributeSyncBuffers, "2").toUInt()); + setForceNetworkClock(rootElement.attribute(xmlAttributeForceNetworkClock, "0").toUInt() != 0); - setDeckCount(rootElement.attribute("deck_count", + setDeckCount(rootElement.attribute(xmlAttributeDeckCount, QString(kDefaultDeckCount)).toUInt()); clearOutputs(); clearInputs(); - QDomNodeList devElements(rootElement.elementsByTagName("SoundDevice")); + QDomNodeList devElements(rootElement.elementsByTagName(xmlElementSoundDevice)); VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) { return false; @@ -91,12 +109,12 @@ bool SoundManagerConfig::readFromDisk() { QDomElement devElement(devElements.at(i).toElement()); if (devElement.isNull()) continue; SoundDeviceId deviceIdFromFile; - deviceIdFromFile.name = devElement.attribute("name"); + deviceIdFromFile.name = devElement.attribute(xmlAttributeDeviceName); if (deviceIdFromFile.name.isEmpty()) { continue; } - deviceIdFromFile.alsaHwDevice = devElement.attribute("alsaHwDevice"); - deviceIdFromFile.portAudioIndex = devElement.attribute("portAudioIndex").toInt(); + deviceIdFromFile.alsaHwDevice = devElement.attribute(xmlAttributeAlsaHwDevice); + deviceIdFromFile.portAudioIndex = devElement.attribute(xmlAttributePortAudioIndex).toInt(); int devicesMatchingByName = 0; for (const auto& soundDevice : soundDevices) { @@ -143,8 +161,8 @@ bool SoundManagerConfig::readFromDisk() { } } - QDomNodeList outElements(devElement.elementsByTagName("output")); - QDomNodeList inElements(devElement.elementsByTagName("input")); + QDomNodeList outElements(devElement.elementsByTagName(xmlElementOutput)); + QDomNodeList inElements(devElement.elementsByTagName(xmlElementInput)); for (int j = 0; j < outElements.count(); ++j) { QDomElement outElement(outElements.at(j).toElement()); if (outElement.isNull()) continue; @@ -183,31 +201,30 @@ bool SoundManagerConfig::readFromDisk() { } bool SoundManagerConfig::writeToDisk() const { - QDomDocument doc("SoundManagerConfig"); - QDomElement docElement(doc.createElement("SoundManagerConfig")); - docElement.setAttribute("api", m_api); - docElement.setAttribute("samplerate", m_sampleRate); - // audioBufferSizeIndex is refereed as "latency" in the config file - docElement.setAttribute("latency", m_audioBufferSizeIndex); - docElement.setAttribute("sync_buffers", m_syncBuffers); - docElement.setAttribute("force_network_clock", m_forceNetworkClock); - docElement.setAttribute("deck_count", m_deckCount); + QDomDocument doc(xmlRootElement); + QDomElement docElement(doc.createElement(xmlRootElement)); + docElement.setAttribute(xmlAttributeApi, m_api); + docElement.setAttribute(xmlAttributeSampleRate, m_sampleRate); + docElement.setAttribute(xmlAttributeBufferSize, m_audioBufferSizeIndex); + docElement.setAttribute(xmlAttributeSyncBuffers, m_syncBuffers); + docElement.setAttribute(xmlAttributeForceNetworkClock, m_forceNetworkClock); + docElement.setAttribute(xmlAttributeDeckCount, m_deckCount); doc.appendChild(docElement); for (const auto& deviceId: getDevices()) { - QDomElement devElement(doc.createElement("SoundDevice")); - devElement.setAttribute("name", deviceId.name); - devElement.setAttribute("portAudioIndex", deviceId.portAudioIndex); + QDomElement devElement(doc.createElement(xmlElementSoundDevice)); + devElement.setAttribute(xmlAttributeDeviceName, deviceId.name); + devElement.setAttribute(xmlAttributePortAudioIndex, deviceId.portAudioIndex); if (m_api == MIXXX_PORTAUDIO_ALSA_STRING) { - devElement.setAttribute("alsaHwDevice", deviceId.alsaHwDevice); + devElement.setAttribute(xmlAttributeAlsaHwDevice, deviceId.alsaHwDevice); } for (const AudioInput& in : m_inputs.values(deviceId)) { - QDomElement inElement(doc.createElement("input")); + QDomElement inElement(doc.createElement(xmlElementInput)); in.toXML(&inElement); devElement.appendChild(inElement); } for (const AudioOutput& out : m_outputs.values(deviceId)) { - QDomElement outElement(doc.createElement("output")); + QDomElement outElement(doc.createElement(xmlElementOutput)); out.toXML(&outElement); devElement.appendChild(outElement); } @@ -243,7 +260,7 @@ bool SoundManagerConfig::checkAPI() { VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) { return false; } - if (!m_pSoundManager->getHostAPIList().contains(m_api) && m_api != "None") { + if (!m_pSoundManager->getHostAPIList().contains(m_api) && m_api != kDefaultAPI) { return false; } return true; From 09a344958ae11be1162f672fdccfa73b52982cb2 Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 10:03:55 -0500 Subject: [PATCH 076/103] SoundDeviceId: simplify constructor --- src/soundio/soundmanagerutil.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index aa9e5c5af3fd..f37e2550646b 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -214,9 +214,7 @@ class SoundDeviceId { } SoundDeviceId() - : name(""), - alsaHwDevice(""), - portAudioIndex(-1) {}; + : portAudioIndex(-1) {} }; // This must be registered with QMetaType::registerComparators for From c61cef41b52d0e0464c3394e9d60d0d27161bc68 Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 10:09:37 -0500 Subject: [PATCH 077/103] SoundManagerConfig: make constructor explicit --- src/soundio/soundmanagerconfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/soundio/soundmanagerconfig.h b/src/soundio/soundmanagerconfig.h index 83425183936c..0e030ebcdd98 100644 --- a/src/soundio/soundmanagerconfig.h +++ b/src/soundio/soundmanagerconfig.h @@ -31,7 +31,7 @@ class SoundManager; class SoundManagerConfig { public: - SoundManagerConfig(SoundManager* pSoundManager); + explicit SoundManagerConfig(SoundManager* pSoundManager); enum Defaults { API = (1 << 0), From 5e58431e55f0e8760df3c7a239cf70a5287baa3d Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 1 Oct 2019 11:41:08 -0500 Subject: [PATCH 078/103] overcomplicate code to avoid reconfiguring sound HW one more time ... which users have been doing for years anyway. Revert this commit after releasing Mixxx 2.2.3. --- src/soundio/soundmanagerconfig.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index 9c2b8e2ae502..089b94b045dc 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -13,6 +13,8 @@ * * ***************************************************************************/ +#include + #include "soundio/soundmanagerconfig.h" #include "soundio/soundmanagerutil.h" @@ -49,6 +51,8 @@ const QString xmlAttributePortAudioIndex = "portAudioIndex"; const QString xmlElementOutput = "output"; const QString xmlElementInput = "input"; + +const QRegularExpression kLegacyFormatRegex("((\\d*), )(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)"); } SoundManagerConfig::SoundManagerConfig(SoundManager* pSoundManager) @@ -113,8 +117,17 @@ bool SoundManagerConfig::readFromDisk() { if (deviceIdFromFile.name.isEmpty()) { continue; } - deviceIdFromFile.alsaHwDevice = devElement.attribute(xmlAttributeAlsaHwDevice); - deviceIdFromFile.portAudioIndex = devElement.attribute(xmlAttributePortAudioIndex).toInt(); + + // TODO: remove this ugly hack after Mixxx 2.2.3 is released + QRegularExpressionMatch match = kLegacyFormatRegex.match(deviceIdFromFile.name); + if (match.hasMatch()) { + deviceIdFromFile.name = match.captured(3); + deviceIdFromFile.alsaHwDevice = match.captured(5); + deviceIdFromFile.portAudioIndex = match.captured(2).toInt(); + } else { + deviceIdFromFile.alsaHwDevice = devElement.attribute(xmlAttributeAlsaHwDevice); + deviceIdFromFile.portAudioIndex = devElement.attribute(xmlAttributePortAudioIndex).toInt(); + } int devicesMatchingByName = 0; for (const auto& soundDevice : soundDevices) { From 123c60e0bc4f94ac1bcb11269de74d2c9e318a0d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 1 Oct 2019 23:59:14 +0200 Subject: [PATCH 079/103] Delete self-recursive #include directive --- src/soundio/sounddevice.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/soundio/sounddevice.h b/src/soundio/sounddevice.h index 1d6949255f18..f26335cb585e 100644 --- a/src/soundio/sounddevice.h +++ b/src/soundio/sounddevice.h @@ -24,7 +24,6 @@ #include "util/types.h" #include "preferences/usersettings.h" #include "soundio/sounddeviceerror.h" -#include "soundio/sounddevice.h" #include "soundio/soundmanagerutil.h" class SoundDevice; From ca6c41456ef4da9048c7d9c567007854af240195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Wed, 2 Oct 2019 01:38:04 +0200 Subject: [PATCH 080/103] move freeAllChunks() to the engine thread and use a std::atomic enum for state --- src/engine/cachingreader.cpp | 15 ++++++--------- src/engine/cachingreader.h | 4 +++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 10ee36fbf0a7..1e17ed6187f5 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -205,22 +205,20 @@ void CachingReader::newTrack(TrackPointer pTrack) { // to get ready while the reader switches its internal // state. There are no race conditions, because the // reader polls the worker. + m_state.store(State::TrackLoading, std::memory_order_acquire); m_worker.newTrack(pTrack); m_worker.workReady(); // Don't accept any new read requests until the current // track has been unloaded and the new track has been // loaded. - m_state = State::TrackLoading; - // Free all chunks with sample data from the current track. - freeAllChunks(); } void CachingReader::process() { ReaderStatusUpdate update; while (m_stateFIFO.read(&update, 1) == 1) { - DEBUG_ASSERT(m_state != State::Idle); auto pChunk = update.takeFromWorker(); if (pChunk) { + DEBUG_ASSERT(m_state != State::Idle); // Result of a read request (with a chunk) DEBUG_ASSERT( update.status == CHUNK_READ_SUCCESS || @@ -228,9 +226,6 @@ void CachingReader::process() { update.status == CHUNK_READ_INVALID || update.status == CHUNK_READ_DISCARDED); if (m_state == State::TrackLoading) { - // All chunks have been freed before loading the next track! - DEBUG_ASSERT(!m_mruCachingReaderChunk); - DEBUG_ASSERT(!m_lruCachingReaderChunk); // Discard all results from pending read requests for the // previous track before the next track has been loaded. freeChunk(pChunk); @@ -253,13 +248,15 @@ void CachingReader::process() { } } else { // State update (without a chunk) + // We have a new Track, discharge the chunks from the old track. + freeAllChunks(); DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); if (update.status == TRACK_LOADED) { - m_state = State::TrackLoaded; + m_state.store(State::TrackLoaded, std::memory_order_release); } else { DEBUG_ASSERT(update.status == TRACK_UNLOADED); - m_state = State::Idle; + m_state.store(State::Idle, std::memory_order_release); } // Reset the readable frame index range m_readableFrameIndexRange = update.readableFrameIndexRange(); diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index 1e9ec831fbfd..4d9bfacd0fe0 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -4,6 +4,8 @@ #ifndef ENGINE_CACHINGREADER_H #define ENGINE_CACHINGREADER_H +#include + #include #include #include @@ -156,7 +158,7 @@ class CachingReader : public QObject { TrackLoading, TrackLoaded, }; - State m_state; + std::atomic m_state; // Keeps track of all CachingReaderChunks we've allocated. QVector m_chunks; From bbf2d1773ae45b995e7b7f55d5fff495a61addf1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 30 Sep 2019 22:40:27 +0200 Subject: [PATCH 081/103] Revert renaming of member --- src/engine/cachingreader.cpp | 7 +++---- src/engine/cachingreader.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 1e17ed6187f5..16aeaf2bd8ea 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -56,12 +56,12 @@ CachingReader::CachingReader(QString group, // The capacity of the back channel must be equal to the number of // allocated chunks, because the worker use writeBlocking(). Otherwise // the worker could get stuck in a hot loop!!! - m_stateFIFO(kNumberOfCachedChunksInMemory), + m_readerStatusUpdateFIFO(kNumberOfCachedChunksInMemory), m_state(State::Idle), m_mruCachingReaderChunk(nullptr), m_lruCachingReaderChunk(nullptr), m_sampleBuffer(CachingReaderChunk::kSamples * kNumberOfCachedChunksInMemory), - m_worker(group, &m_chunkReadRequestFIFO, &m_stateFIFO) { + m_worker(group, &m_chunkReadRequestFIFO, &m_readerStatusUpdateFIFO) { m_allocatedCachingReaderChunks.reserve(kNumberOfCachedChunksInMemory); // Divide up the allocated raw memory buffer into total_chunks @@ -215,8 +215,7 @@ void CachingReader::newTrack(TrackPointer pTrack) { void CachingReader::process() { ReaderStatusUpdate update; - while (m_stateFIFO.read(&update, 1) == 1) { - auto pChunk = update.takeFromWorker(); + while (m_readerStatusUpdateFIFO.read(&update, 1) == 1) { if (pChunk) { DEBUG_ASSERT(m_state != State::Idle); // Result of a read request (with a chunk) diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index 4d9bfacd0fe0..766b0f5b41cf 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -126,7 +126,7 @@ class CachingReader : public QObject { // Thread-safe FIFOs for communication between the engine callback and // reader thread. FIFO m_chunkReadRequestFIFO; - FIFO m_stateFIFO; + FIFO m_readerStatusUpdateFIFO; // Looks for the provided chunk number in the index of in-memory chunks and // returns it if it is present. If not, returns nullptr. If it is present then From 5cbab1e8517bc874e9b5d30841a86201225207ef Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 30 Sep 2019 22:43:47 +0200 Subject: [PATCH 082/103] Rename factory function --- src/engine/cachingreaderworker.cpp | 8 ++++---- src/engine/cachingreaderworker.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index dae4f303f3e2..493a44025369 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -135,7 +135,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { if (!pTrack) { // If no new track is available then we are done - const auto update = ReaderStatusUpdate::trackNotLoaded(); + const auto update = ReaderStatusUpdate::trackUnloaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); return; } @@ -149,7 +149,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { << m_group << "File not found" << filename; - const auto update = ReaderStatusUpdate::trackNotLoaded(); + const auto update = ReaderStatusUpdate::trackUnloaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); emit trackLoadFailed( pTrack, QString("The file '%1' could not be found.") @@ -165,7 +165,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { << m_group << "Failed to open file" << filename; - const auto update = ReaderStatusUpdate::trackNotLoaded(); + const auto update = ReaderStatusUpdate::trackUnloaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); emit trackLoadFailed( pTrack, QString("The file '%1' could not be loaded").arg(filename)); @@ -182,7 +182,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { << m_group << "Failed to open empty file" << filename; - const auto update = ReaderStatusUpdate::trackNotLoaded(); + const auto update = ReaderStatusUpdate::trackUnloaded(); m_pReaderStatusFIFO->writeBlocking(&update, 1); emit trackLoadFailed( pTrack, QString("The file '%1' is empty and could not be loaded").arg(filename)); diff --git a/src/engine/cachingreaderworker.h b/src/engine/cachingreaderworker.h index a4c05a2b2376..bf1fa8ded758 100644 --- a/src/engine/cachingreaderworker.h +++ b/src/engine/cachingreaderworker.h @@ -69,7 +69,7 @@ typedef struct ReaderStatusUpdate { return update; } - static ReaderStatusUpdate trackNotLoaded() { + static ReaderStatusUpdate trackUnloaded() { ReaderStatusUpdate update; update.init(TRACK_UNLOADED, nullptr, mixxx::IndexRange()); return update; From 0e7cbf96adac352dcb8e3f945609920096ed35ba Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 2 Oct 2019 18:43:45 +0200 Subject: [PATCH 083/103] Add lost line --- src/engine/cachingreader.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 16aeaf2bd8ea..79d4207a0a5c 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -216,6 +216,7 @@ void CachingReader::newTrack(TrackPointer pTrack) { void CachingReader::process() { ReaderStatusUpdate update; while (m_readerStatusUpdateFIFO.read(&update, 1) == 1) { + auto pChunk = update.takeFromWorker(); if (pChunk) { DEBUG_ASSERT(m_state != State::Idle); // Result of a read request (with a chunk) From f2802daf04c2ceabe7e42b4a119b297b026b8514 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 2 Oct 2019 18:29:47 +0200 Subject: [PATCH 084/103] Use a QAtomicInt for storing the CachingReader state --- src/engine/cachingreader.cpp | 45 +++++++++++++++++++++--------------- src/engine/cachingreader.h | 15 ++++++------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 79d4207a0a5c..771426e33af7 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -57,7 +57,7 @@ CachingReader::CachingReader(QString group, // allocated chunks, because the worker use writeBlocking(). Otherwise // the worker could get stuck in a hot loop!!! m_readerStatusUpdateFIFO(kNumberOfCachedChunksInMemory), - m_state(State::Idle), + m_state(STATE_IDLE), m_mruCachingReaderChunk(nullptr), m_lruCachingReaderChunk(nullptr), m_sampleBuffer(CachingReaderChunk::kSamples * kNumberOfCachedChunksInMemory), @@ -200,17 +200,19 @@ CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex return pChunk; } +// Invoked from the UI thread!! void CachingReader::newTrack(TrackPointer pTrack) { - // Feed the track to the worker as soon as possible - // to get ready while the reader switches its internal - // state. There are no race conditions, because the - // reader polls the worker. - m_state.store(State::TrackLoading, std::memory_order_acquire); - m_worker.newTrack(pTrack); + auto newState = pTrack ? STATE_TRACK_LOADING : STATE_TRACK_UNLOADING; + auto oldState = m_state.fetchAndStoreAcquire(newState); + VERIFY_OR_DEBUG_ASSERT( + oldState != STATE_TRACK_LOADING || + newState != STATE_TRACK_LOADING) { + kLogger.critical() + << "Cannot load new track while loading a track"; + return; + } + m_worker.newTrack(std::move(pTrack)); m_worker.workReady(); - // Don't accept any new read requests until the current - // track has been unloaded and the new track has been - // loaded. } void CachingReader::process() { @@ -218,20 +220,20 @@ void CachingReader::process() { while (m_readerStatusUpdateFIFO.read(&update, 1) == 1) { auto pChunk = update.takeFromWorker(); if (pChunk) { - DEBUG_ASSERT(m_state != State::Idle); // Result of a read request (with a chunk) + DEBUG_ASSERT(m_state.load() != STATE_IDLE); DEBUG_ASSERT( update.status == CHUNK_READ_SUCCESS || update.status == CHUNK_READ_EOF || update.status == CHUNK_READ_INVALID || update.status == CHUNK_READ_DISCARDED); - if (m_state == State::TrackLoading) { + if (m_state.load() == STATE_TRACK_LOADING) { // Discard all results from pending read requests for the // previous track before the next track has been loaded. freeChunk(pChunk); continue; } - DEBUG_ASSERT(m_state == State::TrackLoaded); + DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADED); if (update.status == CHUNK_READ_SUCCESS) { // Insert or freshen the chunk in the MRU/LRU list after // obtaining ownership from the worker. @@ -252,14 +254,19 @@ void CachingReader::process() { freeAllChunks(); DEBUG_ASSERT(!m_mruCachingReaderChunk); DEBUG_ASSERT(!m_lruCachingReaderChunk); + // Reset the readable frame index range + m_readableFrameIndexRange = update.readableFrameIndexRange(); if (update.status == TRACK_LOADED) { - m_state.store(State::TrackLoaded, std::memory_order_release); + DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); + m_state.storeRelease(STATE_TRACK_LOADED); } else { DEBUG_ASSERT(update.status == TRACK_UNLOADED); - m_state.store(State::Idle, std::memory_order_release); + // This message could be processed later when a new + // track is already loading! + if (!m_state.testAndSetRelease(STATE_TRACK_UNLOADING, STATE_IDLE)) { + DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); + } } - // Reset the readable frame index range - m_readableFrameIndexRange = update.readableFrameIndexRange(); } } } @@ -283,7 +290,7 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, } // If no track is loaded, don't do anything. - if (m_state != State::TrackLoaded) { + if (m_state.load() != STATE_TRACK_LOADED) { return ReadResult::UNAVAILABLE; } @@ -480,7 +487,7 @@ CachingReader::ReadResult CachingReader::read(SINT startSample, SINT numSamples, void CachingReader::hintAndMaybeWake(const HintVector& hintList) { // If no file is loaded, skip. - if (m_state != State::TrackLoaded) { + if (m_state.load() != STATE_TRACK_LOADED) { return; } diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index 766b0f5b41cf..49a61ece8f3a 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -4,9 +4,7 @@ #ifndef ENGINE_CACHINGREADER_H #define ENGINE_CACHINGREADER_H -#include - -#include +#include #include #include #include @@ -153,12 +151,13 @@ class CachingReader : public QObject { // Gets a chunk from the free list, frees the LRU CachingReaderChunk if none available. CachingReaderChunkForOwner* allocateChunkExpireLRU(SINT chunkIndex); - enum class State { - Idle, - TrackLoading, - TrackLoaded, + enum State { + STATE_IDLE, + STATE_TRACK_LOADING, + STATE_TRACK_UNLOADING, + STATE_TRACK_LOADED, }; - std::atomic m_state; + QAtomicInt m_state; // Keeps track of all CachingReaderChunks we've allocated. QVector m_chunks; From ff61eebcd2074b9da0499b3760ac0001f9c6304b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 3 Oct 2019 01:40:22 +0200 Subject: [PATCH 085/103] Remove obsolete virtual functions --- src/engine/cachingreader.h | 8 ++++---- src/engine/cachingreaderworker.cpp | 3 --- src/engine/cachingreaderworker.h | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/engine/cachingreader.h b/src/engine/cachingreader.h index 49a61ece8f3a..1d6187033e12 100644 --- a/src/engine/cachingreader.h +++ b/src/engine/cachingreader.h @@ -78,9 +78,9 @@ class CachingReader : public QObject { // Construct a CachingReader with the given group. CachingReader(QString group, UserSettingsPointer _config); - virtual ~CachingReader(); + ~CachingReader() override; - virtual void process(); + void process(); enum class ReadResult { // No samples read and buffer untouched(!), try again later in case of a cache miss @@ -101,12 +101,12 @@ class CachingReader : public QObject { // that is not in the cache. If any hints do request a chunk not in cache, // then wake the reader so that it can process them. Must only be called // from the engine callback. - virtual void hintAndMaybeWake(const HintVector& hintList); + void hintAndMaybeWake(const HintVector& hintList); // Request that the CachingReader load a new track. These requests are // processed in the work thread, so the reader must be woken up via wake() // for this to take effect. - virtual void newTrack(TrackPointer pTrack); + void newTrack(TrackPointer pTrack); void setScheduler(EngineWorkerScheduler* pScheduler) { m_worker.setScheduler(pScheduler); diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index 493a44025369..1d2773c47dbc 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -29,9 +29,6 @@ CachingReaderWorker::CachingReaderWorker( m_stop(0) { } -CachingReaderWorker::~CachingReaderWorker() { -} - ReaderStatusUpdate CachingReaderWorker::processReadRequest( const CachingReaderChunkReadRequest& request) { CachingReaderChunk* pChunk = request.chunk; diff --git a/src/engine/cachingreaderworker.h b/src/engine/cachingreaderworker.h index bf1fa8ded758..83176ed32194 100644 --- a/src/engine/cachingreaderworker.h +++ b/src/engine/cachingreaderworker.h @@ -101,14 +101,14 @@ class CachingReaderWorker : public EngineWorker { CachingReaderWorker(QString group, FIFO* pChunkReadRequestFIFO, FIFO* pReaderStatusFIFO); - virtual ~CachingReaderWorker(); + ~CachingReaderWorker() override = default; // Request to load a new track. wake() must be called afterwards. - virtual void newTrack(TrackPointer pTrack); + void newTrack(TrackPointer pTrack); // Run upkeep operations like loading tracks and reading from file. Run by a // thread pool via the EngineWorkerScheduler. - virtual void run(); + void run() override; void quitWait(); From d02ae6fb839ac032f7f51308c7fd79524b5a2a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 11:30:55 +0200 Subject: [PATCH 086/103] move workReady call to CachingReaderWorker::newTrack() --- src/engine/cachingreader.cpp | 1 - src/engine/cachingreaderworker.cpp | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 771426e33af7..3dea1f0f5380 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -212,7 +212,6 @@ void CachingReader::newTrack(TrackPointer pTrack) { return; } m_worker.newTrack(std::move(pTrack)); - m_worker.workReady(); } void CachingReader::process() { diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index 1d2773c47dbc..c122cd445ef3 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -84,9 +84,12 @@ ReaderStatusUpdate CachingReaderWorker::processReadRequest( // WARNING: Always called from a different thread (GUI) void CachingReaderWorker::newTrack(TrackPointer pTrack) { - QMutexLocker locker(&m_newTrackMutex); - m_pNewTrack = pTrack; - m_newTrackAvailable = true; + { + QMutexLocker locker(&m_newTrackMutex); + m_pNewTrack = pTrack; + m_newTrackAvailable = true; + } + workReady(); } void CachingReaderWorker::run() { From 5990d16b858a6f9a126fbd1432eba4325ee2c738 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 3 Oct 2019 11:46:44 +0200 Subject: [PATCH 087/103] Fix compiler warnings and disambiguate naming --- src/preferences/dialog/dlgprefsound.cpp | 14 +++++++------- src/preferences/dialog/dlgprefsound.h | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 799ce668bfce..3bc5906c84bf 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -30,16 +30,16 @@ * all the controls to the values obtained from SoundManager. */ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, - PlayerManager* pPlayerManager, UserSettingsPointer pConfig) + PlayerManager* pPlayerManager, UserSettingsPointer pSettings) : DlgPreferencePage(pParent), m_pSoundManager(pSoundManager), m_pPlayerManager(pPlayerManager), - m_pConfig(pConfig), + m_pSettings(pSettings), + m_config(pSoundManager), m_settingsModified(false), m_bLatencyChanged(false), m_bSkipConfigClear(true), - m_loading(false), - m_config(pSoundManager) { + m_loading(false) { setupUi(this); connect(m_pSoundManager, SIGNAL(devicesUpdated()), @@ -228,7 +228,7 @@ void DlgPrefSound::slotApply() { { ScopedWaitCursor cursor; m_pKeylockEngine->set(keylockComboBox->currentIndex()); - m_pConfig->set(ConfigKey("[Master]", "keylock_engine"), + m_pSettings->set(ConfigKey("[Master]", "keylock_engine"), ConfigValue(keylockComboBox->currentIndex())); err = m_pSoundManager->setConfig(m_config); @@ -414,12 +414,12 @@ void DlgPrefSound::loadSettings(const SoundManagerConfig &config) { } // Default keylock is Rubberband. - int keylock_engine = m_pConfig->getValue( + int keylock_engine = m_pSettings->getValue( ConfigKey("[Master]", "keylock_engine"), 1); keylockComboBox->setCurrentIndex(keylock_engine); m_loading = false; - // DlgPrefSoundItem has it's own inhibit flag + // DlgPrefSoundItem has it's own inhibit flag emit(loadPaths(m_config)); } diff --git a/src/preferences/dialog/dlgprefsound.h b/src/preferences/dialog/dlgprefsound.h index a2fb180a2435..2b60fc7401cb 100644 --- a/src/preferences/dialog/dlgprefsound.h +++ b/src/preferences/dialog/dlgprefsound.h @@ -44,7 +44,7 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { public: DlgPrefSound(QWidget *parent, SoundManager *soundManager, PlayerManager* pPlayerManager, - UserSettingsPointer config); + UserSettingsPointer pSettings); virtual ~DlgPrefSound(); signals: @@ -96,7 +96,8 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { SoundManager *m_pSoundManager; PlayerManager *m_pPlayerManager; - UserSettingsPointer m_pConfig; + UserSettingsPointer m_pSettings; + SoundManagerConfig m_config; ControlProxy* m_pMasterAudioLatencyOverloadCount; ControlProxy* m_pMasterLatency; ControlProxy* m_pHeadDelay; @@ -112,7 +113,6 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { bool m_settingsModified; bool m_bLatencyChanged; bool m_bSkipConfigClear; - SoundManagerConfig m_config; bool m_loading; }; From 4316f2a47eded9be985891096a75bcb73833fff7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 3 Oct 2019 11:47:56 +0200 Subject: [PATCH 088/103] Add missing initialization --- src/soundio/soundmanager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 5f51547005de..19ca2b9c4dd5 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -73,7 +73,8 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, #endif m_config(this), m_pErrorDevice(NULL), - m_underflowHappened(0) { + m_underflowHappened(0), + m_underflowUpdateCount(0) { // TODO(xxx) some of these ControlObject are not needed by soundmanager, or are unused here. // It is possible to take them out? m_pControlObjectSoundStatusCO = new ControlObject( From 26dd7ce9c1fee7ef2bb5fa58e33f0de8e8d39e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 11:55:08 +0200 Subject: [PATCH 089/103] Reorder calls for a better documentation. Don't clean up chunks before a new track has been loaded. --- src/engine/cachingreader.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index 3dea1f0f5380..baa54ab9bdc7 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -249,19 +249,23 @@ void CachingReader::process() { } } else { // State update (without a chunk) - // We have a new Track, discharge the chunks from the old track. - freeAllChunks(); - DEBUG_ASSERT(!m_mruCachingReaderChunk); - DEBUG_ASSERT(!m_lruCachingReaderChunk); - // Reset the readable frame index range - m_readableFrameIndexRange = update.readableFrameIndexRange(); if (update.status == TRACK_LOADED) { + // We have a new Track ready to go. + // Assert that we had STATE_TRACK_LOADING before and all old chunks + // in the m_readerStatusUpdateFIFO have been discarded. DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); + // now purge also the recently used chunk list from the old track. + freeAllChunks(); + DEBUG_ASSERT(!m_mruCachingReaderChunk); + DEBUG_ASSERT(!m_lruCachingReaderChunk); + // Reset the readable frame index range + m_readableFrameIndexRange = update.readableFrameIndexRange(); m_state.storeRelease(STATE_TRACK_LOADED); } else { DEBUG_ASSERT(update.status == TRACK_UNLOADED); // This message could be processed later when a new - // track is already loading! + // track is already loading! In this case the TRACK_LOADED will + // be the very next status update. if (!m_state.testAndSetRelease(STATE_TRACK_UNLOADING, STATE_IDLE)) { DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); } From b893fe66a63491e41489e4b0c631ff071eea3480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 14:05:27 +0200 Subject: [PATCH 090/103] Fix critical() in case of two consecutive track loads --- src/engine/cachingreader.cpp | 34 ++++++++++++++++++++---------- src/engine/cachingreaderworker.cpp | 3 +++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index baa54ab9bdc7..e4584d8d2afa 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -204,12 +204,17 @@ CachingReaderChunkForOwner* CachingReader::lookupChunkAndFreshen(SINT chunkIndex void CachingReader::newTrack(TrackPointer pTrack) { auto newState = pTrack ? STATE_TRACK_LOADING : STATE_TRACK_UNLOADING; auto oldState = m_state.fetchAndStoreAcquire(newState); - VERIFY_OR_DEBUG_ASSERT( - oldState != STATE_TRACK_LOADING || + + // TODO(): + // BaseTrackPlayerImpl::slotLoadTrack() distributes the new track via + // emit(loadingTrack(pNewTrack, pOldTrack)); + // but the newTrack may change if we load a new track while the previous one + // is still loading. This leads to inconsistent states for example a different + // track in the Mixx Title and the Deck label. + if (oldState != STATE_TRACK_LOADING || newState != STATE_TRACK_LOADING) { - kLogger.critical() - << "Cannot load new track while loading a track"; - return; + kLogger.warning() + << "Loading a new track while loading a track may lead to inconsistent states"; } m_worker.newTrack(std::move(pTrack)); } @@ -218,6 +223,7 @@ void CachingReader::process() { ReaderStatusUpdate update; while (m_readerStatusUpdateFIFO.read(&update, 1) == 1) { auto pChunk = update.takeFromWorker(); + qDebug() << "CachingReader::process()" << update.status; if (pChunk) { // Result of a read request (with a chunk) DEBUG_ASSERT(m_state.load() != STATE_IDLE); @@ -251,13 +257,19 @@ void CachingReader::process() { // State update (without a chunk) if (update.status == TRACK_LOADED) { // We have a new Track ready to go. - // Assert that we had STATE_TRACK_LOADING before and all old chunks - // in the m_readerStatusUpdateFIFO have been discarded. - DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); + // Assert that we either have had STATE_TRACK_LOADING before and all + // chunks in the m_readerStatusUpdateFIFO have been discarded. + // or the cache has been already cleared. + // In case of two consecutive load events, we receive two consecutive + // TRACK_LOADED without a chunk in between, assert this here. + DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING || + (m_state.load() == STATE_TRACK_LOADED && + !m_mruCachingReaderChunk && !m_lruCachingReaderChunk)); // now purge also the recently used chunk list from the old track. - freeAllChunks(); - DEBUG_ASSERT(!m_mruCachingReaderChunk); - DEBUG_ASSERT(!m_lruCachingReaderChunk); + if (m_mruCachingReaderChunk || m_lruCachingReaderChunk) { + DEBUG_ASSERT(m_state.load() == STATE_TRACK_LOADING); + freeAllChunks(); + } // Reset the readable frame index range m_readableFrameIndexRange = update.readableFrameIndexRange(); m_state.storeRelease(STATE_TRACK_LOADED); diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index c122cd445ef3..aa28d5b4afa8 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -143,6 +143,9 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Emit that a new track is loading, stops the current track emit trackLoading(); + // Slows down track loading for debug + //QThread::sleep(10); + QString filename = pTrack->getLocation(); if (filename.isEmpty() || !pTrack->exists()) { kLogger.warning() From c1bab77c4a8e2f8e02ea7f4dea4220042d03d377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 21:31:33 +0200 Subject: [PATCH 091/103] invert warning condition and remove debug leftover. --- src/engine/cachingreader.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/engine/cachingreader.cpp b/src/engine/cachingreader.cpp index e4584d8d2afa..5cd5df50c9ac 100644 --- a/src/engine/cachingreader.cpp +++ b/src/engine/cachingreader.cpp @@ -211,8 +211,8 @@ void CachingReader::newTrack(TrackPointer pTrack) { // but the newTrack may change if we load a new track while the previous one // is still loading. This leads to inconsistent states for example a different // track in the Mixx Title and the Deck label. - if (oldState != STATE_TRACK_LOADING || - newState != STATE_TRACK_LOADING) { + if (oldState == STATE_TRACK_LOADING && + newState == STATE_TRACK_LOADING) { kLogger.warning() << "Loading a new track while loading a track may lead to inconsistent states"; } @@ -223,7 +223,6 @@ void CachingReader::process() { ReaderStatusUpdate update; while (m_readerStatusUpdateFIFO.read(&update, 1) == 1) { auto pChunk = update.takeFromWorker(); - qDebug() << "CachingReader::process()" << update.status; if (pChunk) { // Result of a read request (with a chunk) DEBUG_ASSERT(m_state.load() != STATE_IDLE); From d7737dfafb92bda1ddda9bdcaefc0a8478932c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 21:36:13 +0200 Subject: [PATCH 092/103] Remove commented debugging code --- src/engine/cachingreaderworker.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/engine/cachingreaderworker.cpp b/src/engine/cachingreaderworker.cpp index aa28d5b4afa8..c122cd445ef3 100644 --- a/src/engine/cachingreaderworker.cpp +++ b/src/engine/cachingreaderworker.cpp @@ -143,9 +143,6 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { // Emit that a new track is loading, stops the current track emit trackLoading(); - // Slows down track loading for debug - //QThread::sleep(10); - QString filename = pTrack->getLocation(); if (filename.isEmpty() || !pTrack->exists()) { kLogger.warning() From 04b26e4160d22249bedfab87902ccafda60115e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Thu, 3 Oct 2019 22:46:20 +0200 Subject: [PATCH 093/103] change remaining !size() to isEmpty() --- src/library/autodj/autodjfeature.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index a378ab15c067..140347ad4f48 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -145,7 +145,7 @@ bool AutoDJFeature::dropAccept(QList urls, QObject* pSource) { // tracks already in the DB QList trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls, !pSource); - if (!trackIds.size()) { + if (trackIds.isEmpty()) { return false; } From 3f2c3265be4901d4202628fbdb79eaae5fe8f9fe Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 5 Oct 2019 14:58:34 +0200 Subject: [PATCH 094/103] Recover from FLAC deocding errors --- src/sources/soundsourceflac.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 1a95ac168c5f..bf1744457035 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -143,9 +143,9 @@ ReadableSampleFrames SoundSourceFLAC::readSampleFramesClamped( m_sampleBuffer.clear(); invalidateCurFrameIndex(); if (FLAC__stream_decoder_seek_absolute(m_decoder, seekFrameIndex)) { + DEBUG_ASSERT(FLAC__STREAM_DECODER_SEEK_ERROR != FLAC__stream_decoder_get_state(m_decoder)); // Success: Set the new position m_curFrameIndex = seekFrameIndex; - DEBUG_ASSERT(FLAC__STREAM_DECODER_SEEK_ERROR != FLAC__stream_decoder_get_state(m_decoder)); } else { // Failure kLogger.warning() @@ -179,8 +179,14 @@ ReadableSampleFrames SoundSourceFLAC::readSampleFramesClamped( } else { // We have already reached the beginning of the file // and cannot move the seek position backwards any - // further! - break; // exit loop + // further! As a last resort try to reset the decoder. + kLogger.warning() + << "Resetting decoder after seek errors"; + if (!FLAC__stream_decoder_reset(m_decoder)) { + kLogger.critical() + << "Failed to reset decoder after seek errors"; + break; // exit loop + } } } } @@ -193,8 +199,12 @@ ReadableSampleFrames SoundSourceFLAC::readSampleFramesClamped( && (precedingFrames != readSampleFramesClamped( WritableSampleFrames(precedingFrames)).frameIndexRange())) { kLogger.warning() - << "Failed to skip preceding frames" + << "Resetting decoder after failure to skip preceding frames" << precedingFrames; + if (!FLAC__stream_decoder_reset(m_decoder)) { + kLogger.critical() + << "Failed to reset decoder after skip errors"; + } // Abort return ReadableSampleFrames( IndexRange::between( From 83bfa9e609aaaf201b2ad05f3a751959665712c9 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 5 Oct 2019 15:11:28 +0200 Subject: [PATCH 095/103] Fix debug assertion when decoding corrupt files --- src/engine/cachingreaderchunk.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engine/cachingreaderchunk.cpp b/src/engine/cachingreaderchunk.cpp index 701fb00bf50c..b337786ebcc7 100644 --- a/src/engine/cachingreaderchunk.cpp +++ b/src/engine/cachingreaderchunk.cpp @@ -72,7 +72,8 @@ mixxx::IndexRange CachingReaderChunk::bufferSampleFrames( mixxx::WritableSampleFrames( sourceFrameIndexRange, mixxx::SampleBuffer::WritableSlice(m_sampleBuffer))); - DEBUG_ASSERT(m_bufferedSampleFrames.frameIndexRange() <= sourceFrameIndexRange); + DEBUG_ASSERT(m_bufferedSampleFrames.frameIndexRange().empty() || + m_bufferedSampleFrames.frameIndexRange() <= sourceFrameIndexRange); return m_bufferedSampleFrames.frameIndexRange(); } From c11765c8301c624102240894c7279a83c7f4616b Mon Sep 17 00:00:00 2001 From: ronso0 Date: Mon, 7 Oct 2019 20:46:16 +0200 Subject: [PATCH 096/103] Tango: scaling fixes for sliders --- res/skins/Tango/mixer_channel_left.xml | 5 +++-- res/skins/Tango/mixer_channel_right.xml | 5 +++-- res/skins/Tango/mixer_headphone.xml | 6 +++--- res/skins/Tango/mixer_master_booth.xml | 6 +++--- res/skins/Tango/rate_pitch_key.xml | 2 +- res/skins/Tango/sampler.xml | 4 ++-- res/skins/Tango/spinnyCover_maxi.xml | 2 +- res/skins/Tango/spinnyCover_mini.xml | 2 +- res/skins/Tango/topbar.xml | 5 +++-- 9 files changed, 20 insertions(+), 17 deletions(-) diff --git a/res/skins/Tango/mixer_channel_left.xml b/res/skins/Tango/mixer_channel_left.xml index 40fd923ca942..a64f8eb2dc03 100644 --- a/res/skins/Tango/mixer_channel_left.xml +++ b/res/skins/Tango/mixer_channel_left.xml @@ -66,10 +66,11 @@ Variables: f,min + 34f,103f VolumeSlider channel_volume - knobs_sliders/volume_scale.svg - knobs_sliders/volume_handle.svg + knobs_sliders/volume_scale.svg + knobs_sliders/volume_handle.svg false ,volume diff --git a/res/skins/Tango/mixer_channel_right.xml b/res/skins/Tango/mixer_channel_right.xml index 21e073ca5ea3..3cb0333046fa 100644 --- a/res/skins/Tango/mixer_channel_right.xml +++ b/res/skins/Tango/mixer_channel_right.xml @@ -54,10 +54,11 @@ Variables: f,min + 34f,103f VolumeSlider channel_volume - knobs_sliders/volume_scale.svg - knobs_sliders/volume_handle.svg + knobs_sliders/volume_scale.svg + knobs_sliders/volume_handle.svg false ,volume diff --git a/res/skins/Tango/mixer_headphone.xml b/res/skins/Tango/mixer_headphone.xml index 551d499387c8..7552410b6227 100644 --- a/res/skins/Tango/mixer_headphone.xml +++ b/res/skins/Tango/mixer_headphone.xml @@ -66,10 +66,10 @@ Description: headMix + 60f,24f MixerbarSlider - min,min - knobs_sliders/headMix_handle.svg - knobs_sliders/headMix_scale.svg + knobs_sliders/headMix_handle.svg + knobs_sliders/headMix_scale.svg true [Master],headMix diff --git a/res/skins/Tango/mixer_master_booth.xml b/res/skins/Tango/mixer_master_booth.xml index abaf169d2954..c6b0e3775f45 100644 --- a/res/skins/Tango/mixer_master_booth.xml +++ b/res/skins/Tango/mixer_master_booth.xml @@ -31,9 +31,9 @@ Description: balance MixerbarSlider - min,min - knobs_sliders/balance_handle.svg - knobs_sliders/balance_scale.svg + 48f,24f + knobs_sliders/balance_handle.svg + knobs_sliders/balance_scale.svg true [Master],balance diff --git a/res/skins/Tango/rate_pitch_key.xml b/res/skins/Tango/rate_pitch_key.xml index 1edecf219894..ff4ea47c3ab0 100644 --- a/res/skins/Tango/rate_pitch_key.xml +++ b/res/skins/Tango/rate_pitch_key.xml @@ -169,7 +169,7 @@ Variables: 50,136 f,me rate - knobs_sliders/pitch_handle.svg + knobs_sliders/pitch_handle.svg knobs_sliders/pitch_scale.svg false diff --git a/res/skins/Tango/sampler.xml b/res/skins/Tango/sampler.xml index e1b8269a3a1b..b643ee4919b7 100644 --- a/res/skins/Tango/sampler.xml +++ b/res/skins/Tango/sampler.xml @@ -239,8 +239,8 @@ Variables: SamplerPitchSlider rate 20f,54f - knobs_sliders/pitch_sampler_handle.svg - knobs_sliders/pitch_sampler_scale.svg + knobs_sliders/pitch_sampler_handle.svg + knobs_sliders/pitch_sampler_scale.svg false ,rate diff --git a/res/skins/Tango/spinnyCover_maxi.xml b/res/skins/Tango/spinnyCover_maxi.xml index 198099723c38..8d084d47c670 100644 --- a/res/skins/Tango/spinnyCover_maxi.xml +++ b/res/skins/Tango/spinnyCover_maxi.xml @@ -67,7 +67,7 @@ Variables: coverart 111f,111f - skin:/graphics/cover_default.svg + skin:/graphics/cover_default.svg [Skin],show_coverart visible diff --git a/res/skins/Tango/spinnyCover_mini.xml b/res/skins/Tango/spinnyCover_mini.xml index feb845664f74..c0ab16c487c6 100644 --- a/res/skins/Tango/spinnyCover_mini.xml +++ b/res/skins/Tango/spinnyCover_mini.xml @@ -69,7 +69,7 @@ Variables: coverart 50f,50f [Channel] - skin:/graphics/cover_default_mini_.svg + skin:/graphics/cover_default_mini_.svg [Skin],show_coverart visible diff --git a/res/skins/Tango/topbar.xml b/res/skins/Tango/topbar.xml index 2ab8b54f7bf9..afa6ffafc881 100644 --- a/res/skins/Tango/topbar.xml +++ b/res/skins/Tango/topbar.xml @@ -117,8 +117,9 @@ Description: crossfader - knobs_sliders/crossfader_handle.svg - knobs_sliders/crossfader_scale.svg + 108f,32f + knobs_sliders/crossfader_handle.svg + knobs_sliders/crossfader_scale.svg true [Master],crossfader From 023874753572f859b618f05a85830f3f8d114355 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 8 Oct 2019 22:02:04 +0200 Subject: [PATCH 097/103] Fix database inconsistencies caused by LibraryScanner --- src/library/scanner/libraryscanner.cpp | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/library/scanner/libraryscanner.cpp b/src/library/scanner/libraryscanner.cpp index 896ec4741bf9..38233d417b3d 100644 --- a/src/library/scanner/libraryscanner.cpp +++ b/src/library/scanner/libraryscanner.cpp @@ -11,9 +11,11 @@ #include "util/trace.h" #include "util/file.h" #include "util/timer.h" +#include "util/performancetimer.h" #include "library/scanner/scannerutil.h" #include "util/db/dbconnectionpooler.h" #include "util/db/dbconnectionpooled.h" +#include "util/db/fwdsqlquery.h" namespace { @@ -89,6 +91,25 @@ LibraryScanner::~LibraryScanner() { cancelAndQuit(); } +namespace { + +const QString kDeleteOrphanedLibraryScannerDirectories = + "delete from LibraryHashes where hash <> 0 and directory_path not in (select directory from track_locations)"; + +// Returns the number of affected rows or -1 on error +int execCleanupQuery(QSqlDatabase database, const QString& statement) { + FwdSqlQuery query(database, statement); + VERIFY_OR_DEBUG_ASSERT(query.isPrepared()) { + return -1; + } + if (!query.execPrepared()) { + return -1; + } + return query.numRowsAffected(); +} + +} // anonymous namespace + void LibraryScanner::run() { kLogger.debug() << "Entering thread"; { @@ -103,6 +124,23 @@ void LibraryScanner::run() { return; } + { + kLogger.info() << "Cleaning up database..."; + PerformanceTimer timer; + timer.start(); + auto numRows = execCleanupQuery(dbConnection, kDeleteOrphanedLibraryScannerDirectories); + if (numRows < 0) { + kLogger.warning() + << "Failed to delete orphaned directory hashes"; + } else if (numRows > 0) { + kLogger.info() + << "Deleted" << numRows << "orphaned directory hashes)"; + } + kLogger.info() + << "Finished database cleanup:" + << timer.elapsed().debugMillisWithUnit(); + } + m_libraryHashDao.initialize(dbConnection); m_cueDao.initialize(dbConnection); m_trackDao.initialize(dbConnection); From f5fc635c68513d0f060f6111bb2e3b07a0f79194 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 9 Oct 2019 08:05:41 +0200 Subject: [PATCH 098/103] Move code --- src/library/scanner/libraryscanner.cpp | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/library/scanner/libraryscanner.cpp b/src/library/scanner/libraryscanner.cpp index 38233d417b3d..4382e913bcdc 100644 --- a/src/library/scanner/libraryscanner.cpp +++ b/src/library/scanner/libraryscanner.cpp @@ -26,6 +26,21 @@ mixxx::Logger kLogger("LibraryScanner"); QAtomicInt s_instanceCounter(0); +const QString kDeleteOrphanedLibraryScannerDirectories = + "delete from LibraryHashes where hash <> 0 and directory_path not in (select directory from track_locations)"; + +// Returns the number of affected rows or -1 on error +int execCleanupQuery(QSqlDatabase database, const QString& statement) { + FwdSqlQuery query(database, statement); + VERIFY_OR_DEBUG_ASSERT(query.isPrepared()) { + return -1; + } + if (!query.execPrepared()) { + return -1; + } + return query.numRowsAffected(); +} + } // anonymous namespace LibraryScanner::LibraryScanner( @@ -91,25 +106,6 @@ LibraryScanner::~LibraryScanner() { cancelAndQuit(); } -namespace { - -const QString kDeleteOrphanedLibraryScannerDirectories = - "delete from LibraryHashes where hash <> 0 and directory_path not in (select directory from track_locations)"; - -// Returns the number of affected rows or -1 on error -int execCleanupQuery(QSqlDatabase database, const QString& statement) { - FwdSqlQuery query(database, statement); - VERIFY_OR_DEBUG_ASSERT(query.isPrepared()) { - return -1; - } - if (!query.execPrepared()) { - return -1; - } - return query.numRowsAffected(); -} - -} // anonymous namespace - void LibraryScanner::run() { kLogger.debug() << "Entering thread"; { From 4690111a69ab01d4df7391bd19d9388e1b32237b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 9 Oct 2019 08:05:59 +0200 Subject: [PATCH 099/103] Add comments and link to bug --- src/library/scanner/libraryscanner.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/library/scanner/libraryscanner.cpp b/src/library/scanner/libraryscanner.cpp index 4382e913bcdc..556984626ca3 100644 --- a/src/library/scanner/libraryscanner.cpp +++ b/src/library/scanner/libraryscanner.cpp @@ -120,11 +120,14 @@ void LibraryScanner::run() { return; } + // Clean up the database and fix inconsistencies from previous runs. + // See also: https://bugs.launchpad.net/mixxx/+bug/1846945 { kLogger.info() << "Cleaning up database..."; PerformanceTimer timer; timer.start(); - auto numRows = execCleanupQuery(dbConnection, kDeleteOrphanedLibraryScannerDirectories); + auto numRows = execCleanupQuery(dbConnection, + kDeleteOrphanedLibraryScannerDirectories); if (numRows < 0) { kLogger.warning() << "Failed to delete orphaned directory hashes"; From 10dce7f38488160beb29b34d1aa6a8b800179f70 Mon Sep 17 00:00:00 2001 From: Philip Gottschling Date: Sat, 19 Oct 2019 15:00:48 +0200 Subject: [PATCH 100/103] reduce number of sent midi messages for wheel leds --- res/controllers/Pioneer-DDJ-SX-scripts.js | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/res/controllers/Pioneer-DDJ-SX-scripts.js b/res/controllers/Pioneer-DDJ-SX-scripts.js index b88e31896ea3..6653fae629b6 100644 --- a/res/controllers/Pioneer-DDJ-SX-scripts.js +++ b/res/controllers/Pioneer-DDJ-SX-scripts.js @@ -14,10 +14,10 @@ var PioneerDDJSX = function() {}; Version: 1.19, 05/01/2018 Description: Pioneer DDJ-SX Controller Mapping for Mixxx Source: http://github.com/DJMaxergy/mixxx/tree/pioneerDDJSX_mapping - + Copyright (c) 2018 DJMaxergy, licensed under GPL version 2 or later Copyright (c) 2014-2015 various contributors, base for this mapping, licensed under MIT license - + Contributors: - Michael Stahl (DG3NEC): original DDJ-SB2 mapping for Mixxx 2.0 - Sophia Herzog: midiAutoDJ-scripts @@ -26,31 +26,31 @@ var PioneerDDJSX = function() {}; https://github.com/wingcom/Mixxx-Pioneer-DDJ-SB - Hilton Rudham: Pioneer DDJ-SR mapping https://github.com/hrudham/Mixxx-Pioneer-DDJ-SR - + GPL license notice for current version: 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - + + MIT License for earlier versions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, @@ -79,17 +79,17 @@ PioneerDDJSX.autoDJTickInterval = 1000; // Sets the maximum adjustment of BPM allowed for beats to sync if AutoDJ is enabled [BPM] (default: 10). PioneerDDJSX.autoDJMaxBpmAdjustment = 10; // If true, AutoDJ queue is being shuffled after skipping a track (default: false). -// When using a fixed set of tracks without manual intervention, some tracks may be unreachable, +// When using a fixed set of tracks without manual intervention, some tracks may be unreachable, // due to having an unfortunate place in the queue ordering. This solves the issue. PioneerDDJSX.autoDJShuffleAfterSkip = false; -// If true, by releasing rotary selector, +// If true, by releasing rotary selector, // track in preview player jumps forward to "jumpPreviewPosition" -// (default: jumpPreviewEnabled = true, jumpPreviewPosition = 0.3). +// (default: jumpPreviewEnabled = true, jumpPreviewPosition = 0.3). PioneerDDJSX.jumpPreviewEnabled = true; PioneerDDJSX.jumpPreviewPosition = 0.3; -// If true, pad press in SAMPLER-PAD-MODE repeatedly causes sampler to play +// If true, pad press in SAMPLER-PAD-MODE repeatedly causes sampler to play // loaded track from cue-point, else it causes to play loaded track from the beginning (default: false). PioneerDDJSX.samplerCueGotoAndPlay = false; @@ -114,6 +114,7 @@ PioneerDDJSX.chFaderStart = [null, null, null, null]; PioneerDDJSX.toggledBrake = [false, false, false, false]; PioneerDDJSX.scratchMode = [true, true, true, true]; PioneerDDJSX.wheelLedsBlinkStatus = [0, 0, 0, 0]; +PioneerDDJSX.wheelLedsPosition = [0, 0, 0, 0]; PioneerDDJSX.setUpSpeedSliderRange = [0.08, 0.08, 0.08, 0.08]; // PAD mode storage: @@ -1570,7 +1571,7 @@ PioneerDDJSX.panelSelectButton = function(channel, control, value, status, group PioneerDDJSX.shiftPanelSelectButton = function(channel, control, value, status, group) { var channelGroup; PioneerDDJSX.shiftPanelSelectPressed = value ? true : false; - + for (var index in PioneerDDJSX.fxUnitGroups) { if (PioneerDDJSX.fxUnitGroups.hasOwnProperty(index)) { if (PioneerDDJSX.fxUnitGroups[index] < 2) { @@ -1846,7 +1847,13 @@ PioneerDDJSX.wheelLeds = function(value, group, control) { } PioneerDDJSX.wheelLedsBlinkStatus[deck]++; } - PioneerDDJSX.wheelLedControl(group, wheelPos); + wheelPos = parseInt(wheelPos); + // Only send midi message when the position is actually updated. + // Otherwise, the amount of messages may exceed the maximum rate at high position update rates. + if (PioneerDDJSX.wheelLedsPosition[deck] !== wheelPos) { + PioneerDDJSX.wheelLedControl(group, wheelPos); + } + PioneerDDJSX.wheelLedsPosition[deck] = wheelPos; }; PioneerDDJSX.cueLed = function(value, group, control) { @@ -2322,4 +2329,4 @@ PioneerDDJSX.slicerBeatActive = function(value, group, control) { PioneerDDJSX.slicerPreviousBeatsPassed[deck] = 0; PioneerDDJSX.slicerActive[deck] = false; } -}; \ No newline at end of file +}; From 67057ec0a549efba69167047c73484b26cb35644 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 20 Oct 2019 20:28:18 +0200 Subject: [PATCH 101/103] Backport AcoustidClient from master --- src/musicbrainz/acoustidclient.cpp | 90 ++++++++++++++++++++---------- src/musicbrainz/acoustidclient.h | 4 +- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/musicbrainz/acoustidclient.cpp b/src/musicbrainz/acoustidclient.cpp index 35156494b87a..8e03c89c3ae7 100644 --- a/src/musicbrainz/acoustidclient.cpp +++ b/src/musicbrainz/acoustidclient.cpp @@ -18,16 +18,20 @@ #include "musicbrainz/gzip.h" #include "musicbrainz/network.h" +namespace { + // see API-KEY site here http://acoustid.org/application/496 // I registered the KEY for version 1.12 -- kain88 (may 2013) const QString CLIENT_APIKEY = "czKxnkyO"; const QString ACOUSTID_URL = "http://api.acoustid.org/v2/lookup"; -const int AcoustidClient::m_DefaultTimeout = 5000; // msec +constexpr int kDefaultTimeout = 5000; // msec + +} // anonymous namespace AcoustidClient::AcoustidClient(QObject* parent) : QObject(parent), m_network(this), - m_timeouts(m_DefaultTimeout, this) { + m_timeouts(kDefaultTimeout, this) { } void AcoustidClient::setTimeout(int msec) { @@ -35,15 +39,17 @@ void AcoustidClient::setTimeout(int msec) { } void AcoustidClient::start(int id, const QString& fingerprint, int duration) { - QUrl url; - url.addQueryItem("format", "xml"); - url.addQueryItem("client", CLIENT_APIKEY); - url.addQueryItem("duration", QString::number(duration)); - url.addQueryItem("meta", "recordingids"); - url.addQueryItem("fingerprint", fingerprint); - QByteArray body = url.encodedQuery(); - - QNetworkRequest req(QUrl::fromEncoded(ACOUSTID_URL.toAscii())); + QUrlQuery urlQuery; + urlQuery.addQueryItem("format", "xml"); + urlQuery.addQueryItem("client", CLIENT_APIKEY); + urlQuery.addQueryItem("duration", QString::number(duration)); + urlQuery.addQueryItem("meta", "recordingids"); + urlQuery.addQueryItem("fingerprint", fingerprint); + // application/x-www-form-urlencoded request bodies must be percent encoded. + QByteArray body = urlQuery.query(QUrl::FullyEncoded).toLatin1(); + + QUrl url(ACOUSTID_URL); + QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); req.setRawHeader("Content-Encoding", "gzip"); @@ -51,7 +57,7 @@ void AcoustidClient::start(int id, const QString& fingerprint, int duration) { << "body:" << body; QNetworkReply* reply = m_network.post(req, gzipCompress(body)); - connect(reply, SIGNAL(finished()), SLOT(requestFinished())); + connect(reply, &QNetworkReply::finished, this, &AcoustidClient::requestFinished); m_requests[reply] = id; m_timeouts.addReply(reply); @@ -64,8 +70,13 @@ void AcoustidClient::cancel(int id) { } void AcoustidClient::cancelAll() { - qDeleteAll(m_requests.keys()); + auto requests = m_requests; m_requests.clear(); + + for (auto it = requests.constBegin(); + it != requests.constEnd(); ++it) { + delete it.key(); + } } void AcoustidClient::requestFinished() { @@ -80,30 +91,53 @@ void AcoustidClient::requestFinished() { int id = m_requests.take(reply); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (status != 200) { - QTextStream body(reply); - qDebug() << "AcoustIdClient POST reply status:" << status << "body:" << body.readAll(); + QTextStream textReader(reply); + const QByteArray body(reply->readAll()); + QXmlStreamReader reader(body); + + QString statusText; + while (!reader.atEnd() && statusText.isEmpty()) { + if (reader.readNextStartElement()) { + const QStringRef name = reader.name(); + if (name == "status") { + statusText = reader.readElementText(); + } + } + } + + if (status != 200 || statusText != "ok") { + qDebug() << "AcoustIdClient POST reply status:" << status << "body:" << body; + QString message; + QString code; + while (!reader.atEnd() && (message.isEmpty() || code.isEmpty())) { + if (reader.readNextStartElement()) { + const QStringRef name = reader.name(); + if (name == "message") { + DEBUG_ASSERT(name.isEmpty()); // fail if we have duplicated message elements. + message = reader.readElementText(); + } else if (name == "code") { + DEBUG_ASSERT(code.isEmpty()); // fail if we have duplicated code elements. + code = reader.readElementText(); + } + } + } emit(networkError( reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), - "AcoustID")); + "AcoustID", message, code.toInt())); return; } - - QTextStream textReader(reply); - QString body = textReader.readAll(); qDebug() << "AcoustIdClient POST reply status:" << status << "body:" << body; - QXmlStreamReader reader(body); - QString ID; - while (!reader.atEnd()) { - if (reader.readNext() == QXmlStreamReader::StartElement - && reader.name()== "results") { - ID = parseResult(reader); - } + QString resultId; + while (!reader.atEnd() && resultId.isEmpty()) { + if (reader.readNextStartElement() + && reader.name()== "results") { + resultId = parseResult(reader); + } } - emit(finished(id, ID)); + emit(finished(id, resultId)); } QString AcoustidClient::parseResult(QXmlStreamReader& reader) { diff --git a/src/musicbrainz/acoustidclient.h b/src/musicbrainz/acoustidclient.h index a7ce14e136b2..551dce9fa665 100644 --- a/src/musicbrainz/acoustidclient.h +++ b/src/musicbrainz/acoustidclient.h @@ -52,14 +52,12 @@ class AcoustidClient : public QObject { signals: void finished(int id, const QString& mbid); - void networkError(int, QString); + void networkError(int httpStatus, QString app, QString message, int code); private slots: void requestFinished(); private: - static const int m_DefaultTimeout; - QNetworkAccessManager m_network; NetworkTimeouts m_timeouts; QMap m_requests; From 7e54420ed589b10b8d91ef78417735fef4f3e95a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 20 Oct 2019 20:29:26 +0200 Subject: [PATCH 102/103] Switch from HTTP to HTTPS --- src/musicbrainz/acoustidclient.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/musicbrainz/acoustidclient.cpp b/src/musicbrainz/acoustidclient.cpp index 8e03c89c3ae7..154a82aecbc5 100644 --- a/src/musicbrainz/acoustidclient.cpp +++ b/src/musicbrainz/acoustidclient.cpp @@ -23,7 +23,7 @@ namespace { // see API-KEY site here http://acoustid.org/application/496 // I registered the KEY for version 1.12 -- kain88 (may 2013) const QString CLIENT_APIKEY = "czKxnkyO"; -const QString ACOUSTID_URL = "http://api.acoustid.org/v2/lookup"; +const QString ACOUSTID_URL = "https://api.acoustid.org/v2/lookup"; constexpr int kDefaultTimeout = 5000; // msec } // anonymous namespace @@ -113,10 +113,10 @@ void AcoustidClient::requestFinished() { if (reader.readNextStartElement()) { const QStringRef name = reader.name(); if (name == "message") { - DEBUG_ASSERT(name.isEmpty()); // fail if we have duplicated message elements. + DEBUG_ASSERT(name.isEmpty()); // fail if we have duplicated message elements. message = reader.readElementText(); } else if (name == "code") { - DEBUG_ASSERT(code.isEmpty()); // fail if we have duplicated code elements. + DEBUG_ASSERT(code.isEmpty()); // fail if we have duplicated code elements. code = reader.readElementText(); } } From 9e0e35543e4eecfc3e2724b65fea328e076a6841 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 20 Oct 2019 20:37:56 +0200 Subject: [PATCH 103/103] Switch from XML to JSON and fix response parsing --- src/musicbrainz/acoustidclient.cpp | 93 +++++++++++------------------- src/musicbrainz/acoustidclient.h | 4 -- 2 files changed, 35 insertions(+), 62 deletions(-) diff --git a/src/musicbrainz/acoustidclient.cpp b/src/musicbrainz/acoustidclient.cpp index 154a82aecbc5..fd1aadf69440 100644 --- a/src/musicbrainz/acoustidclient.cpp +++ b/src/musicbrainz/acoustidclient.cpp @@ -7,11 +7,10 @@ * See http://www.wtfpl.net/ for more details. * *****************************************************************************/ -#include #include -#include -#include +#include #include +#include #include #include "musicbrainz/acoustidclient.h" @@ -22,6 +21,7 @@ namespace { // see API-KEY site here http://acoustid.org/application/496 // I registered the KEY for version 1.12 -- kain88 (may 2013) +// See also: https://acoustid.org/webservice const QString CLIENT_APIKEY = "czKxnkyO"; const QString ACOUSTID_URL = "https://api.acoustid.org/v2/lookup"; constexpr int kDefaultTimeout = 5000; // msec @@ -40,7 +40,8 @@ void AcoustidClient::setTimeout(int msec) { void AcoustidClient::start(int id, const QString& fingerprint, int duration) { QUrlQuery urlQuery; - urlQuery.addQueryItem("format", "xml"); + // The default format is JSON + //urlQuery.addQueryItem("format", "json"); urlQuery.addQueryItem("client", CLIENT_APIKEY); urlQuery.addQueryItem("duration", QString::number(duration)); urlQuery.addQueryItem("meta", "recordingids"); @@ -90,68 +91,44 @@ void AcoustidClient::requestFinished() { int id = m_requests.take(reply); - int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - QTextStream textReader(reply); - const QByteArray body(reply->readAll()); - QXmlStreamReader reader(body); + const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const QByteArray body = reply->readAll(); + qDebug() << "AcoustIdClient POST reply status:" << statusCode << "body:" << body; + const auto jsonResponse = QJsonDocument::fromJson(body); QString statusText; - while (!reader.atEnd() && statusText.isEmpty()) { - if (reader.readNextStartElement()) { - const QStringRef name = reader.name(); - if (name == "status") { - statusText = reader.readElementText(); - } - } + if (jsonResponse.isObject()) { + statusText = jsonResponse.object().value("status").toString(); } - if (status != 200 || statusText != "ok") { - qDebug() << "AcoustIdClient POST reply status:" << status << "body:" << body; - QString message; - QString code; - while (!reader.atEnd() && (message.isEmpty() || code.isEmpty())) { - if (reader.readNextStartElement()) { - const QStringRef name = reader.name(); - if (name == "message") { - DEBUG_ASSERT(name.isEmpty()); // fail if we have duplicated message elements. - message = reader.readElementText(); - } else if (name == "code") { - DEBUG_ASSERT(code.isEmpty()); // fail if we have duplicated code elements. - code = reader.readElementText(); - } - } + if (statusCode != 200 || statusText != "ok") { + QString errorMessage; + int errorCode = 0; + if (jsonResponse.isObject() && statusText == "error") { + const auto error = jsonResponse.object().value("error").toObject(); + errorMessage = error.value("message").toString(); + errorCode = error.value("message").toInt(errorCode); } - emit(networkError( - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), - "AcoustID", message, code.toInt())); + emit networkError( + statusCode, + "AcoustID", + errorMessage, + errorCode); return; } - qDebug() << "AcoustIdClient POST reply status:" << status << "body:" << body; - - QString resultId; - while (!reader.atEnd() && resultId.isEmpty()) { - if (reader.readNextStartElement() - && reader.name()== "results") { - resultId = parseResult(reader); - } - } - - emit(finished(id, resultId)); -} - -QString AcoustidClient::parseResult(QXmlStreamReader& reader) { - while (!reader.atEnd()) { - QXmlStreamReader::TokenType type = reader.readNext(); - if (type== QXmlStreamReader::StartElement) { - QStringRef name = reader.name(); - if (name == "id") { - return reader.readElementText(); - } - } - if (type == QXmlStreamReader::EndElement && reader.name()=="result") { - break; + QString recordingId; + DEBUG_ASSERT(jsonResponse.isObject()); + DEBUG_ASSERT(jsonResponse.object().value("results").isArray()); + const QJsonArray results = jsonResponse.object().value("results").toArray(); + if (!results.isEmpty()) { + // Only take the first result with the maximum(?) score + DEBUG_ASSERT(results.at(0).toObject().value("recordings").isArray()); + const QJsonArray recordings = results.at(0).toObject().value("recordings").toArray(); + if (!recordings.isEmpty()) { + // Only take the first recording + recordingId = recordings.at(0).toObject().value("id").toString(); } } - return QString(); + emit finished(id, recordingId); } diff --git a/src/musicbrainz/acoustidclient.h b/src/musicbrainz/acoustidclient.h index 551dce9fa665..4616e7562c7e 100644 --- a/src/musicbrainz/acoustidclient.h +++ b/src/musicbrainz/acoustidclient.h @@ -17,8 +17,6 @@ #include "musicbrainz/network.h" #include "track/track.h" -class QXmlStreamReader; - class AcoustidClient : public QObject { Q_OBJECT @@ -48,8 +46,6 @@ class AcoustidClient : public QObject { // requests. void cancelAll(); - QString parseResult(QXmlStreamReader& reader); - signals: void finished(int id, const QString& mbid); void networkError(int httpStatus, QString app, QString message, int code);