Inital commit

This commit is contained in:
Carl Philipp Klemm 2026-03-04 18:55:12 +01:00
commit d9190ed756
12 changed files with 1198 additions and 0 deletions

206
AceStepWorker.cpp Normal file
View file

@ -0,0 +1,206 @@
#include "AceStepWorker.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QDir>
#include <QStandardPaths>
#include <QDebug>
#include <QCoreApplication>
AceStepWorker::AceStepWorker(QObject *parent)
: QObject(parent),
currentWorker(nullptr)
{
}
AceStepWorker::~AceStepWorker()
{
cancelGeneration();
}
void AceStepWorker::generateSong(const QString &caption, const QString &lyrics, 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, caption, lyrics, jsonTemplate, aceStepPath, qwen3ModelPath,
textEncoderModelPath, ditModelPath, vaeModelPath);
QThreadPool::globalInstance()->start(currentWorker);
}
void AceStepWorker::cancelGeneration()
{
// Note: In a real implementation, we would need to implement proper cancellation
// For now, we just clear the reference
currentWorker = nullptr;
}
void AceStepWorker::workerFinished()
{
emit generationFinished();
}
// Worker implementation
void AceStepWorker::Worker::run()
{
// Create temporary JSON file for the request
QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QString requestFile = tempDir + "/request_" + QString::number(QCoreApplication::applicationPid()) + ".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"] = caption;
if (!lyrics.isEmpty()) {
requestObj["lyrics"] = lyrics;
} else {
// Remove lyrics field if empty to let the LLM generate them
requestObj.remove("lyrics");
}
// 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(30000)) { // 30 second timeout
qwen3Process.terminate();
qwen3Process.waitForFinished(5000);
emit parent->generationError("ace-qwen3 timed out after 30 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;
}
emit parent->progressUpdate(50);
// Step 2: Run dit-vae to generate audio
QProcess ditVaeProcess;
QStringList ditVaeArgs;
ditVaeArgs << "--request" << requestFile;
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
QDir requestDir(QFileInfo(requestFile).absolutePath());
QStringList wavFiles = requestDir.entryList(QStringList("request*.wav"), QDir::Files, QDir::Name);
if (wavFiles.isEmpty()) {
emit parent->generationError("No WAV file generated");
return;
}
QString wavFile = requestDir.absoluteFilePath(wavFiles.first());
// Clean up temporary files
QFile::remove(requestFile);
emit parent->progressUpdate(100);
emit parent->songGenerated(wavFile);
}

61
AceStepWorker.h Normal file
View file

@ -0,0 +1,61 @@
#ifndef ACESTEPWORKER_H
#define ACESTEPWORKER_H
#include <QObject>
#include <QRunnable>
#include <QThreadPool>
#include <QString>
#include <QJsonObject>
class AceStepWorker : public QObject
{
Q_OBJECT
public:
explicit AceStepWorker(QObject *parent = nullptr);
~AceStepWorker();
void generateSong(const QString &caption, const QString &lyrics, const QString &jsonTemplate,
const QString &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath);
void cancelGeneration();
signals:
void songGenerated(const QString &filePath);
void generationFinished();
void generationError(const QString &error);
void progressUpdate(int percent);
private slots:
void workerFinished();
private:
class Worker : public QRunnable {
public:
Worker(AceStepWorker *parent, const QString &caption, const QString &lyrics, const QString &jsonTemplate,
const QString &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath)
: parent(parent), caption(caption), lyrics(lyrics), jsonTemplate(jsonTemplate),
aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath),
textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath),
vaeModelPath(vaeModelPath) {}
void run() override;
private:
AceStepWorker *parent;
QString caption;
QString lyrics;
QString jsonTemplate;
QString aceStepPath;
QString qwen3ModelPath;
QString textEncoderModelPath;
QString ditModelPath;
QString vaeModelPath;
};
Worker *currentWorker;
};
#endif // ACESTEPWORKER_H

76
AudioPlayer.cpp Normal file
View file

@ -0,0 +1,76 @@
#include "AudioPlayer.h"
#include <QDebug>
AudioPlayer::AudioPlayer(QObject *parent)
: QObject(parent),
mediaPlayer(new QMediaPlayer(this)),
audioOutput(new QAudioOutput(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);
}
AudioPlayer::~AudioPlayer()
{
stop();
}
void AudioPlayer::play(const QString &filePath)
{
if (isPlaying()) {
stop();
}
mediaPlayer->setSource(QUrl::fromLocalFile(filePath));
mediaPlayer->play();
}
void AudioPlayer::stop()
{
mediaPlayer->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
} else if (status == QMediaPlayer::InvalidMedia) {
emit playbackError(mediaPlayer->errorString());
}
}

39
AudioPlayer.h Normal file
View file

@ -0,0 +1,39 @@
#ifndef AUDIOPLAYER_H
#define AUDIOPLAYER_H
#include <QObject>
#include <QMediaPlayer>
#include <QAudioOutput>
#include <QFileInfo>
#include <QString>
#include <QMediaDevices>
#include <QAudioDevice>
class AudioPlayer : public QObject
{
Q_OBJECT
public:
explicit AudioPlayer(QObject *parent = nullptr);
~AudioPlayer();
void play(const QString &filePath);
void stop();
bool isPlaying() const;
int duration() const;
int position() const;
signals:
void playbackStarted();
void playbackFinished();
void playbackError(const QString &error);
private slots:
void handlePlaybackStateChanged(QMediaPlayer::PlaybackState state);
void handleMediaStatusChanged(QMediaPlayer::MediaStatus status);
private:
QMediaPlayer *mediaPlayer;
QAudioOutput *audioOutput;
};
#endif // AUDIOPLAYER_H

49
CMakeLists.txt Normal file
View file

@ -0,0 +1,49 @@
cmake_minimum_required(VERSION 3.14)
project(MusicGeneratorGUI LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Qt packages
find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia REQUIRED)
# Include acestep.cpp as a subdirectory
#add_subdirectory(acestep.cpp)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
# Add executable
add_executable(MusicGeneratorGUI
main.cpp
MainWindow.ui
MainWindow.cpp
SongListModel.cpp
AudioPlayer.cpp
AceStepWorker.cpp
${MusicGeneratorGUI_H}
)
# UI file
target_include_directories(MusicGeneratorGUI PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Link libraries
target_link_libraries(MusicGeneratorGUI PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
acestep-core
)
# Include directories
target_include_directories(MusicGeneratorGUI PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/acestep.cpp/src
${CMAKE_CURRENT_SOURCE_DIR}/acestep.cpp/ggml/include
)
# Copy models directory if it exists
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/acestep.cpp/models")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/acestep.cpp/models" DESTINATION "${CMAKE_BINARY_DIR}")
endif()

433
MainWindow.cpp Normal file
View file

@ -0,0 +1,433 @@
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QSettings>
#include <QDebug>
#include <QTextEdit>
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include <QTabWidget>
#include <QLineEdit>
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)),
currentSongIndex(-1),
isPlaying(false),
shuffleMode(false)
{
ui->setupUi(this);
// Setup UI
setupUI();
// Load settings
loadSettings();
// Connect signals and slots
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked);
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
connect(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated);
connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError);
connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue);
// Connect audio player error signal
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) {
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
});
}
MainWindow::~MainWindow()
{
saveSettings();
delete ui;
}
void MainWindow::setupUI()
{
// Setup song list view
ui->songListView->setModel(songModel);
// Add some default songs
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);
}
}
void MainWindow::loadSettings()
{
QSettings settings("MusicGenerator", "AceStepGUI");
// Load JSON template (default to simple configuration)
jsonTemplate = settings.value("jsonTemplate",
"{\"inference_steps\": 8, \"shift\": 3.0, \"vocal_language\": \"en\"}").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);
}
void MainWindow::updateControls()
{
bool hasSongs = songModel->rowCount() > 0;
ui->playButton->setEnabled(hasSongs && !isPlaying);
ui->skipButton->setEnabled(isPlaying);
ui->addSongButton->setEnabled(true);
ui->editSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid());
ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid());
}
void MainWindow::on_playButton_clicked()
{
if (isPlaying) {
audioPlayer->stop();
isPlaying = false;
updateControls();
return;
}
// Start playback from current song or first song
int startIndex = ui->songListView->currentIndex().isValid()
? ui->songListView->currentIndex().row()
: 0;
currentSongIndex = startIndex;
generateAndPlayNext();
}
void MainWindow::on_skipButton_clicked()
{
if (isPlaying) {
// Stop current playback and move to next song
audioPlayer->stop();
playNextSong();
}
}
void MainWindow::on_shuffleButton_clicked()
{
shuffleMode = ui->shuffleButton->isChecked();
updateControls();
}
void MainWindow::on_addSongButton_clicked()
{
bool ok;
QString caption = QInputDialog::getText(this, "Add Song", "Enter song caption:", QLineEdit::Normal, "", &ok);
if (ok && !caption.isEmpty()) {
QString lyrics = QInputDialog::getMultiLineText(this, "Add Song", "Enter lyrics (optional):", "", &ok);
if (ok) {
SongItem newSong(caption, lyrics);
songModel->addSong(newSong);
// Select the new item
QModelIndex newIndex = songModel->index(songModel->rowCount() - 1, 0);
ui->songListView->setCurrentIndex(newIndex);
}
}
}
void MainWindow::on_editSongButton_clicked()
{
QModelIndex currentIndex = ui->songListView->currentIndex();
if (!currentIndex.isValid()) return;
int row = currentIndex.row();
SongItem song = songModel->getSong(row);
bool ok;
QString caption = QInputDialog::getText(this, "Edit Song", "Enter song caption:", QLineEdit::Normal, song.caption, &ok);
if (ok && !caption.isEmpty()) {
QString lyrics = QInputDialog::getMultiLineText(this, "Edit Song", "Enter lyrics (optional):", song.lyrics, &ok);
if (ok) {
SongItem editedSong(caption, lyrics);
// Update the model
songModel->setData(songModel->index(row, 0), caption, SongListModel::CaptionRole);
songModel->setData(songModel->index(row, 0), lyrics, SongListModel::LyricsRole);
}
}
}
void MainWindow::on_removeSongButton_clicked()
{
QModelIndex currentIndex = ui->songListView->currentIndex();
if (!currentIndex.isValid()) return;
int row = currentIndex.row();
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, "Remove Song", "Are you sure you want to remove this song?",
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
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()
{
// Create a dialog for advanced settings
QDialog dialog(this);
dialog.setWindowTitle("Advanced Settings");
dialog.resize(600, 400);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
// Tab widget for organized settings
QTabWidget *tabWidget = new QTabWidget(&dialog);
layout->addWidget(tabWidget);
// JSON Template tab
QWidget *jsonTab = new QWidget();
QVBoxLayout *jsonLayout = new QVBoxLayout(jsonTab);
QLabel *jsonLabel = new QLabel("JSON Template for AceStep generation:");
jsonLabel->setWordWrap(true);
jsonLayout->addWidget(jsonLabel);
QTextEdit *jsonTemplateEdit = new QTextEdit();
jsonTemplateEdit->setPlainText(jsonTemplate);
jsonTemplateEdit->setMinimumHeight(200);
jsonLayout->addWidget(jsonTemplateEdit);
QLabel *fieldsLabel = new QLabel("Available fields: caption, lyrics, instrumental, bpm, duration, keyscale, timesignature,\nvocal_language, seed, lm_temperature, lm_cfg_scale, lm_top_p, lm_top_k, lm_negative_prompt,\naudio_codes, inference_steps, guidance_scale, shift");
fieldsLabel->setWordWrap(true);
jsonLayout->addWidget(fieldsLabel);
tabWidget->addTab(jsonTab, "JSON Template");
// Path Settings tab
QWidget *pathsTab = new QWidget();
QFormLayout *pathsLayout = new QFormLayout(pathsTab);
pathsLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
QLineEdit *aceStepPathEdit = new QLineEdit(aceStepPath);
QPushButton *aceStepBrowseBtn = new QPushButton("Browse...");
connect(aceStepBrowseBtn, &QPushButton::clicked, [this, aceStepPathEdit]() {
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", aceStepPathEdit->text());
if (!dir.isEmpty()) {
aceStepPathEdit->setText(dir);
}
});
QHBoxLayout *aceStepLayout = new QHBoxLayout();
aceStepLayout->addWidget(aceStepPathEdit);
aceStepLayout->addWidget(aceStepBrowseBtn);
pathsLayout->addRow("AceStep Path:", aceStepLayout);
QLineEdit *qwen3ModelEdit = new QLineEdit(qwen3ModelPath);
QPushButton *qwen3BrowseBtn = new QPushButton("Browse...");
connect(qwen3BrowseBtn, &QPushButton::clicked, [this, qwen3ModelEdit]() {
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", qwen3ModelEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) {
qwen3ModelEdit->setText(file);
}
});
QHBoxLayout *qwen3Layout = new QHBoxLayout();
qwen3Layout->addWidget(qwen3ModelEdit);
qwen3Layout->addWidget(qwen3BrowseBtn);
pathsLayout->addRow("Qwen3 Model:", qwen3Layout);
QLineEdit *textEncoderEdit = new QLineEdit(textEncoderModelPath);
QPushButton *textEncoderBrowseBtn = new QPushButton("Browse...");
connect(textEncoderBrowseBtn, &QPushButton::clicked, [this, textEncoderEdit]() {
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", textEncoderEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) {
textEncoderEdit->setText(file);
}
});
QHBoxLayout *textEncoderLayout = new QHBoxLayout();
textEncoderLayout->addWidget(textEncoderEdit);
textEncoderLayout->addWidget(textEncoderBrowseBtn);
pathsLayout->addRow("Text Encoder Model:", textEncoderLayout);
QLineEdit *ditModelEdit = new QLineEdit(ditModelPath);
QPushButton *ditModelBrowseBtn = new QPushButton("Browse...");
connect(ditModelBrowseBtn, &QPushButton::clicked, [this, ditModelEdit]() {
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ditModelEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) {
ditModelEdit->setText(file);
}
});
QHBoxLayout *ditModelLayout = new QHBoxLayout();
ditModelLayout->addWidget(ditModelEdit);
ditModelLayout->addWidget(ditModelBrowseBtn);
pathsLayout->addRow("DiT Model:", ditModelLayout);
QLineEdit *vaeModelEdit = new QLineEdit(vaeModelPath);
QPushButton *vaeModelBrowseBtn = new QPushButton("Browse...");
connect(vaeModelBrowseBtn, &QPushButton::clicked, [this, vaeModelEdit]() {
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", vaeModelEdit->text(), "GGUF Files (*.gguf)");
if (!file.isEmpty()) {
vaeModelEdit->setText(file);
}
});
QHBoxLayout *vaeModelLayout = new QHBoxLayout();
vaeModelLayout->addWidget(vaeModelEdit);
vaeModelLayout->addWidget(vaeModelBrowseBtn);
pathsLayout->addRow("VAE Model:", vaeModelLayout);
tabWidget->addTab(pathsTab, "Model Paths");
// Buttons
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, [&dialog, this, jsonTemplateEdit, aceStepPathEdit, qwen3ModelEdit, textEncoderEdit, ditModelEdit, vaeModelEdit]() {
// Validate JSON template
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonTemplateEdit->toPlainText().toUtf8(), &parseError);
if (!doc.isObject()) {
QMessageBox::warning(this, "Invalid JSON", "Please enter valid JSON: " + QString(parseError.errorString()));
return;
}
// Update settings
jsonTemplate = jsonTemplateEdit->toPlainText();
aceStepPath = aceStepPathEdit->text();
qwen3ModelPath = qwen3ModelEdit->text();
textEncoderModelPath = textEncoderEdit->text();
ditModelPath = ditModelEdit->text();
vaeModelPath = vaeModelEdit->text();
saveSettings();
dialog.accept();
});
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
// Show the dialog
if (dialog.exec() == QDialog::Accepted) {
QMessageBox::information(this, "Settings Saved", "Advanced settings have been saved successfully.");
}
}
void MainWindow::generateAndPlayNext()
{
if (currentSongIndex < 0 || currentSongIndex >= songModel->rowCount()) {
return;
}
SongItem song = songModel->getSong(currentSongIndex);
// Show status
ui->statusLabel->setText("Generating: " + song.caption);
isPlaying = true;
updateControls();
// Generate the song with configurable paths
aceStepWorker->generateSong(song.caption, song.lyrics, jsonTemplate,
aceStepPath, qwen3ModelPath,
textEncoderModelPath, ditModelPath,
vaeModelPath);
}
void MainWindow::songGenerated(const QString &filePath)
{
if (!QFile::exists(filePath)) {
generationError("Generated file not found: " + filePath);
return;
}
ui->statusLabel->setText("Playing: " + QFileInfo(filePath).baseName());
// Play the generated song
audioPlayer->play(filePath);
}
void MainWindow::playNextSong()
{
if (!isPlaying) return;
// Find next song index
int nextIndex = songModel->findNextIndex(currentSongIndex, shuffleMode);
if (nextIndex >= 0 && nextIndex < songModel->rowCount()) {
currentSongIndex = nextIndex;
generateAndPlayNext();
} else {
// No more songs
isPlaying = false;
ui->statusLabel->setText("Finished playback");
updateControls();
}
}
void MainWindow::generationError(const QString &error)
{
QMessageBox::critical(this, "Generation Error", "Failed to generate song: " + error);
isPlaying = false;
ui->statusLabel->setText("Error: " + error);
updateControls();
}
void MainWindow::generationFinished()
{
// This slot is declared but not used in the current implementation
// It's here for potential future use
}
void MainWindow::updatePlaybackStatus(bool playing)
{
isPlaying = playing;
updateControls();
}

65
MainWindow.h Normal file
View file

@ -0,0 +1,65 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QListWidgetItem>
#include <QStandardItemModel>
#include <QTimer>
#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_skipButton_clicked();
void on_shuffleButton_clicked();
void on_addSongButton_clicked();
void on_editSongButton_clicked();
void on_removeSongButton_clicked();
void on_advancedSettingsButton_clicked();
void songGenerated(const QString &filePath);
void playNextSong();
void updatePlaybackStatus(bool playing);
void generationFinished();
void generationError(const QString &error);
private:
Ui::MainWindow *ui;
SongListModel *songModel;
AudioPlayer *audioPlayer;
AceStepWorker *aceStepWorker;
QTimer *playbackTimer;
int currentSongIndex;
bool isPlaying;
bool shuffleMode;
QString jsonTemplate;
// Path settings
QString aceStepPath;
QString qwen3ModelPath;
QString textEncoderModelPath;
QString ditModelPath;
QString vaeModelPath;
void loadSettings();
void saveSettings();
void setupUI();
void updateControls();
void generateAndPlayNext();
};
#endif // MAINWINDOW_H

88
README.md Normal file
View file

@ -0,0 +1,88 @@
# Music Generator GUI
A Qt-based graphical user interface for generating music using acestep.cpp.
## Features
- **Song List Management**: Add, edit, and remove songs with captions and optional lyrics
- **Playback Controls**: Play, skip, and shuffle functionality
- **Settings Tab**: Customize the JSON template for AceStep generation parameters
- **Progress Tracking**: Visual progress bar during music generation
- **Seamless Playback**: Automatically generates and plays the next song when current one finishes
## Requirements
- Qt 5 or Qt 6 (with Core, Gui, Widgets, and Multimedia modules)
- CMake 3.14+
- acestep.cpp properly built with models downloaded
## Building
### Build acestep.cpp first:
```bash
cd acestep.cpp
git submodule update --init
mkdir build && cd build
cmake .. -DGGML_BLAS=ON # or other backend options
cmake --build . --config Release -j$(nproc)
./models.sh # Download models (requires ~7.7 GB free space)
```
### Build the GUI application:
```bash
cd ..
mkdir build && cd build
cmake ..
cmake --build . --config Release -j$(nproc)
```
## Usage
1. **Add Songs**: Click "Add Song" to create new song entries with captions and optional lyrics
2. **Edit Songs**: Select a song and click "Edit Song" to modify it
3. **Remove Songs**: Select a song and click "Remove Song" to delete it
4. **Play Music**: Click "Play" to start generating and playing music from the selected song or first song in the list
5. **Skip Songs**: Click "Skip" to move to the next song immediately
6. **Shuffle Mode**: Toggle "Shuffle" to play songs in random order
7. **Settings**: Click "Settings" in the menu bar to edit the JSON template for generation parameters
## Settings (JSON Template)
The JSON template allows you to customize AceStep generation parameters:
```json
{
"inference_steps": 8,
"shift": 3.0,
"vocal_language": "en",
"lm_temperature": 0.85,
"lm_cfg_scale": 2.0,
"lm_top_p": 0.9
}
```
Available fields:
- `caption` (required, will be overridden by song entry)
- `lyrics` (optional, can be empty to let LLM generate)
- `instrumental` (boolean)
- `bpm` (integer)
- `duration` (float in seconds)
- `keyscale` (string like "C major")
- `timesignature` (string like "4/4")
- `vocal_language` (string like "en", "fr", etc.)
- `seed` (integer for reproducibility)
- `lm_temperature`, `lm_cfg_scale`, `lm_top_p`, `lm_top_k` (LM generation parameters)
- `lm_negative_prompt` (string)
- `audio_codes` (string, for advanced users)
- `inference_steps` (integer)
- `guidance_scale` (float)
- `shift` (float)
## Notes
- The first time you generate a song, it may take several minutes as the models load into memory
- Generated WAV files are created in your system's temporary directory and played immediately
- Shuffle mode uses simple random selection without replacement within a playback session
- Skip button works even during generation - it will wait for current generation to finish then play next song

