Compare commits
10 commits
887e14add9
...
14f49aa3e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 14f49aa3e4 | |||
| 29d3a5b43b | |||
| e7a5125725 | |||
| b854188244 | |||
| fb257cbf1f | |||
| 439ebf71b4 | |||
| 6042b479a4 | |||
| ee30799ac7 | |||
| 18e1588d73 | |||
| 6eea5688a8 |
12 changed files with 690 additions and 643 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
70
src/elidedlabel.cpp
Normal 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
32
src/elidedlabel.h
Normal 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#include "MainWindow.h"
|
||||
#include <QApplication>
|
||||
#include <QStyleFactory>
|
||||
#include <QIcon>
|
||||
#include <QThread>
|
||||
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue