Compare commits

...

10 commits

12 changed files with 690 additions and 643 deletions

View file

@ -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)

View file

@ -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.

View file

@ -8,269 +8,205 @@
#include <QDebug>
#include <QCoreApplication>
#include <QRandomGenerator>
#include <cstdint>
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:"<<song.caption;
return false;
}
else
busy = true;
request = {song, QRandomGenerator::global()->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"<<request.requestLlmFilePath;
ditVaeArgs << "--text-encoder"<<request.textEncoderModelPath;
ditVaeArgs << "--dit"<<request.ditModelPath;
ditVaeArgs << "--vae"<<request.vaeModelPath;
emit parent->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;
}

View file

@ -2,64 +2,55 @@
#define ACESTEPWORKER_H
#include <QObject>
#include <QRunnable>
#include <QThreadPool>
#include <QString>
#include <QJsonObject>
#include <QMutex>
#include <QProcess>
#include <QStandardPaths>
#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

View file

@ -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<SongItem> &
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.");
}

View file

@ -7,7 +7,7 @@
#include <QTimer>
#include <QQueue>
#include <QPair>
#include <cstdint>
#include <QThread>
#include <QStandardPaths>
#include <QJsonArray>
#include <QJsonObject>
@ -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<SongItem> 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<SongItem> generatedSongQueue;
private:
void loadSettings();
void saveSettings();

View file

