Song handling simplification

Add languge field to songs

Add lyric display
This commit is contained in:
Carl Philipp Klemm 2026-03-05 00:50:23 +01:00
parent 134e827053
commit 1fec61140c
13 changed files with 384 additions and 231 deletions

View file

@ -21,7 +21,7 @@ AceStepWorker::~AceStepWorker()
cancelGeneration(); 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 &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath, const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath) const QString &vaeModelPath)
@ -30,21 +30,31 @@ void AceStepWorker::generateSong(const QString &caption, const QString &lyrics,
cancelGeneration(); cancelGeneration();
// Create worker and start it // 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); textEncoderModelPath, ditModelPath, vaeModelPath);
currentWorker->setAutoDelete(true);
QThreadPool::globalInstance()->start(currentWorker); QThreadPool::globalInstance()->start(currentWorker);
} }
void AceStepWorker::cancelGeneration() void AceStepWorker::cancelGeneration()
{ {
// Note: In a real implementation, we would need to implement proper cancellation
// For now, we just clear the reference
currentWorker = nullptr; 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 // Worker implementation
@ -64,15 +74,20 @@ void AceStepWorker::Worker::run()
} }
QJsonObject requestObj = templateDoc.object(); QJsonObject requestObj = templateDoc.object();
requestObj["caption"] = caption; requestObj["caption"] = song.caption;
if (!lyrics.isEmpty()) { if (!song.lyrics.isEmpty()) {
requestObj["lyrics"] = lyrics; requestObj["lyrics"] = song.lyrics;
} else { } else {
// Remove lyrics field if empty to let the LLM generate them // Remove lyrics field if empty to let the LLM generate them
requestObj.remove("lyrics"); requestObj.remove("lyrics");
} }
// Apply vocal language override if set
if (!song.vocalLanguage.isEmpty()) {
requestObj["vocal_language"] = song.vocalLanguage;
}
// Write the request file // Write the request file
QFile requestFileHandle(requestFile); QFile requestFileHandle(requestFile);
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) {
@ -161,6 +176,21 @@ void AceStepWorker::Worker::run()
return; 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); emit parent->progressUpdate(50);
// Step 2: Run dit-vae to generate audio // Step 2: Run dit-vae to generate audio
@ -207,5 +237,14 @@ void AceStepWorker::Worker::run()
QFile::remove(requestFile); QFile::remove(requestFile);
emit parent->progressUpdate(100); 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;
} }

View file

@ -6,6 +6,9 @@
#include <QThreadPool> #include <QThreadPool>
#include <QString> #include <QString>
#include <QJsonObject> #include <QJsonObject>
#include <QMutex>
#include "SongItem.h"
class AceStepWorker : public QObject class AceStepWorker : public QObject
{ {
@ -14,39 +17,38 @@ public:
explicit AceStepWorker(QObject *parent = nullptr); explicit AceStepWorker(QObject *parent = nullptr);
~AceStepWorker(); ~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 &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath, const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath); const QString &vaeModelPath);
void cancelGeneration(); void cancelGeneration();
bool songGenerateing(SongItem* song);
signals: signals:
void songGenerated(const QString &filePath); void songGenerated(const SongItem& song);
void generationFinished(); void generationFinished();
void generationError(const QString &error); void generationError(const QString &error);
void progressUpdate(int percent); void progressUpdate(int percent);
private slots:
void workerFinished();
private: private:
class Worker : public QRunnable { class Worker : public QRunnable {
public: 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 &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath, const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath) const QString &vaeModelPath)
: parent(parent), caption(caption), lyrics(lyrics), jsonTemplate(jsonTemplate), : parent(parent), song(song), jsonTemplate(jsonTemplate),
aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath), aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath),
textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath), textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath),
vaeModelPath(vaeModelPath) {} vaeModelPath(vaeModelPath) {}
void run() override; void run() override;
const SongItem& getSong();
private: private:
AceStepWorker *parent; AceStepWorker *parent;
QString caption; SongItem song;
QString lyrics;
QString jsonTemplate; QString jsonTemplate;
QString aceStepPath; QString aceStepPath;
QString qwen3ModelPath; QString qwen3ModelPath;
@ -55,6 +57,7 @@ private:
QString vaeModelPath; QString vaeModelPath;
}; };
QMutex workerMutex;
Worker *currentWorker; Worker *currentWorker;
}; };

