commit d9190ed75662dc35848a634bb54fc6d48142a9d7 Author: Carl Philipp Klemm Date: Wed Mar 4 18:55:12 2026 +0100 Inital commit 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; +}