@ -1,359 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Aceradio</string>
</property>
<property name="windowIcon">
<iconset resource="../res/resources.qrc">
<normaloff>:/icons/xyz.uvos.aceradio.png</normaloff>:/icons/xyz.uvos.aceradio.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="mainTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="songsTab">
<attribute name="title">
<string>Songs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="songListView">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="infoTab">
<attribute name="title">
<string>Info</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPlainTextEdit" name="jsonTextEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="lyricsTab">
<attribute name="title">
<string>Lyrics</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="lyricsTextEdit">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="addSongButton">
<property name="text">
<string>Add Song</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeSongButton">
<property name="text">
<string>Remove Song</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="nowPlayingLabel">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>Now Playing:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="timeControlsLayout">
<item>
<widget class="QLabel" name="elapsedTimeLabel">
<property name="text">
<string>0:00</string>
</property>
</widget>
</item>
<item>
<widget class="ClickableSlider" name="positionSlider" native="true">
<property name="mouseTracking">
<bool>false</bool>
</property>
<property name="tabletTracking">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="durationLabel">
<property name="text">
<string>0:00</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="controlsFrame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="controlsLayout">
<item>
<widget class="QPushButton" name="playButton">
<property name="text">
<string>Play</string>
</property>
<property name="icon">
<iconset theme="media-playback-start"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pauseButton">
<property name="text">
<string>Pause</string>
</property>
<property name="icon">
<iconset theme="media-playback-pause"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="skipButton">
<property name="text">
<string>Skip</string>
</property>
<property name="icon">
<iconset theme="media-skip-forward"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>Stop</string>
</property>
<property name="icon">
<iconset theme="media-playback-stop"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="shuffleButton">
<property name="text">
<string>Shuffle</string>
</property>
<property name="icon">
<iconset theme="media-playlist-shuffle"/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="statusLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>32</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionSaveSong"/>
<addaction name="actionSavePlaylist"/>
<addaction name="actionLoadPlaylist"/>
<addaction name="actionAppendPlaylist"/>
<addaction name="actionClearPlaylist"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuSettings">
<property name="title">
<string>Settings</string>
</property>
<addaction name="actionAdvancedSettings"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuSettings"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionSavePlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentSaveAs"/>
</property>
<property name="text">
<string>Save Playlist</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionLoadPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
<property name="text">
<string>Load Playlist...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionAdvancedSettings">
<property name="text">
<string>Ace Step</string>
</property>
</action>
<action name="actionQuit">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
</property>
<property name="text">
<string>Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionClearPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::EditDelete"/>
</property>
<property name="text">
<string>Clear Playlist</string>
</property>
</action>
<action name="actionSaveSong">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentSaveAs"/>
</property>
<property name="text">
<string>Save Song</string>
</property>
</action>
<action name="actionAppendPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
<property name="text">
<string>Append Playlist</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ClickableSlider</class>
<extends>QWidget</extends>
<header>src/clickableslider.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../res/resources.qrc"/>
</resources>
<connections/>
</ui>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Aceradio</string>
</property>
<property name="windowIcon">
<iconset resource="../res/resources.qrc">
<normaloff>:/icons/xyz.uvos.aceradio.png</normaloff>:/icons/xyz.uvos.aceradio.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="mainTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="songsTab">
<attribute name="title">
<string>Songs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="songListView">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="infoTab">
<attribute name="title">
<string>Info</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPlainTextEdit" name="jsonTextEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="lyricsTab">
<attribute name="title">
<string>Lyrics</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="lyricsTextEdit">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="addSongButton">
<property name="text">
<string>Add Song</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeSongButton">
<property name="text">
<string>Remove Song</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="ElidedLabel" name="nowPlayingLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="timeControlsLayout">
<item>
<widget class="QLabel" name="elapsedTimeLabel">
<property name="text">
<string>0:00</string>
</property>
</widget>
</item>
<item>
<widget class="ClickableSlider" name="positionSlider" native="true">
<property name="mouseTracking">
<bool>false</bool>
</property>
<property name="tabletTracking">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="durationLabel">
<property name="text">
<string>0:00</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="controlsFrame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="controlsLayout">
<item>
<widget class="QPushButton" name="playButton">
<property name="text">
<string>Play</string>
</property>
<property name="icon">
<iconset theme="media-playback-start"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pauseButton">
<property name="text">
<string>Pause</string>
</property>
<property name="icon">
<iconset theme="media-playback-pause"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="skipButton">
<property name="text">
<string>Skip</string>
</property>
<property name="icon">
<iconset theme="media-skip-forward"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>Stop</string>
</property>
<property name="icon">
<iconset theme="media-playback-stop"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="shuffleButton">
<property name="text">
<string>Shuffle</string>
</property>
<property name="icon">
<iconset theme="media-playlist-shuffle"/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="statusLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>32</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionSaveSong"/>
<addaction name="actionSavePlaylist"/>
<addaction name="actionLoadPlaylist"/>
<addaction name="actionAppendPlaylist"/>
<addaction name="actionClearPlaylist"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuSettings">
<property name="title">
<string>Settings</string>
</property>
<addaction name="actionAdvancedSettings"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuSettings"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionSavePlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentSaveAs"/>
</property>
<property name="text">
<string>Save Playlist</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionLoadPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
<property name="text">
<string>Load Playlist...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionAdvancedSettings">
<property name="text">
<string>Ace Step</string>
</property>
</action>
<action name="actionQuit">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
</property>
<property name="text">
<string>Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionClearPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::EditDelete"/>
</property>
<property name="text">
<string>Clear Playlist</string>
</property>
</action>
<action name="actionSaveSong">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentSaveAs"/>
</property>
<property name="text">
<string>Save Song</string>
</property>
</action>
<action name="actionAppendPlaylist">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
<property name="text">
<string>Append Playlist</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ClickableSlider</class>
<extends>QWidget</extends>
<header>src/clickableslider.h</header>
</customwidget>
<customwidget>
<class>ElidedLabel</class>
<extends>QFrame</extends>
<header>src/elidedlabel.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../res/resources.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -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;

70
src/elidedlabel.cpp Normal file
View file

@ -0,0 +1,70 @@
#include "elidedlabel.h"
#include <QTextLayout>
#include <QPainter>
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);
}
}

32
src/elidedlabel.h Normal file
View file

@ -0,0 +1,32 @@
#ifndef ELIDEDLABEL_H
#define ELIDEDLABEL_H
#include <QFrame>
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

View file

@ -1,7 +1,7 @@
#include "MainWindow.h"
#include <QApplication>
#include <QStyleFactory>
#include <QIcon>
#include <QThread>
#include "MainWindow.h"
int main(int argc, char *argv[])
{