Song handling simplification
Add languge field to songs Add lyric display
This commit is contained in:
parent
134e827053
commit
1fec61140c
13 changed files with 384 additions and 231 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
#include <QThreadPool>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
|
||||
#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;
|
||||
|
|
@ -55,6 +57,7 @@ private:
|
|||
QString vaeModelPath;
|
||||
};
|
||||
|
||||
QMutex workerMutex;
|
||||
Worker *currentWorker;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
|
|
|||
285
MainWindow.cpp
285
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;
|
||||
}
|
||||
isGeneratingNext = false;
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (!isPaused && isPlaying && !audioPlayer->isPlaying()) {
|
||||
playSong(song);
|
||||
}
|
||||
else {
|
||||
generatedSongQueue.enqueue(song);
|
||||
}
|
||||
ui->statusLabel->setText("idle");
|
||||
|
||||
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);
|
||||
ensureSongsInQueue();
|
||||
}
|
||||
|
||||
void MainWindow::playNextSong()
|
||||
{
|
||||
if (!isPlaying) return;
|
||||
if (!isPlaying)
|
||||
return;
|
||||
|
||||
// Check if we have a pre-generated next song
|
||||
if (!nextSongFilePath.isEmpty()) {
|
||||
audioPlayer->play(nextSongFilePath);
|
||||
nextSongFilePath.clear();
|
||||
// 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...");
|
||||
}
|
||||
|
||||
// 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<SongIte
|
|||
QJsonObject songObj;
|
||||
songObj["caption"] = song.caption;
|
||||
songObj["lyrics"] = song.lyrics;
|
||||
songObj["vocalLanguage"] = song.vocalLanguage;
|
||||
songObj["uniqueId"] = static_cast<qint64>(song.uniqueId); // Store as qint64 for JSON compatibility
|
||||
songsArray.append(songObj);
|
||||
}
|
||||
|
||||
|
|
@ -708,6 +680,19 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
24
MainWindow.h
24
MainWindow.h
|
|
@ -5,6 +5,9 @@
|
|||
#include <QListWidgetItem>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTimer>
|
||||
#include <QQueue>
|
||||
#include <QPair>
|
||||
#include <cstdint>
|
||||
#include <QStandardPaths>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -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();
|
||||
|
|
@ -62,7 +64,7 @@ private:
|
|||
|
||||
QString formatTime(int milliseconds);
|
||||
|
||||
int currentSongIndex;
|
||||
SongItem currentSong;
|
||||
bool isPlaying;
|
||||
bool isPaused;
|
||||
bool shuffleMode;
|
||||
|
|
@ -76,12 +78,11 @@ private:
|
|||
QString ditModelPath;
|
||||
QString vaeModelPath;
|
||||
|
||||
// Pre-generated song file path
|
||||
QString nextSongFilePath;
|
||||
// Queue for generated songs
|
||||
static constexpr int generationTresh = 2;
|
||||
QQueue<SongItem> generatedSongQueue;
|
||||
|
||||
private:
|
||||
void highlightCurrentSong();
|
||||
|
||||
void loadSettings();
|
||||
void saveSettings();
|
||||
void loadPlaylist();
|
||||
|
|
@ -89,12 +90,15 @@ private:
|
|||
void autoSavePlaylist();
|
||||
void autoLoadPlaylist();
|
||||
|
||||
void playSong(const SongItem& song);
|
||||
|
||||
bool savePlaylistToJson(const QString &filePath, const QList<SongItem> &songs);
|
||||
bool loadPlaylistFromJson(const QString &filePath, QList<SongItem> &songs);
|
||||
|
||||
void setupUI();
|
||||
void updateControls();
|
||||
void generateAndPlayNext();
|
||||
void updateControls();
|
||||
void ensureSongsInQueue(bool enqeueCurrent = false);
|
||||
void flushGenerationQueue();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
|
|
|||
|
|
@ -16,29 +16,52 @@
|
|||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="titleLabel">
|
||||
<property name="text">
|
||||
<string>Music Generator</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="songListView">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
|
||||
<widget class="QTabWidget" name="mainTabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="songsTab">
|
||||
<attribute name="title">
|
||||
<string>Songs</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableView" name="songListView">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
|
|
@ -72,6 +95,19 @@
|
|||
</item>
|
||||
</layout>
|
||||
</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>
|
||||
<layout class="QHBoxLayout" name="timeControlsLayout">
|
||||
<item>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#include "ui_SongDialog.h"
|
||||
#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),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -54,6 +54,23 @@
|
|||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
|
|
|
|||
19
SongItem.h
Normal file
19
SongItem.h
Normal 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();
|
||||
}
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QRandomGenerator>
|
||||
#include <QDebug>
|
||||
#include <QFont>
|
||||
#include <QUuid>
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@
|
|||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QRandomGenerator>
|
||||
#include <cstdint>
|
||||
|
||||
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<SongItem> songList;
|
||||
int m_playingIndex;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue