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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<ui version="4.0">
<class>AdvancedSettingsDialog</class>
<widget class="QDialog" name="AdvancedSettingsDialog">
<property name="geometry">
@ -200,4 +200,4 @@
</hints>
</connection>
</connections>
</ui>
</ui>

View file

@ -17,8 +17,10 @@ AudioPlayer::AudioPlayer(QObject *parent)
// Set up position timer for updating playback position
positionTimer->setInterval(500); // Update every 500ms
connect(positionTimer, &QTimer::timeout, [this]() {
if (isPlaying()) {
connect(positionTimer, &QTimer::timeout, [this]()
{
if (isPlaying())
{
emit positionChanged(mediaPlayer->position());
}
});
@ -31,7 +33,8 @@ AudioPlayer::~AudioPlayer()
void AudioPlayer::play(const QString &filePath)
{
if (isPlaying()) {
if (isPlaying())
{
stop();
}
@ -44,7 +47,8 @@ void AudioPlayer::play(const QString &filePath)
void AudioPlayer::play()
{
if (!isPlaying()) {
if (!isPlaying())
{
mediaPlayer->play();
positionTimer->start();
}
@ -52,7 +56,8 @@ void AudioPlayer::play()
void AudioPlayer::pause()
{
if (isPlaying()) {
if (isPlaying())
{
mediaPlayer->pause();
positionTimer->stop();
}
@ -86,12 +91,16 @@ int AudioPlayer::position() const
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
{
if (state == QMediaPlayer::PlayingState) {
if (state == QMediaPlayer::PlayingState)
{
emit playbackStarted();
} else if (state == QMediaPlayer::StoppedState ||
state == QMediaPlayer::PausedState) {
}
else if (state == QMediaPlayer::StoppedState ||
state == QMediaPlayer::PausedState)
{
// Check if we reached the end
if (mediaPlayer->position() >= mediaPlayer->duration() - 100) {
if (mediaPlayer->position() >= mediaPlayer->duration() - 100)
{
emit playbackFinished();
}
}
@ -99,16 +108,22 @@ void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status)
{
if (status == QMediaPlayer::EndOfMedia) {
if (status == QMediaPlayer::EndOfMedia)
{
emit playbackFinished();
} else if (status == QMediaPlayer::LoadedMedia ||
status == QMediaPlayer::BufferedMedia) {
}
else if (status == QMediaPlayer::LoadedMedia ||
status == QMediaPlayer::BufferedMedia)
{
// Media loaded successfully, emit duration
int duration = mediaPlayer->duration();
if (duration > 0) {
if (duration > 0)
{
emit durationChanged(duration);
}
} else if (status == QMediaPlayer::InvalidMedia) {
}
else if (status == QMediaPlayer::InvalidMedia)
{
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->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();});
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this](){songModel->clear();});
connect(ui->actionQuit, &QAction::triggered, this, [this]()
{
close();
});
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this]()
{
songModel->clear();
});
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
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 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);
});
// Add some default songs
if(songModel->songCount() == 0) {
if(songModel->songCount() == 0)
{
SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", "");
SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", "");
SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", "");
@ -75,7 +83,8 @@ MainWindow::MainWindow(QWidget *parent)
}
// Select first item
if (songModel->rowCount() > 0) {
if (songModel->rowCount() > 0)
{
QModelIndex firstIndex = songModel->index(0, 0);
ui->songListView->setCurrentIndex(firstIndex);
}
@ -117,7 +126,8 @@ void MainWindow::loadSettings()
QSettings settings("MusicGenerator", "AceStepGUI");
// 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
shuffleMode = settings.value("shuffleMode", false).toBool();
@ -126,8 +136,10 @@ void MainWindow::loadSettings()
// Load path settings with defaults based on application directory
QString appDir = QCoreApplication::applicationDirPath();
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
qwen3ModelPath = settings.value("qwen3ModelPath", 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();
qwen3ModelPath = settings.value("qwen3ModelPath",
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();
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
}
@ -197,7 +209,8 @@ void MainWindow::updateControls()
void MainWindow::on_playButton_clicked()
{
if (isPaused) {
if (isPaused)
{
// Resume playback
audioPlayer->play();
isPaused = false;
@ -217,7 +230,8 @@ void MainWindow::on_playButton_clicked()
void MainWindow::on_pauseButton_clicked()
{
if (isPlaying && !isPaused) {
if (isPlaying && !isPaused)
{
// Pause playback
audioPlayer->pause();
isPaused = true;
@ -227,7 +241,8 @@ void MainWindow::on_pauseButton_clicked()
void MainWindow::on_skipButton_clicked()
{
if (isPlaying) {
if (isPlaying)
{
audioPlayer->stop();
isPaused = false;
playNextSong();
@ -236,7 +251,8 @@ void MainWindow::on_skipButton_clicked()
void MainWindow::on_stopButton_clicked()
{
if (isPlaying) {
if (isPlaying)
{
// Stop current playback completely
audioPlayer->stop();
ui->nowPlayingLabel->setText("Now Playing:");
@ -257,7 +273,8 @@ void MainWindow::on_addSongButton_clicked()
{
SongDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
if (dialog.exec() == QDialog::Accepted)
{
QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage();
@ -274,7 +291,8 @@ void MainWindow::on_addSongButton_clicked()
void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
{
if (!index.isValid()) return;
if (!index.isValid())
return;
// Temporarily disconnect the signal to prevent multiple invocations
// 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();
// 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
if (isPlaying) {
if (isPlaying)
{
audioPlayer->stop();
} else {
}
else
{
isPlaying = true;
updateControls();
}
@ -296,13 +318,16 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
flushGenerationQueue();
currentSong = songModel->getSong(row);
ensureSongsInQueue(true);
} else if (index.column() == 1 || index.column() == 2) {
}
else if (index.column() == 1 || index.column() == 2)
{
// Column 1 (caption): Edit the song
SongItem song = songModel->getSong(row);
SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage);
if (dialog.exec() == QDialog::Accepted) {
if (dialog.exec() == QDialog::Accepted)
{
QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage();
@ -321,7 +346,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
void MainWindow::on_removeSongButton_clicked()
{
QModelIndex currentIndex = ui->songListView->currentIndex();
if (!currentIndex.isValid()) return;
if (!currentIndex.isValid())
return;
// Get the row from the current selection (works with table view)
int row = currentIndex.row();
@ -330,7 +356,8 @@ void MainWindow::on_removeSongButton_clicked()
// Select next item or previous if at end
int newRow = qMin(row, songModel->rowCount() - 1);
if (newRow >= 0) {
if (newRow >= 0)
{
QModelIndex newIndex = songModel->index(newRow, 0);
ui->songListView->setCurrentIndex(newIndex);
}
@ -348,11 +375,13 @@ void MainWindow::on_advancedSettingsButton_clicked()
dialog.setDiTModelPath(ditModelPath);
dialog.setVAEModelPath(vaeModelPath);
if (dialog.exec() == QDialog::Accepted) {
if (dialog.exec() == QDialog::Accepted)
{
// Validate JSON template
QJsonParseError 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()));
return;
}
@ -389,10 +418,12 @@ void MainWindow::songGenerated(const SongItem& song)
{
isGeneratingNext = false;
if (!isPaused && isPlaying && !audioPlayer->isPlaying()) {
if (!isPaused && isPlaying && !audioPlayer->isPlaying())
{
playSong(song);
}
else {
else
{
generatedSongQueue.enqueue(song);
}
ui->statusLabel->setText("idle");
@ -406,10 +437,13 @@ void MainWindow::playNextSong()
return;
// Check if we have a pre-generated next song in the queue
if (!generatedSongQueue.isEmpty()) {
if (!generatedSongQueue.isEmpty())
{
SongItem generatedSong = generatedSongQueue.dequeue();
playSong(generatedSong);
} else {
}
else
{
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
}
@ -460,7 +494,8 @@ void MainWindow::updatePlaybackStatus(bool playing)
void MainWindow::on_positionSlider_sliderMoved(int position)
{
if (isPlaying && audioPlayer->isPlaying()) {
if (isPlaying && audioPlayer->isPlaying())
{
audioPlayer->setPosition(position);
}
}
@ -468,7 +503,8 @@ void MainWindow::on_positionSlider_sliderMoved(int position)
void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
{
// 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;
}
@ -482,10 +518,12 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
lastSong = currentSong;
SongItem nextSong;
if(enqeueCurrent) {
if(enqeueCurrent)
{
nextSong = lastSong;
}
else {
else
{
int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode);
nextSong = songModel->getSong(nextIndex);
}
@ -513,7 +551,8 @@ void MainWindow::on_actionSavePlaylist()
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
"JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) {
if (!filePath.isEmpty())
{
savePlaylist(filePath);
}
}
@ -523,7 +562,8 @@ void MainWindow::on_actionLoadPlaylist()
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
"JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) {
if (!filePath.isEmpty())
{
songModel->clear();
flushGenerationQueue();
loadPlaylist(filePath);
@ -535,7 +575,8 @@ void MainWindow::on_actionAppendPlaylist()
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
"JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) {
if (!filePath.isEmpty())
{
loadPlaylist(filePath);
}
}
@ -545,7 +586,8 @@ void MainWindow::on_actionSaveSong()
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
"JSON Files (*.json);;All Files (*)");
if (!filePath.isEmpty()) {
if (!filePath.isEmpty())
{
QJsonArray songsArray;
QJsonParseError 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
QList<SongItem> songs;
for (int i = 0; i < songModel->rowCount(); ++i) {
for (int i = 0; i < songModel->rowCount(); ++i)
{
songs.append(songModel->getSong(i));
}
@ -583,9 +626,11 @@ void MainWindow::savePlaylist(const QString &filePath)
void MainWindow::loadPlaylist(const QString& filePath)
{
QList<SongItem> songs;
if (loadPlaylistFromJson(filePath, songs)) {
if (loadPlaylistFromJson(filePath, songs))
{
// Add loaded songs
for (const SongItem &song : songs) {
for (const SongItem &song : songs)
{
songModel->addSong(song);
}
}
@ -603,7 +648,8 @@ void MainWindow::autoSavePlaylist()
// Get current songs from the model
QList<SongItem> songs;
for (int i = 0; i < songModel->rowCount(); ++i) {
for (int i = 0; i < songModel->rowCount(); ++i)
{
songs.append(songModel->getSong(i));
}
@ -617,9 +663,11 @@ void MainWindow::autoLoadPlaylist()
QString filePath = appConfigPath + "/playlist.json";
// Check if the auto-save file exists
if (QFile::exists(filePath)) {
if (QFile::exists(filePath))
{
QList<SongItem> songs;
if (loadPlaylistFromJson(filePath, songs)) {
if (loadPlaylistFromJson(filePath, songs))
{
songModel->clear();
for (const SongItem &song : songs)
songModel->addSong(song);
@ -631,7 +679,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
{
QJsonArray songsArray;
for (const SongItem &song : songs) {
for (const SongItem &song : songs)
{
QJsonObject songObj;
songObj["caption"] = song.caption;
songObj["lyrics"] = song.lyrics;
@ -648,7 +697,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
QByteArray jsonData = doc.toJson();
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;
return false;
}
@ -662,7 +712,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
{
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;
return false;
}
@ -675,12 +726,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
if (parseError.error != QJsonParseError::NoError)
{
qWarning() << "JSON parse error:" << parseError.errorString();
return false;
}
if (!doc.isObject()) {
if (!doc.isObject())
{
qWarning() << "JSON root is not an object";
return false;
}
@ -688,12 +741,14 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
QJsonObject rootObj = doc.object();
// 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();
return false;
}
if (!rootObj.contains("songs") || !rootObj["songs"].isArray()) {
if (!rootObj.contains("songs") || !rootObj["songs"].isArray())
{
qWarning() << "Invalid playlist format: missing songs array";
return false;
}
@ -702,29 +757,37 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
qDebug()<<"Loading"<<songsArray.size()<<"songs";
for (const QJsonValue &value : songsArray) {
if (!value.isObject()) continue;
for (const QJsonValue &value : songsArray)
{
if (!value.isObject())
continue;
QJsonObject songObj = value.toObject();
SongItem song;
if (songObj.contains("caption")) {
if (songObj.contains("caption"))
{
song.caption = songObj["caption"].toString();
}
if (songObj.contains("lyrics")) {
if (songObj.contains("lyrics"))
{
song.lyrics = songObj["lyrics"].toString();
}
// Load vocalLanguage if present
if (songObj.contains("vocalLanguage")) {
if (songObj.contains("vocalLanguage"))
{
song.vocalLanguage = songObj["vocalLanguage"].toString();
}
// Load uniqueId if present (for backward compatibility)
if (songObj.contains("uniqueId")) {
if (songObj.contains("uniqueId"))
{
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
} else {
}
else
{
// Generate new ID for old playlists without uniqueId
song.uniqueId = QRandomGenerator::global()->generate64();
}

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
@ -356,4 +356,4 @@
<include location="../res/resources.qrc"/>
</resources>
<connections/>
</ui>
</ui>

View file

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

View file

@ -4,7 +4,8 @@
#include <QDialog>
#include <QString>
namespace Ui {
namespace Ui
{
class SongDialog;
}
@ -13,7 +14,8 @@ class SongDialog : public QDialog
Q_OBJECT
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();
QString getCaption() const;

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<ui version="4.0">
<class>SongDialog</class>
<widget class="QDialog" name="SongDialog">
<property name="geometry">
@ -106,4 +106,4 @@
</widget>
<resources/>
<connections/>
</ui>
</ui>

View file

@ -3,7 +3,8 @@
#include <QRandomGenerator>
#include <cstdint>
class SongItem {
class SongItem
{
public:
QString caption;
QString lyrics;
@ -13,7 +14,8 @@ public:
QString json;
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
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()];
switch (role) {
switch (role)
{
case Qt::DisplayRole:
// Column 0: Play indicator column
if (index.column() == 0) {
if (index.column() == 0)
{
return index.row() == m_playingIndex ? "" : "";
}
// Column 1: Song name
else if (index.column() == 1) {
else if (index.column() == 1)
{
return song.caption;
}
// Column 2: Vocal language
else if (index.column() == 2) {
else if (index.column() == 2)
{
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
}
break;
case Qt::FontRole:
// 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();
font.setBold(true);
return font;
@ -57,7 +62,8 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
break;
case Qt::TextAlignmentRole:
// Center align the play indicator
if (index.column() == 0) {
if (index.column() == 0)
{
return Qt::AlignCenter;
}
break;
@ -83,7 +89,8 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int
SongItem &song = songList[index.row()];
switch (role) {
switch (role)
{
case CaptionRole:
song.caption = value.toString();
break;
@ -119,7 +126,8 @@ void SongListModel::addSong(const SongItem &song)
void SongListModel::removeSong(int index)
{
if (index >= 0 && index < songList.size()) {
if (index >= 0 && index < songList.size())
{
beginRemoveRows(QModelIndex(), index, index);
songList.removeAt(index);
endRemoveRows();
@ -140,7 +148,8 @@ bool SongListModel::empty()
SongItem SongListModel::getSong(int index) const
{
if (index >= 0 && index < songList.size()) {
if (index >= 0 && index < songList.size())
{
return songList[index];
}
return SongItem();
@ -148,7 +157,8 @@ SongItem SongListModel::getSong(int index) 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
return QVariant();
}
@ -161,11 +171,13 @@ void SongListModel::setPlayingIndex(int index)
m_playingIndex = index;
// 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));
}
if (index >= 0 && index < songList.size()) {
if (index >= 0 && index < songList.size())
{
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())
return -1;
if (shuffle) {
if (shuffle)
{
// Simple random selection for shuffle mode
QRandomGenerator generator;
return generator.bounded(songList.size());
@ -188,7 +201,8 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
// Sequential playback
int nextIndex = currentIndex + 1;
if (nextIndex >= songList.size()) {
if (nextIndex >= songList.size())
{
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
{
for (int i = 0; i < songList.size(); ++i) {
if (songList[i].uniqueId == uniqueId) {
for (int i = 0; i < songList.size(); ++i)
{
if (songList[i].uniqueId == uniqueId)
{
return i;
}
}

View file

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

View file

@ -13,7 +13,8 @@ ClickableSlider::ClickableSlider(Qt::Orientation orientation, QWidget *parent)
void ClickableSlider::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
if (event->button() == Qt::LeftButton)
{
int val = pixelPosToRangeValue(event->pos());
// 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 valueChanged(val);
emit sliderMoved(val);
} else {
}
else
{
// Call base class implementation for other buttons
QSlider::mousePressEvent(event);
}
@ -42,11 +45,14 @@ int ClickableSlider::pixelPosToRangeValue(const QPoint &pos)
int sliderMin;
int sliderMax;
if (orientation() == Qt::Horizontal) {
if (orientation() == Qt::Horizontal)
{
sliderLength = sr.width();
sliderMin = gr.x();
sliderMax = gr.right() - sliderLength + 1;
} else {
}
else
{
sliderLength = sr.height();
sliderMin = gr.y();
sliderMax = gr.bottom() - sliderLength + 1;