From d9190ed75662dc35848a634bb54fc6d48142a9d7 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 4 Mar 2026 18:55:12 +0100 Subject: [PATCH] Inital commit --- AceStepWorker.cpp | 206 ++++++++++++++++++++ AceStepWorker.h | 61 ++++++ AudioPlayer.cpp | 76 ++++++++ AudioPlayer.h | 39 ++++ CMakeLists.txt | 49 +++++ MainWindow.cpp | 433 +++++++++++++++++++++++++++++++++++++++++++ MainWindow.h | 65 +++++++ README.md | 88 +++++++++ SongListModel.cpp | 108 +++++++++++ SongListModel.h | 47 +++++ main.cpp | 16 ++ test_compilation.cpp | 10 + 12 files changed, 1198 insertions(+) create mode 100644 AceStepWorker.cpp create mode 100644 AceStepWorker.h create mode 100644 AudioPlayer.cpp create mode 100644 AudioPlayer.h create mode 100644 CMakeLists.txt create mode 100644 MainWindow.cpp create mode 100644 MainWindow.h create mode 100644 README.md create mode 100644 SongListModel.cpp create mode 100644 SongListModel.h create mode 100644 main.cpp create mode 100644 test_compilation.cpp diff --git a/AceStepWorker.cpp b/AceStepWorker.cpp new file mode 100644 index 0000000..123ed19 --- /dev/null +++ b/AceStepWorker.cpp @@ -0,0 +1,206 @@ +#include "AceStepWorker.h" +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/AceStepWorker.h b/AceStepWorker.h new file mode 100644 index 0000000..81debd9 --- /dev/null +++ b/AceStepWorker.h @@ -0,0 +1,61 @@ +#ifndef ACESTEPWORKER_H +#define ACESTEPWORKER_H + +#include +#include +#include +#include +#include + +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 diff --git a/AudioPlayer.cpp b/AudioPlayer.cpp new file mode 100644 index 0000000..423de7d --- /dev/null +++ b/AudioPlayer.cpp @@ -0,0 +1,76 @@ +#include "AudioPlayer.h" +#include + +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()); + } +} diff --git a/AudioPlayer.h b/AudioPlayer.h new file mode 100644 index 0000000..8b12f97 --- /dev/null +++ b/AudioPlayer.h @@ -0,0 +1,39 @@ +#ifndef AUDIOPLAYER_H +#define AUDIOPLAYER_H + +#include +#include +#include +#include +#include +#include +#include + +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 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..939cdcc --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/MainWindow.cpp b/MainWindow.cpp new file mode 100644 index 0000000..9575393 --- /dev/null +++ b/MainWindow.cpp @@ -0,0 +1,433 @@ +#include "MainWindow.h" +#include "ui_MainWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/MainWindow.h b/MainWindow.h new file mode 100644 index 0000000..bfeb6c2 --- /dev/null +++ b/MainWindow.h @@ -0,0 +1,65 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b10aa6f --- /dev/null +++ b/README.md @@ -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 diff --git a/SongListModel.cpp b/SongListModel.cpp new file mode 100644 index 0000000..d13b0ae --- /dev/null +++ b/SongListModel.cpp @@ -0,0 +1,108 @@ +#include "SongListModel.h" +#include +#include +#include + +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; +} diff --git a/SongListModel.h b/SongListModel.h new file mode 100644 index 0000000..c23d26b --- /dev/null +++ b/SongListModel.h @@ -0,0 +1,47 @@ +#ifndef SONGLISTMODEL_H +#define SONGLISTMODEL_H + +#include +#include +#include + +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 songList; +}; + +#endif // SONGLISTMODEL_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..5b3a511 --- /dev/null +++ b/main.cpp @@ -0,0 +1,16 @@ +#include "MainWindow.h" +#include +#include + +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(); +} diff --git a/test_compilation.cpp b/test_compilation.cpp new file mode 100644 index 0000000..f92b540 --- /dev/null +++ b/test_compilation.cpp @@ -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; +}