108
SongListModel.cpp Normal file
View file

@ -0,0 +1,108 @@
#include "SongListModel.h"
#include <QTime>
#include <QRandomGenerator>
#include <QDebug>
SongListModel::SongListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
int SongListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return songList.size();
}
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:
case CaptionRole:
return song.caption;
case LyricsRole:
return song.lyrics;
default:
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;
default:
return false;
}
emit dataChanged(index, index, {role});
return true;
}
Qt::ItemFlags SongListModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
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();
}
}
SongItem SongListModel::getSong(int index) const
{
if (index >= 0 && index < songList.size()) {
return songList[index];
}
return SongItem();
}
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;
}

47
SongListModel.h Normal file
View file

@ -0,0 +1,47 @@
#ifndef SONGLISTMODEL_H
#define SONGLISTMODEL_H
#include <QAbstractListModel>
#include <QList>
#include <QString>
class SongItem {
public:
QString caption;
QString lyrics;
SongItem(const QString &caption = "", const QString &lyrics = "")
: caption(caption), lyrics(lyrics) {}
};
class SongListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
CaptionRole = Qt::UserRole + 1,
LyricsRole = Qt::UserRole + 2
};
explicit SongListModel(QObject *parent = nullptr);
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, 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;
private:
QList<SongItem> songList;
};
#endif // SONGLISTMODEL_H

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

10
test_compilation.cpp Normal file
View file

@ -0,0 +1,10 @@
// Test compilation of header files
#include "../MainWindow.h"
#include "../SongListModel.h"
#include "../AudioPlayer.h"
#include "../AceStepWorker.h"
int main() {
// This file just tests if all headers compile correctly
return 0;
}