View file

@ -17,13 +17,20 @@ add_executable(${PROJECT_NAME}
main.cpp main.cpp
MainWindow.ui MainWindow.ui
MainWindow.cpp MainWindow.cpp
MainWindow.h
AdvancedSettingsDialog.ui AdvancedSettingsDialog.ui
AdvancedSettingsDialog.cpp AdvancedSettingsDialog.cpp
AdvancedSettingsDialog.h
SongDialog.ui SongDialog.ui
SongDialog.cpp SongDialog.cpp
SongDialog.h
SongListModel.cpp SongListModel.cpp
SongListModel.h
AudioPlayer.cpp AudioPlayer.cpp
AudioPlayer.h
AceStepWorker.cpp AceStepWorker.cpp
AceStepWorker.h
SongItem.h
${MusicGeneratorGUI_H} ${MusicGeneratorGUI_H}
) )

View file

@ -20,7 +20,6 @@ MainWindow::MainWindow(QWidget *parent)
audioPlayer(new AudioPlayer(this)), audioPlayer(new AudioPlayer(this)),
aceStepWorker(new AceStepWorker(this)), aceStepWorker(new AceStepWorker(this)),
playbackTimer(new QTimer(this)), playbackTimer(new QTimer(this)),
currentSongIndex(-1),
isPlaying(false), isPlaying(false),
isPaused(false), isPaused(false),
shuffleMode(false), shuffleMode(false),
@ -28,6 +27,9 @@ MainWindow::MainWindow(QWidget *parent)
{ {
ui->setupUi(this); ui->setupUi(this);
// Setup lyrics display
ui->lyricsTextEdit->setReadOnly(true);
// Setup UI // Setup UI
setupUI(); setupUI();
@ -44,6 +46,8 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionQuit, &QAction::triggered, this, [this](){close();}); connect(ui->actionQuit, &QAction::triggered, this, [this](){close();});
connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong); connect(audioPlayer, &AudioPlayer::playbackFinished, this, &MainWindow::playNextSong);
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); 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::songGenerated, this, &MainWindow::songGenerated);
connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError); connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError);
connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue);
@ -55,6 +59,25 @@ MainWindow::MainWindow(QWidget *parent)
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) { connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) {
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + 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() MainWindow::~MainWindow()
@ -84,21 +107,6 @@ void MainWindow::setupUI()
// Enable row selection and disable column selection // Enable row selection and disable column selection
ui->songListView->setSelectionBehavior(QAbstractItemView::SelectRows); 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() void MainWindow::loadSettings()
@ -141,7 +149,8 @@ void MainWindow::saveSettings()
QString MainWindow::formatTime(int milliseconds) QString MainWindow::formatTime(int milliseconds)
{ {
if (milliseconds < 0) return "0:00"; if (milliseconds < 0)
return "0:00";
int seconds = milliseconds / 1000; int seconds = milliseconds / 1000;
int minutes = seconds / 60; int minutes = seconds / 60;
@ -152,7 +161,8 @@ QString MainWindow::formatTime(int milliseconds)
void MainWindow::updatePosition(int position) void MainWindow::updatePosition(int position)
{ {
if (position < 0) return; if (position < 0)
return;
// Update slider and time labels // Update slider and time labels
ui->positionSlider->setValue(position); ui->positionSlider->setValue(position);
@ -161,7 +171,8 @@ void MainWindow::updatePosition(int position)
void MainWindow::updateDuration(int duration) void MainWindow::updateDuration(int duration)
{ {
if (duration <= 0) return; if (duration <= 0)
return;
// Set slider range and update duration label // Set slider range and update duration label
ui->positionSlider->setRange(0, duration); ui->positionSlider->setRange(0, duration);
@ -191,21 +202,11 @@ void MainWindow::on_playButton_clicked()
return; return;
} }
if (isPlaying) { isPlaying = true;
audioPlayer->stop(); ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
isPlaying = false; flushGenerationQueue();
isPaused = false; ensureSongsInQueue(true);
updateControls(); 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_pauseButton_clicked() void MainWindow::on_pauseButton_clicked()
@ -221,13 +222,9 @@ void MainWindow::on_pauseButton_clicked()
void MainWindow::on_skipButton_clicked() void MainWindow::on_skipButton_clicked()
{ {
if (isPlaying) { if (isPlaying) {
// Stop current playback and move to next song
audioPlayer->stop(); audioPlayer->stop();
isPaused = false; isPaused = false;
playNextSong(); 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
} }
} }
@ -236,9 +233,11 @@ void MainWindow::on_stopButton_clicked()
if (isPlaying) { if (isPlaying) {
// Stop current playback completely // Stop current playback completely
audioPlayer->stop(); audioPlayer->stop();
ui->nowPlayingLabel->setText("Now Playing:");
isPlaying = false; isPlaying = false;
isPaused = false; isPaused = false;
updateControls(); updateControls();
flushGenerationQueue();
} }
} }
@ -255,8 +254,10 @@ void MainWindow::on_addSongButton_clicked()
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
QString caption = dialog.getCaption(); QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics(); QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage();
SongItem newSong(caption, lyrics); SongItem newSong(caption, lyrics);
newSong.vocalLanguage = vocalLanguage;
songModel->addSong(newSong); songModel->addSong(newSong);
// Select the new item // Select the new item
@ -282,22 +283,25 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
audioPlayer->stop(); audioPlayer->stop();
} }
// Set this as the current song and generate/play it // Flush the generation queue when user selects a different song
currentSongIndex = row; flushGenerationQueue();
generateAndPlayNext(); currentSong = songModel->getSong(row);
ensureSongsInQueue(true);
} else if (index.column() == 1) { } else if (index.column() == 1) {
// Column 1 (caption): Edit the song // Column 1 (caption): Edit the song
SongItem song = songModel->getSong(row); 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) { if (dialog.exec() == QDialog::Accepted) {
QString caption = dialog.getCaption(); QString caption = dialog.getCaption();
QString lyrics = dialog.getLyrics(); QString lyrics = dialog.getLyrics();
QString vocalLanguage = dialog.getVocalLanguage();
// Update the model - use column 1 for the song name // 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), caption, SongListModel::CaptionRole);
songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole); 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() void MainWindow::playbackStarted()
{ {
// When playback starts, immediately start generating the next song ensureSongsInQueue();
startNextSongGeneration();
} }
void MainWindow::highlightCurrentSong() void MainWindow::playSong(const SongItem& song)
{ {
if (currentSongIndex >= 0 && currentSongIndex < songModel->rowCount()) { currentSong = song;
// Update the model to show play icon for current song audioPlayer->play(song.file);
songModel->setPlayingIndex(currentSongIndex); 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)) { isGeneratingNext = false;
generationError("Generated file not found: " + filePath);
return; if (!isPaused && isPlaying && !audioPlayer->isPlaying()) {
playSong(song);
} }
else {
// If we're in the middle of playback, this is a pre-generated next song generatedSongQueue.enqueue(song);
if (isPlaying && audioPlayer->isPlaying()) {
// Store the generated file path for when playback finishes
nextSongFilePath = filePath;
return;
} }
ui->statusLabel->setText("idle");
ui->statusLabel->setText(""); ensureSongsInQueue();
// 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);
} }
void MainWindow::playNextSong() void MainWindow::playNextSong()
{ {
if (!isPlaying) return; if (!isPlaying)
return;
// Check if we have a pre-generated next song // Check if we have a pre-generated next song in the queue
if (!nextSongFilePath.isEmpty()) { if (!generatedSongQueue.isEmpty()) {
audioPlayer->play(nextSongFilePath); SongItem generatedSong = generatedSongQueue.dequeue();
nextSongFilePath.clear(); playSong(generatedSong);
// 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 { } else {
// Find next song index and generate it ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
int nextIndex = songModel->findNextIndex(currentSongIndex, shuffleMode); }
if (nextIndex >= 0 && nextIndex < songModel->rowCount()) { // Ensure we have songs in the queue for smooth playback
currentSongIndex = nextIndex; ensureSongsInQueue();
generateAndPlayNext();
} else {
// No more songs
isPlaying = false;
isPaused = false;
updateControls();
}
}
} }
void MainWindow::generationError(const QString &error) void MainWindow::generationError(const QString &error)
@ -508,12 +444,6 @@ void MainWindow::generationError(const QString &error)
updateControls(); updateControls();
} }
void MainWindow::generationFinished()
{
// Reset the generation flag when a generation completes
isGeneratingNext = false;
}
void MainWindow::updatePlaybackStatus(bool playing) void MainWindow::updatePlaybackStatus(bool playing)
{ {
isPlaying = playing; isPlaying = playing;
@ -523,11 +453,51 @@ void MainWindow::updatePlaybackStatus(bool playing)
void MainWindow::on_positionSlider_sliderMoved(int position) void MainWindow::on_positionSlider_sliderMoved(int position)
{ {
if (isPlaying && audioPlayer->isPlaying()) { if (isPlaying && audioPlayer->isPlaying()) {
// Seek to the new position when slider is moved
audioPlayer->setPosition(position); 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 // Playlist save/load methods
void MainWindow::on_actionSavePlaylist() void MainWindow::on_actionSavePlaylist()
{ {
@ -633,6 +603,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
QJsonObject songObj; QJsonObject songObj;
songObj["caption"] = song.caption; songObj["caption"] = song.caption;
songObj["lyrics"] = song.lyrics; songObj["lyrics"] = song.lyrics;
songObj["vocalLanguage"] = song.vocalLanguage;
songObj["uniqueId"] = static_cast<qint64>(song.uniqueId); // Store as qint64 for JSON compatibility
songsArray.append(songObj); songsArray.append(songObj);
} }
@ -708,6 +680,19 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
song.lyrics = songObj["lyrics"].toString(); 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<uint64_t>(songObj["uniqueId"].toInteger());
} else {
// Generate new ID for old playlists without uniqueId
song.uniqueId = QRandomGenerator::global()->generate64();
}
songs.append(song); songs.append(song);
} }

View file

@ -5,6 +5,9 @@
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QTimer> #include <QTimer>
#include <QQueue>
#include <QPair>
#include <cstdint>
#include <QStandardPaths> #include <QStandardPaths>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
@ -40,11 +43,10 @@ private slots:
void on_songListView_doubleClicked(const QModelIndex &index); void on_songListView_doubleClicked(const QModelIndex &index);
void songGenerated(const QString &filePath); void songGenerated(const SongItem& song);
void playNextSong(); void playNextSong();
void playbackStarted(); void playbackStarted();
void updatePlaybackStatus(bool playing); void updatePlaybackStatus(bool playing);
void generationFinished();
void generationError(const QString &error); void generationError(const QString &error);
void on_actionSavePlaylist(); void on_actionSavePlaylist();
@ -62,7 +64,7 @@ private:
QString formatTime(int milliseconds); QString formatTime(int milliseconds);
int currentSongIndex; SongItem currentSong;
bool isPlaying; bool isPlaying;
bool isPaused; bool isPaused;
bool shuffleMode; bool shuffleMode;
@ -76,12 +78,11 @@ private:
QString ditModelPath; QString ditModelPath;
QString vaeModelPath; QString vaeModelPath;
// Pre-generated song file path // Queue for generated songs
QString nextSongFilePath; static constexpr int generationTresh = 2;
QQueue<SongItem> generatedSongQueue;
private: private:
void highlightCurrentSong();
void loadSettings(); void loadSettings();
void saveSettings(); void saveSettings();
void loadPlaylist(); void loadPlaylist();
@ -89,12 +90,15 @@ private:
void autoSavePlaylist(); void autoSavePlaylist();
void autoLoadPlaylist(); void autoLoadPlaylist();
void playSong(const SongItem& song);
bool savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs); bool savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs);
bool loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs); bool loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs);
void setupUI(); void setupUI();
void updateControls(); void updateControls();
void generateAndPlayNext(); void ensureSongsInQueue(bool enqeueCurrent = false);
void flushGenerationQueue();
}; };
#endif // MAINWINDOW_H #endif // MAINWINDOW_H

