This commit is contained in:
Carl Philipp Klemm 2026-03-06 00:06:09 +01:00
parent 10769eef09
commit 58e8345542
20 changed files with 2063 additions and 1901 deletions

View file

@ -55,3 +55,13 @@ target_include_directories(${PROJECT_NAME} PRIVATE
) )
# Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime # Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime
# Install targets
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
# Install .desktop file
install(FILES aceradio.desktop DESTINATION share/applications)
# Install icon files
install(FILES res/xyz.uvos.aceradio.png DESTINATION share/icons/hicolor/256x256/apps RENAME xyz.uvos.aceradio.png)
install(FILES res/xyz.uvos.aceradio.svg DESTINATION share/icons/hicolor/scalable/apps RENAME xyz.uvos.aceradio.svg)

View file

@ -44,11 +44,13 @@ void AceStepWorker::cancelGeneration()
bool AceStepWorker::songGenerateing(SongItem* song) bool AceStepWorker::songGenerateing(SongItem* song)
{ {
workerMutex.lock(); workerMutex.lock();
if(!currentWorker) { if(!currentWorker)
{
workerMutex.unlock(); workerMutex.unlock();
return false; return false;
} }
else { else
{
SongItem workerSong = currentWorker->getSong(); SongItem workerSong = currentWorker->getSong();
workerMutex.unlock(); workerMutex.unlock();
if(song) if(song)
@ -68,7 +70,8 @@ void AceStepWorker::Worker::run()
// Parse and modify the template // Parse and modify the template
QJsonParseError parseError; QJsonParseError parseError;
QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError); QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError);
if (!templateDoc.isObject()) { if (!templateDoc.isObject())
{
emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString())); emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString()));
return; return;
} }
@ -76,21 +79,26 @@ void AceStepWorker::Worker::run()
QJsonObject requestObj = templateDoc.object(); QJsonObject requestObj = templateDoc.object();
requestObj["caption"] = song.caption; requestObj["caption"] = song.caption;
if (!song.lyrics.isEmpty()) { if (!song.lyrics.isEmpty())
{
requestObj["lyrics"] = song.lyrics; requestObj["lyrics"] = song.lyrics;
} else { }
else
{
// Remove lyrics field if empty to let the LLM generate them // Remove lyrics field if empty to let the LLM generate them
requestObj.remove("lyrics"); requestObj.remove("lyrics");
} }
// Apply vocal language override if set // Apply vocal language override if set
if (!song.vocalLanguage.isEmpty()) { if (!song.vocalLanguage.isEmpty())
{
requestObj["vocal_language"] = song.vocalLanguage; requestObj["vocal_language"] = song.vocalLanguage;
} }
// Write the request file // Write the request file
QFile requestFileHandle(requestFile); QFile requestFileHandle(requestFile);
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text))
{
emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString()); emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString());
return; return;
} }
@ -106,12 +114,14 @@ void AceStepWorker::Worker::run()
QFileInfo qwen3Info(qwen3Binary); QFileInfo qwen3Info(qwen3Binary);
QFileInfo ditVaeInfo(ditVaeBinary); QFileInfo ditVaeInfo(ditVaeBinary);
if (!qwen3Info.exists() || !qwen3Info.isExecutable()) { if (!qwen3Info.exists() || !qwen3Info.isExecutable())
{
emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary); emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary);
return; return;
} }
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) { if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable())
{
emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary); emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary);
return; return;
} }
@ -122,22 +132,26 @@ void AceStepWorker::Worker::run()
QString ditModel = this->ditModelPath; QString ditModel = this->ditModelPath;
QString vaeModel = this->vaeModelPath; QString vaeModel = this->vaeModelPath;
if (!QFileInfo::exists(qwen3Model)) { if (!QFileInfo::exists(qwen3Model))
{
emit parent->generationError("Qwen3 model not found: " + qwen3Model); emit parent->generationError("Qwen3 model not found: " + qwen3Model);
return; return;
} }
if (!QFileInfo::exists(textEncoderModel)) { if (!QFileInfo::exists(textEncoderModel))
{
emit parent->generationError("Text encoder model not found: " + textEncoderModel); emit parent->generationError("Text encoder model not found: " + textEncoderModel);
return; return;
} }
if (!QFileInfo::exists(ditModel)) { if (!QFileInfo::exists(ditModel))
{
emit parent->generationError("DiT model not found: " + ditModel); emit parent->generationError("DiT model not found: " + ditModel);
return; return;
} }
if (!QFileInfo::exists(vaeModel)) { if (!QFileInfo::exists(vaeModel))
{
emit parent->generationError("VAE model not found: " + vaeModel); emit parent->generationError("VAE model not found: " + vaeModel);
return; return;
} }
@ -151,12 +165,14 @@ void AceStepWorker::Worker::run()
emit parent->progressUpdate(20); emit parent->progressUpdate(20);
qwen3Process.start(qwen3Binary, qwen3Args); qwen3Process.start(qwen3Binary, qwen3Args);
if (!qwen3Process.waitForStarted()) { if (!qwen3Process.waitForStarted())
{
emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString()); emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString());
return; return;
} }
if (!qwen3Process.waitForFinished(60000)) { // 60 second timeout if (!qwen3Process.waitForFinished(60000)) // 60 second timeout
{
qwen3Process.terminate(); qwen3Process.terminate();
qwen3Process.waitForFinished(5000); qwen3Process.waitForFinished(5000);
emit parent->generationError("ace-qwen3 timed out after 60 seconds"); emit parent->generationError("ace-qwen3 timed out after 60 seconds");
@ -164,29 +180,34 @@ void AceStepWorker::Worker::run()
} }
int exitCode = qwen3Process.exitCode(); int exitCode = qwen3Process.exitCode();
if (exitCode != 0) { if (exitCode != 0)
{
QString errorOutput = qwen3Process.readAllStandardError(); QString errorOutput = qwen3Process.readAllStandardError();
emit parent->generationError("ace-qwen3 exited with code " + QString::number(exitCode) + ": " + errorOutput); emit parent->generationError("ace-qwen3 exited with code " + QString::number(exitCode) + ": " + errorOutput);
return; return;
} }
QString requestLmOutputFile = tempDir + "/request_" + QString::number(uid) + "0.json"; QString requestLmOutputFile = tempDir + "/request_" + QString::number(uid) + "0.json";
if (!QFileInfo::exists(requestLmOutputFile)) { if (!QFileInfo::exists(requestLmOutputFile))
{
emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile); emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile);
return; return;
} }
// Load lyrics from the enhanced request file // Load lyrics from the enhanced request file
QFile lmOutputFile(requestLmOutputFile); QFile lmOutputFile(requestLmOutputFile);
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) { if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text))
{
QJsonParseError parseError; QJsonParseError parseError;
song.json = lmOutputFile.readAll(); song.json = lmOutputFile.readAll();
QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError); QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError);
lmOutputFile.close(); lmOutputFile.close();
if (doc.isObject() && !parseError.error) { if (doc.isObject() && !parseError.error)
{
QJsonObject obj = doc.object(); QJsonObject obj = doc.object();
if (obj.contains("lyrics") && obj["lyrics"].isString()) { if (obj.contains("lyrics") && obj["lyrics"].isString())
{
song.lyrics = obj["lyrics"].toString(); song.lyrics = obj["lyrics"].toString();
} }
} }
@ -205,12 +226,14 @@ void AceStepWorker::Worker::run()
emit parent->progressUpdate(60); emit parent->progressUpdate(60);
ditVaeProcess.start(ditVaeBinary, ditVaeArgs); ditVaeProcess.start(ditVaeBinary, ditVaeArgs);
if (!ditVaeProcess.waitForStarted()) { if (!ditVaeProcess.waitForStarted())
{
emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString()); emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString());
return; return;
} }
if (!ditVaeProcess.waitForFinished(120000)) { // 2 minute timeout if (!ditVaeProcess.waitForFinished(120000)) // 2 minute timeout
{
ditVaeProcess.terminate(); ditVaeProcess.terminate();
ditVaeProcess.waitForFinished(5000); ditVaeProcess.waitForFinished(5000);
emit parent->generationError("dit-vae timed out after 2 minutes"); emit parent->generationError("dit-vae timed out after 2 minutes");
@ -218,7 +241,8 @@ void AceStepWorker::Worker::run()
} }
exitCode = ditVaeProcess.exitCode(); exitCode = ditVaeProcess.exitCode();
if (exitCode != 0) { if (exitCode != 0)
{
QString errorOutput = ditVaeProcess.readAllStandardError(); QString errorOutput = ditVaeProcess.readAllStandardError();
emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput); emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput);
return; return;
@ -228,7 +252,8 @@ void AceStepWorker::Worker::run()
// Find the generated WAV file // Find the generated WAV file
QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav"; QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav";
if (!QFileInfo::exists(wavFile)) { if (!QFileInfo::exists(wavFile))
{
emit parent->generationError("No WAV file generated at "+wavFile); emit parent->generationError("No WAV file generated at "+wavFile);
return; return;
} }

