You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Qt/C++高效构建可多维度排序的音乐库技术问询

Efficient Qt/C++ Music Library Implementation

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:

  1. Artist List: Use a QAbstractListModel backed by the sorted m_artists map for instant alphabetical order.
  2. Track List: Use a QSortFilterProxyModel to 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

  1. Parallel Scanning: Uses QtConcurrent to process multiple files at once, reducing scan time for large libraries.
  2. Incremental Scans: Checks file modification times to skip re-reading unchanged tracks.
  3. Early Filtering: Filters unsupported formats before processing, avoiding unnecessary metadata reads.
  4. Deduplication:
    • Tracks: Normalized file paths ensure no duplicate track entries.
    • Covers: Hash-based cache stores each unique cover only once.
  5. Low Memory Overhead: Pointers to tracks instead of copies mean each track is stored exactly once.

内容的提问来源于stack exchange,提问作者user11281688

火山引擎 最新活动