View file

@ -16,15 +16,15 @@
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="titleLabel"> <widget class="QTabWidget" name="mainTabWidget">
<property name="text"> <property name="currentIndex">
<string>Music Generator</string> <number>0</number>
</property> </property>
<property name="alignment"> <widget class="QWidget" name="songsTab">
<set>Qt::AlignmentFlag::AlignCenter</set> <attribute name="title">
</property> <string>Songs</string>
</widget> </attribute>
</item> <layout class="QVBoxLayout" name="verticalLayout_2">
<item> <item>
<widget class="QTableView" name="songListView"> <widget class="QTableView" name="songListView">
<property name="minimumSize"> <property name="minimumSize">
@ -41,6 +41,29 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</widget>
<widget class="QWidget" name="lyricsTab">
<attribute name="title">
<string>Lyrics</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="lyricsTextEdit">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item> <item>
<layout class="QHBoxLayout" name="buttonLayout"> <layout class="QHBoxLayout" name="buttonLayout">
<item> <item>
@ -72,6 +95,19 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QLabel" name="nowPlayingLabel">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>Now Playing:</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
<item> <item>
<layout class="QHBoxLayout" name="timeControlsLayout"> <layout class="QHBoxLayout" name="timeControlsLayout">
<item> <item>

View file

@ -2,7 +2,7 @@
#include "ui_SongDialog.h" #include "ui_SongDialog.h"
#include <QMessageBox> #include <QMessageBox>
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), : QDialog(parent),
ui(new Ui::SongDialog) ui(new Ui::SongDialog)
{ {
@ -15,6 +15,28 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l
if (!lyrics.isEmpty()) { if (!lyrics.isEmpty()) {
ui->lyricsEdit->setPlainText(lyrics); 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() SongDialog::~SongDialog()
@ -32,6 +54,11 @@ QString SongDialog::getLyrics() const
return ui->lyricsEdit->toPlainText(); return ui->lyricsEdit->toPlainText();
} }
QString SongDialog::getVocalLanguage() const
{
return ui->vocalLanguageCombo->currentData().toString();
}
void SongDialog::on_okButton_clicked() void SongDialog::on_okButton_clicked()
{ {
// Validate that caption is not empty // Validate that caption is not empty

View file

@ -13,11 +13,12 @@ class SongDialog : public QDialog
Q_OBJECT Q_OBJECT
public: 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(); ~SongDialog();
QString getCaption() const; QString getCaption() const;
QString getLyrics() const; QString getLyrics() const;
QString getVocalLanguage() const;
private slots: private slots:
void on_okButton_clicked(); void on_okButton_clicked();

View file

@ -54,6 +54,23 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="vocalLanguageLabel">
<property name="text">
<string>Vocal Language:</string>
</property>
<property name="alignment">
<set>Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="vocalLanguageCombo">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item> <item>
<layout class="QHBoxLayout" name="buttonLayout"> <layout class="QHBoxLayout" name="buttonLayout">
<item> <item>

19
SongItem.h Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include <QString>
#include <QRandomGenerator>
#include <cstdint>
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();
}
};

View file

@ -4,6 +4,7 @@
#include <QRandomGenerator> #include <QRandomGenerator>
#include <QDebug> #include <QDebug>
#include <QFont> #include <QFont>
#include <QUuid>
SongListModel::SongListModel(QObject *parent) SongListModel::SongListModel(QObject *parent)
: QAbstractTableModel(parent), : QAbstractTableModel(parent),
@ -20,8 +21,8 @@ int SongListModel::rowCount(const QModelIndex &parent) const
int SongListModel::columnCount(const QModelIndex &parent) const int SongListModel::columnCount(const QModelIndex &parent) const
{ {
// We have 2 columns: play indicator and song name // We have 3 columns: play indicator, song name, and vocal language (read-only)
return 2; return 3;
} }
QVariant SongListModel::data(const QModelIndex &index, int role) const 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) { else if (index.column() == 1) {
return song.caption; return song.caption;
} }
// Column 2: Vocal language
else if (index.column() == 2) {
return !song.vocalLanguage.isEmpty() ? song.vocalLanguage : "--";
}
break; break;
case Qt::FontRole: case Qt::FontRole:
// Make play indicator bold and larger // Make play indicator bold and larger
@ -60,6 +65,8 @@ QVariant SongListModel::data(const QModelIndex &index, int role) const
return song.caption; return song.caption;
case LyricsRole: case LyricsRole:
return song.lyrics; return song.lyrics;
case VocalLanguageRole:
return song.vocalLanguage;
case IsPlayingRole: case IsPlayingRole:
return index.row() == m_playingIndex; return index.row() == m_playingIndex;
default: default:
@ -83,6 +90,9 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int
case LyricsRole: case LyricsRole:
song.lyrics = value.toString(); song.lyrics = value.toString();
break; break;
case VocalLanguageRole:
song.vocalLanguage = value.toString();
break;
default: default:
return false; 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 int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
{ {
if (songList.isEmpty()) if (songList.isEmpty())
@ -167,3 +182,13 @@ int SongListModel::findNextIndex(int currentIndex, bool shuffle) const
return nextIndex; 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
}

View file

@ -4,15 +4,10 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QList> #include <QList>
#include <QString> #include <QString>
#include <QRandomGenerator>
#include <cstdint>
class SongItem { #include "SongItem.h"
public:
QString caption;
QString lyrics;
SongItem(const QString &caption = "", const QString &lyrics = "")
: caption(caption), lyrics(lyrics) {}
};
class SongListModel : public QAbstractTableModel class SongListModel : public QAbstractTableModel
{ {
@ -22,7 +17,8 @@ public:
enum Roles { enum Roles {
CaptionRole = Qt::UserRole + 1, CaptionRole = Qt::UserRole + 1,
LyricsRole = Qt::UserRole + 2, LyricsRole = Qt::UserRole + 2,
IsPlayingRole = Qt::UserRole + 3 VocalLanguageRole = Qt::UserRole + 3,
IsPlayingRole = Qt::UserRole + 4
}; };
explicit SongListModel(QObject *parent = nullptr); explicit SongListModel(QObject *parent = nullptr);
@ -47,6 +43,10 @@ public:
void setPlayingIndex(int index); void setPlayingIndex(int index);
int playingIndex() const { return m_playingIndex; } int playingIndex() const { return m_playingIndex; }
// Find song by unique ID
int findSongIndexById(uint64_t uniqueId) const;
int songCount();
private: private:
QList<SongItem> songList; QList<SongItem> songList;
int m_playingIndex; int m_playingIndex;

View file

@ -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;
}