diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a808f1..c3dcea3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(${PROJECT_NAME} src/clickableslider.cpp ${MusicGeneratorGUI_H} res/resources.qrc + src/elidedlabel.h src/elidedlabel.cpp ) # UI file @@ -60,7 +61,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE install(TARGETS ${PROJECT_NAME} DESTINATION bin) # Install .desktop file -install(FILES aceradio.desktop DESTINATION share/applications) +install(FILES res/xyz.uvos.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) diff --git a/README.md b/README.md index 1daa6c7..2333021 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Screenshot](res/scrot.png) -A C++ Qt graphical user interface for generating music using acestep.cpp. +A C++ Qt graphical user interface for generating music using [acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp). ## Requirements @@ -26,13 +26,13 @@ make -j$(nproc) ### Build the GUI: ```bash -git clone git@github.com:IMbackK/aceradio.git +git clone https://github.com/IMbackK/aceradio.git cd aceradio mkdir build && cd build cmake .. make -j$(nproc) ``` -### Setup Paths: +## Setup Paths: Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models. diff --git a/aceradio.desktop b/res/xyz.uvos.aceradio.desktop similarity index 100% rename from aceradio.desktop rename to res/xyz.uvos.aceradio.desktop diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 3da8847..e318e8f 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -8,269 +8,205 @@ #include #include #include -#include -AceStepWorker::AceStepWorker(QObject *parent) - : QObject(parent), - currentWorker(nullptr) +AceStep::AceStep(QObject* parent): QObject(parent) { + connect(&qwenProcess, &QProcess::finished, this, &AceStep::qwenProcFinished); + connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished); } -AceStepWorker::~AceStepWorker() +bool AceStep::isGenerateing(SongItem* song) { - cancelGeneration(); + if(!busy && song) + *song = this->request.song; + return busy; } -void AceStepWorker::generateSong(const SongItem& song, const QString &jsonTemplate, - const QString &aceStepPath, const QString &qwen3ModelPath, - const QString &textEncoderModelPath, const QString &ditModelPath, - const QString &vaeModelPath) +void AceStep::cancleGenerateion() { - // Cancel any ongoing generation - cancelGeneration(); + qwenProcess.blockSignals(true); + qwenProcess.terminate(); + qwenProcess.waitForFinished(); + qwenProcess.blockSignals(false); - // Create worker and start it - currentWorker = new Worker(this, song, jsonTemplate, aceStepPath, qwen3ModelPath, - textEncoderModelPath, ditModelPath, vaeModelPath); - currentWorker->setAutoDelete(true); - QThreadPool::globalInstance()->start(currentWorker); + ditVaeProcess.blockSignals(true); + ditVaeProcess.terminate(); + ditVaeProcess.waitForFinished(); + ditVaeProcess.blockSignals(false); + + progressUpdate(100); + if(busy) + generationCancled(request.song); + + busy = false; } -void AceStepWorker::cancelGeneration() +bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, + QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, + QString vaeModelPath) { - currentWorker = nullptr; -} - -bool AceStepWorker::songGenerateing(SongItem* song) -{ - workerMutex.lock(); - if(!currentWorker) + if(busy) { - workerMutex.unlock(); + qWarning()<<"Droping song:"<generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath}; + + QString qwen3Binary = aceStepPath + "/ace-qwen3"; + QFileInfo qwen3Info(qwen3Binary); + if (!qwen3Info.exists() || !qwen3Info.isExecutable()) { - SongItem workerSong = currentWorker->getSong(); - workerMutex.unlock(); - if(song) - *song = workerSong; - return true; + generationError("ace-qwen3 binary not found at: " + qwen3Binary); + busy = false; + return false; + } + if (!QFileInfo::exists(qwen3ModelPath)) + { + generationError("Qwen3 model not found: " + qwen3ModelPath); + busy = false; + return false; + } + if (!QFileInfo::exists(textEncoderModelPath)) + { + generationError("Text encoder model not found: " + textEncoderModelPath); + busy = false; + return false; + } + if (!QFileInfo::exists(ditModelPath)) + { + generationError("DiT model not found: " + ditModelPath); + busy = false; + return false; + } + if (!QFileInfo::exists(vaeModelPath)) + { + generationError("VAE model not found: " + vaeModelPath); + busy = false; + return false; } -} -// Worker implementation -void AceStepWorker::Worker::run() -{ - uint64_t uid = QRandomGenerator::global()->generate(); - // Create temporary JSON file for the request - QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - QString requestFile = tempDir + "/request_" + QString::number(uid) + ".json"; + request.requestFilePath = tempDir + "/request_" + QString::number(request.uid) + ".json"; - // Parse and modify the template QJsonParseError parseError; - QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError); + QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError); if (!templateDoc.isObject()) { - emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString())); - return; + generationError("Invalid JSON template: " + QString(parseError.errorString())); + busy = false; + return false; } QJsonObject requestObj = templateDoc.object(); requestObj["caption"] = song.caption; if (!song.lyrics.isEmpty()) - { requestObj["lyrics"] = song.lyrics; - } - 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()) - { requestObj["vocal_language"] = song.vocalLanguage; - } // Write the request file - QFile requestFileHandle(requestFile); + QFile requestFileHandle(request.requestFilePath); if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) { - emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString()); - return; + emit generationError("Failed to create request file: " + requestFileHandle.errorString()); + busy = false; + return false; } - requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented)); requestFileHandle.close(); - // Use provided paths for acestep.cpp binaries - QString qwen3Binary = this->aceStepPath + "/ace-qwen3"; - QString ditVaeBinary = this->aceStepPath + "/dit-vae"; + QStringList qwen3Args; + qwen3Args << "--request" << request.requestFilePath; + qwen3Args << "--model" << qwen3ModelPath; - // Check if binaries exist - QFileInfo qwen3Info(qwen3Binary); - QFileInfo ditVaeInfo(ditVaeBinary); + progressUpdate(30); - if (!qwen3Info.exists() || !qwen3Info.isExecutable()) + qwenProcess.start(qwen3Binary, qwen3Args); + return true; +} + +void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) +{ + QFile::remove(request.requestFilePath); + if(code != 0) { - emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary); + QString errorOutput = qwenProcess.readAllStandardError(); + generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput); + busy = false; return; } + QString ditVaeBinary = request.aceStepPath + "/dit-vae"; + QFileInfo ditVaeInfo(ditVaeBinary); if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) { - emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary); + generationError("dit-vae binary not found at: " + ditVaeBinary); + busy = false; return; } - // Use provided model paths - QString qwen3Model = this->qwen3ModelPath; - QString textEncoderModel = this->textEncoderModelPath; - QString ditModel = this->ditModelPath; - QString vaeModel = this->vaeModelPath; - - if (!QFileInfo::exists(qwen3Model)) + request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json"; + if (!QFileInfo::exists(request.requestLlmFilePath)) { - emit parent->generationError("Qwen3 model not found: " + qwen3Model); - return; - } - - if (!QFileInfo::exists(textEncoderModel)) - { - emit parent->generationError("Text encoder model not found: " + textEncoderModel); - return; - } - - if (!QFileInfo::exists(ditModel)) - { - emit parent->generationError("DiT model not found: " + ditModel); - return; - } - - if (!QFileInfo::exists(vaeModel)) - { - emit parent->generationError("VAE model not found: " + vaeModel); - return; - } - - // Step 1: Run ace-qwen3 to generate lyrics and audio codes - QProcess qwen3Process; - QStringList qwen3Args; - qwen3Args << "--request" << requestFile; - qwen3Args << "--model" << qwen3Model; - - emit parent->progressUpdate(20); - - qwen3Process.start(qwen3Binary, qwen3Args); - if (!qwen3Process.waitForStarted()) - { - emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString()); - return; - } - - if (!qwen3Process.waitForFinished(60000)) // 60 second timeout - { - qwen3Process.terminate(); - qwen3Process.waitForFinished(5000); - emit parent->generationError("ace-qwen3 timed out after 60 seconds"); - return; - } - - int exitCode = qwen3Process.exitCode(); - 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)) - { - emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile); + generationError("ace-qwen3 failed to create enhaced request file "+request.requestLlmFilePath); + busy = false; return; } // Load lyrics from the enhanced request file - QFile lmOutputFile(requestLmOutputFile); + QFile lmOutputFile(request.requestLlmFilePath); if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QJsonParseError parseError; - song.json = lmOutputFile.readAll(); - QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError); + request.song.json = lmOutputFile.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(request.song.json.toUtf8(), &parseError); lmOutputFile.close(); if (doc.isObject() && !parseError.error) { QJsonObject obj = doc.object(); if (obj.contains("lyrics") && obj["lyrics"].isString()) - { - song.lyrics = obj["lyrics"].toString(); - } + request.song.lyrics = obj["lyrics"].toString(); } } - emit parent->progressUpdate(50); - // Step 2: Run dit-vae to generate audio - QProcess ditVaeProcess; QStringList ditVaeArgs; - ditVaeArgs << "--request" << requestLmOutputFile; - ditVaeArgs << "--text-encoder" << textEncoderModel; - ditVaeArgs << "--dit" << ditModel; - ditVaeArgs << "--vae" << vaeModel; + ditVaeArgs << "--request"<progressUpdate(60); + progressUpdate(60); ditVaeProcess.start(ditVaeBinary, ditVaeArgs); - if (!ditVaeProcess.waitForStarted()) - { - emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString()); - return; - } +} - if (!ditVaeProcess.waitForFinished(120000)) // 2 minute timeout - { - ditVaeProcess.terminate(); - ditVaeProcess.waitForFinished(5000); - emit parent->generationError("dit-vae timed out after 2 minutes"); - return; - } - - exitCode = ditVaeProcess.exitCode(); - if (exitCode != 0) +void AceStep::ditProcFinished(int code, QProcess::ExitStatus status) +{ + QFile::remove(request.requestLlmFilePath); + if (code != 0) { QString errorOutput = ditVaeProcess.readAllStandardError(); - emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput); + generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput); + busy = false; return; } - emit parent->progressUpdate(90); - // Find the generated WAV file - QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav"; + QString wavFile = tempDir+"/request_" + QString::number(request.uid) + "00.wav"; if (!QFileInfo::exists(wavFile)) { - emit parent->generationError("No WAV file generated at "+wavFile); + generationError("No WAV file generated at "+wavFile); + busy = false; return; } + busy = false; - // Clean up temporary files - QFile::remove(requestLmOutputFile); - QFile::remove(requestFile); - - emit parent->progressUpdate(100); - song.file = wavFile; - emit parent->songGenerated(song); - parent->workerMutex.lock(); - parent->currentWorker = nullptr; - parent->workerMutex.unlock(); + progressUpdate(100); + request.song.file = wavFile; + songGenerated(request.song); } -const SongItem& AceStepWorker::Worker::getSong() -{ - return song; -} diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index fa7bbb8..41c4676 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -2,64 +2,55 @@ #define ACESTEPWORKER_H #include -#include -#include #include -#include -#include +#include +#include #include "SongItem.h" -class AceStepWorker : public QObject +class AceStep : public QObject { Q_OBJECT -public: - explicit AceStepWorker(QObject *parent = nullptr); - ~AceStepWorker(); + QProcess qwenProcess; + QProcess ditVaeProcess; - void generateSong(const SongItem& song, const QString &jsonTemplate, - const QString &aceStepPath, const QString &qwen3ModelPath, - const QString &textEncoderModelPath, const QString &ditModelPath, - const QString &vaeModelPath); - void cancelGeneration(); - bool songGenerateing(SongItem* song); + bool busy = false; -signals: - void songGenerated(const SongItem& song); - void generationFinished(); - void generationError(const QString &error); - void progressUpdate(int percent); - -private: - class Worker : public QRunnable + struct Request { - public: - Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate, - const QString &aceStepPath, const QString &qwen3ModelPath, - const QString &textEncoderModelPath, const QString &ditModelPath, - const QString &vaeModelPath) - : parent(parent), song(song), jsonTemplate(jsonTemplate), - aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath), - textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath), - vaeModelPath(vaeModelPath) {} - - void run() override; - - const SongItem& getSong(); - - private: - AceStepWorker *parent; SongItem song; - QString jsonTemplate; + uint64_t uid; QString aceStepPath; - QString qwen3ModelPath; QString textEncoderModelPath; QString ditModelPath; QString vaeModelPath; + QString requestFilePath; + QString requestLlmFilePath; }; - QMutex workerMutex; - Worker *currentWorker; + Request request; + + const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + +signals: + void songGenerated(SongItem song); + void generationCancled(SongItem song); + void generationError(QString error); + void progressUpdate(int progress); + +public slots: + bool requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, + QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, + QString vaeModelPath); + +public: + AceStep(QObject* parent = nullptr); + bool isGenerateing(SongItem* song = nullptr); + void cancleGenerateion(); + +private slots: + void qwenProcFinished(int code, QProcess::ExitStatus status); + void ditProcFinished(int code, QProcess::ExitStatus status); }; #endif // ACESTEPWORKER_H diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0ce0b14..62c8a63 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -18,13 +18,15 @@ MainWindow::MainWindow(QWidget *parent) ui(new Ui::MainWindow), songModel(new SongListModel(this)), audioPlayer(new AudioPlayer(this)), - aceStepWorker(new AceStepWorker(this)), + aceStep(new AceStep(this)), playbackTimer(new QTimer(this)), isPlaying(false), isPaused(false), shuffleMode(false), isGeneratingNext(false) { + aceStep->moveToThread(&aceThread); + ui->setupUi(this); // Setup lyrics display @@ -57,9 +59,10 @@ MainWindow::MainWindow(QWidget *parent) connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration); - connect(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated); - connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError); - connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); + connect(aceStep, &AceStep::songGenerated, this, &MainWindow::songGenerated); + connect(aceStep, &AceStep::generationCancled, this, &MainWindow::generationCanceld); + connect(aceStep, &AceStep::generationError, this, &MainWindow::generationError); + connect(aceStep, &AceStep::progressUpdate, ui->progressBar, &QProgressBar::setValue); // Connect double-click on song list for editing (works with QTableView too) connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); @@ -89,14 +92,16 @@ MainWindow::MainWindow(QWidget *parent) ui->songListView->setCurrentIndex(firstIndex); } + ui->nowPlayingLabel->setText("Now Playing:"); + currentSong = songModel->getSong(0); } MainWindow::~MainWindow() { - // Auto-save playlist before closing - autoSavePlaylist(); + aceStep->cancleGenerateion(); + autoSavePlaylist(); saveSettings(); delete ui; } @@ -125,6 +130,8 @@ void MainWindow::loadSettings() { QSettings settings("MusicGenerator", "AceStepGUI"); + isFirstRun = settings.value("firstRun", true).toBool(); + // 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(); @@ -160,6 +167,8 @@ void MainWindow::saveSettings() settings.setValue("textEncoderModelPath", textEncoderModelPath); settings.setValue("ditModelPath", ditModelPath); settings.setValue("vaeModelPath", vaeModelPath); + + settings.setValue("firstRun", false); } QString MainWindow::formatTime(int milliseconds) @@ -267,6 +276,10 @@ void MainWindow::on_shuffleButton_clicked() { shuffleMode = ui->shuffleButton->isChecked(); updateControls(); + + flushGenerationQueue(); + if(isPlaying) + ensureSongsInQueue(); } void MainWindow::on_addSongButton_clicked() @@ -294,16 +307,12 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) if (!index.isValid()) return; - // Temporarily disconnect the signal to prevent multiple invocations - // This happens when the dialog closes and triggers another double-click event disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); int row = index.row(); - // Different behavior based on which column was clicked if (index.column() == 0) { - // Column 0 (play indicator): Stop current playback and play this song if (isPlaying) { audioPlayer->stop(); @@ -314,14 +323,13 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) updateControls(); } - // Flush the generation queue when user selects a different song flushGenerationQueue(); + ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); currentSong = songModel->getSong(row); ensureSongsInQueue(true); } 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); @@ -332,14 +340,12 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) QString lyrics = dialog.getLyrics(); QString vocalLanguage = dialog.getVocalLanguage(); - // Update the model - use column 1 for the song name songModel->setData(songModel->index(row, 1), caption, SongListModel::CaptionRole); songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole); songModel->setData(songModel->index(row, 1), vocalLanguage, SongListModel::VocalLanguageRole); } } - // Reconnect the signal after dialog is closed connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); } @@ -395,7 +401,6 @@ void MainWindow::on_advancedSettingsButton_clicked() vaeModelPath = dialog.getVAEModelPath(); saveSettings(); - QMessageBox::information(this, "Settings Saved", "Advanced settings have been saved successfully."); } } @@ -426,11 +431,16 @@ void MainWindow::songGenerated(const SongItem& song) { generatedSongQueue.enqueue(song); } - ui->statusLabel->setText("idle"); + ui->statusbar->showMessage("idle"); ensureSongsInQueue(); } +void MainWindow::generationCanceld(const SongItem& song) +{ + ui->statusbar->showMessage("Geneartion cancled: " + song.caption); +} + void MainWindow::playNextSong() { if (!isPlaying) @@ -510,7 +520,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) SongItem lastSong; SongItem workerSong; - if(aceStepWorker->songGenerateing(&workerSong)) + if(aceStep->isGenerateing(&workerSong)) lastSong = workerSong; else if(!generatedSongQueue.empty()) lastSong = generatedSongQueue.last(); @@ -530,8 +540,8 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) isGeneratingNext = true; - ui->statusLabel->setText("Generateing: "+nextSong.caption); - aceStepWorker->generateSong(nextSong, jsonTemplate, + ui->statusbar->showMessage("Generateing: "+nextSong.caption); + aceStep->requestGeneration(nextSong, jsonTemplate, aceStepPath, qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath); @@ -540,7 +550,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) void MainWindow::flushGenerationQueue() { generatedSongQueue.clear(); - aceStepWorker->cancelGeneration(); + aceStep->cancleGenerateion(); isGeneratingNext = false; } @@ -797,3 +807,10 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & return true; } + +void MainWindow::show() +{ + QMainWindow::show(); + if(isFirstRun) + QMessageBox::information(this, "Welcome", "Welcome to AceStepGUI! Please configure paths in Settings→Ace Step before generateing your first song."); +} diff --git a/src/MainWindow.h b/src/MainWindow.h index 6a3bfd3..54136f7 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -27,10 +27,41 @@ class MainWindow : public QMainWindow { Q_OBJECT + Ui::MainWindow *ui; + SongListModel *songModel; + AudioPlayer *audioPlayer; + QThread aceThread; + AceStep *aceStep; + QTimer *playbackTimer; + + QString formatTime(int milliseconds); + + SongItem currentSong; + bool isPlaying; + bool isPaused; + bool shuffleMode; + bool isGeneratingNext; + bool isFirstRun; + QString jsonTemplate; + + // Path settings + QString aceStepPath; + QString qwen3ModelPath; + QString textEncoderModelPath; + QString ditModelPath; + QString vaeModelPath; + + // Queue for generated songs + static constexpr int generationTresh = 2; + QQueue generatedSongQueue; + public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); +public slots: + void show(); + private slots: void on_playButton_clicked(); void on_pauseButton_clicked(); @@ -47,6 +78,7 @@ private slots: void on_songListView_doubleClicked(const QModelIndex &index); void songGenerated(const SongItem& song); + void generationCanceld(const SongItem& song); void playNextSong(); void playbackStarted(); void updatePlaybackStatus(bool playing); @@ -57,36 +89,6 @@ private slots: void on_actionAppendPlaylist(); void on_actionSaveSong(); -private: - void startNextSongGeneration(); - -private: - Ui::MainWindow *ui; - SongListModel *songModel; - AudioPlayer *audioPlayer; - AceStepWorker *aceStepWorker; - QTimer *playbackTimer; - - QString formatTime(int milliseconds); - - SongItem currentSong; - bool isPlaying; - bool isPaused; - bool shuffleMode; - bool isGeneratingNext; - QString jsonTemplate; - - // Path settings - QString aceStepPath; - QString qwen3ModelPath; - QString textEncoderModelPath; - QString ditModelPath; - QString vaeModelPath; - - // Queue for generated songs - static constexpr int generationTresh = 2; - QQueue generatedSongQueue; - private: void loadSettings(); void saveSettings(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index d67fb70..828189b 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -1,359 +1,361 @@ - - MainWindow - - - - 0 - 0 - 800 - 600 - - - - Aceradio - - - - :/icons/xyz.uvos.aceradio.png:/icons/xyz.uvos.aceradio.png - - - - - - - 0 - - - - Songs - - - - - - - 0 - 200 - - - - QAbstractItemView::EditTrigger::NoEditTriggers - - - QAbstractItemView::SelectionBehavior::SelectRows - - - - - - - - Info - - - - - - true - - - - - - - - Lyrics - - - - - - - Monospace - - - - true - - - - - - - - - - - - - Add Song - - - - - - - Remove Song - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::LayoutDirection::LeftToRight - - - Now Playing: - - - Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter - - - - - - - - - 0:00 - - - - - - - false - - - false - - - - - - - 0:00 - - - - - - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - - Play - - - - - - - - - - Pause - - - - - - - - - - Skip - - - - - - - - - - Stop - - - - - - - - - - Shuffle - - - - - - true - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - 0 - 0 - 800 - 32 - - - - - File - - - - - - - - - - - Settings - - - - - - - - - - - - - Save Playlist - - - Ctrl+S - - - - - - - - Load Playlist... - - - Ctrl+O - - - - - Ace Step - - - - - - - - Quit - - - Ctrl+Q - - - - - - - - Clear Playlist - - - - - - - - Save Song - - - - - - - - Append Playlist - - - - - - ClickableSlider - QWidget -
src/clickableslider.h
-
-
- - - - -
+ + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Aceradio + + + + :/icons/xyz.uvos.aceradio.png:/icons/xyz.uvos.aceradio.png + + + + + + + 0 + + + + Songs + + + + + + + 0 + 200 + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionBehavior::SelectRows + + + + + + + + Info + + + + + + true + + + + + + + + Lyrics + + + + + + + Monospace + + + + true + + + + + + + + + + + + + Add Song + + + + + + + Remove Song + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 20 + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + + + + 0:00 + + + + + + + false + + + false + + + + + + + 0:00 + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + Play + + + + + + + + + + Pause + + + + + + + + + + Skip + + + + + + + + + + Stop + + + + + + + + + + Shuffle + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 0 + + + + + + + + + + + 0 + 0 + 800 + 32 + + + + + File + + + + + + + + + + + Settings + + + + + + + + + + + + + Save Playlist + + + Ctrl+S + + + + + + + + Load Playlist... + + + Ctrl+O + + + + + Ace Step + + + + + + + + Quit + + + Ctrl+Q + + + + + + + + Clear Playlist + + + + + + + + Save Song + + + + + + + + Append Playlist + + + + + + ClickableSlider + QWidget +
src/clickableslider.h
+
+ + ElidedLabel + QFrame +
src/elidedlabel.h
+ 1 +
+
+ + + + +
diff --git a/src/SongListModel.cpp b/src/SongListModel.cpp index c658f64..caa856a 100644 --- a/src/SongListModel.cpp +++ b/src/SongListModel.cpp @@ -193,11 +193,7 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const return -1; if (shuffle) - { - // Simple random selection for shuffle mode - QRandomGenerator generator; - return generator.bounded(songList.size()); - } + return QRandomGenerator::global()->bounded(songList.size()); // Sequential playback int nextIndex = currentIndex + 1; diff --git a/src/elidedlabel.cpp b/src/elidedlabel.cpp new file mode 100644 index 0000000..191a0fd --- /dev/null +++ b/src/elidedlabel.cpp @@ -0,0 +1,70 @@ +#include "elidedlabel.h" + +#include +#include + +ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) + : QFrame(parent) + , elided(false) + , content(text) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); +} + +ElidedLabel::ElidedLabel(QWidget *parent) + : QFrame(parent) + , elided(false) + , content("") +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); +} + +void ElidedLabel::setText(const QString &newText) +{ + content = newText; + update(); +} + +void ElidedLabel::paintEvent(QPaintEvent *event) +{ + QFrame::paintEvent(event); + + QPainter painter(this); + QFontMetrics fontMetrics = painter.fontMetrics(); + + bool didElide = false; + int lineSpacing = fontMetrics.lineSpacing(); + int y = 0; + + QTextLayout textLayout(content, painter.font()); + textLayout.beginLayout(); + forever { + QTextLine line = textLayout.createLine(); + + if (!line.isValid()) + break; + + line.setLineWidth(width()); + int nextLineY = y + lineSpacing; + + if (height() >= nextLineY + lineSpacing) { + line.draw(&painter, QPoint(0, y)); + y = nextLineY; + //! [2] + //! [3] + } else { + QString lastLine = content.mid(line.textStart()); + QString elidedLastLine = fontMetrics.elidedText(lastLine, Qt::ElideRight, width()); + painter.drawText(QPoint(0, y + fontMetrics.ascent()), elidedLastLine); + line = textLayout.createLine(); + didElide = line.isValid(); + break; + } + } + textLayout.endLayout(); + + if (didElide != elided) { + elided = didElide; + emit elisionChanged(didElide); + } +} diff --git a/src/elidedlabel.h b/src/elidedlabel.h new file mode 100644 index 0000000..76df007 --- /dev/null +++ b/src/elidedlabel.h @@ -0,0 +1,32 @@ +#ifndef ELIDEDLABEL_H +#define ELIDEDLABEL_H + +#include + +class ElidedLabel : public QFrame +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText) + Q_PROPERTY(bool isElided READ isElided) + +public: + explicit ElidedLabel(const QString &text, QWidget *parent = 0); + explicit ElidedLabel(QWidget *parent = 0); + + void setText(const QString &text); + const QString & text() const { return content; } + bool isElided() const { return elided; } + +protected: + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + +signals: + void elisionChanged(bool elided); + +private: + bool elided; + QString content; +}; + + +#endif // ELIDEDLABEL_H diff --git a/src/main.cpp b/src/main.cpp index c7bbeb7..c51ac1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,7 @@ -#include "MainWindow.h" #include -#include -#include +#include + +#include "MainWindow.h" int main(int argc, char *argv[]) {