Add icons refactor repo structure
This commit is contained in:
parent
1fec61140c
commit
b719d8cf96
24 changed files with 317 additions and 594 deletions
251
src/AceStepWorker.cpp
Normal file
251
src/AceStepWorker.cpp
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#include "AceStepWorker.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
#include <QRandomGenerator>
|
||||
#include <cstdint>
|
||||
|
||||
AceStepWorker::AceStepWorker(QObject *parent)
|
||||
: QObject(parent),
|
||||
currentWorker(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
AceStepWorker::~AceStepWorker()
|
||||
{
|
||||
cancelGeneration();
|
||||
}
|
||||
|
||||
void AceStepWorker::generateSong(const SongItem& song, const QString &jsonTemplate,
|
||||
const QString &aceStepPath, const QString &qwen3ModelPath,
|
||||
const QString &textEncoderModelPath, const QString &ditModelPath,
|
||||
const QString &vaeModelPath)
|
||||
{
|
||||
// Cancel any ongoing generation
|
||||
cancelGeneration();
|
||||
|
||||
// Create worker and start it
|
||||
currentWorker = new Worker(this, song, jsonTemplate, aceStepPath, qwen3ModelPath,
|
||||
textEncoderModelPath, ditModelPath, vaeModelPath);
|
||||
currentWorker->setAutoDelete(true);
|
||||
QThreadPool::globalInstance()->start(currentWorker);
|
||||
}
|
||||
|
||||
void AceStepWorker::cancelGeneration()
|
||||
{
|
||||
currentWorker = nullptr;
|
||||
}
|
||||
|
||||
bool AceStepWorker::songGenerateing(SongItem* song)
|
||||
{
|
||||
workerMutex.lock();
|
||||
if(!currentWorker) {
|
||||
workerMutex.unlock();
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
SongItem workerSong = currentWorker->getSong();
|
||||
workerMutex.unlock();
|
||||
if(song)
|
||||
*song = workerSong;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
// Parse and modify the template
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError);
|
||||
if (!templateDoc.isObject()) {
|
||||
emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
// Check if binaries exist
|
||||
QFileInfo qwen3Info(qwen3Binary);
|
||||
QFileInfo ditVaeInfo(ditVaeBinary);
|
||||
|
||||
if (!qwen3Info.exists() || !qwen3Info.isExecutable()) {
|
||||
emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) {
|
||||
emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary);
|
||||
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)) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load lyrics from the enhanced request file
|
||||
QFile lmOutputFile(requestLmOutputFile);
|
||||
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QJsonParseError parseError;
|
||||
song.json = lmOutputFile.readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError);
|
||||
lmOutputFile.close();
|
||||
|
||||
if (doc.isObject() && !parseError.error) {
|
||||
QJsonObject obj = doc.object();
|
||||
if (obj.contains("lyrics") && obj["lyrics"].isString()) {
|
||||
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;
|
||||
|
||||
emit parent->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) {
|
||||
QString errorOutput = ditVaeProcess.readAllStandardError();
|
||||
emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
emit parent->progressUpdate(90);
|
||||
|
||||
// Find the generated WAV file
|
||||
QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav";
|
||||
if (!QFileInfo::exists(wavFile)) {
|
||||
emit parent->generationError("No WAV file generated at "+wavFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
const SongItem& AceStepWorker::Worker::getSong()
|
||||
{
|
||||
return song;
|
||||
}
|
||||
64
src/AceStepWorker.h
Normal file
64
src/AceStepWorker.h
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#ifndef ACESTEPWORKER_H
|
||||
#define ACESTEPWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QThreadPool>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
|
||||
#include "SongItem.h"
|
||||
|
||||
class AceStepWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AceStepWorker(QObject *parent = nullptr);
|
||||
~AceStepWorker();
|
||||
|
||||
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);
|
||||
|
||||
signals:
|
||||
void songGenerated(const SongItem& song);
|
||||
void generationFinished();
|
||||
void generationError(const QString &error);
|
||||
void progressUpdate(int percent);
|
||||
|
||||
private:
|
||||
class Worker : public QRunnable {
|
||||
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;
|
||||
QString aceStepPath;
|
||||
QString qwen3ModelPath;
|
||||
QString textEncoderModelPath;
|
||||
QString ditModelPath;
|
||||
QString vaeModelPath;
|
||||
};
|
||||
|
||||
QMutex workerMutex;
|
||||
Worker *currentWorker;
|
||||
};
|
||||
|
||||
#endif // ACESTEPWORKER_H
|
||||
118
src/AdvancedSettingsDialog.cpp
Normal file
118
src/AdvancedSettingsDialog.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#include "AdvancedSettingsDialog.h"
|
||||
#include "ui_AdvancedSettingsDialog.h"
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
AdvancedSettingsDialog::AdvancedSettingsDialog(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
ui(new Ui::AdvancedSettingsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
}
|
||||
|
||||
AdvancedSettingsDialog::~AdvancedSettingsDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getJsonTemplate() const
|
||||
{
|
||||
return ui->jsonTemplateEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getAceStepPath() const
|
||||
{
|
||||
return ui->aceStepPathEdit->text();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getQwen3ModelPath() const
|
||||
{
|
||||
return ui->qwen3ModelEdit->text();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getTextEncoderModelPath() const
|
||||
{
|
||||
return ui->textEncoderEdit->text();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getDiTModelPath() const
|
||||
{
|
||||
return ui->ditModelEdit->text();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getVAEModelPath() const
|
||||
{
|
||||
return ui->vaeModelEdit->text();
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setJsonTemplate(const QString &templateStr)
|
||||
{
|
||||
ui->jsonTemplateEdit->setPlainText(templateStr);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setAceStepPath(const QString &path)
|
||||
{
|
||||
ui->aceStepPathEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setQwen3ModelPath(const QString &path)
|
||||
{
|
||||
ui->qwen3ModelEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setTextEncoderModelPath(const QString &path)
|
||||
{
|
||||
ui->textEncoderEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setDiTModelPath(const QString &path)
|
||||
{
|
||||
ui->ditModelEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setVAEModelPath(const QString &path)
|
||||
{
|
||||
ui->vaeModelEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
|
||||
if (!dir.isEmpty()) {
|
||||
ui->aceStepPathEdit->setText(dir);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty()) {
|
||||
ui->qwen3ModelEdit->setText(file);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty()) {
|
||||
ui->textEncoderEdit->setText(file);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty()) {
|
||||
ui->ditModelEdit->setText(file);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_vaeBrowseButton_clicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty()) {
|
||||
ui->vaeModelEdit->setText(file);
|
||||
}
|
||||
}
|
||||
46
src/AdvancedSettingsDialog.h
Normal file
46
src/AdvancedSettingsDialog.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#ifndef ADVANCEDSETTINGSDIALOG_H
|
||||
#define ADVANCEDSETTINGSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
namespace Ui {
|
||||
class AdvancedSettingsDialog;
|
||||
}
|
||||
|
||||
class AdvancedSettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AdvancedSettingsDialog(QWidget *parent = nullptr);
|
||||
~AdvancedSettingsDialog();
|
||||
|
||||
// Getters for settings
|
||||
QString getJsonTemplate() const;
|
||||
QString getAceStepPath() const;
|
||||
QString getQwen3ModelPath() const;
|
||||
QString getTextEncoderModelPath() const;
|
||||
QString getDiTModelPath() const;
|
||||
QString getVAEModelPath() const;
|
||||
|
||||
// Setters for settings
|
||||
void setJsonTemplate(const QString &templateStr);
|
||||
void setAceStepPath(const QString &path);
|
||||
void setQwen3ModelPath(const QString &path);
|
||||
void setTextEncoderModelPath(const QString &path);
|
||||
void setDiTModelPath(const QString &path);
|
||||
void setVAEModelPath(const QString &path);
|
||||
|
||||
private slots:
|
||||
void on_aceStepBrowseButton_clicked();
|
||||
void on_qwen3BrowseButton_clicked();
|
||||
void on_textEncoderBrowseButton_clicked();
|
||||
void on_ditBrowseButton_clicked();
|
||||
void on_vaeBrowseButton_clicked();
|
||||
|
||||
private:
|
||||
Ui::AdvancedSettingsDialog *ui;
|
||||
};
|
||||
|
||||
#endif // ADVANCEDSETTINGSDIALOG_H
|
||||
203
src/AdvancedSettingsDialog.ui
Normal file
203
src/AdvancedSettingsDialog.ui
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AdvancedSettingsDialog</class>
|
||||
<widget class="QDialog" name="AdvancedSettingsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Advanced Settings</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="jsonTab">
|
||||
<attribute name="title">
|
||||
<string>JSON Template</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="jsonLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="jsonLabel">
|
||||
<property name="text">
|
||||
<string>JSON Template for AceStep generation:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="jsonTemplateEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pathsTab">
|
||||
<attribute name="title">
|
||||
<string>Model Paths</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="pathsLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="aceStepLabel">
|
||||
<property name="text">
|
||||
<string>AceStep Path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="aceStepLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="aceStepPathEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="aceStepBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="qwen3Label">
|
||||
<property name="text">
|
||||
<string>Qwen3 Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="qwen3Layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="qwen3ModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="qwen3BrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="textEncoderLabel">
|
||||
<property name="text">
|
||||
<string>Text Encoder Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="textEncoderLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="textEncoderEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="textEncoderBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="ditLabel">
|
||||
<property name="text">
|
||||
<string>DiT Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="ditLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="ditModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ditBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="vaeLabel">
|
||||
<property name="text">
|
||||
<string>VAE Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="vaeLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="vaeModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="vaeBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
114
src/AudioPlayer.cpp
Normal file
114
src/AudioPlayer.cpp
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#include "AudioPlayer.h"
|
||||
#include <QDebug>
|
||||
|
||||
AudioPlayer::AudioPlayer(QObject *parent)
|
||||
: QObject(parent),
|
||||
mediaPlayer(new QMediaPlayer(this)),
|
||||
audioOutput(new QAudioOutput(this)),
|
||||
positionTimer(new QTimer(this))
|
||||
{
|
||||
// Set up audio output with default device
|
||||
mediaPlayer->setAudioOutput(audioOutput);
|
||||
|
||||
connect(mediaPlayer, &QMediaPlayer::playbackStateChanged,
|
||||
this, &AudioPlayer::handlePlaybackStateChanged);
|
||||
connect(mediaPlayer, &QMediaPlayer::mediaStatusChanged,
|
||||
this, &AudioPlayer::handleMediaStatusChanged);
|
||||
|
||||
// Set up position timer for updating playback position
|
||||
positionTimer->setInterval(500); // Update every 500ms
|
||||
connect(positionTimer, &QTimer::timeout, [this]() {
|
||||
if (isPlaying()) {
|
||||
emit positionChanged(mediaPlayer->position());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AudioPlayer::~AudioPlayer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void AudioPlayer::play(const QString &filePath)
|
||||
{
|
||||
if (isPlaying()) {
|
||||
stop();
|
||||
}
|
||||
|
||||
mediaPlayer->setSource(QUrl::fromLocalFile(filePath));
|
||||
mediaPlayer->play();
|
||||
|
||||
// Start position timer
|
||||
positionTimer->start();
|
||||
}
|
||||
|
||||
void AudioPlayer::play()
|
||||
{
|
||||
if (!isPlaying()) {
|
||||
mediaPlayer->play();
|
||||
positionTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::pause()
|
||||
{
|
||||
if (isPlaying()) {
|
||||
mediaPlayer->pause();
|
||||
positionTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::setPosition(int position)
|
||||
{
|
||||
mediaPlayer->setPosition(position);
|
||||
}
|
||||
|
||||
void AudioPlayer::stop()
|
||||
{
|
||||
mediaPlayer->stop();
|
||||
positionTimer->stop();
|
||||
}
|
||||
|
||||
bool AudioPlayer::isPlaying() const
|
||||
{
|
||||
return mediaPlayer->playbackState() == QMediaPlayer::PlayingState;
|
||||
}
|
||||
|
||||
int AudioPlayer::duration() const
|
||||
{
|
||||
return mediaPlayer->duration();
|
||||
}
|
||||
|
||||
int AudioPlayer::position() const
|
||||
{
|
||||
return mediaPlayer->position();
|
||||
}
|
||||
|
||||
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
|
||||
{
|
||||
if (state == QMediaPlayer::PlayingState) {
|
||||
emit playbackStarted();
|
||||
} else if (state == QMediaPlayer::StoppedState ||
|
||||
state == QMediaPlayer::PausedState) {
|
||||
// Check if we reached the end
|
||||
if (mediaPlayer->position() >= mediaPlayer->duration() - 100) {
|
||||
emit playbackFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::handleMediaStatusChanged(QMediaPlayer::MediaStatus status)
|
||||
{
|
||||
if (status == QMediaPlayer::EndOfMedia) {
|
||||
emit playbackFinished();
|
||||
} else if (status == QMediaPlayer::LoadedMedia ||
|
||||
status == QMediaPlayer::BufferedMedia) {
|
||||
// Media loaded successfully, emit duration
|
||||
int duration = mediaPlayer->duration();
|
||||
if (duration > 0) {
|
||||
emit durationChanged(duration);
|
||||
}
|
||||
} else if (status == QMediaPlayer::InvalidMedia) {
|
||||
emit playbackError(mediaPlayer->errorString());
|
||||
}
|
||||
}
|
||||
46
src/AudioPlayer.h
Normal file
46
src/AudioPlayer.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#ifndef AUDIOPLAYER_H
|
||||
#define AUDIOPLAYER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMediaPlayer>
|
||||
#include <QAudioOutput>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <QMediaDevices>
|
||||
#include <QAudioDevice>
|
||||
#include <QTimer>
|
||||
|
||||
class AudioPlayer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AudioPlayer(QObject *parent = nullptr);
|
||||
~AudioPlayer();
|
||||
|
||||
void play(const QString &filePath);
|
||||
void play();
|
||||
void stop();
|
||||
void pause();
|
||||
void setPosition(int position);
|
||||
bool isPlaying() const;
|
||||
int duration() const;
|
||||
int position() const;
|
||||
|
||||
signals:
|
||||
void playbackStarted();
|
||||
void playbackFinished();
|
||||
void playbackError(const QString &error);
|
||||
void positionChanged(int position);
|
||||
void durationChanged(int duration);
|
||||
|
||||
private slots:
|
||||
void handlePlaybackStateChanged(QMediaPlayer::PlaybackState state);
|
||||
void handleMediaStatusChanged(QMediaPlayer::MediaStatus status);
|
||||
|
||||
private:
|
||||
QMediaPlayer *mediaPlayer;
|
||||
QAudioOutput *audioOutput;
|
||||
QTimer *positionTimer;
|
||||
};
|
||||
|
||||
#endif // AUDIOPLAYER_H
|
||||
736
src/MainWindow.cpp
Normal file
736
src/MainWindow.cpp
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
#include "MainWindow.h"
|
||||
#include "ui_MainWindow.h"
|
||||
#include "SongDialog.h"
|
||||
#include "AdvancedSettingsDialog.h"
|
||||
#include <QMessageBox>
|
||||
#include <QInputDialog>
|
||||
#include <QFileDialog>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
#include <QTextEdit>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent),
|
||||
ui(new Ui::MainWindow),
|
||||
songModel(new SongListModel(this)),
|
||||
audioPlayer(new AudioPlayer(this)),
|
||||
aceStepWorker(new AceStepWorker(this)),
|
||||
playbackTimer(new QTimer(this)),
|
||||
isPlaying(false),
|
||||
isPaused(false),
|
||||
shuffleMode(false),
|
||||
isGeneratingNext(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Setup lyrics display
|
||||
ui->lyricsTextEdit->setReadOnly(true);
|
||||
|
||||
// Setup UI
|
||||
setupUI();
|
||||
|
||||
// Load settings
|
||||
loadSettings();
|
||||
|
||||
// Auto-load playlist from config location on startup
|
||||
autoLoadPlaylist();
|
||||
|
||||
// Connect signals and slots
|
||||
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked);
|
||||
connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::on_actionSavePlaylist);
|
||||
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
|
||||
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
|
||||
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
|
||||
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();});
|
||||
connect(ui->actionClearPlaylist, &QAction::triggered, this, [this](){songModel->clear();});
|
||||
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
|
||||
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 double-click on song list for editing (works with QTableView too)
|
||||
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||
|
||||
// Connect audio player error signal
|
||||
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) {
|
||||
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
|
||||
});
|
||||
|
||||
// Add some default songs
|
||||
if(songModel->songCount() == 0) {
|
||||
SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", "");
|
||||
SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", "");
|
||||
SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", "");
|
||||
|
||||
songModel->addSong(defaultSong1);
|
||||
songModel->addSong(defaultSong2);
|
||||
songModel->addSong(defaultSong3);
|
||||
}
|
||||
|
||||
// Select first item
|
||||
if (songModel->rowCount() > 0) {
|
||||
QModelIndex firstIndex = songModel->index(0, 0);
|
||||
ui->songListView->setCurrentIndex(firstIndex);
|
||||
}
|
||||
|
||||
currentSong = songModel->getSong(0);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
// Auto-save playlist before closing
|
||||
autoSavePlaylist();
|
||||
|
||||
saveSettings();
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MainWindow::setupUI()
|
||||
{
|
||||
// Setup song list view
|
||||
ui->songListView->setModel(songModel);
|
||||
|
||||
// Make sure the table view is read-only (no inline editing)
|
||||
ui->songListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
|
||||
// Hide headers for cleaner appearance
|
||||
ui->songListView->horizontalHeader()->hide();
|
||||
ui->songListView->verticalHeader()->hide();
|
||||
|
||||
// Configure column sizes
|
||||
ui->songListView->setColumnWidth(0, 40); // Fixed width for play indicator column
|
||||
ui->songListView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); // Expand caption column
|
||||
|
||||
// Enable row selection and disable column selection
|
||||
ui->songListView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
}
|
||||
|
||||
void MainWindow::loadSettings()
|
||||
{
|
||||
QSettings settings("MusicGenerator", "AceStepGUI");
|
||||
|
||||
// Load JSON template (default to simple configuration)
|
||||
jsonTemplate = settings.value("jsonTemplate", "{\n\t\"inference_steps\": 8,\n\t\"shift\": 3.0,\n\t\"vocal_language\": \"en\"\n}").toString();
|
||||
|
||||
// Load shuffle mode
|
||||
shuffleMode = settings.value("shuffleMode", false).toBool();
|
||||
ui->shuffleButton->setChecked(shuffleMode);
|
||||
|
||||
// Load path settings with defaults based on application directory
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
|
||||
qwen3ModelPath = settings.value("qwen3ModelPath", appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
|
||||
textEncoderModelPath = settings.value("textEncoderModelPath", appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString();
|
||||
ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
|
||||
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
|
||||
}
|
||||
|
||||
void MainWindow::saveSettings()
|
||||
{
|
||||
QSettings settings("MusicGenerator", "AceStepGUI");
|
||||
|
||||
// Save JSON template
|
||||
settings.setValue("jsonTemplate", jsonTemplate);
|
||||
|
||||
// Save shuffle mode
|
||||
settings.setValue("shuffleMode", shuffleMode);
|
||||
|
||||
// Save path settings
|
||||
settings.setValue("aceStepPath", aceStepPath);
|
||||
settings.setValue("qwen3ModelPath", qwen3ModelPath);
|
||||
settings.setValue("textEncoderModelPath", textEncoderModelPath);
|
||||
settings.setValue("ditModelPath", ditModelPath);
|
||||
settings.setValue("vaeModelPath", vaeModelPath);
|
||||
}
|
||||
|
||||
QString MainWindow::formatTime(int milliseconds)
|
||||
{
|
||||
if (milliseconds < 0)
|
||||
return "0:00";
|
||||
|
||||
int seconds = milliseconds / 1000;
|
||||
int minutes = seconds / 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
return QString("%1:%2").arg(minutes).arg(seconds, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
void MainWindow::updatePosition(int position)
|
||||
{
|
||||
if (position < 0)
|
||||
return;
|
||||
|
||||
// Update slider and time labels
|
||||
ui->positionSlider->setValue(position);
|
||||
ui->elapsedTimeLabel->setText(formatTime(position));
|
||||
}
|
||||
|
||||
void MainWindow::updateDuration(int duration)
|
||||
{
|
||||
if (duration <= 0)
|
||||
return;
|
||||
|
||||
// Set slider range and update duration label
|
||||
ui->positionSlider->setRange(0, duration);
|
||||
ui->durationLabel->setText(formatTime(duration));
|
||||
}
|
||||
|
||||
void MainWindow::updateControls()
|
||||
{
|
||||
bool hasSongs = songModel->rowCount() > 0;
|
||||
|
||||
// Play button is enabled when not playing, or can be used to resume when paused
|
||||
ui->playButton->setEnabled(hasSongs && (!isPlaying || isPaused));
|
||||
ui->pauseButton->setEnabled(isPlaying && !isPaused);
|
||||
ui->skipButton->setEnabled(isPlaying);
|
||||
ui->stopButton->setEnabled(isPlaying);
|
||||
ui->addSongButton->setEnabled(true);
|
||||
ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid());
|
||||
}
|
||||
|
||||
void MainWindow::on_playButton_clicked()
|
||||
{
|
||||
if (isPaused) {
|
||||
// Resume playback
|
||||
audioPlayer->play();
|
||||
isPaused = false;
|
||||
updateControls();
|
||||
return;
|
||||
}
|
||||
|
||||
if(songModel->empty())
|
||||
return;
|
||||
|
||||
isPlaying = true;
|
||||
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
|
||||
flushGenerationQueue();
|
||||
ensureSongsInQueue(true);
|
||||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::on_pauseButton_clicked()
|
||||
{
|
||||
if (isPlaying && !isPaused) {
|
||||
// Pause playback
|
||||
audioPlayer->pause();
|
||||
isPaused = true;
|
||||
updateControls();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_skipButton_clicked()
|
||||
{
|
||||
if (isPlaying) {
|
||||
audioPlayer->stop();
|
||||
isPaused = false;
|
||||
playNextSong();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_stopButton_clicked()
|
||||
{
|
||||
if (isPlaying) {
|
||||
// Stop current playback completely
|
||||
audioPlayer->stop();
|
||||
ui->nowPlayingLabel->setText("Now Playing:");
|
||||
isPlaying = false;
|
||||
isPaused = false;
|
||||
updateControls();
|
||||
flushGenerationQueue();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_shuffleButton_clicked()
|
||||
{
|
||||
shuffleMode = ui->shuffleButton->isChecked();
|
||||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::on_addSongButton_clicked()
|
||||
{
|
||||
SongDialog dialog(this);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QString caption = dialog.getCaption();
|
||||
QString lyrics = dialog.getLyrics();
|
||||
QString vocalLanguage = dialog.getVocalLanguage();
|
||||
|
||||
SongItem newSong(caption, lyrics);
|
||||
newSong.vocalLanguage = vocalLanguage;
|
||||
songModel->addSong(newSong);
|
||||
|
||||
// Select the new item
|
||||
QModelIndex newIndex = songModel->index(songModel->rowCount() - 1, 0);
|
||||
ui->songListView->setCurrentIndex(newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
isPlaying = true;
|
||||
updateControls();
|
||||
}
|
||||
|
||||
// Flush the generation queue when user selects a different song
|
||||
flushGenerationQueue();
|
||||
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);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QString caption = dialog.getCaption();
|
||||
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);
|
||||
}
|
||||
|
||||
void MainWindow::on_removeSongButton_clicked()
|
||||
{
|
||||
QModelIndex currentIndex = ui->songListView->currentIndex();
|
||||
if (!currentIndex.isValid()) return;
|
||||
|
||||
// Get the row from the current selection (works with table view)
|
||||
int row = currentIndex.row();
|
||||
|
||||
songModel->removeSong(row);
|
||||
|
||||
// Select next item or previous if at end
|
||||
int newRow = qMin(row, songModel->rowCount() - 1);
|
||||
if (newRow >= 0) {
|
||||
QModelIndex newIndex = songModel->index(newRow, 0);
|
||||
ui->songListView->setCurrentIndex(newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_advancedSettingsButton_clicked()
|
||||
{
|
||||
AdvancedSettingsDialog dialog(this);
|
||||
|
||||
// Set current values
|
||||
dialog.setJsonTemplate(jsonTemplate);
|
||||
dialog.setAceStepPath(aceStepPath);
|
||||
dialog.setQwen3ModelPath(qwen3ModelPath);
|
||||
dialog.setTextEncoderModelPath(textEncoderModelPath);
|
||||
dialog.setDiTModelPath(ditModelPath);
|
||||
dialog.setVAEModelPath(vaeModelPath);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
// Validate JSON template
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(dialog.getJsonTemplate().toUtf8(), &parseError);
|
||||
if (!doc.isObject()) {
|
||||
QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update settings
|
||||
jsonTemplate = dialog.getJsonTemplate();
|
||||
aceStepPath = dialog.getAceStepPath();
|
||||
qwen3ModelPath = dialog.getQwen3ModelPath();
|
||||
textEncoderModelPath = dialog.getTextEncoderModelPath();
|
||||
ditModelPath = dialog.getDiTModelPath();
|
||||
vaeModelPath = dialog.getVAEModelPath();
|
||||
|
||||
saveSettings();
|
||||
QMessageBox::information(this, "Settings Saved", "Advanced settings have been saved successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::playbackStarted()
|
||||
{
|
||||
ensureSongsInQueue();
|
||||
}
|
||||
|
||||
void MainWindow::playSong(const SongItem& song)
|
||||
{
|
||||
currentSong = song;
|
||||
audioPlayer->play(song.file);
|
||||
songModel->setPlayingIndex(songModel->findSongIndexById(song.uniqueId));
|
||||
ui->nowPlayingLabel->setText("Now Playing: " + song.caption);
|
||||
ui->lyricsTextEdit->setPlainText(song.lyrics);
|
||||
ui->jsonTextEdit->setPlainText(song.json);
|
||||
}
|
||||
|
||||
void MainWindow::songGenerated(const SongItem& song)
|
||||
{
|
||||
isGeneratingNext = false;
|
||||
|
||||
if (!isPaused && isPlaying && !audioPlayer->isPlaying()) {
|
||||
playSong(song);
|
||||
}
|
||||
else {
|
||||
generatedSongQueue.enqueue(song);
|
||||
}
|
||||
ui->statusLabel->setText("idle");
|
||||
|
||||
ensureSongsInQueue();
|
||||
}
|
||||
|
||||
void MainWindow::playNextSong()
|
||||
{
|
||||
if (!isPlaying)
|
||||
return;
|
||||
|
||||
// Check if we have a pre-generated next song in the queue
|
||||
if (!generatedSongQueue.isEmpty()) {
|
||||
SongItem generatedSong = generatedSongQueue.dequeue();
|
||||
playSong(generatedSong);
|
||||
} else {
|
||||
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
|
||||
}
|
||||
|
||||
// Ensure we have songs in the queue for smooth playback
|
||||
ensureSongsInQueue();
|
||||
}
|
||||
|
||||
void MainWindow::generationError(const QString &error)
|
||||
{
|
||||
// Reset the generation flag on error
|
||||
isGeneratingNext = false;
|
||||
|
||||
// Show detailed error in a dialog with QTextEdit
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle("Generation Error");
|
||||
dialog.resize(600, 400);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(&dialog);
|
||||
|
||||
QLabel *errorLabel = new QLabel("Error occurred during music generation:");
|
||||
errorLabel->setStyleSheet("font-weight: bold; color: darkred;");
|
||||
layout->addWidget(errorLabel);
|
||||
|
||||
QTextEdit *errorTextEdit = new QTextEdit();
|
||||
errorTextEdit->setReadOnly(true);
|
||||
errorTextEdit->setPlainText(error);
|
||||
errorTextEdit->setLineWrapMode(QTextEdit::NoWrap);
|
||||
errorTextEdit->setFontFamily("Monospace");
|
||||
layout->addWidget(errorTextEdit);
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
|
||||
layout->addWidget(buttonBox);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
|
||||
dialog.exec();
|
||||
|
||||
isPlaying = false;
|
||||
isPaused = false;
|
||||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::updatePlaybackStatus(bool playing)
|
||||
{
|
||||
isPlaying = playing;
|
||||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::on_positionSlider_sliderMoved(int position)
|
||||
{
|
||||
if (isPlaying && audioPlayer->isPlaying()) {
|
||||
audioPlayer->setPosition(position);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
||||
{
|
||||
// Only generate more songs if we're playing and not already at capacity
|
||||
if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
SongItem lastSong;
|
||||
SongItem workerSong;
|
||||
if(aceStepWorker->songGenerateing(&workerSong))
|
||||
lastSong = workerSong;
|
||||
else if(!generatedSongQueue.empty())
|
||||
lastSong = generatedSongQueue.last();
|
||||
else
|
||||
lastSong = currentSong;
|
||||
|
||||
SongItem nextSong;
|
||||
if(enqeueCurrent) {
|
||||
nextSong = lastSong;
|
||||
}
|
||||
else {
|
||||
int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode);
|
||||
nextSong = songModel->getSong(nextIndex);
|
||||
}
|
||||
|
||||
isGeneratingNext = true;
|
||||
|
||||
ui->statusLabel->setText("Generateing: "+nextSong.caption);
|
||||
aceStepWorker->generateSong(nextSong, jsonTemplate,
|
||||
aceStepPath, qwen3ModelPath,
|
||||
textEncoderModelPath, ditModelPath,
|
||||
vaeModelPath);
|
||||
}
|
||||
|
||||
void MainWindow::flushGenerationQueue()
|
||||
{
|
||||
generatedSongQueue.clear();
|
||||
aceStepWorker->cancelGeneration();
|
||||
isGeneratingNext = false;
|
||||
}
|
||||
|
||||
// Playlist save/load methods
|
||||
void MainWindow::on_actionSavePlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
|
||||
"JSON Files (*.json);;All Files (*)");
|
||||
|
||||
if (!filePath.isEmpty()) {
|
||||
savePlaylist(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionLoadPlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||
"JSON Files (*.json);;All Files (*)");
|
||||
if (!filePath.isEmpty()) {
|
||||
songModel->clear();
|
||||
flushGenerationQueue();
|
||||
loadPlaylist(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionAppendPlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||
"JSON Files (*.json);;All Files (*)");
|
||||
if (!filePath.isEmpty()) {
|
||||
loadPlaylist(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionSaveSong()
|
||||
{
|
||||
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
|
||||
"JSON Files (*.json);;All Files (*)");
|
||||
if (!filePath.isEmpty()) {
|
||||
QJsonArray songsArray;
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument songDoc = QJsonDocument::fromJson(currentSong.json.toUtf8(), &parseError);
|
||||
if(parseError.error)
|
||||
return;
|
||||
songsArray.append(songDoc.object());
|
||||
|
||||
QJsonObject rootObj;
|
||||
rootObj["songs"] = songsArray;
|
||||
rootObj["version"] = "1.0";
|
||||
|
||||
QJsonDocument doc(rootObj);
|
||||
QByteArray jsonData = doc.toJson();
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return;
|
||||
QFile::copy(currentSong.file, filePath + ".wav");
|
||||
file.write(jsonData);
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::savePlaylist(const QString &filePath)
|
||||
{
|
||||
// Get current songs from the model
|
||||
QList<SongItem> songs;
|
||||
for (int i = 0; i < songModel->rowCount(); ++i) {
|
||||
songs.append(songModel->getSong(i));
|
||||
}
|
||||
|
||||
savePlaylistToJson(filePath, songs);
|
||||
}
|
||||
|
||||
void MainWindow::loadPlaylist(const QString& filePath)
|
||||
{
|
||||
QList<SongItem> songs;
|
||||
if (loadPlaylistFromJson(filePath, songs)) {
|
||||
// Add loaded songs
|
||||
for (const SongItem &song : songs) {
|
||||
songModel->addSong(song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::autoSavePlaylist()
|
||||
{
|
||||
QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
QString appConfigPath = configPath + "/MusicGenerator/AceStepGUI";
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
QDir().mkpath(appConfigPath);
|
||||
|
||||
QString filePath = appConfigPath + "/playlist.json";
|
||||
|
||||
// Get current songs from the model
|
||||
QList<SongItem> songs;
|
||||
for (int i = 0; i < songModel->rowCount(); ++i) {
|
||||
songs.append(songModel->getSong(i));
|
||||
}
|
||||
|
||||
savePlaylistToJson(filePath, songs);
|
||||
}
|
||||
|
||||
void MainWindow::autoLoadPlaylist()
|
||||
{
|
||||
QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
QString appConfigPath = configPath + "/MusicGenerator/AceStepGUI";
|
||||
QString filePath = appConfigPath + "/playlist.json";
|
||||
|
||||
// Check if the auto-save file exists
|
||||
if (QFile::exists(filePath)) {
|
||||
QList<SongItem> songs;
|
||||
if (loadPlaylistFromJson(filePath, songs)) {
|
||||
songModel->clear();
|
||||
for (const SongItem &song : songs)
|
||||
songModel->addSong(song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs)
|
||||
{
|
||||
QJsonArray songsArray;
|
||||
|
||||
for (const SongItem &song : songs) {
|
||||
QJsonObject songObj;
|
||||
songObj["caption"] = song.caption;
|
||||
songObj["lyrics"] = song.lyrics;
|
||||
songObj["vocalLanguage"] = song.vocalLanguage;
|
||||
songObj["uniqueId"] = static_cast<qint64>(song.uniqueId); // Store as qint64 for JSON compatibility
|
||||
songsArray.append(songObj);
|
||||
}
|
||||
|
||||
QJsonObject rootObj;
|
||||
rootObj["songs"] = songsArray;
|
||||
rootObj["version"] = "1.0";
|
||||
|
||||
QJsonDocument doc(rootObj);
|
||||
QByteArray jsonData = doc.toJson();
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
qWarning() << "Could not open file for writing:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(jsonData);
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qWarning() << "Could not open file for reading:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug()<<"Loading from"<<filePath;
|
||||
|
||||
QByteArray jsonData = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qWarning() << "JSON parse error:" << parseError.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!doc.isObject()) {
|
||||
qWarning() << "JSON root is not an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject rootObj = doc.object();
|
||||
|
||||
// Check for version compatibility
|
||||
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") {
|
||||
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!rootObj.contains("songs") || !rootObj["songs"].isArray()) {
|
||||
qWarning() << "Invalid playlist format: missing songs array";
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray songsArray = rootObj["songs"].toArray();
|
||||
|
||||
qDebug()<<"Loading"<<songsArray.size()<<"songs";
|
||||
|
||||
for (const QJsonValue &value : songsArray) {
|
||||
if (!value.isObject()) continue;
|
||||
|
||||
QJsonObject songObj = value.toObject();
|
||||
SongItem song;
|
||||
|
||||
if (songObj.contains("caption")) {
|
||||
song.caption = songObj["caption"].toString();
|
||||
}
|
||||
|
||||
if (songObj.contains("lyrics")) {
|
||||
song.lyrics = songObj["lyrics"].toString();
|
||||
}
|
||||
|
||||
// Load vocalLanguage if present
|
||||
if (songObj.contains("vocalLanguage")) {
|
||||
song.vocalLanguage = songObj["vocalLanguage"].toString();
|
||||
}
|
||||
|
||||
// Load uniqueId if present (for backward compatibility)
|
||||
if (songObj.contains("uniqueId")) {
|
||||
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
|
||||
} else {
|
||||
// Generate new ID for old playlists without uniqueId
|
||||
song.uniqueId = QRandomGenerator::global()->generate64();
|
||||
}
|
||||
|
||||
songs.append(song);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
106
src/MainWindow.h
Normal file
106
src/MainWindow.h
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QListWidgetItem>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTimer>
|
||||
#include <QQueue>
|
||||
#include <QPair>
|
||||
#include <cstdint>
|
||||
#include <QStandardPaths>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include "SongListModel.h"
|
||||
#include "AudioPlayer.h"
|
||||
#include "AceStepWorker.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui { class MainWindow; }
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
private slots:
|
||||
void on_playButton_clicked();
|
||||
void on_pauseButton_clicked();
|
||||
void on_skipButton_clicked();
|
||||
void on_stopButton_clicked();
|
||||
void on_shuffleButton_clicked();
|
||||
void on_positionSlider_sliderMoved(int position);
|
||||
void updatePosition(int position);
|
||||
void updateDuration(int duration);
|
||||
void on_addSongButton_clicked();
|
||||
void on_removeSongButton_clicked();
|
||||
void on_advancedSettingsButton_clicked();
|
||||
|
||||
void on_songListView_doubleClicked(const QModelIndex &index);
|
||||
|
||||
void songGenerated(const SongItem& song);
|
||||
void playNextSong();
|
||||
void playbackStarted();
|
||||
void updatePlaybackStatus(bool playing);
|
||||
void generationError(const QString &error);
|
||||
|
||||
void on_actionSavePlaylist();
|
||||
void on_actionLoadPlaylist();
|
||||
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();
|
||||
void loadPlaylist(const QString &filePath);
|
||||
void savePlaylist(const QString &filePath);
|
||||
void autoSavePlaylist();
|
||||
void autoLoadPlaylist();
|
||||
|
||||
void playSong(const SongItem& song);
|
||||
|
||||
bool savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs);
|
||||
bool loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs);
|
||||
|
||||
void setupUI();
|
||||
void updateControls();
|
||||
void ensureSongsInQueue(bool enqeueCurrent = false);
|
||||
void flushGenerationQueue();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
353
src/MainWindow.ui
Normal file
353
src/MainWindow.ui
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
<?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>
|
||||
<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>clickableslider.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
77
src/SongDialog.cpp
Normal file
77
src/SongDialog.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#include "SongDialog.h"
|
||||
#include "ui_SongDialog.h"
|
||||
#include <QMessageBox>
|
||||
|
||||
SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage)
|
||||
: QDialog(parent),
|
||||
ui(new Ui::SongDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Set initial values if provided
|
||||
if (!caption.isEmpty()) {
|
||||
ui->captionEdit->setPlainText(caption);
|
||||
}
|
||||
if (!lyrics.isEmpty()) {
|
||||
ui->lyricsEdit->setPlainText(lyrics);
|
||||
}
|
||||
|
||||
// Setup vocal language combo box
|
||||
ui->vocalLanguageCombo->addItem("--", ""); // Unset
|
||||
ui->vocalLanguageCombo->addItem("English (en)", "en");
|
||||
ui->vocalLanguageCombo->addItem("German (de)", "de");
|
||||
ui->vocalLanguageCombo->addItem("French (fr)", "fr");
|
||||
ui->vocalLanguageCombo->addItem("Spanish (es)", "es");
|
||||
ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja");
|
||||
ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh");
|
||||
ui->vocalLanguageCombo->addItem("Italian (it)", "it");
|
||||
ui->vocalLanguageCombo->addItem("Portuguese (pt)", "pt");
|
||||
ui->vocalLanguageCombo->addItem("Russian (ru)", "ru");
|
||||
|
||||
// Set current language if provided
|
||||
if (!vocalLanguage.isEmpty()) {
|
||||
int index = ui->vocalLanguageCombo->findData(vocalLanguage);
|
||||
if (index >= 0) {
|
||||
ui->vocalLanguageCombo->setCurrentIndex(index);
|
||||
}
|
||||
} else {
|
||||
ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset
|
||||
}
|
||||
}
|
||||
|
||||
SongDialog::~SongDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
QString SongDialog::getCaption() const
|
||||
{
|
||||
return ui->captionEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString SongDialog::getLyrics() const
|
||||
{
|
||||
return ui->lyricsEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString SongDialog::getVocalLanguage() const
|
||||
{
|
||||
return ui->vocalLanguageCombo->currentData().toString();
|
||||
}
|
||||
|
||||
void SongDialog::on_okButton_clicked()
|
||||
{
|
||||
// Validate that caption is not empty
|
||||
QString caption = getCaption();
|
||||
if (caption.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void SongDialog::on_cancelButton_clicked()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
31
src/SongDialog.h
Normal file
31
src/SongDialog.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#ifndef SONGDIALOG_H
|
||||
#define SONGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
namespace Ui {
|
||||
class SongDialog;
|
||||
}
|
||||
|
||||
class SongDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", const QString &vocalLanguage = "");
|
||||
~SongDialog();
|
||||
|
||||
QString getCaption() const;
|
||||
QString getLyrics() const;
|
||||
QString getVocalLanguage() const;
|
||||
|
||||
private slots:
|
||||
void on_okButton_clicked();
|
||||
void on_cancelButton_clicked();
|
||||
|
||||
private:
|
||||
Ui::SongDialog *ui;
|
||||
};
|
||||
|
||||
#endif // SONGDIALOG_H
|
||||
109
src/SongDialog.ui
Normal file
109
src/SongDialog.ui
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SongDialog</class>
|
||||
<widget class="QDialog" name="SongDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Song Details</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="captionLabel">
|
||||
<property name="text">
|
||||
<string>Caption:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="captionEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Enter song caption (e.g., "Upbeat pop rock anthem with driving electric guitars")</string>
|
||||
</property>
|
||||
<property name="maximumHeight">
|
||||
<number>80</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lyricsLabel">
|
||||
<property name="text">
|
||||
<string>Lyrics (optional):</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="lyricsEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Enter lyrics or leave empty for instrumental music</string>
|
||||
</property>
|
||||
<property name="minimumHeight">
|
||||
<number>150</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="vocalLanguageLabel">
|
||||
<property name="text">
|
||||
<string>Vocal Language:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="vocalLanguageCombo">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
20
src/SongItem.h
Normal file
20
src/SongItem.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
#include <QString>
|
||||
#include <QRandomGenerator>
|
||||
#include <cstdint>
|
||||
|
||||
class SongItem {
|
||||
public:
|
||||
QString caption;
|
||||
QString lyrics;
|
||||
uint64_t uniqueId;
|
||||
QString file;
|
||||
QString vocalLanguage;
|
||||
QString json;
|
||||
|
||||
inline SongItem(const QString &caption = "", const QString &lyrics = "")
|
||||
: caption(caption), lyrics(lyrics) {
|
||||
// Generate a unique ID using cryptographically secure random number
|
||||
uniqueId = QRandomGenerator::global()->generate64();
|
||||
}
|
||||
};
|
||||
206
src/SongListModel.cpp
Normal file
206
src/SongListModel.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#include "SongListModel.h"
|
||||
#include <QApplication>
|
||||
#include <QTime>
|
||||
#include <QRandomGenerator>
|
||||
#include <QDebug>
|
||||
#include <QFont>
|
||||
#include <QUuid>
|
||||
|
||||
SongListModel::SongListModel(QObject *parent)
|
||||
: QAbstractTableModel(parent),
|
||||
m_playingIndex(-1)
|
||||
{
|
||||
}
|
||||
|
||||
int SongListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return songList.size();
|
||||
}
|
||||
|
||||
int SongListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
// We have 3 columns: play indicator, song name, and vocal language (read-only)
|
||||
return 3;
|
||||
}
|
||||
|
||||
QVariant SongListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= songList.size())
|
||||
return QVariant();
|
||||
|
||||
const SongItem &song = songList[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
// Column 0: Play indicator column
|
||||
if (index.column() == 0) {
|
||||
return index.row() == m_playingIndex ? "▶" : "";
|
||||
}
|
||||
// Column 1: Song name
|
||||
else if (index.column() == 1) {
|
||||
return song.caption;
|
||||
}
|
||||
// Column 2: Vocal language
|
||||
else if (index.column() == 2) {
|
||||
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
|
||||
}
|
||||
break;
|
||||
case Qt::FontRole:
|
||||
// Make play indicator bold and larger
|
||||
if (index.column() == 0 && index.row() == m_playingIndex) {
|
||||
QFont font = QApplication::font();
|
||||
font.setBold(true);
|
||||
return font;
|
||||
}
|
||||
break;
|
||||
case Qt::TextAlignmentRole:
|
||||
// Center align the play indicator
|
||||
if (index.column() == 0) {
|
||||
return Qt::AlignCenter;
|
||||
}
|
||||
break;
|
||||
case CaptionRole:
|
||||
return song.caption;
|
||||
case LyricsRole:
|
||||
return song.lyrics;
|
||||
case VocalLanguageRole:
|
||||
return song.vocalLanguage;
|
||||
case IsPlayingRole:
|
||||
return index.row() == m_playingIndex;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (!index.isValid() || index.row() >= songList.size())
|
||||
return false;
|
||||
|
||||
SongItem &song = songList[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case CaptionRole:
|
||||
song.caption = value.toString();
|
||||
break;
|
||||
case LyricsRole:
|
||||
song.lyrics = value.toString();
|
||||
break;
|
||||
case VocalLanguageRole:
|
||||
song.vocalLanguage = value.toString();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, {role});
|
||||
return true;
|
||||
}
|
||||
|
||||
Qt::ItemFlags SongListModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
// Remove ItemIsEditable to prevent inline editing and double-click issues
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
void SongListModel::addSong(const SongItem &song)
|
||||
{
|
||||
beginInsertRows(QModelIndex(), songList.size(), songList.size());
|
||||
songList.append(song);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void SongListModel::removeSong(int index)
|
||||
{
|
||||
if (index >= 0 && index < songList.size()) {
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
songList.removeAt(index);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
|
||||
void SongListModel::clear()
|
||||
{
|
||||
beginRemoveRows(QModelIndex(), 0, songList.size()-1);
|
||||
songList.clear();
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
bool SongListModel::empty()
|
||||
{
|
||||
return songList.empty();
|
||||
}
|
||||
|
||||
SongItem SongListModel::getSong(int index) const
|
||||
{
|
||||
if (index >= 0 && index < songList.size()) {
|
||||
return songList[index];
|
||||
}
|
||||
return SongItem();
|
||||
}
|
||||
|
||||
QVariant SongListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
|
||||
// Hide headers since we don't need column titles
|
||||
return QVariant();
|
||||
}
|
||||
return QAbstractTableModel::headerData(section, orientation, role);
|
||||
}
|
||||
|
||||
void SongListModel::setPlayingIndex(int index)
|
||||
{
|
||||
int oldPlayingIndex = m_playingIndex;
|
||||
m_playingIndex = index;
|
||||
|
||||
// Update both the old and new playing indices to trigger UI updates
|
||||
if (oldPlayingIndex >= 0 && oldPlayingIndex < songList.size()) {
|
||||
emit dataChanged(this->index(oldPlayingIndex, 0), this->index(oldPlayingIndex, 0));
|
||||
}
|
||||
|
||||
if (index >= 0 && index < songList.size()) {
|
||||
emit dataChanged(this->index(index, 0), this->index(index, 0));
|
||||
}
|
||||
}
|
||||
|
||||
int SongListModel::songCount()
|
||||
{
|
||||
return songList.count();
|
||||
}
|
||||
|
||||
int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
|
||||
{
|
||||
if (songList.isEmpty())
|
||||
return -1;
|
||||
|
||||
if (shuffle) {
|
||||
// Simple random selection for shuffle mode
|
||||
QRandomGenerator generator;
|
||||
return generator.bounded(songList.size());
|
||||
}
|
||||
|
||||
// Sequential playback
|
||||
int nextIndex = currentIndex + 1;
|
||||
if (nextIndex >= songList.size()) {
|
||||
nextIndex = 0; // Loop back to beginning
|
||||
}
|
||||
|
||||
return nextIndex;
|
||||
}
|
||||
|
||||
int SongListModel::findSongIndexById(uint64_t uniqueId) const
|
||||
{
|
||||
for (int i = 0; i < songList.size(); ++i) {
|
||||
if (songList[i].uniqueId == uniqueId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1; // Song not found
|
||||
}
|
||||
57
src/SongListModel.h
Normal file
57
src/SongListModel.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#ifndef SONGLISTMODEL_H
|
||||
#define SONGLISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QRandomGenerator>
|
||||
#include <cstdint>
|
||||
|
||||
#include "SongItem.h"
|
||||
|
||||
class SongListModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
CaptionRole = Qt::UserRole + 1,
|
||||
LyricsRole = Qt::UserRole + 2,
|
||||
VocalLanguageRole = Qt::UserRole + 3,
|
||||
IsPlayingRole = Qt::UserRole + 4
|
||||
};
|
||||
|
||||
explicit SongListModel(QObject *parent = nullptr);
|
||||
|
||||
// Basic functionality:
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// Editable:
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
|
||||
// Add/remove songs
|
||||
void addSong(const SongItem &song);
|
||||
void removeSong(int index);
|
||||
SongItem getSong(int index) const;
|
||||
int findNextIndex(int currentIndex, bool shuffle = false) const;
|
||||
void clear();
|
||||
|
||||
// Playing indicator
|
||||
void setPlayingIndex(int index);
|
||||
int playingIndex() const { return m_playingIndex; }
|
||||
|
||||
// Find song by unique ID
|
||||
int findSongIndexById(uint64_t uniqueId) const;
|
||||
int songCount();
|
||||
bool empty();
|
||||
|
||||
private:
|
||||
QList<SongItem> songList;
|
||||
int m_playingIndex;
|
||||
};
|
||||
|
||||
#endif // SONGLISTMODEL_H
|
||||
60
src/clickableslider.cpp
Normal file
60
src/clickableslider.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#include "clickableslider.h"
|
||||
#include <QStyle>
|
||||
|
||||
ClickableSlider::ClickableSlider(QWidget *parent)
|
||||
: QSlider(Qt::Orientation::Horizontal, parent)
|
||||
{
|
||||
}
|
||||
|
||||
ClickableSlider::ClickableSlider(Qt::Orientation orientation, QWidget *parent)
|
||||
: QSlider(orientation, parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ClickableSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
int val = pixelPosToRangeValue(event->pos());
|
||||
|
||||
// Block signals temporarily to avoid infinite recursion
|
||||
blockSignals(true);
|
||||
setValue(val);
|
||||
blockSignals(false);
|
||||
|
||||
// Emit both valueChanged and sliderMoved signals for compatibility
|
||||
emit valueChanged(val);
|
||||
emit sliderMoved(val);
|
||||
} else {
|
||||
// Call base class implementation for other buttons
|
||||
QSlider::mousePressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
int ClickableSlider::pixelPosToRangeValue(const QPoint &pos)
|
||||
{
|
||||
QStyleOptionSlider opt;
|
||||
initStyleOption(&opt);
|
||||
|
||||
QRect gr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this);
|
||||
QRect sr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
|
||||
|
||||
int sliderLength;
|
||||
int sliderMin;
|
||||
int sliderMax;
|
||||
|
||||
if (orientation() == Qt::Horizontal) {
|
||||
sliderLength = sr.width();
|
||||
sliderMin = gr.x();
|
||||
sliderMax = gr.right() - sliderLength + 1;
|
||||
} else {
|
||||
sliderLength = sr.height();
|
||||
sliderMin = gr.y();
|
||||
sliderMax = gr.bottom() - sliderLength + 1;
|
||||
}
|
||||
|
||||
QPoint pr = pos - sr.center() + sr.topLeft();
|
||||
int p = orientation() == Qt::Horizontal ? pr.x() : pr.y();
|
||||
|
||||
return QStyle::sliderValueFromPosition(minimum(), maximum(), p - sliderMin,
|
||||
sliderMax - sliderMin, opt.upsideDown);
|
||||
}
|
||||
22
src/clickableslider.h
Normal file
22
src/clickableslider.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef CLICKABLESLIDER_H
|
||||
#define CLICKABLESLIDER_H
|
||||
|
||||
#include <QSlider>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QMouseEvent>
|
||||
|
||||
class ClickableSlider : public QSlider
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ClickableSlider(QWidget *parent = nullptr);
|
||||
explicit ClickableSlider(Qt::Orientation orientation, QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int pixelPosToRangeValue(const QPoint &pos);
|
||||
};
|
||||
|
||||
#endif // CLICKABLESLIDER_H
|
||||
16
src/main.cpp
Normal file
16
src/main.cpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#include "MainWindow.h"
|
||||
#include <QApplication>
|
||||
#include <QStyleFactory>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Set a modern style
|
||||
app.setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue