Inital commit
This commit is contained in:
commit
d9190ed756
12 changed files with 1198 additions and 0 deletions
206
AceStepWorker.cpp
Normal file
206
AceStepWorker.cpp
Normal 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
61
AceStepWorker.h
Normal 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
76
AudioPlayer.cpp
Normal 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
39
AudioPlayer.h
Normal 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
49
CMakeLists.txt
Normal 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
433
MainWindow.cpp
Normal 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
65
MainWindow.h
Normal 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
88
README.md
Normal 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
108
SongListModel.cpp
Normal 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
47
SongListModel.h
Normal 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
16
main.cpp
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#include "MainWindow.h"
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QStyleFactory>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
// Set a modern style
|
||||||
|
app.setStyle(QStyleFactory::create("Fusion"));
|
||||||
|
|
||||||
|
MainWindow window;
|
||||||
|
window.show();
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
10
test_compilation.cpp
Normal file
10
test_compilation.cpp
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue