Add icons refactor repo structure

This commit is contained in:
Carl Philipp Klemm 2026-03-05 23:46:29 +01:00
parent 1fec61140c
commit b719d8cf96
24 changed files with 317 additions and 594 deletions

251
src/AceStepWorker.cpp Normal file
View 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
View 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

View 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);
}
}

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}