Skip to content

Commit

Permalink
export
Browse files Browse the repository at this point in the history
  • Loading branch information
ahigerd committed Feb 9, 2024
1 parent 07a317f commit df080ac
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.build
agbplay-gui
*.wav
*.exe
*.o
moc_*
Expand Down
2 changes: 1 addition & 1 deletion agbplay-gui.pro
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ RESOURCES += resources/agbplay.qrc

GUI_CLASS += PianoKeys VUMeter TrackHeader TrackView TrackList
GUI_CLASS += RomView PlayerWindow SongModel Player UiUtils
GUI_CLASS += PlayerControls PlaylistModel
GUI_CLASS += PlayerControls PlaylistModel RiffWriter
for(F, GUI_CLASS) {
HEADERS += src/$${F}.h
SOURCES += src/$${F}.cpp
Expand Down
147 changes: 144 additions & 3 deletions src/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include "SongModel.h"
#include "UiUtils.h"
#include "Debug.h"
#include <QPointer>
#include "RiffWriter.h"

class PlayerThread : public QThread
{
Expand Down Expand Up @@ -60,7 +60,7 @@ class PlayerThread : public QThread
if (ctx->seq.tracks[i].muted)
continue;

for (size_t j = 0; j < masterAudio.size(); j++) {
for (size_t j = 0; j < samplesPerBuffer; j++) {
masterAudio[j].left += trackAudio[i][j].left;
masterAudio[j].right += trackAudio[i][j].right;
}
Expand Down Expand Up @@ -104,6 +104,7 @@ class PlayerThread : public QThread
ctx->InitSong(ctx->seq.GetSongHeaderPos());
} catch (std::exception& e) {
Debug::print("FATAL ERROR on streaming thread: %s", e.what());
emit player->playbackError(e.what());
}
Pa_StopStream(player->audioStream);
player->vuState.reset();
Expand All @@ -113,6 +114,86 @@ class PlayerThread : public QThread
}
};

class ExportThread : public QThread
{
public:
Player* player;
PlayerContext ctx;
std::size_t samplesPerBuffer;
std::vector<std::int16_t> masterLeft, masterRight;
std::vector<std::vector<sample>> trackAudio;
QList<QPair<QString, quint32>>& exportQueue;

static GameConfig& cfg() {
return ConfigManager::Instance().GetCfg();
}

ExportThread(Player* player)
: QThread(player),
player(player),
ctx(
ConfigManager::Instance().GetMaxLoopsPlaylist(),
cfg().GetTrackLimit(),
EnginePars(
cfg().GetPCMVol(),
cfg().GetEngineRev(),
cfg().GetEngineFreq()
)
),
samplesPerBuffer(ctx.mixer.GetSamplesPerBuffer()),
masterLeft(samplesPerBuffer, 0),
masterRight(samplesPerBuffer, 0),
exportQueue(player->exportQueue)
{
setObjectName("export thread");
setTerminationEnabled(true);
player->abortExport = false;
}

~ExportThread()
{
}

void run()
{
while (!exportQueue.isEmpty() && !player->abortExport) {
auto item = exportQueue.takeFirst();
try {
RiffWriter riff(ctx.mixer.GetSampleRate(), true);
riff.open(item.first.toStdString());
emit player->exportStarted(item.first);
ctx.InitSong(item.second);
while (!ctx.HasEnded() && !player->abortExport) {
// clear high level mixing buffer
fill(masterLeft.begin(), masterLeft.end(), 0);
fill(masterRight.begin(), masterRight.end(), 0);
// render audio buffers for tracks
ctx.Process(trackAudio);
for (size_t i = 0; i < trackAudio.size(); i++) {
for (size_t j = 0; j < samplesPerBuffer; j++) {
masterLeft[j] += trackAudio[i][j].left * 32767;
masterRight[j] += trackAudio[i][j].right * 32767;
}
}
// write to file
riff.write(masterLeft, masterRight);
}
riff.close();
if (player->abortExport) {
break;
} else {
emit player->exportFinished(item.first);
}
} catch (std::exception& e) {
emit player->exportError(e.what());
}
}
if (player->abortExport) {
emit player->exportCancelled();
}
}
};

