From 1fec61140c1ee8dcf4e56710217f546665ef9572 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 5 Mar 2026 00:50:23 +0100 Subject: [PATCH] Song handling simplification Add languge field to songs Add lyric display --- AceStepWorker.cpp | 59 +++++++-- AceStepWorker.h | 25 ++-- CMakeLists.txt | 7 ++ MainWindow.cpp | 293 ++++++++++++++++++++----------------------- MainWindow.h | 26 ++-- MainWindow.ui | 80 ++++++++---- SongDialog.cpp | 29 ++++- SongDialog.h | 3 +- SongDialog.ui | 17 +++ SongItem.h | 19 +++ SongListModel.cpp | 29 ++++- SongListModel.h | 18 +-- test_compilation.cpp | 10 -- 13 files changed, 384 insertions(+), 231 deletions(-) create mode 100644 SongItem.h delete mode 100644 test_compilation.cpp diff --git a/AceStepWorker.cpp b/AceStepWorker.cpp index eaabd7c..c67eac3 100644 --- a/AceStepWorker.cpp +++ b/AceStepWorker.cpp @@ -21,7 +21,7 @@ AceStepWorker::~AceStepWorker() cancelGeneration(); } -void AceStepWorker::generateSong(const QString &caption, const QString &lyrics, const QString &jsonTemplate, +void AceStepWorker::generateSong(const SongItem& song, const QString &jsonTemplate, const QString &aceStepPath, const QString &qwen3ModelPath, const QString &textEncoderModelPath, const QString &ditModelPath, const QString &vaeModelPath) @@ -30,21 +30,31 @@ void AceStepWorker::generateSong(const QString &caption, const QString &lyrics, cancelGeneration(); // Create worker and start it - currentWorker = new Worker(this, caption, lyrics, jsonTemplate, aceStepPath, qwen3ModelPath, + currentWorker = new Worker(this, song, jsonTemplate, aceStepPath, qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath); + currentWorker->setAutoDelete(true); 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() +bool AceStepWorker::songGenerateing(SongItem* song) { - emit generationFinished(); + workerMutex.lock(); + if(!currentWorker) { + workerMutex.unlock(); + return false; + } + else { + SongItem workerSong = currentWorker->getSong(); + workerMutex.unlock(); + if(song) + *song = workerSong; + return true; + } } // Worker implementation @@ -64,15 +74,20 @@ void AceStepWorker::Worker::run() } QJsonObject requestObj = templateDoc.object(); - requestObj["caption"] = caption; + requestObj["caption"] = song.caption; - if (!lyrics.isEmpty()) { - requestObj["lyrics"] = lyrics; + if (!song.lyrics.isEmpty()) { + requestObj["lyrics"] = song.lyrics; } else { // Remove lyrics field if empty to let the LLM generate them requestObj.remove("lyrics"); } + // Apply vocal language override if set + if (!song.vocalLanguage.isEmpty()) { + requestObj["vocal_language"] = song.vocalLanguage; + } + // Write the request file QFile requestFileHandle(requestFile); if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -161,6 +176,21 @@ void AceStepWorker::Worker::run() return; } + // Load lyrics from the enhanced request file + QFile lmOutputFile(requestLmOutputFile); + if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(lmOutputFile.readAll(), &parseError); + lmOutputFile.close(); + + if (doc.isObject() && !parseError.error) { + QJsonObject obj = doc.object(); + if (obj.contains("lyrics") && obj["lyrics"].isString()) { + song.lyrics = obj["lyrics"].toString(); + } + } + } + emit parent->progressUpdate(50); // Step 2: Run dit-vae to generate audio @@ -207,5 +237,14 @@ void AceStepWorker::Worker::run() QFile::remove(requestFile); emit parent->progressUpdate(100); - emit parent->songGenerated(wavFile); + song.file = wavFile; + emit parent->songGenerated(song); + parent->workerMutex.lock(); + parent->currentWorker = nullptr; + parent->workerMutex.unlock(); +} + +const SongItem& AceStepWorker::Worker::getSong() +{ + return song; } diff --git a/AceStepWorker.h b/AceStepWorker.h index 81debd9..b407868 100644 --- a/AceStepWorker.h +++ b/AceStepWorker.h @@ -6,6 +6,9 @@ #include #include #include +#include + +#include "SongItem.h" class AceStepWorker : public QObject { @@ -14,39 +17,38 @@ public: explicit AceStepWorker(QObject *parent = nullptr); ~AceStepWorker(); - void generateSong(const QString &caption, const QString &lyrics, const QString &jsonTemplate, + void generateSong(const SongItem& song, const QString &jsonTemplate, const QString &aceStepPath, const QString &qwen3ModelPath, const QString &textEncoderModelPath, const QString &ditModelPath, const QString &vaeModelPath); void cancelGeneration(); + bool songGenerateing(SongItem* song); signals: - void songGenerated(const QString &filePath); + void songGenerated(const SongItem& song); void generationFinished(); void generationError(const QString &error); - void progressUpdate(int percent); - -private slots: - void workerFinished(); + void progressUpdate(int percent); private: class Worker : public QRunnable { public: - Worker(AceStepWorker *parent, const QString &caption, const QString &lyrics, const QString &jsonTemplate, + Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate, const QString &aceStepPath, const QString &qwen3ModelPath, const QString &textEncoderModelPath, const QString &ditModelPath, const QString &vaeModelPath) - : parent(parent), caption(caption), lyrics(lyrics), jsonTemplate(jsonTemplate), + : parent(parent), song(song), jsonTemplate(jsonTemplate), aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath), textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath), vaeModelPath(vaeModelPath) {} void run() override; + + const SongItem& getSong(); private: AceStepWorker *parent; - QString caption; - QString lyrics; + SongItem song; QString jsonTemplate; QString aceStepPath; QString qwen3ModelPath; @@ -54,7 +56,8 @@ private: QString ditModelPath; QString vaeModelPath; }; - + + QMutex workerMutex; Worker *currentWorker; }; diff --git a/CMakeLists.txt b/CMakeLists.txt index f33dd35..99f4bce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,13 +17,20 @@ add_executable(${PROJECT_NAME} main.cpp MainWindow.ui MainWindow.cpp + MainWindow.h AdvancedSettingsDialog.ui AdvancedSettingsDialog.cpp + AdvancedSettingsDialog.h SongDialog.ui SongDialog.cpp + SongDialog.h SongListModel.cpp + SongListModel.h AudioPlayer.cpp + AudioPlayer.h AceStepWorker.cpp + AceStepWorker.h + SongItem.h ${MusicGeneratorGUI_H} ) diff --git a/MainWindow.cpp b/MainWindow.cpp index 7bb0ee9..c5af068 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -20,7 +20,6 @@ MainWindow::MainWindow(QWidget *parent) audioPlayer(new AudioPlayer(this)), aceStepWorker(new AceStepWorker(this)), playbackTimer(new QTimer(this)), - currentSongIndex(-1), isPlaying(false), isPaused(false), shuffleMode(false), @@ -28,6 +27,9 @@ MainWindow::MainWindow(QWidget *parent) { ui->setupUi(this); + // Setup lyrics display + ui->lyricsTextEdit->setReadOnly(true); + // Setup UI setupUI(); @@ -44,6 +46,8 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->actionQuit, &QAction::triggered, this, [this](){close();}); connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong); connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); + connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); + connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration); connect(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated); connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError); connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); @@ -55,6 +59,25 @@ MainWindow::MainWindow(QWidget *parent) connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) { QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error); }); + + // Add some default songs + if(songModel->songCount() == 0) { + SongItem defaultSong1("Upbeat pop rock anthem with driving electric guitars", ""); + SongItem defaultSong2("Chill electronic music with smooth synths and relaxing beats", ""); + SongItem defaultSong3("Jazz fusion with saxophone solos and complex rhythms", ""); + + songModel->addSong(defaultSong1); + songModel->addSong(defaultSong2); + songModel->addSong(defaultSong3); + } + + // Select first item + if (songModel->rowCount() > 0) { + QModelIndex firstIndex = songModel->index(0, 0); + ui->songListView->setCurrentIndex(firstIndex); + } + + currentSong = songModel->getSong(0); } MainWindow::~MainWindow() @@ -84,21 +107,6 @@ void MainWindow::setupUI() // Enable row selection and disable column selection ui->songListView->setSelectionBehavior(QAbstractItemView::SelectRows); - - // 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() @@ -141,7 +149,8 @@ void MainWindow::saveSettings() QString MainWindow::formatTime(int milliseconds) { - if (milliseconds < 0) return "0:00"; + if (milliseconds < 0) + return "0:00"; int seconds = milliseconds / 1000; int minutes = seconds / 60; @@ -152,7 +161,8 @@ QString MainWindow::formatTime(int milliseconds) void MainWindow::updatePosition(int position) { - if (position < 0) return; + if (position < 0) + return; // Update slider and time labels ui->positionSlider->setValue(position); @@ -161,7 +171,8 @@ void MainWindow::updatePosition(int position) void MainWindow::updateDuration(int duration) { - if (duration <= 0) return; + if (duration <= 0) + return; // Set slider range and update duration label ui->positionSlider->setRange(0, duration); @@ -189,23 +200,13 @@ void MainWindow::on_playButton_clicked() isPaused = false; updateControls(); return; - } - - if (isPlaying) { - audioPlayer->stop(); - isPlaying = false; - isPaused = 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(); + } + + isPlaying = true; + ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); + flushGenerationQueue(); + ensureSongsInQueue(true); + updateControls(); } void MainWindow::on_pauseButton_clicked() @@ -221,13 +222,9 @@ void MainWindow::on_pauseButton_clicked() void MainWindow::on_skipButton_clicked() { if (isPlaying) { - // Stop current playback and move to next song audioPlayer->stop(); isPaused = false; - playNextSong(); - - // After playing the skipped-to song, start generating the next one - // We'll do this in playNextSong by checking if we're already playing + playNextSong(); } } @@ -236,9 +233,11 @@ void MainWindow::on_stopButton_clicked() if (isPlaying) { // Stop current playback completely audioPlayer->stop(); + ui->nowPlayingLabel->setText("Now Playing:"); isPlaying = false; isPaused = false; updateControls(); + flushGenerationQueue(); } } @@ -255,8 +254,10 @@ void MainWindow::on_addSongButton_clicked() if (dialog.exec() == QDialog::Accepted) { QString caption = dialog.getCaption(); QString lyrics = dialog.getLyrics(); + QString vocalLanguage = dialog.getVocalLanguage(); SongItem newSong(caption, lyrics); + newSong.vocalLanguage = vocalLanguage; songModel->addSong(newSong); // Select the new item @@ -282,22 +283,25 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) audioPlayer->stop(); } - // Set this as the current song and generate/play it - currentSongIndex = row; - generateAndPlayNext(); + // Flush the generation queue when user selects a different song + flushGenerationQueue(); + currentSong = songModel->getSong(row); + ensureSongsInQueue(true); } else if (index.column() == 1) { // Column 1 (caption): Edit the song SongItem song = songModel->getSong(row); - SongDialog dialog(this, song.caption, song.lyrics); + SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage); if (dialog.exec() == QDialog::Accepted) { QString caption = dialog.getCaption(); QString lyrics = dialog.getLyrics(); + QString vocalLanguage = dialog.getVocalLanguage(); // Update the model - use column 1 for the song name songModel->setData(songModel->index(row, 1), caption, SongListModel::CaptionRole); songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole); + songModel->setData(songModel->index(row, 1), vocalLanguage, SongListModel::VocalLanguageRole); } } @@ -357,120 +361,52 @@ void MainWindow::on_advancedSettingsButton_clicked() } } -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; - isGeneratingNext = false; // Reset the flag when starting a new generation - updateControls(); - - // Generate the song with configurable paths - aceStepWorker->generateSong(song.caption, song.lyrics, jsonTemplate, - aceStepPath, qwen3ModelPath, - textEncoderModelPath, ditModelPath, - vaeModelPath); -} - -void MainWindow::startNextSongGeneration() -{ - // Start generating the next song if we're playing and not already generating - if (isPlaying && !isGeneratingNext) { - isGeneratingNext = true; - - // Find and generate the next song - int nextIndex = songModel->findNextIndex(currentSongIndex, shuffleMode); - if (nextIndex >= 0 && nextIndex < songModel->rowCount()) { - SongItem nextSong = songModel->getSong(nextIndex); - - // Generate the next song in the background - aceStepWorker->generateSong(nextSong.caption, nextSong.lyrics, jsonTemplate, - aceStepPath, qwen3ModelPath, - textEncoderModelPath, ditModelPath, - vaeModelPath); - } - } -} - void MainWindow::playbackStarted() { - // When playback starts, immediately start generating the next song - startNextSongGeneration(); + ensureSongsInQueue(); } -void MainWindow::highlightCurrentSong() +void MainWindow::playSong(const SongItem& song) { - if (currentSongIndex >= 0 && currentSongIndex < songModel->rowCount()) { - // Update the model to show play icon for current song - songModel->setPlayingIndex(currentSongIndex); - } + currentSong = song; + audioPlayer->play(song.file); + songModel->setPlayingIndex(songModel->findSongIndexById(song.uniqueId)); + ui->nowPlayingLabel->setText("Now Playing: " + song.caption); + + // Update lyrics display + ui->lyricsTextEdit->setPlainText(song.lyrics); } -void MainWindow::songGenerated(const QString &filePath) +void MainWindow::songGenerated(const SongItem& song) { - if (!QFile::exists(filePath)) { - generationError("Generated file not found: " + filePath); - return; - } - - // If we're in the middle of playback, this is a pre-generated next song - if (isPlaying && audioPlayer->isPlaying()) { - // Store the generated file path for when playback finishes - nextSongFilePath = filePath; - return; - } - - ui->statusLabel->setText(""); - - // Play the generated song - audioPlayer->play(filePath); - - // Highlight the current song in the list - highlightCurrentSong(); - - // Connect position and duration updates for the slider - connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); - connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration); + isGeneratingNext = false; + + if (!isPaused && isPlaying && !audioPlayer->isPlaying()) { + playSong(song); + } + else { + generatedSongQueue.enqueue(song); + } + ui->statusLabel->setText("idle"); + + ensureSongsInQueue(); } void MainWindow::playNextSong() { - if (!isPlaying) return; + if (!isPlaying) + return; + + // Check if we have a pre-generated next song in the queue + if (!generatedSongQueue.isEmpty()) { + SongItem generatedSong = generatedSongQueue.dequeue(); + playSong(generatedSong); + } else { + ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); + } - // Check if we have a pre-generated next song - if (!nextSongFilePath.isEmpty()) { - audioPlayer->play(nextSongFilePath); - nextSongFilePath.clear(); - - // Update current index to the next song - int nextIndex = songModel->findNextIndex(currentSongIndex, shuffleMode); - if (nextIndex >= 0 && nextIndex < songModel->rowCount()) { - currentSongIndex = nextIndex; - } - - // Highlight the current song and start generating the next one - highlightCurrentSong(); - startNextSongGeneration(); - } else { - // Find next song index and generate it - int nextIndex = songModel->findNextIndex(currentSongIndex, shuffleMode); - - if (nextIndex >= 0 && nextIndex < songModel->rowCount()) { - currentSongIndex = nextIndex; - generateAndPlayNext(); - } else { - // No more songs - isPlaying = false; - isPaused = false; - updateControls(); - } - } + // Ensure we have songs in the queue for smooth playback + ensureSongsInQueue(); } void MainWindow::generationError(const QString &error) @@ -508,12 +444,6 @@ void MainWindow::generationError(const QString &error) updateControls(); } -void MainWindow::generationFinished() -{ - // Reset the generation flag when a generation completes - isGeneratingNext = false; -} - void MainWindow::updatePlaybackStatus(bool playing) { isPlaying = playing; @@ -522,12 +452,52 @@ void MainWindow::updatePlaybackStatus(bool playing) void MainWindow::on_positionSlider_sliderMoved(int position) { - if (isPlaying && audioPlayer->isPlaying()) { - // Seek to the new position when slider is moved + if (isPlaying && audioPlayer->isPlaying()) { audioPlayer->setPosition(position); } } +void MainWindow::ensureSongsInQueue(bool enqeueCurrent) +{ + // Only generate more songs if we're playing and not already at capacity + if (!isPlaying || isGeneratingNext || generatedSongQueue.size() >= generationTresh) { + return; + } + + SongItem lastSong; + SongItem workerSong; + if(aceStepWorker->songGenerateing(&workerSong)) + lastSong = workerSong; + else if(!generatedSongQueue.empty()) + lastSong = generatedSongQueue.last(); + else + lastSong = currentSong; + + SongItem nextSong; + if(enqeueCurrent) { + nextSong = lastSong; + } + else { + int nextIndex = songModel->findNextIndex(songModel->findSongIndexById(lastSong.uniqueId), shuffleMode); + nextSong = songModel->getSong(nextIndex); + } + + isGeneratingNext = true; + + ui->statusLabel->setText("Generateing: "+nextSong.caption); + aceStepWorker->generateSong(nextSong, jsonTemplate, + aceStepPath, qwen3ModelPath, + textEncoderModelPath, ditModelPath, + vaeModelPath); +} + +void MainWindow::flushGenerationQueue() +{ + generatedSongQueue.clear(); + aceStepWorker->cancelGeneration(); + isGeneratingNext = false; +} + // Playlist save/load methods void MainWindow::on_actionSavePlaylist() { @@ -633,6 +603,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList(song.uniqueId); // Store as qint64 for JSON compatibility songsArray.append(songObj); } @@ -708,6 +680,19 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & song.lyrics = songObj["lyrics"].toString(); } + // Load vocalLanguage if present + if (songObj.contains("vocalLanguage")) { + song.vocalLanguage = songObj["vocalLanguage"].toString(); + } + + // Load uniqueId if present (for backward compatibility) + if (songObj.contains("uniqueId")) { + song.uniqueId = static_cast(songObj["uniqueId"].toInteger()); + } else { + // Generate new ID for old playlists without uniqueId + song.uniqueId = QRandomGenerator::global()->generate64(); + } + songs.append(song); } diff --git a/MainWindow.h b/MainWindow.h index c0111d7..3fda8e4 100644 --- a/MainWindow.h +++ b/MainWindow.h @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -40,11 +43,10 @@ private slots: void on_songListView_doubleClicked(const QModelIndex &index); - void songGenerated(const QString &filePath); + void songGenerated(const SongItem& song); void playNextSong(); void playbackStarted(); - void updatePlaybackStatus(bool playing); - void generationFinished(); + void updatePlaybackStatus(bool playing); void generationError(const QString &error); void on_actionSavePlaylist(); @@ -61,8 +63,8 @@ private: QTimer *playbackTimer; QString formatTime(int milliseconds); - - int currentSongIndex; + + SongItem currentSong; bool isPlaying; bool isPaused; bool shuffleMode; @@ -76,25 +78,27 @@ private: QString ditModelPath; QString vaeModelPath; - // Pre-generated song file path - QString nextSongFilePath; + // Queue for generated songs + static constexpr int generationTresh = 2; + QQueue generatedSongQueue; private: - void highlightCurrentSong(); - void loadSettings(); void saveSettings(); void loadPlaylist(); void savePlaylist(const QString &filePath); void autoSavePlaylist(); void autoLoadPlaylist(); + + void playSong(const SongItem& song); bool savePlaylistToJson(const QString &filePath, const QList &songs); bool loadPlaylistFromJson(const QString &filePath, QList &songs); void setupUI(); - void updateControls(); - void generateAndPlayNext(); + void updateControls(); + void ensureSongsInQueue(bool enqeueCurrent = false); + void flushGenerationQueue(); }; #endif // MAINWINDOW_H diff --git a/MainWindow.ui b/MainWindow.ui index 5c75d55..faaccd0 100644 --- a/MainWindow.ui +++ b/MainWindow.ui @@ -16,29 +16,52 @@ - - - Music Generator - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - 0 - 200 - - - - QAbstractItemView::EditTrigger::NoEditTriggers - - - QAbstractItemView::SelectionBehavior::SelectRows + + + 0 + + + Songs + + + + + + + 0 + 200 + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + QAbstractItemView::SelectionBehavior::SelectRows + + + + + + + + Lyrics + + + + + + + Monospace + + + + true + + + + + @@ -72,6 +95,19 @@ + + + + Qt::LayoutDirection::LeftToRight + + + Now Playing: + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + diff --git a/SongDialog.cpp b/SongDialog.cpp index c941434..a7a3d9e 100644 --- a/SongDialog.cpp +++ b/SongDialog.cpp @@ -2,7 +2,7 @@ #include "ui_SongDialog.h" #include -SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics) +SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage) : QDialog(parent), ui(new Ui::SongDialog) { @@ -15,6 +15,28 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l if (!lyrics.isEmpty()) { ui->lyricsEdit->setPlainText(lyrics); } + + // Setup vocal language combo box + ui->vocalLanguageCombo->addItem("--", ""); // Unset + ui->vocalLanguageCombo->addItem("English (en)", "en"); + ui->vocalLanguageCombo->addItem("German (de)", "de"); + ui->vocalLanguageCombo->addItem("French (fr)", "fr"); + ui->vocalLanguageCombo->addItem("Spanish (es)", "es"); + ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja"); + ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh"); + ui->vocalLanguageCombo->addItem("Italian (it)", "it"); + ui->vocalLanguageCombo->addItem("Portuguese (pt)", "pt"); + ui->vocalLanguageCombo->addItem("Russian (ru)", "ru"); + + // Set current language if provided + if (!vocalLanguage.isEmpty()) { + int index = ui->vocalLanguageCombo->findData(vocalLanguage); + if (index >= 0) { + ui->vocalLanguageCombo->setCurrentIndex(index); + } + } else { + ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset + } } SongDialog::~SongDialog() @@ -32,6 +54,11 @@ QString SongDialog::getLyrics() const return ui->lyricsEdit->toPlainText(); } +QString SongDialog::getVocalLanguage() const +{ + return ui->vocalLanguageCombo->currentData().toString(); +} + void SongDialog::on_okButton_clicked() { // Validate that caption is not empty diff --git a/SongDialog.h b/SongDialog.h index 126de2f..c59f865 100644 --- a/SongDialog.h +++ b/SongDialog.h @@ -13,11 +13,12 @@ class SongDialog : public QDialog Q_OBJECT public: - explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = ""); + explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", const QString &vocalLanguage = ""); ~SongDialog(); QString getCaption() const; QString getLyrics() const; + QString getVocalLanguage() const; private slots: void on_okButton_clicked(); diff --git a/SongDialog.ui b/SongDialog.ui index 21d96e0..7fbba67 100644 --- a/SongDialog.ui +++ b/SongDialog.ui @@ -54,6 +54,23 @@ + + + + Vocal Language: + + + Qt::AlignTop + + + + + + + true + + + diff --git a/SongItem.h b/SongItem.h new file mode 100644 index 0000000..a3d8969 --- /dev/null +++ b/SongItem.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include + +class SongItem { +public: + QString caption; + QString lyrics; + uint64_t uniqueId; // Unique identifier for tracking across playlist changes + QString file; + QString vocalLanguage; // Language override for vocal generation (ISO 639 code or empty) + + inline SongItem(const QString &caption = "", const QString &lyrics = "") + : caption(caption), lyrics(lyrics) { + // Generate a unique ID using cryptographically secure random number + uniqueId = QRandomGenerator::global()->generate64(); + } +}; diff --git a/SongListModel.cpp b/SongListModel.cpp index 97c24fd..bb48a18 100644 --- a/SongListModel.cpp +++ b/SongListModel.cpp @@ -4,6 +4,7 @@ #include #include #include +#include SongListModel::SongListModel(QObject *parent) : QAbstractTableModel(parent), @@ -20,8 +21,8 @@ int SongListModel::rowCount(const QModelIndex &parent) const int SongListModel::columnCount(const QModelIndex &parent) const { - // We have 2 columns: play indicator and song name - return 2; + // We have 3 columns: play indicator, song name, and vocal language (read-only) + return 3; } QVariant SongListModel::data(const QModelIndex &index, int role) const @@ -41,6 +42,10 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const else if (index.column() == 1) { return song.caption; } + // Column 2: Vocal language + else if (index.column() == 2) { + return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--"; + } break; case Qt::FontRole: // Make play indicator bold and larger @@ -60,6 +65,8 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const return song.caption; case LyricsRole: return song.lyrics; + case VocalLanguageRole: + return song.vocalLanguage; case IsPlayingRole: return index.row() == m_playingIndex; default: @@ -83,6 +90,9 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int case LyricsRole: song.lyrics = value.toString(); break; + case VocalLanguageRole: + song.vocalLanguage = value.toString(); + break; default: return false; } @@ -148,6 +158,11 @@ void SongListModel::setPlayingIndex(int index) } } +int SongListModel::songCount() +{ + return songList.count(); +} + int SongListModel::findNextIndex(int currentIndex, bool shuffle) const { if (songList.isEmpty()) @@ -167,3 +182,13 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const return nextIndex; } + +int SongListModel::findSongIndexById(uint64_t uniqueId) const +{ + for (int i = 0; i < songList.size(); ++i) { + if (songList[i].uniqueId == uniqueId) { + return i; + } + } + return -1; // Song not found +} diff --git a/SongListModel.h b/SongListModel.h index cd58a09..9d0f713 100644 --- a/SongListModel.h +++ b/SongListModel.h @@ -4,15 +4,10 @@ #include #include #include +#include +#include -class SongItem { -public: - QString caption; - QString lyrics; - - SongItem(const QString &caption = "", const QString &lyrics = "") - : caption(caption), lyrics(lyrics) {} -}; +#include "SongItem.h" class SongListModel : public QAbstractTableModel { @@ -22,7 +17,8 @@ public: enum Roles { CaptionRole = Qt::UserRole + 1, LyricsRole = Qt::UserRole + 2, - IsPlayingRole = Qt::UserRole + 3 + VocalLanguageRole = Qt::UserRole + 3, + IsPlayingRole = Qt::UserRole + 4 }; explicit SongListModel(QObject *parent = nullptr); @@ -47,6 +43,10 @@ public: void setPlayingIndex(int index); int playingIndex() const { return m_playingIndex; } + // Find song by unique ID + int findSongIndexById(uint64_t uniqueId) const; + int songCount(); + private: QList songList; int m_playingIndex; diff --git a/test_compilation.cpp b/test_compilation.cpp deleted file mode 100644 index f92b540..0000000 --- a/test_compilation.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// 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; -}