Qt/C++高效构建可多维度排序的音乐库技术问询
Let's break down a optimized solution for your music player, addressing memory usage, sorting flexibility, multi-artist support, and deduplication—while keeping performance in mind.
Core Data Structure Overhaul
Instead of duplicating tracks across artist/album containers, we'll store a single master list of tracks and use pointers to reference them in artist/album groups. This keeps memory usage low while maintaining all required relationships.
Updated Track Struct (Qt-Friendly)
We'll switch to Qt types for better UI integration and expand to support multiple artists:
#pragma once #include <QString> #include <QStringList> #include <QByteArray> struct Track { QString filePath; QString title; QStringList artists; // Supports multiple artists per track QString album; QString albumArtist; qint64 durationMs; QByteArray coverHash; // For cover art deduplication qint64 lastModified; // For incremental scans };
Artist & Album Grouping
Use sorted maps for case-insensitive grouping, with minimal overhead:
#include <QMap> #include <QVector> struct Artist { QString displayName; // Original case for UI QVector<const Track*> tracks; // Pointers to avoid duplication }; // Case-insensitive comparator for artist/album maps struct CaseInsensitiveCompare { bool operator()(const QString& a, const QString& b) const { return a.compare(b, Qt::CaseInsensitive) < 0; } }; QMap<QString, Artist, CaseInsensitiveCompare> m_artists; QHash<QString, Track> m_tracks; // Master track list (key: normalized file path) QHash<QByteArray, QPixmap> m_coverCache; // Deduplicated cover art
Optimized Folder Scanning
Parallelized Scanning with Early Filtering
We'll use QtConcurrent to scan files in parallel, skip inaccessible directories, and filter supported formats efficiently:
#include <QtConcurrent> #include <filesystem> #include <QMutex> QMutex m_dataMutex; void LibraryScanner::scanFolder(const QString& scanFolder) { namespace fs = std::filesystem; fs::path folderPath(scanFolder.toStdWString()); if (!fs::exists(folderPath) || !fs::is_directory(folderPath)) { emit scanFinished(); return; } // Collect all music files first (single-threaded to avoid filesystem contention) QStringList musicFiles; const QSet<QString> supportedExts = {"flac", "wav", "alac", "mp3", "m4a", "wma", "opus", "ogg", "aiff"}; try { for (const auto& entry : fs::recursive_directory_iterator( folderPath, fs::directory_options::skip_permission_denied)) { if (entry.is_regular_file()) { QString ext = QString::fromStdWString(entry.path().extension().wstring()) .toLower().remove(0,1); if (supportedExts.contains(ext)) { musicFiles.append(QString::fromStdWString(entry.path().wstring())); } } } } catch (const fs::filesystem_error& e) { qWarning() << "Scan error:" << e.what(); } // Process files in parallel (I/O bound task benefits from parallelism) QtConcurrent::blockingMap(musicFiles, [this](const QString& filePath) { QString normalizedPath = filePath.toLower(); QMutexLocker locker(&m_dataMutex); // Skip already scanned/unchanged tracks if (m_tracks.contains(normalizedPath)) { qint64 fileModified = fs::last_write_time(filePath.toStdWString()).time_since_epoch().count(); if (m_tracks[normalizedPath].lastModified == fileModified) return; } locker.unlock(); // Release lock during slow metadata reading Track track = readTrackMetadata(filePath); locker.relock(); // Update master track list m_tracks.insert(normalizedPath, track); // Add track to all associated artists for (const QString& artist : track.artists) { QString normalizedArtist = artist.toLower().trimmed(); if (m_artists.contains(normalizedArtist)) { m_artists[normalizedArtist].tracks.append(&m_tracks[normalizedPath]); } else { Artist newArtist; newArtist.displayName = artist; newArtist.tracks.append(&m_tracks[normalizedPath]); m_artists.insert(normalizedArtist, newArtist); } } }); emit scanFinished(); }
Metadata Reading with Cover Deduplication
Track LibraryScanner::readTrackMetadata(const QString& filePath) { Track track; track.filePath = filePath; track.lastModified = fs::last_write_time(filePath.toStdWString()).time_since_epoch().count(); TagLib::FileRef f(filePath.toStdWString().c_str()); if (!f.isNull()) { // Read basic tags if (auto tag = f.tag()) { track.title = QString::fromStdWString(tag->title().toWString()); track.artists = splitArtistString(QString::fromStdWString(tag->artist().toWString())); track.album = QString::fromStdWString(tag->album().toWString()); } // Read audio properties if (auto props = f.audioProperties()) { track.durationMs = props->length() * 1000; } // Read & cache cover art if (auto coverData = extractCoverArt(f.file())) { track.coverHash = QCryptographicHash::hash(coverData, QCryptographicHash::Md5); QMutexLocker locker(&m_dataMutex); if (!m_coverCache.contains(track.coverHash)) { QPixmap pixmap; pixmap.loadFromData(coverData); m_coverCache.insert(track.coverHash, pixmap); } } } return track; } // Split artist strings with common separators QStringList LibraryScanner::splitArtistString(const QString& artistStr) { QStringList separators = {";", ",", "&", "feat.", "Feat.", "featuring", "Featuring"}; QString temp = artistStr.trimmed(); for (const QString& sep : separators) { QStringList parts = temp.split(sep, Qt::SkipEmptyParts); if (parts.size() > 1) { return parts.map([](const QString& s) { return s.trimmed(); }); } } return {temp}; }
Dynamic Sorting with Qt Model-View
Leverage Qt's built-in sorting capabilities to avoid pre-sorting overhead:
- Artist List: Use a
QAbstractListModelbacked by the sortedm_artistsmap for instant alphabetical order. - Track List: Use a
QSortFilterProxyModelto dynamically sort tracks by album, title, or duration without modifying the underlying data.
Example Artist Model
class ArtistModel : public QAbstractListModel { Q_OBJECT public: explicit ArtistModel(const QMap<QString, Artist, CaseInsensitiveCompare>& artists, QObject* parent = nullptr) : QAbstractListModel(parent), m_artists(artists) {} int rowCount(const QModelIndex&) const override { return m_artists.size(); } QVariant data(const QModelIndex& index, int role) const override { if (!index.isValid()) return {}; auto it = m_artists.begin(); std::advance(it, index.row()); if (role == Qt::DisplayRole) return it.value().displayName; if (role == Qt::UserRole) return QVariant::fromValue(it.value().tracks); return {}; } private: const QMap<QString, Artist, CaseInsensitiveCompare>& m_artists; };
Dynamic Track Sorting
// When an artist is selected: void MainWindow::onArtistSelected(const QModelIndex& index) { QVector<const Track*> tracks = index.data(Qt::UserRole).value<QVector<const Track*>>(); auto trackModel = new TrackModel(tracks, this); auto proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(trackModel); ui->trackTable->setModel(proxyModel); ui->trackTable->setSortingEnabled(true); // Allow clicking headers to sort }
Key Performance Optimizations
- Parallel Scanning: Uses
QtConcurrentto process multiple files at once, reducing scan time for large libraries. - Incremental Scans: Checks file modification times to skip re-reading unchanged tracks.
- Early Filtering: Filters unsupported formats before processing, avoiding unnecessary metadata reads.
- Deduplication:
- Tracks: Normalized file paths ensure no duplicate track entries.
- Covers: Hash-based cache stores each unique cover only once.
- Low Memory Overhead: Pointers to tracks instead of copies mean each track is stored exactly once.
内容的提问来源于stack exchange,提问作者user11281688