View file

@ -31,7 +31,8 @@ signals:
void progressUpdate(int percent); void progressUpdate(int percent);
private: private:
class Worker : public QRunnable { class Worker : public QRunnable
{
public: public:
Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate, Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate,
const QString &aceStepPath, const QString &qwen3ModelPath, const QString &aceStepPath, const QString &qwen3ModelPath,

View file

@ -80,23 +80,28 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path)
void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked() void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
{ {
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text()); QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
if (!dir.isEmpty()) { if (!dir.isEmpty())
{
ui->aceStepPathEdit->setText(dir); ui->aceStepPathEdit->setText(dir);
} }
} }
void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked() void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
{ {
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), "GGUF Files (*.gguf)"); QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(),
if (!file.isEmpty()) { "GGUF Files (*.gguf)");
if (!file.isEmpty())
{
ui->qwen3ModelEdit->setText(file); ui->qwen3ModelEdit->setText(file);
} }
} }
void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked() void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
{ {
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(), "GGUF Files (*.gguf)"); QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(),
if (!file.isEmpty()) { "GGUF Files (*.gguf)");
if (!file.isEmpty())
{
ui->textEncoderEdit->setText(file); ui->textEncoderEdit->setText(file);
} }
} }
@ -104,7 +109,8 @@ void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
void AdvancedSettingsDialog::on_ditBrowseButton_clicked() void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
{ {
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)"); QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) { if (!file.isEmpty())
{
ui->ditModelEdit->setText(file); ui->ditModelEdit->setText(file);
} }
} }
@ -112,7 +118,8 @@ void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
void AdvancedSettingsDialog::on_vaeBrowseButton_clicked() void AdvancedSettingsDialog::on_vaeBrowseButton_clicked()
{ {
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)"); QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) { if (!file.isEmpty())
{
ui->vaeModelEdit->setText(file); ui->vaeModelEdit->setText(file);
} }
} }