// first portaudio hostapi has highest priority, last hostapi has lowest
// if none are available, the default one is selected.
// they are also the ones which are known to work
Expand Down Expand Up @@ -289,8 +370,8 @@ void Player::play()
}
}
} catch (std::exception& e) {
// TODO: GUI
Debug::print(e.what());
emit threadError(tr("An error occurred while preparing to play:\n\n%1").arg(e.what()));
return;
}
timer.start();
Expand Down Expand Up @@ -375,3 +456,63 @@ int Player::audioCallback(sample* output, size_t frames)
rBuf.Take(output, frames);
return 0;
}

bool Player::exportToWave(const QString& filename, int track)
{
if (!ctx || !exportQueue.isEmpty() || exportThread) {
return false;
}
try {
QModelIndex idx = model->index(track, 0);
quint32 addr = model->songAddress(idx);
exportQueue << qMakePair(filename, addr);
exportThread.reset(new ExportThread(this));
QObject::connect(exportThread.get(), SIGNAL(finished()), this, SLOT(exportDone()), Qt::QueuedConnection);
exportThread->start();
} catch (std::exception& e) {
Debug::print(e.what());
emit threadError(tr("An error occurred while preparing to export:\n\n%1").arg(e.what()));
return false;
}
return true;
}

bool Player::exportToWave(const QDir& path, const QList<int>& tracks)
{
if (!ctx || !exportQueue.isEmpty() || exportThread) {
return false;
}
try {
for (int track : tracks) {
QModelIndex idx = model->index(track, 0);
quint32 addr = model->songAddress(idx);
QString name = model->data(idx, Qt::EditRole).toString();
QString prefix = fixedNumber(track, 4);
if (name.isEmpty()) {
name = QStringLiteral("%1.wav").arg(prefix);
} else {
name = QStringLiteral("%1 - %2.wav").arg(prefix).arg(name);
}
exportQueue << qMakePair(path.absoluteFilePath(name), addr);
}
exportThread.reset(new ExportThread(this));
QObject::connect(exportThread.get(), SIGNAL(finished()), this, SLOT(exportDone()), Qt::QueuedConnection);
exportThread->start();
} catch (std::exception& e) {
Debug::print(e.what());
emit threadError(tr("An error occurred while preparing to export:\n\n%1").arg(e.what()));
return false;
}
return true;
}

void Player::exportDone()
{
exportThread.reset();
exportQueue.clear();
}

void Player::cancelExport()
{
abortExport = true;
}
17 changes: 17 additions & 0 deletions src/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <QObject>
#include <QTimer>
#include <QThread>
#include <QDir>
#include <memory>
#include <atomic>
#include <portaudio.h>
Expand All @@ -21,6 +22,7 @@ class Player : public QObject
{
Q_OBJECT
friend class PlayerThread;
friend class ExportThread;
public:
Player(QObject* parent = nullptr);
~Player();
Expand All @@ -31,11 +33,20 @@ friend class PlayerThread;
SongModel* songModel() const;
void selectSong(int index);

bool exportToWave(const QString& filename, int track);
bool exportToWave(const QDir& path, const QList<int>& tracks);

signals:
void threadError(const QString& message);
void songTableUpdated(SongTable* table);
void songChanged(PlayerContext* context, quint32 addr, const QString& name);
void updated(PlayerContext* context, VUState* vu);
void stateChanged(bool isPlaying, bool isPaused);
void exportStarted(const QString& path);
void exportFinished(const QString& path);
void exportError(const QString& message);
void playbackError(const QString& message);
void exportCancelled();

public slots:
void setMute(int trackIdx, bool on);
Expand All @@ -45,9 +56,12 @@ public slots:
void stop();
void togglePlay();

void cancelExport();

private slots:
void update();
void playbackDone();
void exportDone();

private:
enum class State : int {
Expand All @@ -68,14 +82,17 @@ private slots:
std::unique_ptr<PlayerContext> ctx;
std::unique_ptr<SongTable> songTable;
std::unique_ptr<QThread> playerThread;
std::unique_ptr<QThread> exportThread;
SongModel* model;

std::atomic<State> playerState;
std::atomic<bool> abortExport;

PaStream* audioStream;
uint32_t speedFactor;
Ringbuffer rBuf;

VUState vuState;
std::vector<bool> mutedTracks;
QList<QPair<QString, quint32>> exportQueue;
};
Loading

0 comments on commit df080ac

Please sign in to comment.