rather large commit, it now handles memory and threading properly.
--- more thorough commit message which was generated by the wonderful copilot AI tool: Refactor and enhance audio encoding and decoding pipelines - Introduced CodecUtils.h for utility functions related to audio codec selection, including sample format and bitrate clamping. - Added DecoderPipeline.h to manage audio decoding, including opening files and retrieving audio stream information. - Implemented EncoderPipeline.h to handle audio encoding, including encoder setup, resampling, and writing output files. - Created OutputPathBuilder.h to generate output file paths based on reencode job specifications. - Refactored SpectrogramWidget to improve OpenGL handling and ensure safe context usage. - Updated ReencodeDialog.h to enhance code readability and maintainability. - Improved error handling and logging throughout the audio processing components.
This commit is contained in:
+64
-46
@@ -23,6 +23,8 @@
|
||||
#include <QKeySequence>
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QMetaObject>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
@@ -40,6 +42,8 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
m_uiTimer.setInterval(500);
|
||||
connect(&m_uiTimer, &QTimer::timeout, this, [this] {
|
||||
auto *e = m_playback->engine();
|
||||
if (!e) return;
|
||||
|
||||
m_nowPlaying->updatePosition(
|
||||
e->positionFrames(),
|
||||
e->totalFrames(),
|
||||
@@ -50,58 +54,75 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
});
|
||||
m_uiTimer.start();
|
||||
|
||||
m_library->scan();
|
||||
QMetaObject::invokeMethod(m_library, &LibraryController::scan,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
m_eqWidget->saveState();
|
||||
m_playback->stop();
|
||||
m_library->library()->shutdown();
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void MainWindow::shutdown()
|
||||
{
|
||||
if (m_shutdown) return;
|
||||
m_shutdown = true;
|
||||
|
||||
m_uiTimer.stop();
|
||||
|
||||
if (m_eqWidget)
|
||||
m_eqWidget->saveState();
|
||||
|
||||
if (m_playback)
|
||||
m_playback->stop();
|
||||
|
||||
if (m_library && m_library->library())
|
||||
m_library->library()->shutdown();
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e)
|
||||
{
|
||||
m_eqWidget->saveState();
|
||||
m_playback->stop();
|
||||
m_library->library()->shutdown();
|
||||
shutdown();
|
||||
e->accept();
|
||||
}
|
||||
|
||||
void MainWindow::buildUI()
|
||||
{
|
||||
auto *central = new QWidget;
|
||||
auto *central = new QWidget(this);
|
||||
auto *mainVlay = new QVBoxLayout(central);
|
||||
mainVlay->setSpacing(4);
|
||||
mainVlay->setContentsMargins(4, 4, 4, 4);
|
||||
setCentralWidget(central);
|
||||
|
||||
m_nowPlaying = new NowPlayingPanel;
|
||||
m_nowPlaying = new NowPlayingPanel(central);
|
||||
mainVlay->addWidget(m_nowPlaying);
|
||||
|
||||
m_spectrogram = new SpectrogramWidget;
|
||||
m_spectrogram = new SpectrogramWidget(central);
|
||||
mainVlay->addWidget(m_spectrogram, 1);
|
||||
|
||||
auto *tabs = new QTabWidget;
|
||||
auto *tabs = new QTabWidget(central);
|
||||
|
||||
auto *plPanel = new QWidget;
|
||||
auto *plPanel = new QWidget(tabs);
|
||||
auto *plVlay = new QVBoxLayout(plPanel);
|
||||
plVlay->setContentsMargins(0, 2, 0, 0);
|
||||
|
||||
m_playlistView = new QListView;
|
||||
m_playlistView = new QListView(plPanel);
|
||||
m_playlistView->setModel(m_library->model());
|
||||
m_playlistView->setItemDelegate(new TrackDelegate(this));
|
||||
m_playlistView->setItemDelegate(new TrackDelegate(m_playlistView));
|
||||
m_playlistView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_playlistView->setUniformItemSizes(true);
|
||||
m_playlistView->setAlternatingRowColors(true);
|
||||
m_playlistView->setMinimumHeight(220);
|
||||
|
||||
m_lblStatus = new QLabel("No library loaded.");
|
||||
m_lblStatus = new QLabel("No library loaded.", plPanel);
|
||||
plVlay->addWidget(m_playlistView);
|
||||
plVlay->addWidget(m_lblStatus);
|
||||
tabs->addTab(plPanel, "Playlist");
|
||||
|
||||
m_eqWidget = new EqualizerWidget(m_playback->engine());
|
||||
auto *engine = m_playback->engine();
|
||||
Q_ASSERT_X(engine, "MainWindow::buildUI",
|
||||
"PlaybackController must provide a valid engine before buildUI");
|
||||
m_eqWidget = new EqualizerWidget(engine, tabs);
|
||||
tabs->addTab(m_eqWidget, "Equalizer");
|
||||
|
||||
mainVlay->addWidget(tabs);
|
||||
@@ -117,13 +138,13 @@ void MainWindow::buildMenuBar()
|
||||
auto *chooseAct = file->addAction("Choose Music Location...");
|
||||
chooseAct->setShortcut(QKeySequence::Open);
|
||||
connect(chooseAct, &QAction::triggered, this, [this] {
|
||||
QString dir = QFileDialog::getExistingDirectory(
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this, "Select Music Folder",
|
||||
m_library->library()->rootPath(),
|
||||
QFileDialog::ShowDirsOnly);
|
||||
if (dir.isEmpty()) return;
|
||||
m_library->setRootPath(dir);
|
||||
setWindowTitle("SubWave - " + QDir(dir).dirName());
|
||||
setWindowTitle("SubWave — " + QDir(dir).dirName());
|
||||
m_library->scan();
|
||||
});
|
||||
|
||||
@@ -133,10 +154,12 @@ void MainWindow::buildMenuBar()
|
||||
|
||||
file->addSeparator();
|
||||
|
||||
m_reencodeAct = file->addAction("Reencode Album…");
|
||||
m_reencodeAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
|
||||
m_reencodeAct->setEnabled(false);
|
||||
connect(m_reencodeAct, &QAction::triggered, this, &MainWindow::openReencodeDialog);
|
||||
m_reencodeAct = file->addAction("Reencode Album\u2026");
|
||||
m_reencodeAct->setShortcut(
|
||||
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
|
||||
m_reencodeAct->setEnabled(false);
|
||||
connect(m_reencodeAct, &QAction::triggered,
|
||||
this, &MainWindow::openReencodeDialog);
|
||||
|
||||
file->addSeparator();
|
||||
|
||||
@@ -145,9 +168,9 @@ void MainWindow::buildMenuBar()
|
||||
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
|
||||
|
||||
auto *plMenu = mb->addMenu("&Playlists");
|
||||
connect(plMenu->addAction("Create Playlist…"), &QAction::triggered,
|
||||
connect(plMenu->addAction("Create Playlist\u2026"), &QAction::triggered,
|
||||
m_library, &LibraryController::createPlaylist);
|
||||
connect(plMenu->addAction("Load Playlist…"), &QAction::triggered,
|
||||
connect(plMenu->addAction("Load Playlist\u2026"), &QAction::triggered,
|
||||
m_library, &LibraryController::loadPlaylist);
|
||||
|
||||
auto *help = mb->addMenu("&Help");
|
||||
@@ -155,8 +178,8 @@ void MainWindow::buildMenuBar()
|
||||
connect(about, &QAction::triggered, this, [this] {
|
||||
QMessageBox::about(this, "SubWave",
|
||||
"SubWave\n"
|
||||
"Its a music player.\n\n"
|
||||
"Thats it.\n"
|
||||
"It's a music player.\n\n"
|
||||
"That's it.\n"
|
||||
"~/.subwave/");
|
||||
});
|
||||
}
|
||||
@@ -174,38 +197,32 @@ void MainWindow::wireSignals()
|
||||
connect(m_nowPlaying, &NowPlayingPanel::volumeChanged,
|
||||
m_playback, &PlaybackController::setVolume);
|
||||
|
||||
// shuffle / repeat checkboxes -> PlaybackController
|
||||
connect(m_nowPlaying->shuffleCheckBox(), &QCheckBox::toggled,
|
||||
m_playback, &PlaybackController::setShuffle);
|
||||
connect(m_nowPlaying->repeatCheckBox(), &QCheckBox::toggled,
|
||||
m_playback, &PlaybackController::setRepeat);
|
||||
|
||||
// PlaybackController -> NowPlayingPanel Spectrogram
|
||||
connect(m_playback, &PlaybackController::playbackStarted,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(true); });
|
||||
m_nowPlaying, [this] { m_nowPlaying->setPlaybackActive(true); });
|
||||
connect(m_playback, &PlaybackController::playbackPaused,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(false); });
|
||||
m_nowPlaying, [this] { m_nowPlaying->setPlaybackActive(false); });
|
||||
connect(m_playback, &PlaybackController::playbackStopped,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(false); });
|
||||
m_nowPlaying, [this] { m_nowPlaying->setPlaybackActive(false); });
|
||||
|
||||
connect(m_playback, &PlaybackController::fftReady,
|
||||
m_spectrogram, &SpectrogramWidget::receiveFft, Qt::QueuedConnection);
|
||||
|
||||
// PlaybackController -> LibraryController (resolve track index)
|
||||
connect(m_playback, &PlaybackController::playRequested,
|
||||
m_library, &LibraryController::onPlayRequested);
|
||||
|
||||
// LibraryController -> NowPlayingPanel (new track metadata)
|
||||
connect(m_library, &LibraryController::trackChanged,
|
||||
m_nowPlaying, [this](const engine::Track *t, int) {
|
||||
m_nowPlaying->updateTrack(t);
|
||||
m_spectrogram->reset();
|
||||
m_playlistView->scrollTo(
|
||||
m_library->model()->index(m_playback->currentIndex()));
|
||||
|
||||
m_reencodeAct->setEnabled(t != nullptr);
|
||||
});
|
||||
|
||||
// LibraryController -> ui status / track count
|
||||
connect(m_library, &LibraryController::statusMessage,
|
||||
m_lblStatus, &QLabel::setText);
|
||||
connect(m_library, &LibraryController::trackCountChanged,
|
||||
@@ -232,18 +249,20 @@ void MainWindow::openReencodeDialog()
|
||||
}
|
||||
|
||||
QString albumFilter;
|
||||
|
||||
int currentIdx = m_playback->currentIndex();
|
||||
const int currentIdx = m_playback->currentIndex();
|
||||
if (currentIdx >= 0 && currentIdx < all.size()) {
|
||||
albumFilter = all[currentIdx].album;
|
||||
} else {
|
||||
QModelIndex sel = m_playlistView->currentIndex();
|
||||
const QModelIndex sel = m_playlistView->currentIndex();
|
||||
if (sel.isValid() && sel.row() < all.size())
|
||||
albumFilter = all[sel.row()].album;
|
||||
}
|
||||
|
||||
QList<engine::Track> targets;
|
||||
if (!albumFilter.isEmpty() && albumFilter != "Unknown Album") {
|
||||
const bool validAlbum =
|
||||
!albumFilter.isEmpty() && albumFilter != "Unknown Album";
|
||||
|
||||
if (validAlbum) {
|
||||
for (const auto &t : all)
|
||||
if (t.album.compare(albumFilter, Qt::CaseInsensitive) == 0)
|
||||
targets.append(t);
|
||||
@@ -253,9 +272,8 @@ void MainWindow::openReencodeDialog()
|
||||
targets = all;
|
||||
|
||||
ReencodeDialog dlg(targets, this);
|
||||
|
||||
if (!albumFilter.isEmpty() && albumFilter != "Unknown Album")
|
||||
dlg.setWindowTitle(QString("Reencode Album — %1").arg(albumFilter));
|
||||
if (validAlbum)
|
||||
dlg.setWindowTitle(QString("Reencode Album \u2014 %1").arg(albumFilter));
|
||||
|
||||
dlg.exec();
|
||||
}
|
||||
@@ -269,11 +287,11 @@ void MainWindow::keyPressEvent(QKeyEvent *e)
|
||||
case Qt::Key_Space:
|
||||
m_playback->togglePlayPause(); e->accept(); return;
|
||||
case Qt::Key_MediaNext:
|
||||
m_playback->playNext(); e->accept(); return;
|
||||
m_playback->playNext(); e->accept(); return;
|
||||
case Qt::Key_MediaPrevious:
|
||||
m_playback->playPrev(); e->accept(); return;
|
||||
m_playback->playPrev(); e->accept(); return;
|
||||
case Qt::Key_MediaStop:
|
||||
m_playback->stop(); e->accept(); return;
|
||||
m_playback->stop(); e->accept(); return;
|
||||
default:
|
||||
QMainWindow::keyPressEvent(e);
|
||||
}
|
||||
|
||||
+11
-8
@@ -1,4 +1,6 @@
|
||||
// MainWindow.h
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QTimer>
|
||||
|
||||
@@ -11,11 +13,13 @@ class QListView;
|
||||
class QLabel;
|
||||
class QAction;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
@@ -28,17 +32,16 @@ private:
|
||||
void buildUI();
|
||||
void buildMenuBar();
|
||||
void wireSignals();
|
||||
void shutdown();
|
||||
|
||||
PlaybackController *m_playback { nullptr };
|
||||
LibraryController *m_library { nullptr };
|
||||
|
||||
PlaybackController *m_playback { nullptr };
|
||||
LibraryController *m_library { nullptr };
|
||||
NowPlayingPanel *m_nowPlaying { nullptr };
|
||||
SpectrogramWidget *m_spectrogram { nullptr };
|
||||
EqualizerWidget *m_eqWidget { nullptr };
|
||||
QListView *m_playlistView{ nullptr };
|
||||
QLabel *m_lblStatus { nullptr };
|
||||
|
||||
QAction *m_reencodeAct { nullptr };
|
||||
|
||||
QTimer m_uiTimer;
|
||||
QTimer m_uiTimer;
|
||||
bool m_shutdown { false };
|
||||
};
|
||||
+70
-43
@@ -1,5 +1,5 @@
|
||||
#include "ReencodeDialog.h"
|
||||
#include "engine/AlbumReencoder.h"
|
||||
#include "engine/encoder/AlbumReencoder.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
@@ -15,10 +15,9 @@
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
enum Col { ColArtist=0, ColTitle, ColStatus, ColOutput, COL_COUNT };
|
||||
enum Col { ColArtist = 0, ColTitle, ColStatus, ColOutput, COL_COUNT };
|
||||
|
||||
ReencodeDialog::ReencodeDialog(const QList<engine::Track> &tracks, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
@@ -30,7 +29,6 @@ ReencodeDialog::ReencodeDialog(const QList<engine::Track> &tracks, QWidget *pare
|
||||
setMinimumSize(820, 560);
|
||||
buildUi();
|
||||
populateTrackTable();
|
||||
onPresetChanged(0);
|
||||
|
||||
connect(m_reencoder, &engine::AlbumReencoder::trackStarted,
|
||||
this, &ReencodeDialog::onTrackStarted, Qt::QueuedConnection);
|
||||
@@ -56,15 +54,21 @@ void ReencodeDialog::buildUi()
|
||||
settingsGrid->setColumnStretch(1, 1);
|
||||
|
||||
settingsGrid->addWidget(new QLabel("Preset:", this), 0, 0);
|
||||
m_presetCombo = new QComboBox(this);
|
||||
for (const auto &p : m_presets)
|
||||
m_presetCombo->addItem(p.label);
|
||||
settingsGrid->addWidget(m_presetCombo, 0, 1, 1, 2);
|
||||
|
||||
m_presetCombo = new QComboBox(this);
|
||||
|
||||
// Create the description label BEFORE connecting signals and adding items
|
||||
m_presetDesc = new QLabel(this);
|
||||
m_presetDesc->setWordWrap(true);
|
||||
m_presetDesc->setStyleSheet("color: palette(mid);");
|
||||
settingsGrid->addWidget(m_presetDesc, 1, 1, 1, 2);
|
||||
|
||||
// Now connect the signal and add items (signal may fire during addItem)
|
||||
connect(m_presetCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ReencodeDialog::onPresetChanged);
|
||||
for (const auto &p : m_presets)
|
||||
m_presetCombo->addItem(p.label);
|
||||
settingsGrid->addWidget(m_presetCombo, 0, 1, 1, 2);
|
||||
|
||||
settingsGrid->addWidget(new QLabel("Output folder:", this), 2, 0);
|
||||
m_outputDir = new QLineEdit(this);
|
||||
@@ -77,6 +81,7 @@ void ReencodeDialog::buildUi()
|
||||
|
||||
root->addWidget(settingsGroup);
|
||||
|
||||
// track table
|
||||
m_table = new QTableWidget(0, COL_COUNT, this);
|
||||
m_table->setHorizontalHeaderLabels({"Artist", "Title", "Status", "Output"});
|
||||
m_table->horizontalHeader()->setSectionResizeMode(ColArtist, QHeaderView::ResizeToContents);
|
||||
@@ -89,6 +94,7 @@ void ReencodeDialog::buildUi()
|
||||
m_table->setAlternatingRowColors(true);
|
||||
root->addWidget(m_table, 1);
|
||||
|
||||
// Progress row
|
||||
auto *progressLayout = new QHBoxLayout;
|
||||
m_statusLabel = new QLabel("Ready", this);
|
||||
m_statusLabel->setMinimumWidth(120);
|
||||
@@ -100,6 +106,7 @@ void ReencodeDialog::buildUi()
|
||||
progressLayout->addWidget(m_overallBar, 1);
|
||||
root->addLayout(progressLayout);
|
||||
|
||||
// Button row
|
||||
auto *btnLayout = new QHBoxLayout;
|
||||
btnLayout->addStretch();
|
||||
m_startStopBtn = new QPushButton("Start Encoding", this);
|
||||
@@ -111,28 +118,28 @@ void ReencodeDialog::buildUi()
|
||||
btnLayout->addWidget(m_closeBtn);
|
||||
root->addLayout(btnLayout);
|
||||
|
||||
connect(m_presetCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ReencodeDialog::onPresetChanged);
|
||||
connect(m_browseBtn, &QPushButton::clicked, this, &ReencodeDialog::onBrowseOutput);
|
||||
connect(m_startStopBtn,&QPushButton::clicked, this, &ReencodeDialog::onStartStop);
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(m_browseBtn, &QPushButton::clicked, this, &ReencodeDialog::onBrowseOutput);
|
||||
connect(m_startStopBtn, &QPushButton::clicked, this, &ReencodeDialog::onStartStop);
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void ReencodeDialog::populateTrackTable()
|
||||
{
|
||||
m_table->setRowCount(m_tracks.size());
|
||||
m_table->setRowCount(static_cast<int>(m_tracks.size()));
|
||||
for (int i = 0; i < m_tracks.size(); ++i) {
|
||||
const auto &t = m_tracks[i];
|
||||
m_table->setItem(i, ColArtist, new QTableWidgetItem(t.artist));
|
||||
m_table->setItem(i, ColTitle, new QTableWidgetItem(t.displayTitle()));
|
||||
m_table->setItem(i, ColStatus, new QTableWidgetItem("Pending"));
|
||||
m_table->setItem(i, ColOutput, new QTableWidgetItem(""));
|
||||
m_table->setItem(i, ColOutput, new QTableWidgetItem(QString{}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ReencodeDialog::onPresetChanged(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_presets.size()) return;
|
||||
if (index < 0 || index >= m_presets.size())
|
||||
return;
|
||||
m_presetDesc->setText(m_presets[index].description);
|
||||
}
|
||||
|
||||
@@ -142,9 +149,10 @@ void ReencodeDialog::onBrowseOutput()
|
||||
if (startDir.isEmpty() && !m_tracks.isEmpty())
|
||||
startDir = QFileInfo(m_tracks.first().filePath).absolutePath();
|
||||
|
||||
QString dir = QFileDialog::getExistingDirectory(
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this, "Select Output Folder", startDir,
|
||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
|
||||
|
||||
if (!dir.isEmpty())
|
||||
m_outputDir->setText(dir);
|
||||
}
|
||||
@@ -158,16 +166,25 @@ void ReencodeDialog::onStartStop()
|
||||
return;
|
||||
}
|
||||
|
||||
int presetIdx = m_presetCombo->currentIndex();
|
||||
if (presetIdx < 0) return;
|
||||
if (m_tracks.isEmpty())
|
||||
return;
|
||||
|
||||
const int presetIdx = m_presetCombo->currentIndex();
|
||||
if (presetIdx < 0 || presetIdx >= m_presets.size())
|
||||
return;
|
||||
|
||||
const engine::EncodePreset &preset = m_presets[presetIdx];
|
||||
const QString outDir = m_outputDir->text().trimmed();
|
||||
|
||||
QString outDir = m_outputDir->text().trimmed();
|
||||
|
||||
// reset every row to pending
|
||||
const auto pendingBrush = QApplication::palette().text();
|
||||
for (int i = 0; i < m_tracks.size(); ++i) {
|
||||
m_table->item(i, ColStatus)->setText("Pending");
|
||||
m_table->item(i, ColStatus)->setForeground(QApplication::palette().text());
|
||||
m_table->item(i, ColOutput)->setText("");
|
||||
if (auto *st = m_table->item(i, ColStatus)) {
|
||||
st->setText("Pending");
|
||||
st->setForeground(pendingBrush);
|
||||
}
|
||||
if (auto *op = m_table->item(i, ColOutput))
|
||||
op->setText(QString{});
|
||||
}
|
||||
|
||||
QList<engine::ReencodeJob> jobs;
|
||||
@@ -185,7 +202,7 @@ void ReencodeDialog::onStartStop()
|
||||
m_reencoder->setJobs(jobs);
|
||||
m_doneCount = 0;
|
||||
m_totalCount = jobs.size();
|
||||
m_overallBar->setRange(0, m_totalCount);
|
||||
m_overallBar->setRange(0, static_cast<int>(m_totalCount));
|
||||
m_overallBar->setValue(0);
|
||||
|
||||
setRunning(true);
|
||||
@@ -194,14 +211,18 @@ void ReencodeDialog::onStartStop()
|
||||
|
||||
void ReencodeDialog::onTrackStarted(int index, const QString &title)
|
||||
{
|
||||
m_statusLabel->setText(QString("Encoding %1/%2")
|
||||
.arg(index + 1).arg(m_totalCount));
|
||||
m_statusLabel->setText(
|
||||
QString("Encoding %1/%2 — %3")
|
||||
.arg(index + 1)
|
||||
.arg(m_totalCount)
|
||||
.arg(title));
|
||||
|
||||
if (QTableWidgetItem *it = m_table->item(index, ColStatus)) {
|
||||
it->setText("Encoding...");
|
||||
}
|
||||
m_table->scrollToItem(m_table->item(index, ColTitle));
|
||||
Q_UNUSED(title)
|
||||
if (auto *st = m_table->item(index, ColStatus))
|
||||
st->setText("Encoding…");
|
||||
|
||||
// returns nullptr, gurad this shit
|
||||
if (auto *cell = m_table->item(index, ColTitle))
|
||||
m_table->scrollToItem(cell);
|
||||
}
|
||||
|
||||
void ReencodeDialog::onTrackFinished(int index, bool ok,
|
||||
@@ -211,15 +232,17 @@ void ReencodeDialog::onTrackFinished(int index, bool ok,
|
||||
++m_doneCount;
|
||||
updateOverallProgress();
|
||||
|
||||
if (QTableWidgetItem *st = m_table->item(index, ColStatus)) {
|
||||
if (auto *st = m_table->item(index, ColStatus)) {
|
||||
if (ok) {
|
||||
st->setText("Done");
|
||||
} else {
|
||||
st->setText(QString("Failed: %1").arg(err));
|
||||
st->setForeground(Qt::red);
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
if (QTableWidgetItem *op = m_table->item(index, ColOutput))
|
||||
if (auto *op = m_table->item(index, ColOutput))
|
||||
op->setText(QFileInfo(outPath).fileName());
|
||||
}
|
||||
}
|
||||
@@ -227,35 +250,39 @@ void ReencodeDialog::onTrackFinished(int index, bool ok,
|
||||
void ReencodeDialog::onAllFinished(int succeeded, int failed)
|
||||
{
|
||||
setRunning(false);
|
||||
m_doneCount = m_totalCount;
|
||||
updateOverallProgress();
|
||||
m_statusLabel->setText(
|
||||
QString("Done - %1 succeeded, %2 failed").arg(succeeded).arg(failed));
|
||||
m_overallBar->setValue(m_totalCount);
|
||||
QString("Done — %1 succeeded, %2 failed").arg(succeeded).arg(failed));
|
||||
|
||||
if (failed == 0) {
|
||||
QMessageBox::information(this, "Reencode Complete",
|
||||
QString("All %1 tracks encoded successfully.").arg(succeeded));
|
||||
} else {
|
||||
QMessageBox::warning(this, "Reencode Complete",
|
||||
QString("%1 tracks succeeded, %2 failed.\n"
|
||||
QString("%1 track(s) succeeded, %2 failed.\n"
|
||||
"Check the Status column for details.")
|
||||
.arg(succeeded).arg(failed));
|
||||
.arg(succeeded).arg(failed));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ReencodeDialog::setRunning(bool running)
|
||||
{
|
||||
m_presetCombo->setEnabled(!running);
|
||||
m_outputDir->setEnabled(!running);
|
||||
m_browseBtn->setEnabled(!running);
|
||||
m_closeBtn->setEnabled(!running);
|
||||
m_startStopBtn->setText(running ? "Stop" : "Start Encoding");
|
||||
m_startStopBtn->setEnabled(true);
|
||||
m_closeBtn->setEnabled(!running);
|
||||
|
||||
if (running)
|
||||
m_statusLabel->setText("Starting…");
|
||||
}
|
||||
|
||||
void ReencodeDialog::updateOverallProgress()
|
||||
{
|
||||
m_overallBar->setValue(m_doneCount);
|
||||
m_overallBar->setFormat(
|
||||
QString("%1 / %2").arg(m_doneCount).arg(m_totalCount));
|
||||
const int done = static_cast<int>(m_doneCount);
|
||||
const int total = static_cast<int>(m_totalCount);
|
||||
m_overallBar->setValue(done);
|
||||
m_overallBar->setFormat(QString("%1 / %2").arg(done).arg(total));
|
||||
}
|
||||
+22
-17
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
#include "engine/Track.h"
|
||||
#include "engine/encoder/ReencodeJob.h"
|
||||
|
||||
@@ -12,20 +14,23 @@ class QLabel;
|
||||
class QProgressBar;
|
||||
|
||||
namespace engine { class AlbumReencoder; }
|
||||
class ReencodeDialog : public QDialog {
|
||||
|
||||
class ReencodeDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ReencodeDialog(const QList<engine::Track> &tracks,
|
||||
QWidget *parent = nullptr);
|
||||
~ReencodeDialog();
|
||||
~ReencodeDialog() override;
|
||||
|
||||
private slots:
|
||||
void onPresetChanged(int index);
|
||||
void onBrowseOutput();
|
||||
void onStartStop();
|
||||
void onTrackStarted (int index, const QString &title);
|
||||
void onTrackFinished(int index, bool ok, const QString &outPath,
|
||||
const QString &err);
|
||||
void onTrackFinished(int index, bool ok,
|
||||
const QString &outPath, const QString &err);
|
||||
void onAllFinished (int succeeded, int failed);
|
||||
|
||||
private:
|
||||
@@ -34,21 +39,21 @@ private:
|
||||
void setRunning(bool running);
|
||||
void updateOverallProgress();
|
||||
|
||||
QList<engine::Track> m_tracks;
|
||||
QList<engine::EncodePreset> m_presets;
|
||||
QList<engine::Track> m_tracks;
|
||||
QList<engine::EncodePreset> m_presets;
|
||||
|
||||
QComboBox *m_presetCombo { nullptr };
|
||||
QLabel *m_presetDesc { nullptr };
|
||||
QLineEdit *m_outputDir { nullptr };
|
||||
QPushButton *m_browseBtn { nullptr };
|
||||
QTableWidget *m_table { nullptr };
|
||||
QProgressBar *m_overallBar { nullptr };
|
||||
QLabel *m_statusLabel { nullptr };
|
||||
QPushButton *m_startStopBtn { nullptr };
|
||||
QPushButton *m_closeBtn { nullptr };
|
||||
QComboBox *m_presetCombo { nullptr };
|
||||
QLabel *m_presetDesc { nullptr };
|
||||
QLineEdit *m_outputDir { nullptr };
|
||||
QPushButton *m_browseBtn { nullptr };
|
||||
QTableWidget *m_table { nullptr };
|
||||
QProgressBar *m_overallBar { nullptr };
|
||||
QLabel *m_statusLabel { nullptr };
|
||||
QPushButton *m_startStopBtn { nullptr };
|
||||
QPushButton *m_closeBtn { nullptr };
|
||||
|
||||
engine::AlbumReencoder *m_reencoder { nullptr };
|
||||
|
||||
int m_doneCount { 0 };
|
||||
int m_totalCount { 0 };
|
||||
qsizetype m_doneCount { 0 };
|
||||
qsizetype m_totalCount { 0 };
|
||||
};
|
||||
+194
-97
@@ -1,5 +1,7 @@
|
||||
// SpectrogramWidget.cpp
|
||||
#include "SpectrogramWidget.h"
|
||||
#include "GradientDialog.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QFile>
|
||||
@@ -11,6 +13,10 @@
|
||||
#include <QStandardPaths>
|
||||
#include <cmath>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Construction / destruction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SpectrogramWidget::SpectrogramWidget(QWidget *parent)
|
||||
: QOpenGLWidget(parent)
|
||||
{
|
||||
@@ -28,6 +34,8 @@ SpectrogramWidget::SpectrogramWidget(QWidget *parent)
|
||||
|
||||
loadBarColors();
|
||||
|
||||
// Qt won't invoke paintGL until after initializeGL completes,
|
||||
// so starting the timer here is safe even before the context exists.
|
||||
auto *timer = new QTimer(this);
|
||||
connect(timer, &QTimer::timeout,
|
||||
this, QOverload<>::of(&SpectrogramWidget::update));
|
||||
@@ -38,17 +46,27 @@ SpectrogramWidget::SpectrogramWidget(QWidget *parent)
|
||||
|
||||
SpectrogramWidget::~SpectrogramWidget()
|
||||
{
|
||||
// FIX #1: if GL context creation failed (e.g. WSL / no GPU),
|
||||
// initializeOpenGLFunctions() was never called, so glDelete* function
|
||||
// pointers are null. Guard on m_glReady to avoid calling through them.
|
||||
if (!m_glReady)
|
||||
return;
|
||||
|
||||
makeCurrent();
|
||||
glDeleteTextures(1, &m_heatmapTex);
|
||||
glDeleteVertexArrays(1, &m_vaoBars);
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public slots
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::receiveFft(const QVector<float> &mag)
|
||||
{
|
||||
QMutexLocker lk(&m_latestMtx);
|
||||
m_latestMag = mag;
|
||||
m_newMagAvailable = true;
|
||||
m_latestMag = mag;
|
||||
m_newMagAvailable = true;
|
||||
}
|
||||
|
||||
void SpectrogramWidget::reset()
|
||||
@@ -58,23 +76,47 @@ void SpectrogramWidget::reset()
|
||||
m_latestMag.clear();
|
||||
m_newMagAvailable = false;
|
||||
}
|
||||
// Everything below is main-thread only — no additional lock needed.
|
||||
m_currentMag.fill(0.f);
|
||||
m_barSmoothed.fill(0.f);
|
||||
m_barPeak.fill(0.f);
|
||||
m_barSmoothed .fill(0.f);
|
||||
m_barPeak .fill(0.f);
|
||||
m_peakHoldTimer.fill(0.f);
|
||||
m_dirty = false;
|
||||
m_barHeights .fill(0.f);
|
||||
m_barPeaksVec .fill(0.f);
|
||||
update();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GL lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
// FIX #1 (cont): initializeOpenGLFunctions resolves all GL 3.3 function
|
||||
// pointers. If it returns false (no suitable context — common in WSL
|
||||
// without a real GPU), leave m_glReady false and bail out.
|
||||
// Every subsequent GL call site checks m_glReady first.
|
||||
if (!initializeOpenGLFunctions()) {
|
||||
qWarning() << "[Spectrogram] OpenGL 3.3 Core not available — "
|
||||
"rendering disabled (WSL/no GPU?)";
|
||||
return;
|
||||
}
|
||||
|
||||
initShaders();
|
||||
updateHeatmapTexture();
|
||||
|
||||
// FIX #5: only proceed if shaders actually linked.
|
||||
// initShaders() already printed a warning on failure.
|
||||
if (!m_barsProg.isLinked()) {
|
||||
qWarning() << "[Spectrogram] Shader program failed — rendering disabled";
|
||||
return;
|
||||
}
|
||||
|
||||
glGenVertexArrays(1, &m_vaoBars);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
buildHeatmapTexture();
|
||||
|
||||
// Pre-compute the logarithmic frequency-to-bin mapping.
|
||||
const int bins = engine::SpectrumAnalyzer::BIN_COUNT;
|
||||
const double logMin = std::log10(20.0);
|
||||
const double logMax = std::log10(22050.0);
|
||||
@@ -83,30 +125,100 @@ void SpectrogramWidget::initializeGL()
|
||||
for (int b = 0; b < BAR_COUNT; ++b) {
|
||||
double f1 = std::pow(10.0, logMin + range * b / BAR_COUNT);
|
||||
double f2 = std::pow(10.0, logMin + range * (b + 1) / BAR_COUNT);
|
||||
m_binMap[b].lo = std::clamp(int(f1 / 22050.0 * bins), 0, bins - 1);
|
||||
m_binMap[b].hi = std::clamp(int(f2 / 22050.0 * bins), m_binMap[b].lo, bins - 1);
|
||||
int lo = std::clamp(int(f1 / 22050.0 * bins), 0, bins - 1);
|
||||
int hi = std::clamp(int(f2 / 22050.0 * bins), lo, bins - 1);
|
||||
m_binMap[b] = { lo, hi };
|
||||
}
|
||||
|
||||
m_glReady = true; // all setup succeeded
|
||||
}
|
||||
|
||||
void SpectrogramWidget::resizeGL(int w, int h)
|
||||
{
|
||||
// Viewport is managed by QOpenGLWidget automatically.
|
||||
// Override present so Qt's virtual dispatch is explicit and future
|
||||
// per-frame uniform updates (e.g. aspect ratio) have a home.
|
||||
Q_UNUSED(w) Q_UNUSED(h)
|
||||
}
|
||||
|
||||
void SpectrogramWidget::paintGL()
|
||||
{
|
||||
// FIX #3: GL functions are unresolved if context creation failed.
|
||||
// Without this guard, glClear → null pointer → crash.
|
||||
if (!m_glReady)
|
||||
return;
|
||||
|
||||
const float dt = static_cast<float>(m_frameTimer.restart()) / 1000.f;
|
||||
|
||||
// Swap in any new FFT data from the audio thread.
|
||||
{
|
||||
QMutexLocker lk(&m_latestMtx);
|
||||
if (m_newMagAvailable) {
|
||||
m_currentMag = std::move(m_latestMag);
|
||||
m_latestMag = {};
|
||||
m_newMagAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Always run computeBars even with no new data — peaks decay and
|
||||
// smoothing converges continuously via the dt parameter.
|
||||
computeBars(dt);
|
||||
|
||||
const QColor bg = palette().color(QPalette::Window);
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const float barWidth = 2.0f / BAR_COUNT;
|
||||
const float gap = barWidth * 0.2f;
|
||||
const float drawWidth = barWidth - gap;
|
||||
|
||||
m_barsProg.bind();
|
||||
glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .data());
|
||||
glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.data());
|
||||
glUniform1i (m_uBarCount, BAR_COUNT);
|
||||
glUniform1f (m_uBarWidth, drawWidth);
|
||||
glUniform1f (m_uGap, gap);
|
||||
glUniform1f (m_uBottomY, -1.0f);
|
||||
glUniform1f (m_uTopClip, 1.0f);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
|
||||
glUniform1i(m_uHeatmap, 0);
|
||||
glBindVertexArray(m_vaoBars);
|
||||
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT);
|
||||
glBindVertexArray(0);
|
||||
m_barsProg.release();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private: GL helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::initShaders()
|
||||
{
|
||||
auto load = [](const QString &path) -> QByteArray {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning() << "Cannot open shader:" << path;
|
||||
qWarning() << "[Spectrogram] Cannot open shader:" << path;
|
||||
return {};
|
||||
}
|
||||
return f.readAll();
|
||||
};
|
||||
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex,
|
||||
load(":/shaders/bars.vert")))
|
||||
qWarning() << "Bars vert error:" << m_barsProg.log();
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment,
|
||||
load(":/shaders/bars.frag")))
|
||||
qWarning() << "Bars frag error:" << m_barsProg.log();
|
||||
if (!m_barsProg.link())
|
||||
qWarning() << "Bars link error:" << m_barsProg.log();
|
||||
const QByteArray vertSrc = load(":/shaders/bars.vert");
|
||||
const QByteArray fragSrc = load(":/shaders/bars.frag");
|
||||
|
||||
// FIX #5: bail before trying to compile empty source
|
||||
if (vertSrc.isEmpty() || fragSrc.isEmpty())
|
||||
return;
|
||||
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex, vertSrc))
|
||||
qWarning() << "[Spectrogram] Vert error:" << m_barsProg.log();
|
||||
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment, fragSrc))
|
||||
qWarning() << "[Spectrogram] Frag error:" << m_barsProg.log();
|
||||
if (!m_barsProg.link()) {
|
||||
qWarning() << "[Spectrogram] Link error:" << m_barsProg.log();
|
||||
return;
|
||||
}
|
||||
|
||||
m_uBarHeights = m_barsProg.uniformLocation("barHeights");
|
||||
m_uBarPeaks = m_barsProg.uniformLocation("barPeaks");
|
||||
@@ -118,23 +230,27 @@ void SpectrogramWidget::initShaders()
|
||||
m_uHeatmap = m_barsProg.uniformLocation("heatmap");
|
||||
}
|
||||
|
||||
void SpectrogramWidget::updateHeatmapTexture()
|
||||
void SpectrogramWidget::buildHeatmapTexture()
|
||||
{
|
||||
if (m_stops.size() < 2)
|
||||
m_stops = { QColor(0, 0, 0), QColor(255, 255, 255) };
|
||||
|
||||
const int nSegs = m_stops.size() - 1;
|
||||
auto lerp = [](int x, int y, float f) {
|
||||
auto lerpCh = [](int x, int y, float f) {
|
||||
return std::clamp(int(x + (y - x) * f), 0, 255);
|
||||
};
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
float t = float(i) / 255.f * nSegs;
|
||||
int seg = std::min(int(t), nSegs - 1);
|
||||
float f = t - seg;
|
||||
QColor a = m_stops[seg], b = m_stops[seg + 1];
|
||||
m_heatmap[i] = qRgb(lerp(a.red(), b.red(), f),
|
||||
lerp(a.green(), b.green(), f),
|
||||
lerp(a.blue(), b.blue(), f));
|
||||
float t = float(i) / 255.f * float(nSegs);
|
||||
int seg = std::min(int(t), nSegs - 1);
|
||||
float f = t - float(seg);
|
||||
// FIX #8: renamed locals to ca/cb to avoid shadowing the
|
||||
// outer QColor variable `b` from the original code
|
||||
const QColor &ca = m_stops[seg];
|
||||
const QColor &cb = m_stops[seg + 1];
|
||||
m_heatmap[i] = qRgb(lerpCh(ca.red(), cb.red(), f),
|
||||
lerpCh(ca.green(), cb.green(), f),
|
||||
lerpCh(ca.blue(), cb.blue(), f));
|
||||
}
|
||||
|
||||
std::array<GLubyte, 256 * 3> td;
|
||||
@@ -157,30 +273,42 @@ void SpectrogramWidget::updateHeatmapTexture()
|
||||
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private: gradient persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::setStops(const QVector<QColor> &stops)
|
||||
{
|
||||
if (stops.size() < 2) return;
|
||||
if (stops.size() < 2)
|
||||
return;
|
||||
m_stops = stops;
|
||||
makeCurrent();
|
||||
updateHeatmapTexture();
|
||||
doneCurrent();
|
||||
|
||||
if (m_glReady) {
|
||||
// FIX #2: makeCurrent() can return false (no valid context).
|
||||
// Only call GL functions if it succeeded.
|
||||
makeCurrent();
|
||||
buildHeatmapTexture();
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
update();
|
||||
saveBarColors();
|
||||
}
|
||||
|
||||
void SpectrogramWidget::loadBarColors()
|
||||
{
|
||||
QString path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
|
||||
+ "/.subwave/barcolors.json";
|
||||
const QString path =
|
||||
QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
|
||||
+ "/.subwave/barcolors.json";
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) return;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||
if (!doc.isArray()) return;
|
||||
|
||||
QVector<QColor> loaded;
|
||||
for (const auto &v : doc.array()) {
|
||||
QJsonObject obj = v.toObject();
|
||||
for (const QJsonValue &v : doc.array()) {
|
||||
const QJsonObject obj = v.toObject();
|
||||
loaded.append(QColor(obj["r"].toInt(), obj["g"].toInt(), obj["b"].toInt()));
|
||||
}
|
||||
if (loaded.size() >= 2)
|
||||
@@ -189,10 +317,12 @@ void SpectrogramWidget::loadBarColors()
|
||||
|
||||
void SpectrogramWidget::saveBarColors()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
|
||||
+ "/.subwave";
|
||||
QDir().mkpath(dir);
|
||||
QFile f(dir + "/barcolors.json");
|
||||
const QString dirPath =
|
||||
QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
|
||||
+ "/.subwave";
|
||||
QDir().mkpath(dirPath);
|
||||
|
||||
QFile f(dirPath + "/barcolors.json");
|
||||
if (!f.open(QIODevice::WriteOnly)) return;
|
||||
|
||||
QJsonArray arr;
|
||||
@@ -206,21 +336,16 @@ void SpectrogramWidget::saveBarColors()
|
||||
f.write(QJsonDocument(arr).toJson());
|
||||
}
|
||||
|
||||
|
||||
void SpectrogramWidget::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
GradientDialog dlg(m_stops, this);
|
||||
if (dlg.exec() == QDialog::Accepted)
|
||||
setStops(dlg.stops());
|
||||
}
|
||||
QOpenGLWidget::mousePressEvent(e);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private: bar computation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::computeBars(float dt)
|
||||
{
|
||||
// FIX #4: fftLen could be 0 if m_currentMag was somehow cleared;
|
||||
// fftLen - 1 would wrap to INT_MAX as unsigned, or be -1 as signed.
|
||||
const int fftLen = m_currentMag.size();
|
||||
if (fftLen == 0) return;
|
||||
|
||||
for (int b = 0; b < BAR_COUNT; ++b) {
|
||||
const int lo = m_binMap[b].lo;
|
||||
@@ -228,14 +353,16 @@ void SpectrogramWidget::computeBars(float dt)
|
||||
|
||||
float sum = 0.f;
|
||||
int cnt = 0;
|
||||
for (int k = lo; k <= hi; ++k) { sum += m_currentMag[k]; ++cnt; }
|
||||
float raw = cnt > 0 ? sum / cnt : 0.f;
|
||||
for (int k = lo; k <= hi; ++k) {
|
||||
sum += m_currentMag[k];
|
||||
++cnt;
|
||||
}
|
||||
const float raw = cnt > 0 ? sum / float(cnt) : 0.f;
|
||||
const float db = raw > 0.f ? 20.f * std::log10(raw) : DB_MIN;
|
||||
const float bar = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f);
|
||||
|
||||
float db = raw > 0.f ? 20.f * std::log10(raw) : DB_MIN;
|
||||
float bar = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f);
|
||||
|
||||
const float coeff = bar > m_barSmoothed[b] ? 0.50f : 0.18f;
|
||||
m_barSmoothed[b] += (bar - m_barSmoothed[b]) * coeff;
|
||||
const float coeff = bar > m_barSmoothed[b] ? 0.50f : 0.18f;
|
||||
m_barSmoothed[b] += (bar - m_barSmoothed[b]) * coeff;
|
||||
|
||||
if (m_barSmoothed[b] >= m_barPeak[b]) {
|
||||
m_barPeak[b] = m_barSmoothed[b];
|
||||
@@ -251,46 +378,16 @@ void SpectrogramWidget::computeBars(float dt)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mouse
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SpectrogramWidget::paintGL()
|
||||
void SpectrogramWidget::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
const float dt = static_cast<float>(m_frameTimer.restart()) / 1000.f;
|
||||
|
||||
{
|
||||
QMutexLocker lk(&m_latestMtx);
|
||||
if (m_newMagAvailable) {
|
||||
m_currentMag = std::move(m_latestMag);
|
||||
m_latestMag = {};
|
||||
m_newMagAvailable = false;
|
||||
m_dirty = true;
|
||||
}
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
GradientDialog dlg(m_stops, this);
|
||||
if (dlg.exec() == QDialog::Accepted)
|
||||
setStops(dlg.stops());
|
||||
}
|
||||
|
||||
computeBars(dt);
|
||||
|
||||
QColor bg = palette().color(QPalette::Window);
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (!m_barsProg.isLinked()) return;
|
||||
|
||||
const float barWidth = 2.0f / BAR_COUNT;
|
||||
const float gap = barWidth * 0.2f;
|
||||
const float drawWidth = barWidth - gap;
|
||||
|
||||
m_barsProg.bind();
|
||||
glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .data());
|
||||
glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.data());
|
||||
glUniform1i (m_uBarCount, BAR_COUNT);
|
||||
glUniform1f (m_uBarWidth, drawWidth);
|
||||
glUniform1f (m_uGap, gap);
|
||||
glUniform1f (m_uBottomY, -1.0f);
|
||||
glUniform1f (m_uTopClip, 1.0f);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
|
||||
glUniform1i(m_uHeatmap, 0);
|
||||
glBindVertexArray(m_vaoBars);
|
||||
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT);
|
||||
glBindVertexArray(0);
|
||||
m_barsProg.release();
|
||||
QOpenGLWidget::mousePressEvent(e);
|
||||
}
|
||||
+38
-41
@@ -1,25 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLFunctions_3_3_Core>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMutex>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <QColor>
|
||||
#include <QVector>
|
||||
#include "SpectrumAnalyzer.h"
|
||||
|
||||
class SpectrogramWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
|
||||
class SpectrogramWidget : public QOpenGLWidget,
|
||||
protected QOpenGLFunctions_3_3_Core
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static constexpr int BAR_COUNT = 96;
|
||||
static constexpr float DB_MIN = -80.f;
|
||||
static constexpr float DB_MAX = 0.f;
|
||||
|
||||
static constexpr float PEAK_HOLD_SEC = 0.4f;
|
||||
static constexpr float PEAK_DECAY_SEC = 0.5f;
|
||||
static constexpr int BAR_COUNT = 96;
|
||||
static constexpr float DB_MIN = -80.f;
|
||||
static constexpr float DB_MAX = 0.f;
|
||||
static constexpr float PEAK_HOLD_SEC = 0.4f;
|
||||
static constexpr float PEAK_DECAY_SEC = 0.5f;
|
||||
|
||||
public:
|
||||
explicit SpectrogramWidget(QWidget *parent = nullptr);
|
||||
@@ -30,60 +30,57 @@ public slots:
|
||||
void reset();
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
void paintGL() override;
|
||||
void initializeGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void paintGL() override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
// GL setup
|
||||
void initShaders();
|
||||
void updateHeatmapTexture();
|
||||
void buildHeatmapTexture();
|
||||
|
||||
// Data
|
||||
void setStops(const QVector<QColor> &stops);
|
||||
void loadBarColors();
|
||||
void saveBarColors();
|
||||
|
||||
void computeBars(float dt);
|
||||
|
||||
QMutex m_latestMtx;
|
||||
QVector<float> m_latestMag; // written by audio thread
|
||||
bool m_newMagAvailable{false};
|
||||
QVector<float> m_latestMag;
|
||||
bool m_newMagAvailable { false };
|
||||
|
||||
QVector<float> m_currentMag =
|
||||
QVector<float>(engine::SpectrumAnalyzer::BIN_COUNT, 0.f);
|
||||
|
||||
// --- bar state ---
|
||||
std::array<float, BAR_COUNT> m_barSmoothed{};
|
||||
std::array<float, BAR_COUNT> m_barPeak{};
|
||||
std::array<float, BAR_COUNT> m_peakHoldTimer{}; // seconds remaining in hold
|
||||
|
||||
// Uploaded to GPU each frame
|
||||
std::array<float, BAR_COUNT> m_barHeights{};
|
||||
std::array<float, BAR_COUNT> m_barPeaksVec{};
|
||||
std::array<float, BAR_COUNT> m_barSmoothed {};
|
||||
std::array<float, BAR_COUNT> m_barPeak {};
|
||||
std::array<float, BAR_COUNT> m_peakHoldTimer {};
|
||||
std::array<float, BAR_COUNT> m_barHeights {};
|
||||
std::array<float, BAR_COUNT> m_barPeaksVec {};
|
||||
|
||||
struct BinRange { int lo, hi; };
|
||||
std::array<BinRange, BAR_COUNT> m_binMap{};
|
||||
std::array<BinRange, BAR_COUNT> m_binMap {};
|
||||
|
||||
QVector<QColor> m_stops = { QColor("#078D70"),
|
||||
QColor("#26CEAA"),
|
||||
QColor("#98E8C1"),
|
||||
QColor("#FFFFFF") };
|
||||
|
||||
std::array<QRgb, 256> m_heatmap{};
|
||||
QVector<QColor> m_stops = {
|
||||
QColor("#078D70"), QColor("#26CEAA"),
|
||||
QColor("#98E8C1"), QColor("#FFFFFF")
|
||||
};
|
||||
std::array<QRgb, 256> m_heatmap {};
|
||||
|
||||
QElapsedTimer m_frameTimer;
|
||||
bool m_dirty{false}; // true when new data arrived since last paint
|
||||
|
||||
bool m_glReady { false };
|
||||
|
||||
QOpenGLShaderProgram m_barsProg;
|
||||
GLuint m_heatmapTex = 0;
|
||||
GLuint m_vaoBars = 0;
|
||||
GLuint m_heatmapTex { 0 };
|
||||
GLuint m_vaoBars { 0 };
|
||||
|
||||
int m_uBarHeights = -1;
|
||||
int m_uBarPeaks = -1;
|
||||
int m_uBarCount = -1;
|
||||
int m_uBarWidth = -1;
|
||||
int m_uGap = -1;
|
||||
int m_uBottomY = -1;
|
||||
int m_uTopClip = -1;
|
||||
int m_uHeatmap = -1;
|
||||
int m_uBarHeights { -1 };
|
||||
int m_uBarPeaks { -1 };
|
||||
int m_uBarCount { -1 };
|
||||
int m_uBarWidth { -1 };
|
||||
int m_uGap { -1 };
|
||||
int m_uBottomY { -1 };
|
||||
int m_uTopClip { -1 };
|
||||
int m_uHeatmap { -1 };
|
||||
};
|
||||
Reference in New Issue
Block a user