View file

@ -4,7 +4,8 @@
#include <QDialog> #include <QDialog>
#include <QString> #include <QString>
namespace Ui { namespace Ui
{
class AdvancedSettingsDialog; class AdvancedSettingsDialog;
} }

View file

@ -17,8 +17,10 @@ AudioPlayer::AudioPlayer(QObject *parent)
// Set up position timer for updating playback position // Set up position timer for updating playback position
positionTimer->setInterval(500); // Update every 500ms positionTimer->setInterval(500); // Update every 500ms
connect(positionTimer, &QTimer::timeout, [this]() { connect(positionTimer, &QTimer::timeout, [this]()
if (isPlaying()) { {
if (isPlaying())
{
emit positionChanged(mediaPlayer->position()); emit positionChanged(mediaPlayer->position());
} }
}); });
@ -31,7 +33,8 @@ AudioPlayer::~AudioPlayer()
void AudioPlayer::play(const QString &filePath) void AudioPlayer::play(const QString &filePath)
{ {
if (isPlaying()) { if (isPlaying())
{
stop(); stop();
} }
@ -44,7 +47,8 @@ void AudioPlayer::play(const QString &filePath)
void AudioPlayer::play() void AudioPlayer::play()
{ {
if (!isPlaying()) { if (!isPlaying())
{
mediaPlayer->play(); mediaPlayer->play();
positionTimer->start(); positionTimer->start();
} }
@ -52,7 +56,8 @@ void AudioPlayer::play()
void AudioPlayer::pause() void AudioPlayer::pause()
{ {
if (isPlaying()) { if (isPlaying())
{
mediaPlayer->pause(); mediaPlayer->pause();
positionTimer->stop(); positionTimer->stop();
} }
@ -86,12 +91,16 @@ int AudioPlayer::position() const
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state) void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
{ {
if (state == QMediaPlayer::PlayingState) { if (state == QMediaPlayer::PlayingState)
{
emit playbackStarted(); emit playbackStarted();
} else if (state == QMediaPlayer::StoppedState || }
state == QMediaPlayer::PausedState) { else if (state == QMediaPlayer::StoppedState ||
state == QMediaPlayer::PausedState)
{
// Check if we reached the end // Check if we reached the end
if (mediaPlayer->position() >= mediaPlayer->duration() - 100) { if (mediaPlayer->position() >= mediaPlayer->duration() - 100)
{
emit playbackFinished(); emit playbackFinished();
} }
} }
@ -99,16 +108,22 @@ void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status) void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status)
{ {
if (status == QMediaPlayer::EndOfMedia) { if (status == QMediaPlayer::EndOfMedia)
{
emit playbackFinished(); emit playbackFinished();
} else if (status == QMediaPlayer::LoadedMedia || }
status == QMediaPlayer::BufferedMedia) { else if (status == QMediaPlayer::LoadedMedia ||
status == QMediaPlayer::BufferedMedia)
{
// Media loaded successfully, emit duration // Media loaded successfully, emit duration
int duration = mediaPlayer->duration(); int duration = mediaPlayer->duration();
if (duration > 0) { if (duration > 0)
{
emit durationChanged(duration); emit durationChanged(duration);
} }
} else if (status == QMediaPlayer::InvalidMedia) { }
else if (status == QMediaPlayer::InvalidMedia)
{
emit playbackError(mediaPlayer->errorString()); emit playbackError(mediaPlayer->errorString());
} }
} }

View file

@ -45,8 +45,14 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist); connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist); connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong); connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();}); connect(ui->actionQuit, &QAction::triggered, this, [this]()
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this](){songModel->clear();}); {
close();
});
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this]()
{
songModel->clear();
});
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong); connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
@ -59,12 +65,14 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
// Connect audio player error signal // Connect audio player error signal
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) { connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error)
{
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error); QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
}); });
// Add some default songs // Add some default songs
if(songModel->songCount() == 0) { if(songModel->songCount() == 0)
{
SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", ""); SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", "");
SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", ""); SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", "");
SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", ""); SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", "");
@ -75,7 +83,8 @@ MainWindow::MainWindow(QWidget *parent)
} }
// Select first item // Select first item
if (songModel->rowCount() > 0) { if (songModel->rowCount() > 0)
{
QModelIndex firstIndex = songModel->index(0, 0); QModelIndex firstIndex = songModel->index(0, 0);
ui->songListView->setCurrentIndex(firstIndex); ui->songListView->setCurrentIndex(firstIndex);
} }
@ -117,7 +126,8 @@ void MainWindow::loadSettings()
QSettings settings("MusicGenerator", "AceStepGUI"); QSettings settings("MusicGenerator", "AceStepGUI");
// Load JSON template (default to simple configuration) // Load JSON template (default to simple configuration)
jsonTemplate = settings.value("jsonTemplate", "{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString(); jsonTemplate = settings.value("jsonTemplate",
"{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString();
// Load shuffle mode // Load shuffle mode
shuffleMode = settings.value("shuffleMode", false).toBool(); shuffleMode = settings.value("shuffleMode", false).toBool();
@ -126,8 +136,10 @@ void MainWindow::loadSettings()
// Load path settings with defaults based on application directory // Load path settings with defaults based on application directory
QString appDir = QCoreApplication::applicationDirPath(); QString appDir = QCoreApplication::applicationDirPath();
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString(); aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
qwen3ModelPath = settings.value("qwen3ModelPath", appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString(); qwen3ModelPath = settings.value("qwen3ModelPath",
textEncoderModelPath = settings.value("textEncoderModelPath", appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString(); appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
textEncoderModelPath = settings.value("textEncoderModelPath",
appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString();
ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString(); ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString(); vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
} }
@ -197,7 +209,8 @@ void MainWindow::updateControls()
void MainWindow::on_playButton_clicked() void MainWindow::on_playButton_clicked()
{ {
if (isPaused) { if (isPaused)
{
// Resume playback // Resume playback
audioPlayer->play(); audioPlayer->play();
isPaused = false; isPaused = false;
@ -217,7 +230,8 @@ void MainWindow::on_playButton_clicked()
void MainWindow::on_pauseButton_clicked() void MainWindow::on_pauseButton_clicked()
{ {
if (isPlaying && !isPaused) { if (isPlaying && !isPaused)
{
// Pause playback // Pause playback
audioPlayer->pause(); audioPlayer->pause();
isPaused = true; isPaused = true;
@ -227,7 +241,8 @@ void MainWindow::on_pauseButton_clicked()
void MainWindow::on_skipButton_clicked() void MainWindow::on_skipButton_clicked()
{ {
if (isPlaying) { if (isPlaying)
{
audioPlayer->stop(); audioPlayer->stop();
isPaused = false; isPaused = false;
playNextSong(); playNextSong();
@ -236,7 +251,8 @@ void MainWindow::on_skipButton_clicked()
void MainWindow::on_stopButton_clicked() void MainWindow::on_stopButton_clicked()
{ {
if (isPlaying) { if (isPlaying)
{
// Stop current playback completely // Stop current playback completely
audioPlayer->stop(); audioPlayer->stop();
ui->nowPlayingLabel->setText("Now Playing:"); ui->nowPlayingLabel->setText("Now Playing:");
@ -257,7 +273,8 @@ void MainWindow::on_addSongButton_clicked()
{ {
SongDialog dialog(this); SongDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted)
{
QString caption = dialog.getCaption(); QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics(); QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage(); QString vocalLanguage = dialog.getVocalLanguage();
@ -274,7 +291,8 @@ void MainWindow::on_addSongButton_clicked()
void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
{ {
if (!index.isValid()) return; if (!index.isValid())
return;
// Temporarily disconnect the signal to prevent multiple invocations // Temporarily disconnect the signal to prevent multiple invocations
// This happens when the dialog closes and triggers another double-click event // This happens when the dialog closes and triggers another double-click event
@ -283,11 +301,15 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
int row = index.row(); int row = index.row();
// Different behavior based on which column was clicked // Different behavior based on which column was clicked
if (index.column() == 0) { if (index.column() == 0)
{
// Column 0 (play indicator): Stop current playback and play this song // Column 0 (play indicator): Stop current playback and play this song
if (isPlaying) { if (isPlaying)
{
audioPlayer->stop(); audioPlayer->stop();
} else { }
else
{
isPlaying = true; isPlaying = true;
updateControls(); updateControls();
} }
@ -296,13 +318,16 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
flushGenerationQueue(); flushGenerationQueue();
currentSong = songModel->getSong(row); currentSong = songModel->getSong(row);
ensureSongsInQueue(true); ensureSongsInQueue(true);
} else if (index.column() == 1 || index.column() == 2) { }
else if (index.column() == 1 || index.column() == 2)
{
// Column 1 (caption): Edit the song // Column 1 (caption): Edit the song
SongItem song = songModel->getSong(row); SongItem song = songModel->getSong(row);
SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage); SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted)
{
QString caption = dialog.getCaption(); QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics(); QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage(); QString vocalLanguage = dialog.getVocalLanguage();
@ -321,7 +346,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
void MainWindow::on_removeSongButton_clicked() void MainWindow::on_removeSongButton_clicked()
{ {
QModelIndex currentIndex = ui->songListView->currentIndex(); QModelIndex currentIndex = ui->songListView->currentIndex();
if (!currentIndex.isValid()) return; if (!currentIndex.isValid())
return;
// Get the row from the current selection (works with table view) // Get the row from the current selection (works with table view)
int row = currentIndex.row(); int row = currentIndex.row();
@ -330,7 +356,8 @@ void MainWindow::on_removeSongButton_clicked()
// Select next item or previous if at end // Select next item or previous if at end
int newRow = qMin(row, songModel->rowCount() - 1); int newRow = qMin(row, songModel->rowCount() - 1);
if (newRow >= 0) { if (newRow >= 0)
{
QModelIndex newIndex = songModel->index(newRow, 0); QModelIndex newIndex = songModel->index(newRow, 0);
ui->songListView->setCurrentIndex(newIndex); ui->songListView->setCurrentIndex(newIndex);
} }
@ -348,11 +375,13 @@ void MainWindow::on_advancedSettingsButton_clicked()
dialog.setDiTModelPath(ditModelPath); dialog.setDiTModelPath(ditModelPath);
dialog.setVAEModelPath(vaeModelPath); dialog.setVAEModelPath(vaeModelPath);
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted)
{
// Validate JSON template // Validate JSON template
QJsonParseError parseError; QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(dialog.getJsonTemplate().toUtf8(), &parseError); QJsonDocument doc = QJsonDocument::fromJson(dialog.getJsonTemplate().toUtf8(), &parseError);
if (!doc.isObject()) { if (!doc.isObject())
{
QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString())); QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString()));
return; return;
} }
@ -389,10 +418,12 @@ void MainWindow::songGenerated(const SongItem& song)
{ {
isGeneratingNext = false; isGeneratingNext = false;
if (!isPaused && isPlaying && !audioPlayer->isPlaying()) { if (!isPaused && isPlaying && !audioPlayer->isPlaying())
{
playSong(song); playSong(song);
} }
else { else
{
generatedSongQueue.enqueue(song); generatedSongQueue.enqueue(song);
} }
ui->statusLabel->setText("idle"); ui->statusLabel->setText("idle");
@ -406,10 +437,13 @@ void MainWindow::playNextSong()
return; return;
// Check if we have a pre-generated next song in the queue // Check if we have a pre-generated next song in the queue
if (!generatedSongQueue.isEmpty()) { if (!generatedSongQueue.isEmpty())
{
SongItem generatedSong = generatedSongQueue.dequeue(); SongItem generatedSong = generatedSongQueue.dequeue();
playSong(generatedSong); playSong(generatedSong);
} else { }
else
{
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
} }
@ -460,7 +494,8 @@ void MainWindow::updatePlaybackStatus(bool playing)
void MainWindow::on_positionSlider_sliderMoved(int position) void MainWindow::on_positionSlider_sliderMoved(int position)
{ {
if (isPlaying && audioPlayer->isPlaying()) { if (isPlaying && audioPlayer->isPlaying())
{
audioPlayer->setPosition(position); audioPlayer->setPosition(position);
} }
} }
@ -468,7 +503,8 @@ void MainWindow::on_positionSlider_sliderMoved(int position)
void MainWindow::ensureSongsInQueue(bool enqeueCurrent) void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
{ {
// Only generate more songs if we're playing and not already at capacity // Only generate more songs if we're playing and not already at capacity
if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh) { if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh)
{
return; return;
} }
@ -482,10 +518,12 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
lastSong = currentSong; lastSong = currentSong;
SongItem nextSong; SongItem nextSong;
if(enqeueCurrent) { if(enqeueCurrent)
{
nextSong = lastSong; nextSong = lastSong;
} }
else { else
{
int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode); int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode);
nextSong = songModel->getSong(nextIndex); nextSong = songModel->getSong(nextIndex);
} }
@ -513,7 +551,8 @@ void MainWindow::on_actionSavePlaylist()
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
"JSON Files (*.json);;All Files (*)"); "JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) { if (!filePath.isEmpty())
{
savePlaylist(filePath); savePlaylist(filePath);
} }
} }
@ -523,7 +562,8 @@ void MainWindow::on_actionLoadPlaylist()
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
"JSON Files (*.json);;All Files (*)"); "JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) { if (!filePath.isEmpty())
{
songModel->clear(); songModel->clear();
flushGenerationQueue(); flushGenerationQueue();
loadPlaylist(filePath); loadPlaylist(filePath);
@ -535,7 +575,8 @@ void MainWindow::on_actionAppendPlaylist()
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
"JSON Files (*.json);;All Files (*)"); "JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) { if (!filePath.isEmpty())
{
loadPlaylist(filePath); loadPlaylist(filePath);
} }
} }
@ -545,7 +586,8 @@ void MainWindow::on_actionSaveSong()
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist", QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
"JSON Files (*.json);;All Files (*)"); "JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) { if (!filePath.isEmpty())
{
QJsonArray songsArray; QJsonArray songsArray;
QJsonParseError parseError; QJsonParseError parseError;
QJsonDocument songDoc = QJsonDocument::fromJson(currentSong.json.toUtf8(), &parseError); QJsonDocument songDoc = QJsonDocument::fromJson(currentSong.json.toUtf8(), &parseError);
@ -573,7 +615,8 @@ void MainWindow::savePlaylist(const QString &filePath)
{ {
// Get current songs from the model // Get current songs from the model
QList<SongItem> songs; QList<SongItem> songs;
for (int i = 0; i < songModel->rowCount(); ++i) { for (int i = 0; i < songModel->rowCount(); ++i)
{
songs.append(songModel->getSong(i)); songs.append(songModel->getSong(i));
} }
@ -583,9 +626,11 @@ void MainWindow::savePlaylist(const QString &filePath)
void MainWindow::loadPlaylist(const QString& filePath) void MainWindow::loadPlaylist(const QString& filePath)
{ {
QList<SongItem> songs; QList<SongItem> songs;
if (loadPlaylistFromJson(filePath, songs)) { if (loadPlaylistFromJson(filePath, songs))
{
// Add loaded songs // Add loaded songs
for (const SongItem &song : songs) { for (const SongItem &song : songs)
{
songModel->addSong(song); songModel->addSong(song);
} }
} }
@ -603,7 +648,8 @@ void MainWindow::autoSavePlaylist()
// Get current songs from the model // Get current songs from the model
QList<SongItem> songs; QList<SongItem> songs;
for (int i = 0; i < songModel->rowCount(); ++i) { for (int i = 0; i < songModel->rowCount(); ++i)
{
songs.append(songModel->getSong(i)); songs.append(songModel->getSong(i));
} }
@ -617,9 +663,11 @@ void MainWindow::autoLoadPlaylist()
QString filePath = appConfigPath + "/playlist.json"; QString filePath = appConfigPath + "/playlist.json";
// Check if the auto-save file exists // Check if the auto-save file exists
if (QFile::exists(filePath)) { if (QFile::exists(filePath))
{
QList<SongItem> songs; QList<SongItem> songs;
if (loadPlaylistFromJson(filePath, songs)) { if (loadPlaylistFromJson(filePath, songs))
{
songModel->clear(); songModel->clear();
for (const SongItem &song : songs) for (const SongItem &song : songs)
songModel->addSong(song); songModel->addSong(song);
@ -631,7 +679,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
{ {
QJsonArray songsArray; QJsonArray songsArray;
for (const SongItem &song : songs) { for (const SongItem &song : songs)
{
QJsonObject songObj; QJsonObject songObj;
songObj["caption"] = song.caption; songObj["caption"] = song.caption;
songObj["lyrics"] = song.lyrics; songObj["lyrics"] = song.lyrics;
@ -648,7 +697,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
QByteArray jsonData = doc.toJson(); QByteArray jsonData = doc.toJson();
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
qWarning() << "Could not open file for writing:" << filePath; qWarning() << "Could not open file for writing:" << filePath;
return false; return false;
} }
@ -662,7 +712,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs) bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
{ {
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
qWarning() << "Could not open file for reading:" << filePath; qWarning() << "Could not open file for reading:" << filePath;
return false; return false;
} }
@ -675,12 +726,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
QJsonParseError parseError; QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError); QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error != QJsonParseError::NoError) { if (parseError.error != QJsonParseError::NoError)
{
qWarning() << "JSON parse error:" << parseError.errorString(); qWarning() << "JSON parse error:" << parseError.errorString();
return false; return false;
} }
if (!doc.isObject()) { if (!doc.isObject())
{
qWarning() << "JSON root is not an object"; qWarning() << "JSON root is not an object";
return false; return false;
} }
@ -688,12 +741,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
QJsonObject rootObj = doc.object(); QJsonObject rootObj = doc.object();
// Check for version compatibility // Check for version compatibility
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") { if (rootObj.contains("version") && rootObj["version"].toString() != "1.0")
{
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString(); qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
return false; return false;
} }
if (!rootObj.contains("songs") || !rootObj["songs"].isArray()) { if (!rootObj.contains("songs") || !rootObj["songs"].isArray())
{
qWarning() << "Invalid playlist format: missing songs array"; qWarning() << "Invalid playlist format: missing songs array";
return false; return false;
} }
@ -702,29 +757,37 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
qDebug()<<"Loading"<<songsArray.size()<<"songs"; qDebug()<<"Loading"<<songsArray.size()<<"songs";
for (const QJsonValue &value : songsArray) { for (const QJsonValue &value : songsArray)
if (!value.isObject()) continue; {
if (!value.isObject())
continue;
QJsonObject songObj = value.toObject(); QJsonObject songObj = value.toObject();
SongItem song; SongItem song;
if (songObj.contains("caption")) { if (songObj.contains("caption"))
{
song.caption = songObj["caption"].toString(); song.caption = songObj["caption"].toString();
} }
if (songObj.contains("lyrics")) { if (songObj.contains("lyrics"))
{
song.lyrics = songObj["lyrics"].toString(); song.lyrics = songObj["lyrics"].toString();
} }
// Load vocalLanguage if present // Load vocalLanguage if present
if (songObj.contains("vocalLanguage")) { if (songObj.contains("vocalLanguage"))
{
song.vocalLanguage = songObj["vocalLanguage"].toString(); song.vocalLanguage = songObj["vocalLanguage"].toString();
} }
// Load uniqueId if present (for backward compatibility) // Load uniqueId if present (for backward compatibility)
if (songObj.contains("uniqueId")) { if (songObj.contains("uniqueId"))
{
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger()); song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
} else { }
else
{
// Generate new ID for old playlists without uniqueId // Generate new ID for old playlists without uniqueId
song.uniqueId = QRandomGenerator::global()->generate64(); song.uniqueId = QRandomGenerator::global()->generate64();
} }

View file

@ -17,7 +17,10 @@
#include "AceStepWorker.h" #include "AceStepWorker.h"
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; } namespace Ui
{
class MainWindow;
}
QT_END_NAMESPACE QT_END_NAMESPACE
class MainWindow : public QMainWindow class MainWindow : public QMainWindow

View file

@ -9,10 +9,12 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l
ui->setupUi(this); ui->setupUi(this);
// Set initial values if provided // Set initial values if provided
if (!caption.isEmpty()) { if (!caption.isEmpty())
{
ui->captionEdit->setPlainText(caption); ui->captionEdit->setPlainText(caption);
} }
if (!lyrics.isEmpty()) { if (!lyrics.isEmpty())
{
ui->lyricsEdit->setPlainText(lyrics); ui->lyricsEdit->setPlainText(lyrics);
} }
@ -29,12 +31,16 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l
ui->vocalLanguageCombo->addItem("Russian (ru)", "ru"); ui->vocalLanguageCombo->addItem("Russian (ru)", "ru");
// Set current language if provided // Set current language if provided
if (!vocalLanguage.isEmpty()) { if (!vocalLanguage.isEmpty())
{
int index = ui->vocalLanguageCombo->findData(vocalLanguage); int index = ui->vocalLanguageCombo->findData(vocalLanguage);
if (index >= 0) { if (index >= 0)
{
ui->vocalLanguageCombo->setCurrentIndex(index); ui->vocalLanguageCombo->setCurrentIndex(index);
} }
} else { }
else
{
ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset
} }
} }
@ -63,7 +69,8 @@ void SongDialog::on_okButton_clicked()
{ {
// Validate that caption is not empty // Validate that caption is not empty
QString caption = getCaption(); QString caption = getCaption();
if (caption.trimmed().isEmpty()) { if (caption.trimmed().isEmpty())
{
QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty."); QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty.");
return; return;
} }

View file

@ -4,7 +4,8 @@
#include <QDialog> #include <QDialog>
#include <QString> #include <QString>
namespace Ui { namespace Ui
{
class SongDialog; class SongDialog;
} }
@ -13,7 +14,8 @@ class SongDialog : public QDialog
Q_OBJECT Q_OBJECT
public: public:
explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", const QString &vocalLanguage = ""); explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "",
const QString &vocalLanguage = "");
~SongDialog(); ~SongDialog();
QString getCaption() const; QString getCaption() const;

View file

@ -3,7 +3,8 @@
#include <QRandomGenerator> #include <QRandomGenerator>
#include <cstdint> #include <cstdint>
class SongItem { class SongItem
{
public: public:
QString caption; QString caption;
QString lyrics; QString lyrics;
@ -13,7 +14,8 @@ public:
QString json; QString json;
inline SongItem(const QString &caption = "", const QString &lyrics = "") inline SongItem(const QString &caption = "", const QString &lyrics = "")
: caption(caption), lyrics(lyrics) { : caption(caption), lyrics(lyrics)
{
// Generate a unique ID using cryptographically secure random number // Generate a unique ID using cryptographically secure random number
uniqueId = QRandomGenerator::global()->generate64(); uniqueId = QRandomGenerator::global()->generate64();
} }

View file

@ -32,24 +32,29 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
const SongItem &song = songList[index.row()]; const SongItem &song = songList[index.row()];
switch (role) { switch (role)
{
case Qt::DisplayRole: case Qt::DisplayRole:
// Column 0: Play indicator column // Column 0: Play indicator column
if (index.column() == 0) { if (index.column() == 0)
{
return index.row() == m_playingIndex ? "" : ""; return index.row() == m_playingIndex ? "" : "";
} }
// Column 1: Song name // Column 1: Song name
else if (index.column() == 1) { else if (index.column() == 1)
{
return song.caption; return song.caption;
} }
// Column 2: Vocal language // Column 2: Vocal language
else if (index.column() == 2) { else if (index.column() == 2)
{
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--"; return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
} }
break; break;
case Qt::FontRole: case Qt::FontRole:
// Make play indicator bold and larger // Make play indicator bold and larger
if (index.column() == 0 && index.row() == m_playingIndex) { if (index.column() == 0 && index.row() == m_playingIndex)
{
QFont font = QApplication::font(); QFont font = QApplication::font();
font.setBold(true); font.setBold(true);
return font; return font;
@ -57,7 +62,8 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
break; break;
case Qt::TextAlignmentRole: case Qt::TextAlignmentRole:
// Center align the play indicator // Center align the play indicator
if (index.column() == 0) { if (index.column() == 0)
{
return Qt::AlignCenter; return Qt::AlignCenter;
} }
break; break;
@ -83,7 +89,8 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int
SongItem &song = songList[index.row()]; SongItem &song = songList[index.row()];
switch (role) { switch (role)
{
case CaptionRole: case CaptionRole:
song.caption = value.toString(); song.caption = value.toString();
break; break;
@ -119,7 +126,8 @@ void SongListModel::addSong(const SongItem &song)
void SongListModel::removeSong(int index) void SongListModel::removeSong(int index)
{ {
if (index >= 0 && index < songList.size()) { if (index >= 0 && index < songList.size())
{
beginRemoveRows(QModelIndex(), index, index); beginRemoveRows(QModelIndex(), index, index);
songList.removeAt(index); songList.removeAt(index);
endRemoveRows(); endRemoveRows();
@ -140,7 +148,8 @@ bool SongListModel::empty()
SongItem SongListModel::getSong(int index) const SongItem SongListModel::getSong(int index) const
{ {
if (index >= 0 && index < songList.size()) { if (index >= 0 && index < songList.size())
{
return songList[index]; return songList[index];
} }
return SongItem(); return SongItem();
@ -148,7 +157,8 @@ SongItem SongListModel::getSong(int index) const
QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const
{ {
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
{
// Hide headers since we don't need column titles // Hide headers since we don't need column titles
return QVariant(); return QVariant();
} }
@ -161,11 +171,13 @@ void SongListModel::setPlayingIndex(int index)
m_playingIndex = index; m_playingIndex = index;
// Update both the old and new playing indices to trigger UI updates // Update both the old and new playing indices to trigger UI updates
if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size()) { if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size())
{
emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0)); emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0));
} }
if (index >= 0 && index < songList.size()) { if (index >= 0 && index < songList.size())
{
emit dataChanged(this->index(index, 0), this->index(index, 0)); emit dataChanged(this->index(index, 0), this->index(index, 0));
} }
} }
@ -180,7 +192,8 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
if (songList.isEmpty()) if (songList.isEmpty())
return -1; return -1;
if (shuffle) { if (shuffle)
{
// Simple random selection for shuffle mode // Simple random selection for shuffle mode
QRandomGenerator generator; QRandomGenerator generator;
return generator.bounded(songList.size()); return generator.bounded(songList.size());
@ -188,7 +201,8 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
// Sequential playback // Sequential playback
int nextIndex = currentIndex + 1; int nextIndex = currentIndex + 1;
if (nextIndex >= songList.size()) { if (nextIndex >= songList.size())
{
nextIndex = 0; // Loop back to beginning nextIndex = 0; // Loop back to beginning
} }
@ -197,8 +211,10 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
int SongListModel::findSongIndexById(uint64_t uniqueId) const int SongListModel::findSongIndexById(uint64_t uniqueId) const
{ {
for (int i = 0; i < songList.size(); ++i) { for (int i = 0; i < songList.size(); ++i)
if (songList[i].uniqueId == uniqueId) { {
if (songList[i].uniqueId == uniqueId)
{
return i; return i;
} }
} }

View file

@ -14,7 +14,8 @@ class SongListModel : public QAbstractTableModel
Q_OBJECT Q_OBJECT
public: public:
enum Roles { enum Roles
{
CaptionRole = Qt::UserRole + 1, CaptionRole = Qt::UserRole + 1,
LyricsRole = Qt::UserRole + 2, LyricsRole = Qt::UserRole + 2,
VocalLanguageRole = Qt::UserRole + 3, VocalLanguageRole = Qt::UserRole + 3,
@ -42,7 +43,10 @@ public:
// Playing indicator // Playing indicator
void setPlayingIndex(int index); void setPlayingIndex(int index);
int playingIndex() const { return m_playingIndex; } int playingIndex() const
{
return m_playingIndex;
}
// Find song by unique ID // Find song by unique ID
int findSongIndexById(uint64_t uniqueId) const; int findSongIndexById(uint64_t uniqueId) const;

View file

@ -13,7 +13,8 @@ ClickableSlider::ClickableSlider(Qt::Orientation orientation, QWidget *parent)
void ClickableSlider::mousePressEvent(QMouseEvent *event) void ClickableSlider::mousePressEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton)
{
int val = pixelPosToRangeValue(event->pos()); int val = pixelPosToRangeValue(event->pos());
// Block signals temporarily to avoid infinite recursion // Block signals temporarily to avoid infinite recursion
@ -24,7 +25,9 @@ void ClickableSlider::mousePressEvent(QMouseEvent *event)
// Emit both valueChanged and sliderMoved signals for compatibility // Emit both valueChanged and sliderMoved signals for compatibility
emit valueChanged(val); emit valueChanged(val);
emit sliderMoved(val); emit sliderMoved(val);
} else { }
else
{
// Call base class implementation for other buttons // Call base class implementation for other buttons
QSlider::mousePressEvent(event); QSlider::mousePressEvent(event);
} }
@ -42,11 +45,14 @@ int ClickableSlider::pixelPosToRangeValue(const QPoint &pos)
int sliderMin; int sliderMin;
int sliderMax; int sliderMax;
if (orientation() == Qt::Horizontal) { if (orientation() == Qt::Horizontal)
{
sliderLength = sr.width(); sliderLength = sr.width();
sliderMin = gr.x(); sliderMin = gr.x();
sliderMax = gr.right() - sliderLength + 1; sliderMax = gr.right() - sliderLength + 1;
} else { }
else
{
sliderLength = sr.height(); sliderLength = sr.height();
sliderMin = gr.y(); sliderMin = gr.y();
sliderMax = gr.bottom() - sliderLength + 1; sliderMax = gr.bottom() - sliderLength + 1;