8 |
9 | namespace boost {
10 | namespace math {
11 | float sinc_pi(float x) {
12 | if (x == 0) {
13 | return x;
14 | }
15 | return std::sin(x) / x;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/resources/about.html:
--------------------------------------------------------------------------------
1 |
2 | agbplay is a music player for GBA ROMs that use the MusicPlayer2000
3 | (mp2k/m4a/"Sappy") sound engine.
4 |
5 |
6 | agbplay-gui is created by Adam Higerd. It is derived from agbplay by
7 | ipatix. Both agbplay and agbplay-gui are distributed under the terms of
8 | the LGPLv3.
9 |
10 |
11 | This program is free software: you can redistribute it and/or modify it
12 | under the terms of the GNU Lesser General Public License as published by the
13 | Free Software Foundation, either version 3 of the License, or (at your
14 | option) any later version.
15 |
16 |
17 | This program is distributed in the hope that it will be useful, but
18 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
20 | License for more details.
21 |
22 |
23 | For more information about the GNU Lesser General Public License, see
24 | https://www.gnu.org/licenses .
25 |
26 |
27 | agbplay source code: https://github.com/ipatix/agbplay
28 |
29 |
30 | agbplay-gui source code: https://github.com/ahigerd/agbplay-gui
31 |
32 |
--------------------------------------------------------------------------------
/resources/agbplay.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | logo.png
4 | ../agbplay/agbplay.json
5 | about.html
6 |
7 |
8 | about.html
9 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahigerd/agbplay-gui/233c8fcbcdea9caf0b10b66118e7afaae9e00b3b/resources/logo.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahigerd/agbplay-gui/233c8fcbcdea9caf0b10b66118e7afaae9e00b3b/screenshot.png
--------------------------------------------------------------------------------
/src/AudioThread.cpp:
--------------------------------------------------------------------------------
1 | #include "AudioThread.h"
2 | #include "ConfigManager.h"
3 | #include "Xcept.h"
4 | #include "Debug.h"
5 | #include "RiffWriter.h"
6 | #include
7 |
8 | AudioThread::AudioThread(Player* player, const QString& name, PlayerContext* ctx)
9 | : QThread(player),
10 | player(player),
11 | ctx(ctx),
12 | samplesPerBuffer(ctx->mixer.GetSamplesPerBuffer())
13 | {
14 | setObjectName(name);
15 | setTerminationEnabled(true);
16 | }
17 |
18 | AudioThread::~AudioThread()
19 | {
20 | }
21 |
22 | void AudioThread::prepare(quint32 addr)
23 | {
24 | ctx->InitSong(addr);
25 | uint8_t numTracks = static_cast(ctx->seq.tracks.size());
26 | trackAudio.resize(numTracks);
27 | for (auto& buffer : trackAudio) {
28 | std::fill(buffer.begin(), buffer.end(), sample{0.0f, 0.0f});
29 | buffer.resize(samplesPerBuffer, sample{0.0f, 0.0f});
30 | }
31 | }
32 |
33 | bool AudioThread::process()
34 | {
35 | prepareBuffers();
36 | // render audio buffers for tracks
37 | ctx->Process(trackAudio);
38 | for (size_t i = 0; i < trackAudio.size(); i++) {
39 | processTrack(i, trackAudio[i], ctx->seq.tracks[i].muted);
40 | }
41 | outputBuffers();
42 | return ctx->HasEnded();
43 | }
44 |
45 | PlayerThread::PlayerThread(Player* player)
46 | : AudioThread(player, "mixer thread", player->ctx.get()),
47 | silence(samplesPerBuffer, sample{0.0f, 0.0f}),
48 | masterAudio(samplesPerBuffer, sample{0.0f, 0.0f})
49 | {
50 | PaError err = Pa_StartStream(player->audioStream);
51 | if (err != paNoError) {
52 | throw Xcept("Pa_StartStream(): unable to start stream: %s", Pa_GetErrorText(err));
53 | }
54 | player->setState(State::PLAYING);
55 | }
56 |
57 | PlayerThread::~PlayerThread()
58 | {
59 | }
60 |
61 | void PlayerThread::run()
62 | {
63 | try {
64 | runStream();
65 | // reset song state after it has finished
66 | prepare(ctx->seq.GetSongHeaderPos());
67 | } catch (std::exception& e) {
68 | Debug::print("FATAL ERROR on streaming thread: %s", e.what());
69 | emit player->playbackError(e.what());
70 | }
71 | Pa_StopStream(player->audioStream);
72 | player->vuState.reset();
73 | // flush buffer
74 | player->rBuf.Clear();
75 | player->playerState = State::TERMINATED;
76 | }
77 |
78 | void PlayerThread::runStream()
79 | {
80 | while (true) {
81 | switch (player->playerState) {
82 | case State::SHUTDOWN:
83 | case State::TERMINATED:
84 | return;
85 | case State::RESTART:
86 | restart();
87 | [[fallthrough]];
88 | case State::PLAYING:
89 | if (process()) {
90 | player->setState(State::SHUTDOWN);
91 | return;
92 | }
93 | break;
94 | case State::PAUSED:
95 | player->rBuf.Put(silence.data(), silence.size());
96 | break;
97 | default:
98 | throw Xcept("Internal PlayerInterface error: %d", (int)player->playerState.load());
99 | }
100 | }
101 | }
102 |
103 | void PlayerThread::restart()
104 | {
105 | prepare(ctx->seq.GetSongHeaderPos());
106 | player->setState(State::PLAYING);
107 | }
108 |
109 | void PlayerThread::prepareBuffers()
110 | {
111 | fill(masterAudio.begin(), masterAudio.end(), sample{0.0f, 0.0f});
112 | }
113 |
114 | void PlayerThread::processTrack(std::size_t index, std::vector& samples, bool mute)
115 | {
116 | player->vuState.loudness[index].CalcLoudness(samples.data(), samplesPerBuffer);
117 | if (mute) {
118 | return;
119 | }
120 |
121 | for (size_t j = 0; j < samplesPerBuffer; j++) {
122 | masterAudio[j].left += samples[j].left;
123 | masterAudio[j].right += samples[j].right;
124 | }
125 | }
126 |
127 | void PlayerThread::outputBuffers()
128 | {
129 | player->rBuf.Put(masterAudio.data(), masterAudio.size());
130 | player->vuState.masterLoudness.CalcLoudness(masterAudio.data(), samplesPerBuffer);
131 | player->vuState.update();
132 | }
133 |
134 | static GameConfig& cfg() {
135 | return ConfigManager::Instance().GetCfg();
136 | }
137 |
138 | ExportThread::ExportThread(Player* player)
139 | : AudioThread(player, "export thread", new PlayerContext(
140 | ConfigManager::Instance().GetMaxLoopsPlaylist(),
141 | cfg().GetTrackLimit(),
142 | EnginePars(
143 | cfg().GetPCMVol(),
144 | cfg().GetEngineRev(),
145 | cfg().GetEngineFreq()
146 | )
147 | )),
148 | masterLeft(samplesPerBuffer, 0),
149 | masterRight(samplesPerBuffer, 0),
150 | silence(samplesPerBuffer, 0)
151 | {
152 | player->abortExport = false;
153 | }
154 |
155 | ExportThread::~ExportThread()
156 | {
157 | if (ctx) {
158 | delete ctx;
159 | }
160 | }
161 |
162 | void ExportThread::prepareBuffers()
163 | {
164 | if (!exportTracks) {
165 | std::fill(masterLeft.begin(), masterLeft.end(), 0);
166 | std::fill(masterRight.begin(), masterRight.end(), 0);
167 | }
168 | }
169 |
170 | void ExportThread::processTrack(std::size_t index, std::vector& samples, bool)
171 | {
172 | if (exportTracks) {
173 | for (size_t j = 0; j < samplesPerBuffer; j++) {
174 | masterLeft[j] = samples[j].left * 32767;
175 | masterRight[j] = samples[j].right * 32767;
176 | }
177 | riffs[index]->write(masterLeft, masterRight);
178 | } else {
179 | for (size_t j = 0; j < samplesPerBuffer; j++) {
180 | masterLeft[j] += samples[j].left * 32767;
181 | masterRight[j] += samples[j].right * 32767;
182 | }
183 | }
184 | }
185 |
186 | void ExportThread::outputBuffers()
187 | {
188 | if (!exportTracks) {
189 | riff->write(masterLeft, masterRight);
190 | }
191 | }
192 |
193 | void ExportThread::pad(RiffWriter* riff, std::uint32_t samples) const
194 | {
195 | while (samples > samplesPerBuffer) {
196 | riff->write(silence, silence);
197 | samples -= samplesPerBuffer;
198 | }
199 | if (samples > 0) {
200 | std::vector shortSilence(samples, 0);
201 | riff->write(shortSilence, shortSilence);
202 | }
203 | }
204 |
205 | void ExportThread::run()
206 | {
207 | std::uint32_t padStart = ConfigManager::Instance().GetPadSecondsStart() * ctx->mixer.GetSampleRate();
208 | std::uint32_t padEnd = ConfigManager::Instance().GetPadSecondsEnd() * ctx->mixer.GetSampleRate();
209 | while (!player->exportQueue.isEmpty() && !player->abortExport) {
210 | auto item = player->exportQueue.takeFirst();
211 | exportTracks = item.splitTracks;
212 | try {
213 | prepare(item.trackAddr);
214 | if (exportTracks) {
215 | int numTracks = trackAudio.size();
216 | QDir dir(item.outputPath);
217 | if (!dir.mkpath(".")) {
218 | throw Xcept("Unable to create directory %s", qPrintable(item.outputPath));
219 | }
220 | riffs.clear();
221 | for (int i = 0; i < numTracks; i++) {
222 | RiffWriter* riff = new RiffWriter(ctx->mixer.GetSampleRate(), true);
223 | riffs.emplace_back(riff);
224 | QString filename = dir.absoluteFilePath(QStringLiteral("%1.wav").arg(i));
225 | bool ok = riff->open(filename);
226 | if (!ok) {
227 | riffs.clear();
228 | throw Xcept("Unable to open %s", qPrintable(filename));
229 | }
230 | pad(riff, padStart);
231 | }
232 | } else {
233 | riff.reset(new RiffWriter(ctx->mixer.GetSampleRate(), true));
234 | bool ok = riff->open(item.outputPath);
235 | if (!ok) {
236 | riff.reset();
237 | throw Xcept("Unable to open file");
238 | }
239 | pad(riff.get(), padStart);
240 | }
241 | emit player->exportStarted(item.outputPath);
242 | while (!player->abortExport) {
243 | if (process()) {
244 | break;
245 | }
246 | }
247 | if (exportTracks) {
248 | for (auto& riff : riffs) {
249 | pad(riff.get(), padEnd);
250 | riff->close();
251 | }
252 | } else {
253 | pad(riff.get(), padEnd);
254 | riff->close();
255 | }
256 | if (player->abortExport) {
257 | break;
258 | } else {
259 | emit player->exportFinished(item.outputPath);
260 | }
261 | } catch (std::exception& e) {
262 | emit player->exportError(e.what());
263 | }
264 | }
265 | if (player->abortExport) {
266 | emit player->exportCancelled();
267 | }
268 | }
269 |
270 |
--------------------------------------------------------------------------------
/src/AudioThread.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include "Player.h"
6 | #include "Types.h"
7 | class RiffWriter;
8 |
9 | class AudioThread : public QThread
10 | {
11 | public:
12 | using State = Player::State;
13 |
14 | ~AudioThread();
15 |
16 | protected:
17 | AudioThread(Player* player, const QString& name, PlayerContext* ctx);
18 |
19 | bool process();
20 | void prepare(quint32 addr);
21 | virtual void prepareBuffers() = 0;
22 | virtual void processTrack(std::size_t index, std::vector& samples, bool mute) = 0;
23 | virtual void outputBuffers() = 0;
24 |
25 | Player* player;
26 | PlayerContext* ctx;
27 | std::size_t samplesPerBuffer;
28 | std::vector> trackAudio;
29 | };
30 |
31 | class PlayerThread : public AudioThread
32 | {
33 | public:
34 | PlayerThread(Player* player);
35 | ~PlayerThread();
36 |
37 | protected:
38 | virtual void run() override;
39 |
40 | virtual void prepareBuffers() override;
41 | virtual void processTrack(std::size_t index, std::vector& samples, bool mute) override;
42 | virtual void outputBuffers() override;
43 |
44 | private:
45 | void runStream();
46 | void restart();
47 | void play();
48 |
49 | std::vector silence, masterAudio;
50 | };
51 |
52 | class ExportThread : public AudioThread
53 | {
54 | public:
55 | ExportThread(Player* player);
56 | ~ExportThread();
57 |
58 | protected:
59 | virtual void run() override;
60 |
61 | virtual void prepareBuffers() override;
62 | virtual void processTrack(std::size_t index, std::vector& samples, bool mute) override;
63 | virtual void outputBuffers() override;
64 |
65 | private:
66 | void pad(RiffWriter* riff, std::uint32_t samples) const;
67 |
68 | std::unique_ptr riff;
69 | std::vector> riffs;
70 | std::vector masterLeft, masterRight, silence;
71 |
72 | bool exportTracks;
73 | };
74 |
--------------------------------------------------------------------------------
/src/ConfigManager.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include "ConfigManager.h"
15 | #include "Util.h"
16 | #include "Xcept.h"
17 | #include "Debug.h"
18 | #include "OS.h"
19 |
20 | #ifdef Q_OS_WIN
21 | #define CONFIG_PATH QStandardPaths::AppDataLocation
22 | #else
23 | #define CONFIG_PATH QStandardPaths::ConfigLocation
24 | #endif
25 |
26 | ConfigManager& ConfigManager::Instance()
27 | {
28 | static ConfigManager cm;
29 | return cm;
30 | }
31 |
32 | GameConfig& ConfigManager::GetCfg()
33 | {
34 | if (curCfg)
35 | return *curCfg;
36 | else
37 | throw Xcept("Trying to get the game config without setting the game code");
38 | }
39 |
40 | const GameConfig& ConfigManager::GetCfg() const
41 | {
42 | if (curCfg)
43 | return *curCfg;
44 | else
45 | throw Xcept("Trying to get the game config without setting the game code");
46 | }
47 |
48 | void ConfigManager::SetGameCode(const std::string& gameCode)
49 | {
50 | for (GameConfig& config : configs)
51 | {
52 | const auto &gameCodesToCheck = config.GetGameCodes();
53 | if (std::find(gameCodesToCheck.begin(), gameCodesToCheck.end(), gameCode) != gameCodesToCheck.end()) {
54 | curCfg = &config;
55 | return;
56 | }
57 | }
58 | configs.emplace_back(gameCode);
59 | curCfg = &configs.back();
60 | }
61 |
62 | void ConfigManager::Load()
63 | {
64 | /* Parse things from config file.
65 | * If the config file in home directory is not found,
66 | * try reading it from /etc/agbplay/agbplay.json.
67 | * If this isn't found either, use an empty config file. */
68 | QJsonObject root;
69 | QDir localDir(QStandardPaths::writableLocation(CONFIG_PATH));
70 | QString localPath = localDir.absoluteFilePath("agbplay.json");
71 | QString configPath = QStandardPaths::locate(CONFIG_PATH, "agbplay.json");
72 | #ifdef Q_OS_WIN
73 | // On Windows, AppDataLocation refers to AppData/Roaming/agbplay-gui
74 | // But agbplay CLI looks in AppData/Roaming
75 | // If someone happens to already have a file there, honor it
76 | if (configPath.isEmpty()) {
77 | localDir.cdUp();
78 | if (localDir.exists("agbplay.json")) {
79 | configPath = localPath = localDir.absoluteFilePath("agbplay.json");;
80 | }
81 | }
82 | #endif
83 | if (configPath.isEmpty()) {
84 | configPath = ":/agbplay.json";
85 | Debug::print("No configuration file found. Loading from defaults.");
86 | } else {
87 | if (configPath == localPath) {
88 | Debug::print("User local configuration found!");
89 | } else {
90 | Debug::print("Global configuration found!");
91 | }
92 | }
93 | QFile f(configPath);
94 | if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
95 | throw Xcept("Can't read file: %s", qPrintable(configPath));
96 | }
97 | QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
98 | root = doc.object();
99 |
100 | if (root["id"].toString() != "agbplay")
101 | throw Xcept("Bad JSON ID: %s", qPrintable(root["id"].toString()));
102 |
103 | // output directory used for saving rendered sogs
104 | if (root.contains("wav-output-dir")) {
105 | confWavOutputDir = root["wav-output-dir"].toString().toStdString();
106 | } else {
107 | confWavOutputDir = QDir(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).absoluteFilePath("agbplay").toStdString();
108 | }
109 |
110 | // CGB channel polyphony configuration
111 | confCgbPolyphony = str2cgbPoly(root.value("cgb-polyphony").toString("mono-strict").toStdString());
112 |
113 | // Loop configuration
114 | maxLoopsPlaylist = static_cast(root.value("max-loops-playlist").toInt(1));
115 | maxLoopsExport = static_cast(root.value("max-loops-export").toInt(1));
116 |
117 | // Silence padding
118 | padSecondsStart = root.value("pad-seconds-start").toDouble();
119 | padSecondsEnd = root.value("pad-seconds-end").toDouble();
120 |
121 | for (const QJsonValue& playlistValue : root["playlists"].toArray()) {
122 | QJsonObject playlist = playlistValue.toObject();
123 | // parse games
124 | std::vector games;
125 | for (const QJsonValue& game : playlist["games"].toArray())
126 | games.emplace_back(game.toString().toStdString());
127 | configs.emplace_back(games);
128 |
129 | // parse other parameters
130 | configs.back().SetPCMVol(uint8_t(std::clamp(playlist.value("pcm-master-volume").toInt(15), 0, 15)));
131 | configs.back().SetEngineFreq(uint8_t(std::clamp(playlist.value("pcm-samplerate").toInt(4), 0, 15)));
132 | configs.back().SetEngineRev(uint8_t(std::clamp(playlist.value("pcm-reverb-level").toInt(0), 0, 255)));
133 | configs.back().SetRevBufSize(uint16_t(playlist.value("pcm-reverb-buffer-len").toDouble(0x630)));
134 | configs.back().SetRevType(str2rev(playlist.value("pcm-reverb-type").toString("normal").toStdString()));
135 | configs.back().SetResType(str2res(playlist.value("pcm-resampling-algo").toString("linear").toStdString()));
136 | configs.back().SetResTypeFixed(str2res(playlist.value("pcm-fixed-rate-resampling-algo").toString("linear").toStdString()));
137 | configs.back().SetTrackLimit(uint8_t(std::clamp(playlist.value("song-track-limit").toInt(16), 0, 16)));
138 | configs.back().SetAccurateCh3Volume(playlist.value("accurate-ch3-volume").toBool());
139 | configs.back().SetAccurateCh3Quantization(playlist.value("accurate-ch3-quantization").toBool());
140 | configs.back().SetSimulateCGBSustainBug(playlist.value("simulate-cgb-sustain-bug").toBool());
141 |
142 | for (const QJsonValue& songValue : playlist["songs"].toArray()) {
143 | QJsonObject song = songValue.toObject();
144 | configs.back().GetGameEntries().emplace_back(
145 | song.value("name").toString("?").toStdString(),
146 | static_cast(song.value("index").toInt()));
147 | }
148 | }
149 | }
150 |
151 | void ConfigManager::Save()
152 | {
153 | QJsonArray playlists;
154 | for (GameConfig& cfg : configs)
155 | {
156 | QJsonObject playlist;
157 | playlist["pcm-master-volume"] = static_cast(cfg.GetPCMVol());
158 | playlist["pcm-samplerate"] = static_cast(cfg.GetEngineFreq());
159 | playlist["pcm-reverb-level"] = static_cast(cfg.GetEngineRev());
160 | playlist["pcm-reverb-buffer-len"] = static_cast(cfg.GetRevBufSize());
161 | playlist["pcm-reverb-type"] = QString::fromStdString(rev2str(cfg.GetRevType()));
162 | playlist["pcm-resampling-algo"] = QString::fromStdString(res2str(cfg.GetResType()));
163 | playlist["pcm-fixed-rate-resampling-algo"] = QString::fromStdString(res2str(cfg.GetResTypeFixed()));
164 | playlist["song-track-limit"] = static_cast(cfg.GetTrackLimit());
165 | playlist["accurate-ch3-volume"] = cfg.GetAccurateCh3Volume();
166 | playlist["accurate-ch3-quantization"] = cfg.GetAccurateCh3Quantization();
167 | playlist["simulate-cgb-sustain-bug"] = cfg.GetSimulateCGBSustainBug();
168 |
169 | QJsonArray games;
170 | for (const std::string& code : cfg.GetGameCodes())
171 | games.append(QString::fromStdString(code));
172 | playlist["games"] = games;
173 |
174 | QJsonArray songs;
175 | for (SongEntry entr : cfg.GetGameEntries()) {
176 | QJsonObject song;
177 | song["index"] = entr.GetUID();
178 | song["name"] = QString::fromStdString(entr.name);
179 | songs.append(song);
180 | }
181 | playlist["songs"] = songs;
182 | playlists.append(playlist);
183 | }
184 |
185 | QJsonObject root;
186 | root["id"] = "agbplay";
187 | root["wav-output-dir"] = QString::fromStdString(confWavOutputDir.string());
188 | root["cgb-polyphony"] = QString::fromStdString(cgbPoly2str(confCgbPolyphony));
189 | root["playlists"] = playlists;
190 | root["max-loops-playlist"] = maxLoopsPlaylist;
191 | root["max-loops-export"] = maxLoopsExport;
192 | root["pad-seconds-start"] = padSecondsStart;
193 | root["pad-seconds-end"] = padSecondsEnd;
194 |
195 | QDir localDir(QStandardPaths::writableLocation(CONFIG_PATH));
196 | localDir.mkpath(".");
197 | QString localPath = localDir.absoluteFilePath("agbplay.json");
198 | QFile jsonFile(localPath);
199 | // XXX: QFileDevice::FileError isn't declared with Q_ENUM so we can't get a string description
200 | if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
201 | throw Xcept("Error while writing agbplay.json: %s", QString::number(jsonFile.error()).toStdString());
202 |
203 | QJsonDocument doc(root);
204 | jsonFile.write(doc.toJson());
205 | jsonFile.write("\n");
206 |
207 | Debug::print("Configuration/Playlist saved!");
208 | }
209 |
210 | const std::filesystem::path& ConfigManager::GetWavOutputDir()
211 | {
212 | return confWavOutputDir;
213 | }
214 |
215 | CGBPolyphony ConfigManager::GetCgbPolyphony() const
216 | {
217 | return confCgbPolyphony;
218 | }
219 |
220 | void ConfigManager::SetCgbPolyphony(CGBPolyphony value)
221 | {
222 | confCgbPolyphony = value;
223 | }
224 |
225 | int8_t ConfigManager::GetMaxLoopsPlaylist() const
226 | {
227 | return maxLoopsPlaylist < -1 ? 0 : maxLoopsPlaylist;
228 | }
229 |
230 | void ConfigManager::SetMaxLoopsPlaylist(int8_t value)
231 | {
232 | maxLoopsPlaylist = value;
233 | }
234 |
235 | int8_t ConfigManager::GetMaxLoopsExport() const
236 | {
237 | return maxLoopsExport < 0 ? 0 : maxLoopsExport;
238 | }
239 |
240 | void ConfigManager::SetMaxLoopsExport(int8_t value)
241 | {
242 | maxLoopsExport = value;
243 | }
244 |
245 | double ConfigManager::GetPadSecondsStart() const
246 | {
247 | return padSecondsStart;
248 | }
249 |
250 | void ConfigManager::SetPadSecondsStart(double value)
251 | {
252 | padSecondsStart = value;
253 | }
254 |
255 | double ConfigManager::GetPadSecondsEnd() const
256 | {
257 | return padSecondsEnd;
258 | }
259 |
260 | void ConfigManager::SetPadSecondsEnd(double value)
261 | {
262 | padSecondsEnd = value;
263 | }
264 |
265 |
--------------------------------------------------------------------------------
/src/OS.cpp:
--------------------------------------------------------------------------------
1 | #include "OS.h"
2 |
3 | #if defined(_WIN32)
4 | // if we compile for Windows native
5 |
6 | #include
7 |
8 | void OS::LowerThreadPriority()
9 | {
10 | // ignore errors if this fails
11 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST);
12 | }
13 |
14 | #elif __has_include()
15 | // if we compile for a UNIX'oid
16 |
17 | #include
18 |
19 | void OS::LowerThreadPriority()
20 | {
21 | // we don't really care about errors here, so ignore errno
22 | (void)!nice(15);
23 | }
24 |
25 | #else
26 | // Unsupported OS
27 | #error "Apparently your OS is neither Windows nor appears to be a UNIX variant (no unistd.h). You will have to add support for your OS in src/OS.cpp :/"
28 | #endif
29 |
--------------------------------------------------------------------------------
/src/PianoKeys.cpp:
--------------------------------------------------------------------------------
1 | #include "PianoKeys.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | static const int numWhiteKeys = 7 * 10 + 5;
8 | static const bool hasBlack[] = { true, true, false, true, true, true, false };
9 | static const bool isBlack[] = { false, true, false, true, false, false, true, false, true, false, true, false };
10 | static const int posInOctave[] = { 0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6 };
11 | static const int posToNote[] = { 0, 2, 4, 5, 7, 9, 11 };
12 |
13 | PianoKeys::PianoKeys(QWidget* parent)
14 | : QWidget(parent)
15 | {
16 | setMinimumSize(numWhiteKeys * 3 + 1, 8);
17 | setMaximumWidth(numWhiteKeys * 14 + 1);
18 | setSizeIncrement(75, 2);
19 | setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
20 | }
21 |
22 | int PianoKeys::preferredWidth(int maxWidth)
23 | {
24 | int keyWidth = maxWidth / numWhiteKeys;
25 | if (keyWidth < 3) {
26 | keyWidth = 3;
27 | }
28 | return keyWidth * numWhiteKeys + 1;
29 | }
30 |
31 | void PianoKeys::setNoteOn(int noteNumber, bool on)
32 | {
33 | if (activeKeys[noteNumber] != on) {
34 | activeKeys[noteNumber] = on;
35 | update(keyRect(noteNumber));
36 | }
37 | }
38 |
39 | QRect PianoKeys::keyRect(int noteNum) const
40 | {
41 | int octave = noteNum / 12;
42 | int note = noteNum % 12;
43 | int x = (octave * 7 + posInOctave[note]) * whiteWidth;
44 | if (isBlack[note]) {
45 | return QRect(x + blackOffset, 0, blackWidth, blackHeight);
46 | } else {
47 | return QRect(x, 0, whiteWidth, whiteHeight);
48 | }
49 | }
50 |
51 | void PianoKeys::resizeEvent(QResizeEvent*)
52 | {
53 | whiteWidth = width() / numWhiteKeys;
54 | blackWidth = 2 * whiteWidth / 3;
55 | blackOffset = 2 * whiteWidth / 3;
56 | whiteHeight = height() - 1;
57 | blackHeight = whiteHeight / 2;
58 | }
59 |
60 | int PianoKeys::noteAt(const QPoint& pos) const
61 | {
62 | int x = pos.x() / whiteWidth;
63 | return (x / 7) * 12 + posToNote[x % 7];
64 | }
65 |
66 | void PianoKeys::paintEvent(QPaintEvent* e)
67 | {
68 | int left = std::clamp(noteAt(e->rect().topLeft()) - 1, 0, 126);
69 | int right = std::clamp(noteAt(e->rect().bottomRight()) + 1, left + 1, 127);
70 |
71 | QPainter p(this);
72 | QPalette pal = palette();
73 | QBrush white = pal.base();
74 | QBrush altWhite = pal.alternateBase();
75 | QBrush black = pal.shadow();
76 | QBrush light = pal.highlight();
77 | QBrush blackLight(pal.color(QPalette::Highlight).darker(120));
78 | QColor lightFrame(pal.color(QPalette::Highlight).darker(150));
79 |
80 | // First draw the white keys
81 | for (int i = left; i <= right; i++) {
82 | int degree = i % 12;
83 | if (isBlack[degree]) {
84 | continue;
85 | }
86 | QRect r = keyRect(i);
87 | if (activeKeys[i]) {
88 | p.fillRect(r, light);
89 | p.setPen(lightFrame);
90 | } else {
91 | p.fillRect(r, degree == 0 ? altWhite : white);
92 | p.setPen(QPalette::Text);
93 | }
94 | p.drawRect(r);
95 | }
96 |
97 | // Then draw the black keys on top
98 | for (int i = left; i <= right; i++) {
99 | int degree = i % 12;
100 | if (!isBlack[degree]) {
101 | continue;
102 | }
103 | QRect r = keyRect(i);
104 | if (activeKeys[i]) {
105 | p.fillRect(r, blackLight);
106 | p.setPen(lightFrame);
107 | } else {
108 | p.fillRect(r, black);
109 | p.setPen(QPalette::Text);
110 | }
111 | p.drawRect(r);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/PianoKeys.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | class PianoKeys : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | PianoKeys(QWidget* parent = nullptr);
11 |
12 | static int preferredWidth(int maxWidth);
13 |
14 | public slots:
15 | void setNoteOn(int noteNumber, bool on);
16 |
17 | protected:
18 | void resizeEvent(QResizeEvent*);
19 | void paintEvent(QPaintEvent*);
20 |
21 | private:
22 | QRect keyRect(int noteNum) const;
23 | int noteAt(const QPoint& pos) const;
24 |
25 | int keyboardLeft;
26 | int whiteWidth;
27 | int whiteHeight;
28 | int blackOffset;
29 | int blackWidth;
30 | int blackHeight;
31 |
32 | std::bitset<128> activeKeys;
33 | };
34 |
--------------------------------------------------------------------------------
/src/Player.cpp:
--------------------------------------------------------------------------------
1 | #include "Player.h"
2 | #include "AudioThread.h"
3 | #include "ConfigManager.h"
4 | #include "Rom.h"
5 | #include "SongModel.h"
6 | #include "UiUtils.h"
7 | #include "Debug.h"
8 | #include "RiffWriter.h"
9 | #include
10 |
11 | // first portaudio hostapi has highest priority, last hostapi has lowest
12 | // if none are available, the default one is selected.
13 | // they are also the ones which are known to work
14 | static const std::vector hostApiPriority = {
15 | // Unix
16 | paJACK,
17 | paALSA,
18 | // Windows
19 | paWASAPI,
20 | paMME,
21 | // Mac OS
22 | paCoreAudio,
23 | paSoundManager,
24 | };
25 |
26 | void Player::detectHostApi()
27 | {
28 | outputStreamParameters.channelCount = 2; // stereo
29 | outputStreamParameters.sampleFormat = paFloat32;
30 |
31 | // init host api
32 | std::vector hostApiPrioritiesWithFallback = hostApiPriority;
33 | const PaHostApiIndex defaultHostApiIndex = Pa_GetDefaultHostApi();
34 | if (defaultHostApiIndex < 0)
35 | throw Xcept("Pa_GetDefaultHostApi(): No host API avilable: %s", Pa_GetErrorText(defaultHostApiIndex));
36 | const PaHostApiInfo *defaultHostApiInfo = Pa_GetHostApiInfo(defaultHostApiIndex);
37 | if (defaultHostApiInfo == nullptr)
38 | throw Xcept("Pa_GetHostApiInfo(): failed with valid index");
39 | const auto f = std::find(hostApiPrioritiesWithFallback.begin(), hostApiPrioritiesWithFallback.end(), defaultHostApiInfo->type);
40 | if (f == hostApiPrioritiesWithFallback.end())
41 | hostApiPrioritiesWithFallback.push_back(defaultHostApiInfo->type);
42 |
43 | for (const auto apiType : hostApiPrioritiesWithFallback) {
44 | const PaHostApiIndex hostApiIndex = Pa_HostApiTypeIdToHostApiIndex(apiType);
45 | // prioritized host api available ?
46 | if (hostApiIndex < 0)
47 | continue;
48 |
49 | const PaHostApiInfo *apiInfo = Pa_GetHostApiInfo(hostApiIndex);
50 | if (apiInfo == nullptr)
51 | throw Xcept("Pa_GetHostApiInfo with valid index failed");
52 | const PaDeviceIndex deviceIndex = apiInfo->defaultOutputDevice;
53 |
54 | const PaDeviceInfo *devInfo = Pa_GetDeviceInfo(deviceIndex);
55 | if (devInfo == nullptr)
56 | throw Xcept("Pa_GetDeviceInfo(): failed with valid index");
57 |
58 | outputStreamParameters.device = deviceIndex;
59 | outputStreamParameters.suggestedLatency = devInfo->defaultLowOutputLatency;
60 | outputStreamParameters.hostApiSpecificStreamInfo = nullptr;
61 |
62 | #if __has_include()
63 | if (apiType == paWASAPI) {
64 | memset(&wasapiStreamInfo, 0, sizeof(wasapiStreamInfo));
65 | wasapiStreamInfo.size = sizeof(wasapiStreamInfo);
66 | wasapiStreamInfo.hostApiType = paWASAPI;
67 | wasapiStreamInfo.version = 1;
68 | wasapiStreamInfo.flags = paWinWasapiAutoConvert;
69 | }
70 | #endif
71 |
72 | PaError err = Pa_OpenStream(&audioStream, nullptr, &outputStreamParameters, STREAM_SAMPLERATE, paFramesPerBufferUnspecified, paNoFlag, audioCallback, this);
73 | if (err != paNoError) {
74 | Debug::print("Pa_OpenStream(): unable to open stream with host API %s: %s", apiInfo->name, Pa_GetErrorText(err));
75 | continue;
76 | }
77 |
78 | err = Pa_StartStream(audioStream);
79 | if (err != paNoError) {
80 | Debug::print("Pa_StartStream(): unable to start stream for Host API %s: %s", apiInfo->name, Pa_GetErrorText(err));
81 | err = Pa_CloseStream(audioStream);
82 | if (err != paNoError) {
83 | Debug::print("Pa_CloseStream(): unable to close stream for Host API %s: %s", apiInfo->name, Pa_GetErrorText(err));
84 | }
85 | audioStream = nullptr;
86 | continue;
87 | }
88 | Pa_StopStream(audioStream);
89 | return;
90 | }
91 |
92 | throw Xcept("Unable to initialize sound output: Host API could not be initialized");
93 | }
94 |
95 | Player::Player(QObject* parent)
96 | : QObject(parent), ctx(nullptr), playerState(State::TERMINATED), audioStream(nullptr),
97 | speedFactor(64), rBuf(STREAM_BUF_SIZE)
98 | {
99 | detectHostApi();
100 |
101 | model = new SongModel(this);
102 |
103 | timer.setTimerType(Qt::PreciseTimer);
104 | timer.setSingleShot(false);
105 | timer.setInterval(1000/30);
106 | QObject::connect(&timer, SIGNAL(timeout()), &updateThrottle, SLOT(start()));
107 |
108 | updateThrottle.setSingleShot(true);
109 | updateThrottle.setInterval(0);
110 | QObject::connect(&updateThrottle, SIGNAL(timeout()), this, SLOT(update()));
111 | }
112 |
113 | Player::~Player()
114 | {
115 | if (audioStream) {
116 | Pa_StopStream(audioStream);
117 | PaError err = Pa_CloseStream(audioStream);
118 | if (err != paNoError) {
119 | Debug::print("Pa_CloseStream: %s", Pa_GetErrorText(err));
120 | }
121 | }
122 | }
123 |
124 | Rom* Player::openRom(const QString& path)
125 | {
126 | stop();
127 | if (path.isEmpty()) {
128 | ctx.reset();
129 | return nullptr;
130 | }
131 |
132 | Rom::CreateInstance(qPrintable(path));
133 | Rom* rom = &Rom::Instance();
134 | ConfigManager::Instance().SetGameCode(rom->GetROMCode());
135 | const auto& cfg = ConfigManager::Instance().GetCfg();
136 |
137 | ctx = std::make_unique(
138 | ConfigManager::Instance().GetMaxLoopsPlaylist(),
139 | cfg.GetTrackLimit(),
140 | EnginePars(cfg.GetPCMVol(), cfg.GetEngineRev(), cfg.GetEngineFreq())
141 | );
142 |
143 | songTableAddrs.clear();
144 | for (SongTable& table : SongTable::ScanForTables()) {
145 | songTableAddrs.push_back(table.GetSongTablePos());
146 | }
147 | emit songTablesFound(songTableAddrs);
148 |
149 | setSongTable(songTableAddrs[0]);
150 | return rom;
151 | }
152 |
153 | void Player::setSongTable(quint32 addr)
154 | {
155 | Rom* rom = &Rom::Instance();
156 | auto iter = std::find(songTableAddrs.begin(), songTableAddrs.end(), addr);
157 | if (iter != songTableAddrs.begin() && iter != songTableAddrs.end()) {
158 | int index = iter - songTableAddrs.begin();
159 | ConfigManager::Instance().SetGameCode(QStringLiteral("%1:%2").arg(QString::fromStdString(rom->GetROMCode())).arg(index).toStdString());
160 | } else {
161 | ConfigManager::Instance().SetGameCode(rom->GetROMCode());
162 | }
163 | songTable.reset(new SongTable(addr));
164 | model->setSongTable(songTable.get());
165 | emit songTableUpdated(songTable.get());
166 |
167 | selectSong(0);
168 | }
169 |
170 | SongModel* Player::songModel() const
171 | {
172 | return model;
173 | }
174 |
175 | void Player::selectSong(int index)
176 | {
177 | stop();
178 | QModelIndex idx = model->index(index, 0);
179 | std::uint32_t addr = model->songAddress(idx);
180 | ctx->InitSong(addr);
181 |
182 | vuState.setTrackCount(int(ctx->seq.tracks.size()));
183 |
184 | emit songChanged(ctx.get(), addr, idx.data(Qt::DisplayRole).toString());
185 | }
186 |
187 | void Player::play()
188 | {
189 | if (!ctx) {
190 | return;
191 | }
192 | try {
193 | if (!playerThread) {
194 | playerThread.reset(new PlayerThread(this));
195 | QObject::connect(playerThread.get(), SIGNAL(finished()), this, SLOT(playbackDone()), Qt::QueuedConnection);
196 | playerThread->start();
197 | } else {
198 | State state = playerState;
199 | if (state == State::PLAYING) {
200 | setState(State::RESTART);
201 | } else if (state == State::PAUSED) {
202 | setState(State::PLAYING);
203 | }
204 | }
205 | } catch (std::exception& e) {
206 | Debug::print(e.what());
207 | emit threadError(tr("An error occurred while preparing to play:\n\n%1").arg(e.what()));
208 | return;
209 | }
210 | timer.start();
211 | }
212 |
213 | void Player::pause()
214 | {
215 | timer.stop();
216 | if (!ctx || !playerThread) {
217 | return;
218 | }
219 | State state = playerState;
220 | if (state == State::PLAYING) {
221 | setState(State::PAUSED);
222 | } else if (state == State::PAUSED) {
223 | setState(State::PLAYING);
224 | timer.start();
225 | }
226 | }
227 |
228 | void Player::stop()
229 | {
230 | timer.stop();
231 | if (!ctx) {
232 | return;
233 | }
234 | while (playerState == State::RESTART) {
235 | QThread::msleep(5);
236 | }
237 | if (playerState != State::TERMINATED) {
238 | setState(State::SHUTDOWN);
239 | }
240 | while (playerState != State::TERMINATED) {
241 | QThread::msleep(5);
242 | }
243 | }
244 |
245 | void Player::playbackDone()
246 | {
247 | playerThread.reset();
248 | setState(State::TERMINATED);
249 | vuState.reset();
250 | update();
251 | }
252 |
253 | void Player::togglePlay()
254 | {
255 | if (playerState == State::TERMINATED) {
256 | play();
257 | } else {
258 | pause();
259 | }
260 | }
261 |
262 | void Player::setState(Player::State state)
263 | {
264 | playerState = state;
265 | emit stateChanged(state == State::RESTART || state == State::PLAYING || state == State::PAUSED, state == State::PAUSED);
266 | }
267 |
268 | void Player::update()
269 | {
270 | emit updated(ctx.get(), &vuState);
271 | }
272 |
273 | void Player::setMute(int trackIdx, bool on)
274 | {
275 | auto& track = ctx->seq.tracks[trackIdx];
276 | if (track.muted != on) {
277 | track.muted = on;
278 | updateThrottle.start();
279 | }
280 | }
281 |
282 | void Player::setSpeed(double mult)
283 | {
284 | if (!ctx) {
285 | return;
286 | }
287 | ctx->reader.SetSpeedFactor(mult);
288 | }
289 |
290 | int Player::audioCallback(const void*, void* output, unsigned long frames, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void* self)
291 | {
292 | return reinterpret_cast(self)->audioCallback(reinterpret_cast(output), frames);
293 | }
294 |
295 | int Player::audioCallback(sample* output, size_t frames)
296 | {
297 | rBuf.Take(output, frames);
298 | return 0;
299 | }
300 |
301 | bool Player::exportToWave(const QString& filename, int track)
302 | {
303 | if (!ctx || !exportQueue.isEmpty() || exportThread) {
304 | return false;
305 | }
306 | try {
307 | QModelIndex idx = model->index(track, 0);
308 | quint32 addr = model->songAddress(idx);
309 | ExportItem item;
310 | item.outputPath = filename;
311 | item.trackAddr = addr;
312 | item.splitTracks = false;
313 | exportQueue << item;
314 | exportThread.reset(new ExportThread(this));
315 | QObject::connect(exportThread.get(), SIGNAL(finished()), this, SLOT(exportDone()), Qt::QueuedConnection);
316 | exportThread->start();
317 | } catch (std::exception& e) {
318 | Debug::print(e.what());
319 | emit threadError(tr("An error occurred while preparing to export:\n\n%1").arg(e.what()));
320 | return false;
321 | }
322 | return true;
323 | }
324 |
325 | bool Player::exportToWave(const QDir& path, const QList& tracks, bool split)
326 | {
327 | if (!ctx || !exportQueue.isEmpty() || exportThread) {
328 | return false;
329 | }
330 | try {
331 | for (int track : tracks) {
332 | QModelIndex idx = model->index(track, 0);
333 | quint32 addr = model->songAddress(idx);
334 | QString name = model->data(idx, Qt::EditRole).toString();
335 | if (!split || tracks.length() > 1) {
336 | QString prefix = fixedNumber(track, 4);
337 | if (name.isEmpty()) {
338 | name = prefix;
339 | } else {
340 | name = QStringLiteral("%1 - %2").arg(prefix).arg(name);
341 | }
342 | if (!split) {
343 | name = name + ".wav";
344 | }
345 | }
346 | ExportItem item;
347 | item.outputPath = path.absoluteFilePath(name);
348 | if (split && !item.outputPath.endsWith(path.separator())) {
349 | item.outputPath += path.separator();
350 | }
351 | item.trackAddr = addr;
352 | item.splitTracks = split;
353 | exportQueue << item;
354 | }
355 | exportThread.reset(new ExportThread(this));
356 | QObject::connect(exportThread.get(), SIGNAL(finished()), this, SLOT(exportDone()), Qt::QueuedConnection);
357 | exportThread->start();
358 | } catch (std::exception& e) {
359 | Debug::print(e.what());
360 | emit threadError(tr("An error occurred while preparing to export:\n\n%1").arg(e.what()));
361 | return false;
362 | }
363 | return true;
364 | }
365 |
366 | void Player::exportDone()
367 | {
368 | exportThread.reset();
369 | exportQueue.clear();
370 | }
371 |
372 | void Player::cancelExport()
373 | {
374 | abortExport = true;
375 | }
376 |
--------------------------------------------------------------------------------
/src/Player.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #if __has_include()
11 | #include
12 | #endif
13 | #include "PlayerContext.h"
14 | #include "LoudnessCalculator.h"
15 | #include "SoundData.h"
16 | #include "Ringbuffer.h"
17 | #include "VUMeter.h"
18 | class SongModel;
19 | class Rom;
20 |
21 | struct ExportItem {
22 | QString outputPath;
23 | quint32 trackAddr;
24 | bool splitTracks;
25 | };
26 |
27 | class Player : public QObject
28 | {
29 | Q_OBJECT
30 | friend class AudioThread;
31 | friend class PlayerThread;
32 | friend class ExportThread;
33 | public:
34 | Player(QObject* parent = nullptr);
35 | ~Player();
36 |
37 | void detectHostApi();
38 |
39 | Rom* openRom(const QString& path);
40 | SongModel* songModel() const;
41 | void selectSong(int index);
42 |
43 | bool exportToWave(const QString& filename, int track);
44 | bool exportToWave(const QDir& path, const QList& tracks, bool split);
45 |
46 | signals:
47 | void threadError(const QString& message);
48 | void songTablesFound(const std::vector& addrs);
49 | void songTableUpdated(SongTable* table);
50 | void songChanged(PlayerContext* context, quint32 addr, const QString& name);
51 | void updated(PlayerContext* context, VUState* vu);
52 | void stateChanged(bool isPlaying, bool isPaused);
53 | void exportStarted(const QString& path);
54 | void exportFinished(const QString& path);
55 | void exportError(const QString& message);
56 | void playbackError(const QString& message);
57 | void exportCancelled();
58 |
59 | public slots:
60 | void setSongTable(quint32 addr);
61 | void setMute(int trackIdx, bool on);
62 | void setSpeed(double mult);
63 |
64 | void play();
65 | void pause();
66 | void stop();
67 | void togglePlay();
68 |
69 | void cancelExport();
70 |
71 | private slots:
72 | void update();
73 | void playbackDone();
74 | void exportDone();
75 |
76 | private:
77 | enum class State : int {
78 | RESTART, PLAYING, PAUSED, TERMINATED, SHUTDOWN
79 | };
80 |
81 | static int audioCallback(const void*, void*, unsigned long, const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void*);
82 | int audioCallback(sample* output, size_t frames);
83 | void setState(State state);
84 |
85 | PaStreamParameters outputStreamParameters;
86 | #if __has_include()
87 | PaWasapiStreamInfo wasapiStreamInfo;
88 | #endif
89 |
90 | QTimer timer, updateThrottle;
91 |
92 | std::unique_ptr ctx;
93 | std::unique_ptr songTable;
94 | std::unique_ptr playerThread;
95 | std::unique_ptr exportThread;
96 | SongModel* model;
97 |
98 | std::atomic playerState;
99 | std::atomic abortExport;
100 |
101 | PaStream* audioStream;
102 | uint32_t speedFactor;
103 | Ringbuffer rBuf;
104 |
105 | VUState vuState;
106 | std::vector mutedTracks;
107 | QList exportQueue;
108 | std::vector songTableAddrs;
109 | };
110 |
--------------------------------------------------------------------------------
/src/PlayerControls.cpp:
--------------------------------------------------------------------------------
1 | #include "PlayerControls.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | static const QPair speedPresets[] = {
13 | { "1/16x", -34 },
14 | { "1/8x", -26 },
15 | { "1/4x", -18 },
16 | { "1/2x", -10 },
17 | { "1x", 0 },
18 | { "2x", 10 },
19 | { "4x", 18 },
20 | { "8x", 26 },
21 | { "16x", 34 },
22 | };
23 |
24 | PlayerControls::PlayerControls(QWidget* parent)
25 | : QWidget(parent), trackLoaded(false)
26 | {
27 | QVBoxLayout* layout = new QVBoxLayout(this);
28 | QHBoxLayout* hbox = new QHBoxLayout;
29 |
30 | toggle = new QAction(tr("Play/Pause"), this);
31 | toggle->setShortcut(Qt::Key_Space);
32 | toggle->setEnabled(false);
33 | QObject::connect(toggle, SIGNAL(triggered(bool)), this, SIGNAL(togglePlay()));
34 |
35 | playButton = makeButton(QStyle::SP_MediaPlay, tr("&Play"), SIGNAL(play()));
36 | pauseButton = makeButton(QStyle::SP_MediaPause, tr("P&ause"), SIGNAL(pause()));
37 | stopButton = makeButton(QStyle::SP_MediaStop, tr("&Stop"), SIGNAL(stop()));
38 | stopAction()->setShortcut(Qt::Key_Escape);
39 |
40 | hbox->addStretch(1);
41 | hbox->addWidget(playButton);
42 | hbox->addWidget(pauseButton);
43 | hbox->addWidget(stopButton);
44 | hbox->addStretch(1);
45 |
46 | QHBoxLayout* speed = new QHBoxLayout;
47 | QLabel* speedLabel = new QLabel(tr("&Speed:"), this);
48 | speedSlider = new QSlider(Qt::Horizontal, this);
49 | speedLabel->setBuddy(speedSlider);
50 | speedSlider->setRange(-34, 34);
51 | speedSlider->setTickPosition(QSlider::TicksBothSides);
52 | speedSlider->setTickInterval(34);
53 | speedSlider->setTracking(true);
54 | speedSlider->setPageStep(10);
55 | speedSlider->setContextMenuPolicy(Qt::CustomContextMenu);
56 | speed->addWidget(speedLabel, 0);
57 | speed->addWidget(speedSlider, 1);
58 |
59 | layout->addStretch(1);
60 | layout->addLayout(hbox);
61 | layout->addLayout(speed);
62 |
63 | speedMenu = new QMenu(speedSlider);
64 | for (const auto& pair : speedPresets) {
65 | QAction* action = new QAction(pair.first, speedSlider);
66 | action->setData(pair.second);
67 | action->setCheckable(true);
68 | speedMenu->addAction(action);
69 | }
70 | speedSlider->installEventFilter(this);
71 | updateSpeed();
72 | QObject::connect(speedSlider, SIGNAL(valueChanged(int)), this, SLOT(speedSliderChanged(int)));
73 | QObject::connect(speedSlider, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showSpeedMenu(QPoint)));
74 | QObject::connect(speedMenu, SIGNAL(triggered(QAction*)), this, SLOT(setSpeedByAction(QAction*)));
75 | }
76 |
77 | QAction* PlayerControls::toggleAction() const
78 | {
79 | return toggle;
80 | }
81 |
82 | QAction* PlayerControls::playAction() const
83 | {
84 | return playButton->defaultAction();
85 | }
86 |
87 | QAction* PlayerControls::pauseAction() const
88 | {
89 | return pauseButton->defaultAction();
90 | }
91 |
92 | QAction* PlayerControls::stopAction() const
93 | {
94 | return stopButton->defaultAction();
95 | }
96 |
97 | QToolButton* PlayerControls::makeButton(QStyle::StandardPixmap icon, const QString& text, const char* slot)
98 | {
99 | QToolButton* btn = new QToolButton(this);
100 | QAction* action = new QAction(style()->standardIcon(icon, nullptr, this), text, this);
101 | action->setEnabled(false);
102 | btn->setDefaultAction(action);
103 | if (slot) {
104 | QObject::connect(action, SIGNAL(triggered(bool)), this, slot);
105 | }
106 | return btn;
107 | }
108 |
109 | void PlayerControls::songChanged(PlayerContext* ctx)
110 | {
111 | trackLoaded = !!ctx;
112 | updateState(false, false);
113 | }
114 |
115 | void PlayerControls::updateState(bool isPlaying, bool)
116 | {
117 | toggle->setEnabled(trackLoaded);
118 | playAction()->setEnabled(trackLoaded);
119 | pauseAction()->setEnabled(isPlaying);
120 | stopAction()->setEnabled(isPlaying);
121 | }
122 |
123 | void PlayerControls::speedSliderChanged(int value)
124 | {
125 | if (value >= -2 && value <= 2) {
126 | speedSlider->setValue(0);
127 | speedSlider->setPageStep(10);
128 | updateSpeed();
129 | return;
130 | }
131 | speedSlider->setPageStep(8);
132 | updateSpeed();
133 | }
134 |
135 | void PlayerControls::setSpeedByAction(QAction* action)
136 | {
137 | int value = action->data().toInt();
138 | speedSlider->setValue(value);
139 | speedSliderChanged(value);
140 | }
141 |
142 | void PlayerControls::updateSpeed()
143 | {
144 | double mult = speedMultiplier();
145 | if (mult < 1) {
146 | speedSlider->setToolTip(QStringLiteral("1/%L1x").arg(1.0 / mult, 0, 'g', 2));
147 | } else {
148 | speedSlider->setToolTip(QStringLiteral("%L1x").arg(mult, 0, 'g', 2));
149 | }
150 | for (QAction* action : speedMenu->actions()) {
151 | action->setChecked(std::abs(speedMultiplier(action->data().toInt()) - mult) < 0.001);
152 | }
153 | emit setSpeed(mult);
154 | }
155 |
156 | double PlayerControls::speedMultiplier() const
157 | {
158 | return speedMultiplier(speedSlider->value());
159 | }
160 |
161 | double PlayerControls::speedMultiplier(int value) const
162 | {
163 | if (value < 0) {
164 | value += 2;
165 | } else if (value > 0) {
166 | value -= 2;
167 | }
168 | // should range from -4.0 to +4.0
169 | double mag = value / 8.0;
170 | // should range from 1/16x to 16x
171 | return std::exp2(mag);
172 | }
173 |
174 | bool PlayerControls::eventFilter(QObject* obj, QEvent* event)
175 | {
176 | if (obj == speedSlider) {
177 | if (event->type() == QEvent::MouseButtonDblClick) {
178 | QMouseEvent* me = static_cast(event);
179 | if (me->button() == Qt::LeftButton) {
180 | speedSliderChanged(0);
181 | return true;
182 | }
183 | }
184 | }
185 | return false;
186 | }
187 |
188 | void PlayerControls::showSpeedMenu(const QPoint& pos)
189 | {
190 | speedMenu->exec(speedSlider->mapToGlobal(pos));
191 | }
192 |
--------------------------------------------------------------------------------
/src/PlayerControls.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | class QToolButton;
6 | class QSlider;
7 | class QMenu;
8 | class QAction;
9 | class PlayerContext;
10 |
11 | class PlayerControls : public QWidget
12 | {
13 | Q_OBJECT
14 | public:
15 | PlayerControls(QWidget* parent = nullptr);
16 |
17 | QAction* toggleAction() const;
18 | QAction* playAction() const;
19 | QAction* pauseAction() const;
20 | QAction* stopAction() const;
21 |
22 | double speedMultiplier() const;
23 |
24 | bool eventFilter(QObject* obj, QEvent* event);
25 |
26 | public slots:
27 | void songChanged(PlayerContext*);
28 | void updateState(bool isPlaying, bool isPaused);
29 |
30 | signals:
31 | void togglePlay();
32 | void play();
33 | void pause();
34 | void stop();
35 | void setSpeed(double multiplier);
36 |
37 | private slots:
38 | void showSpeedMenu(const QPoint& pos);
39 | void setSpeedByAction(QAction* action);
40 | void speedSliderChanged(int value);
41 |
42 | private:
43 | QToolButton* makeButton(QStyle::StandardPixmap icon, const QString& text, const char* slot = nullptr);
44 | void updateSpeed();
45 | double speedMultiplier(int value) const;
46 |
47 | QAction* toggle;
48 | QToolButton* playButton;
49 | QToolButton* pauseButton;
50 | QToolButton* stopButton;
51 | QSlider* speedSlider;
52 | QMenu* speedMenu;
53 |
54 | bool trackLoaded;
55 | };
56 |
--------------------------------------------------------------------------------
/src/PlayerWindow.cpp:
--------------------------------------------------------------------------------
1 | #include "PlayerWindow.h"
2 | #include "TrackList.h"
3 | #include "VUMeter.h"
4 | #include "RomView.h"
5 | #include "Rom.h"
6 | #include "ConfigManager.h"
7 | #include "SongModel.h"
8 | #include "Player.h"
9 | #include "PlayerControls.h"
10 | #include "PlaylistModel.h"
11 | #include "PreferencesWindow.h"
12 | #include "UiUtils.h"
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include
23 | #include
24 | #include
25 | #include
26 | #include
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include
32 |
33 | PlayerWindow::PlayerWindow(Player* player, QWidget* parent)
34 | : QMainWindow(parent), player(player), playlistIsDirty(false)
35 | {
36 | setWindowTitle("agbplay");
37 | songs = player->songModel();
38 | playlist = new PlaylistModel(songs);
39 |
40 | QWidget* base = new QWidget(this);
41 | setCentralWidget(base);
42 |
43 | QVBoxLayout* vbox = new QVBoxLayout(base);
44 | vbox->addLayout(makeTop(), 0);
45 |
46 | QHBoxLayout* hbox = new QHBoxLayout;
47 | hbox->addLayout(makeLeft(), 1);
48 | hbox->addLayout(makeRight(), 4);
49 | vbox->addLayout(hbox, 1);
50 |
51 | setMenuBar(new QMenuBar(this));
52 | makeMenu();
53 |
54 | QObject::connect(this, SIGNAL(romUpdated(Rom*)), romView, SLOT(updateRom(Rom*)));
55 | QObject::connect(player, SIGNAL(songTablesFound(std::vector)), romView, SLOT(songTablesFound(std::vector)));
56 | QObject::connect(player, SIGNAL(songTableUpdated(SongTable*)), romView, SLOT(updateSongTable(SongTable*)));
57 | QObject::connect(romView, SIGNAL(songTableSelected(quint32)), player, SLOT(setSongTable(quint32)));
58 | QObject::connect(songList, SIGNAL(activated(QModelIndex)), this, SLOT(selectSong(QModelIndex)));
59 | QObject::connect(songList, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(selectSong(QModelIndex)));
60 | QObject::connect(songList->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(clearOtherSelection(QItemSelection)));
61 | QObject::connect(playlistView, SIGNAL(activated(QModelIndex)), this, SLOT(selectSong(QModelIndex)));
62 | QObject::connect(playlistView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(selectSong(QModelIndex)));
63 | QObject::connect(playlistView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(clearOtherSelection(QItemSelection)));
64 | QObject::connect(player, SIGNAL(songChanged(PlayerContext*,quint32,QString)), trackList, SLOT(selectSong(PlayerContext*,quint32,QString)));
65 | QObject::connect(player, SIGNAL(songChanged(PlayerContext*,quint32,QString)), songs, SLOT(songChanged(PlayerContext*,quint32)));
66 | QObject::connect(player, SIGNAL(songChanged(PlayerContext*,quint32,QString)), controls, SLOT(songChanged(PlayerContext*)));
67 | QObject::connect(player, SIGNAL(updated(PlayerContext*,VUState*)), trackList, SLOT(update(PlayerContext*,VUState*)));
68 | QObject::connect(player, SIGNAL(updated(PlayerContext*,VUState*)), this, SLOT(updateVU(PlayerContext*,VUState*)));
69 | QObject::connect(trackList, SIGNAL(muteToggled(int,bool)), player, SLOT(setMute(int,bool)));
70 | QObject::connect(controls, SIGNAL(togglePlay()), player, SLOT(togglePlay()));
71 | QObject::connect(controls, SIGNAL(play()), player, SLOT(play()));
72 | QObject::connect(controls, SIGNAL(pause()), player, SLOT(pause()));
73 | QObject::connect(controls, SIGNAL(stop()), player, SLOT(stop()));
74 | QObject::connect(controls, SIGNAL(setSpeed(double)), player, SLOT(setSpeed(double)));
75 | QObject::connect(player, SIGNAL(stateChanged(bool,bool)), controls, SLOT(updateState(bool,bool)));
76 | QObject::connect(player, SIGNAL(stateChanged(bool,bool)), songs, SLOT(stateChanged(bool,bool)));
77 | QObject::connect(recentsMenu, SIGNAL(triggered(QAction*)), this, SLOT(openRecent(QAction*)));
78 | QObject::connect(playlist, SIGNAL(playlistDirty(bool)), this, SLOT(playlistDirty(bool)));
79 | QObject::connect(player, SIGNAL(exportStarted(QString)), this, SLOT(exportStarted(QString)));
80 | QObject::connect(player, SIGNAL(exportFinished(QString)), this, SLOT(exportFinished(QString)));
81 | QObject::connect(player, SIGNAL(exportError(QString)), this, SLOT(exportError(QString)));
82 | QObject::connect(player, SIGNAL(exportCancelled()), this, SLOT(exportCancelled()));
83 | QObject::connect(player, SIGNAL(playbackError(QString)), this, SLOT(playbackError(QString)));
84 | }
85 |
86 | QLayout* PlayerWindow::makeTop()
87 | {
88 | QHBoxLayout* layout = new QHBoxLayout;
89 |
90 | layout->addWidget(makeTitle(), 0);
91 |
92 | masterVU = new VUMeter(this);
93 | masterVU->setStereoLayout(Qt::Vertical);
94 | layout->addWidget(masterVU, 1);
95 |
96 | return layout;
97 | }
98 |
99 | QLayout* PlayerWindow::makeLeft()
100 | {
101 | QVBoxLayout* layout = new QVBoxLayout;
102 |
103 | songList = makeView(songs);
104 | songList->setDragDropMode(QAbstractItemView::DragOnly);
105 | layout->addWidget(songList);
106 |
107 | playlistView = makeView(playlist);
108 | playlistView->setDragEnabled(true);
109 | playlistView->setDropIndicatorShown(true);
110 | playlistView->setAcceptDrops(true);
111 | playlistView->setDragDropMode(QAbstractItemView::DragDrop);
112 | layout->addWidget(playlistView);
113 |
114 | return layout;
115 | }
116 |
117 | QLayout* PlayerWindow::makeRight()
118 | {
119 | QGridLayout* grid = new QGridLayout;
120 | grid->setRowStretch(0, 1);
121 | grid->setColumnStretch(0, 1);
122 |
123 | grid->addWidget(trackList = new TrackList(this), 0, 0, 2, 1);
124 | grid->addWidget(romView = new RomView(this), 0, 1);
125 | grid->addWidget(controls = new PlayerControls(this), 1, 1);
126 | grid->addWidget(log = new QPlainTextEdit(this), 2, 0, 1, 2);
127 |
128 | progressPanel = new QWidget(this);
129 | QHBoxLayout* hbox = new QHBoxLayout(progressPanel);;
130 | hbox->setContentsMargins(0, 0, 0, 0);
131 | hbox->addWidget(new QLabel(tr("Exporting:"), this), 0);
132 | hbox->addWidget(exportProgress = new QProgressBar(this), 1);
133 | exportProgress->setFormat("%v / %m");
134 | QPushButton* abort = new QPushButton(tr("Abort"), this);
135 | hbox->addWidget(abort, 0);
136 | grid->addWidget(progressPanel, 3, 0, 1, 2);
137 | progressPanel->hide();
138 |
139 | log->setReadOnly(true);
140 | log->setMaximumHeight(100);
141 |
142 | QObject::connect(abort, SIGNAL(clicked()), player, SLOT(cancelExport()));
143 |
144 | return grid;
145 | }
146 |
147 | void PlayerWindow::makeMenu()
148 | {
149 | QMenuBar* mb = menuBar();
150 | QMenu* fileMenu = mb->addMenu(tr("&File"));
151 | fileMenu->addAction(tr("&Open ROM..."), this, SLOT(openRom()), QKeySequence::Open);
152 | recentsMenu = fileMenu->addMenu(tr("Open &Recent"));
153 | fillRecents();
154 | fileMenu->addSeparator();
155 | saveAction = fileMenu->addAction(tr("&Save Playlist"), playlist, SLOT(save()), QKeySequence::Save);
156 | fileMenu->addSeparator();
157 | exportAction = fileMenu->addAction(tr("&Export Selected..."), this, SLOT(promptForExport()), Qt::CTRL | Qt::Key_E);
158 | exportChannelsAction = fileMenu->addAction(tr("Export &Channels for Selected..."), this, SLOT(promptForExportChannels()));
159 | exportAllAction = fileMenu->addAction(tr("Export &All..."), this, SLOT(promptForExportAll()));
160 | exportPlaylistAction = fileMenu->addAction(tr("Export Tracks in &Playlist..."), this, SLOT(promptForExportPlaylist()));
161 | fileMenu->addSeparator();
162 | fileMenu->addAction(tr("E&xit"), qApp, SLOT(quit()));
163 |
164 | QMenu* controlMenu = mb->addMenu(tr("&Control"));
165 | controlMenu->addAction(controls->toggleAction());
166 | controlMenu->addAction(controls->playAction());
167 | controlMenu->addAction(controls->pauseAction());
168 | controlMenu->addAction(controls->stopAction());
169 | controlMenu->addSeparator();
170 | QAction* prefsAction = controlMenu->addAction(tr("&Preferences..."), this, SLOT(openPreferences()), QKeySequence::Preferences);
171 | if (prefsAction->shortcut().isEmpty()) {
172 | prefsAction->setShortcut(Qt::CTRL | Qt::Key_Comma);
173 | }
174 | prefsAction->setMenuRole(QAction::PreferencesRole);
175 |
176 | QMenu* helpMenu = mb->addMenu(tr("&Help"));
177 | helpMenu->addAction(tr("&About..."), this, SLOT(about()));
178 | helpMenu->addAction(tr("About &Qt..."), qApp, SLOT(aboutQt()));
179 |
180 | saveAction->setEnabled(false);
181 | exportAction->setEnabled(false);
182 | exportChannelsAction->setEnabled(false);
183 | exportAllAction->setEnabled(false);
184 | exportPlaylistAction->setEnabled(false);
185 | }
186 |
187 | QLabel* PlayerWindow::makeTitle()
188 | {
189 | static const char* agbplayTitle =
190 | R"( _ _ )" "\n"
191 | R"( __ _ __ _| |__ _ __| |__ _ _ _ )" "\n"
192 | R"(/ _` / _` | '_ \ '_ \ / _` | || |)" "\n"
193 | R"(\__,_\__, |_.__/ .__/_\__,_|\_, |)" "\n"
194 | R"( |___/ |_| |__/ )" "\n";
195 |
196 | QLabel* title = new QLabel(agbplayTitle, this);
197 | title->setAlignment(Qt::AlignLeft | Qt::AlignTop);
198 | QFont font(QFontDatabase::systemFont(QFontDatabase::FixedFont));
199 | font.setStyleHint(QFont::Monospace);
200 | font.setPointSize(10);
201 | font.setBold(true);
202 | title->setFont(font);
203 | return title;
204 | }
205 |
206 | QTreeView* PlayerWindow::makeView(QAbstractItemModel* model)
207 | {
208 | QTreeView* view = new QTreeView(this);
209 | view->setRootIsDecorated(false);
210 | view->header()->resizeSection(0, 150);
211 | view->setModel(model);
212 | view->setSelectionMode(QAbstractItemView::ExtendedSelection);
213 | view->setEditTriggers(QAbstractItemView::EditKeyPressed | QAbstractItemView::SelectedClicked);
214 | view->setContextMenuPolicy(Qt::CustomContextMenu);
215 | QObject::connect(view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(songListMenu(QPoint)));
216 | return view;
217 | }
218 |
219 | void PlayerWindow::openRom()
220 | {
221 | QSettings settings;
222 | QStringList recents = settings.value("recentFiles").toStringList();
223 | QString lastPath;
224 | if (!recents.isEmpty()) {
225 | lastPath = QFileInfo(recents.first()).absolutePath();
226 | }
227 |
228 | QString path = QFileDialog::getOpenFileName(
229 | this,
230 | tr("Open GBA ROM File"),
231 | lastPath,
232 | QStringLiteral("%1 (*.gba);;%2 (*)").arg(tr("GBA ROM files")).arg(tr("All files"))
233 | );
234 | if (path.isEmpty()) {
235 | return;
236 | }
237 | openRom(path);
238 | }
239 |
240 | void PlayerWindow::openRom(const QString& path)
241 | {
242 | Rom* rom;
243 | try {
244 | rom = player->openRom(path);
245 | } catch (std::exception& e) {
246 | player->openRom(QString());
247 | QMessageBox::warning(nullptr, "agbplay", e.what());
248 | setWindowFilePath(QString());
249 | setWindowTitle("agbplay");
250 | return;
251 | }
252 |
253 | addRecent(path);
254 | setWindowFilePath(path);
255 | setWindowTitle(QStringLiteral("agbplay - %1").arg(QFileInfo(path).fileName()));
256 | emit romUpdated(rom);
257 |
258 | songList->setCurrentIndex(songs->index(0, 0));
259 | saveAction->setEnabled(true);
260 | exportAction->setEnabled(true);
261 | exportChannelsAction->setEnabled(true);
262 | exportAllAction->setEnabled(true);
263 | exportPlaylistAction->setEnabled(playlist->rowCount() > 0);
264 | player->setSpeed(controls->speedMultiplier());
265 | }
266 |
267 | void PlayerWindow::about()
268 | {
269 | QFile about(":/about.html");
270 | about.open(QIODevice::ReadOnly | QIODevice::Text);
271 | QMessageBox::about(
272 | this,
273 | tr("agbplay-gui (%1)").arg(qApp->applicationVersion()),
274 | QString::fromUtf8(about.readAll())
275 | );
276 | }
277 |
278 | void PlayerWindow::selectSong(const QModelIndex& index)
279 | {
280 | QModelIndex songIndex, playlistIndex;
281 | if (index.model() == songs) {
282 | songIndex = index;
283 | playlistIndex = playlist->mapFromSource(index);
284 | } else {
285 | playlistIndex = index;
286 | songIndex = playlist->mapToSource(index);
287 | }
288 | try {
289 | player->selectSong(songIndex.row());
290 | QTimer::singleShot(0, player, SLOT(play()));
291 | } catch (std::exception& e) {
292 | QMessageBox::warning(nullptr, "agbplay", e.what());
293 | return;
294 | }
295 | songList->scrollTo(songIndex);
296 | if (playlistIndex.isValid()) {
297 | playlistView->scrollTo(playlistIndex);
298 | }
299 | }
300 |
301 | void PlayerWindow::closeEvent(QCloseEvent*)
302 | {
303 | if (playlistIsDirty) {
304 | auto choice = QMessageBox::question(this, "agbplay", tr("Do you want to save your changes to the playlist?"));
305 | if (choice == QMessageBox::Yes) {
306 | playlist->save();
307 | }
308 | }
309 | player->stop();
310 | }
311 |
312 | void PlayerWindow::updateVU(PlayerContext*, VUState* vu)
313 | {
314 | masterVU->setLeft(vu->master.left);
315 | masterVU->setRight(vu->master.right);
316 | }
317 |
318 | void PlayerWindow::fillRecents()
319 | {
320 | QSettings settings;
321 | QStringList recents = settings.value("recentFiles").toStringList();
322 |
323 | recentsMenu->clear();
324 | if (recents.isEmpty()) {
325 | recentsMenu->setEnabled(false);
326 | return;
327 | }
328 | recentsMenu->setEnabled(true);
329 | int i = 1;
330 | for (const QString& path : recents) {
331 | QAction* action = recentsMenu->addAction(QStringLiteral("&%1 - %2").arg(i).arg(QFileInfo(path).fileName()));
332 | action->setData(path);
333 | }
334 | recentsMenu->addSeparator();
335 | recentsMenu->addAction(tr("&Clear Recent"), this, SLOT(clearRecents()));
336 | }
337 |
338 | void PlayerWindow::addRecent(const QString& path)
339 | {
340 | QSettings settings;
341 | QStringList recents = settings.value("recentFiles").toStringList();
342 | recents.removeAll(path);
343 | recents.insert(0, path);
344 | while (recents.length() > 4) {
345 | recents.removeLast();
346 | }
347 |
348 | settings.setValue("recentFiles", recents);
349 | fillRecents();
350 | }
351 |
352 | void PlayerWindow::clearRecents()
353 | {
354 | QSettings settings;
355 | settings.remove("recentFiles");
356 | fillRecents();
357 | }
358 |
359 | void PlayerWindow::openRecent(QAction* action)
360 | {
361 | QString path = action->data().toString();
362 | if (!path.isEmpty()) {
363 | openRom(path);
364 | }
365 | }
366 |
367 | void PlayerWindow::clearOtherSelection(const QItemSelection& sel)
368 | {
369 | exportAction->setEnabled(playlistView->selectionModel()->hasSelection() || songList->selectionModel()->hasSelection());
370 | exportChannelsAction->setEnabled(exportAction->isEnabled());
371 |
372 | if (sel.isEmpty()) {
373 | return;
374 | }
375 | QItemSelectionModel* view = qobject_cast(sender());
376 | if (view == songList->selectionModel()) {
377 | playlistView->clearSelection();
378 | } else if (view == playlistView->selectionModel()) {
379 | songList->clearSelection();
380 | }
381 | }
382 |
383 | void PlayerWindow::songListMenu(const QPoint& pos)
384 | {
385 | QTreeView* view = qobject_cast(sender());
386 | if (!view) {
387 | return;
388 | }
389 |
390 | QModelIndexList items = view->selectionModel()->selectedIndexes();
391 | if (items.isEmpty()) {
392 | QModelIndex item = view->indexAt(pos);
393 | if (item.isValid()) {
394 | items << item;
395 | } else {
396 | return;
397 | }
398 | }
399 |
400 | QAction play(style()->standardIcon(QStyle::SP_MediaPlay, nullptr, this), PlayerControls::tr("&Play"));
401 | QAction enqueue(tr("&Add to Playlist"));
402 | QAction remove(tr("&Remove from Playlist"));
403 | QAction rename(tr("Re&name"));
404 | QAction exportTrack(tr("&Export..."));
405 | QAction exportChannels(tr("Export &Channels..."));
406 |
407 | QList actions;
408 | if (items.length() == 1) {
409 | actions << &play;
410 | }
411 | if (view == songList) {
412 | actions << &enqueue;
413 | }
414 | if (view == playlistView) {
415 | actions << &remove;
416 | }
417 | if (items.length() == 1) {
418 | actions << &rename;
419 | }
420 | actions << &exportTrack << &exportChannels;
421 |
422 | QAction* action = QMenu::exec(actions, view->mapToGlobal(pos), actions.first(), view);
423 | if (action == &play) {
424 | selectSong(items[0]);
425 | } else if (action == &enqueue) {
426 | int end = playlist->rowCount();
427 | playlist->append(items);
428 | playlistView->clearSelection();
429 | for (int i = items.length() - 1; i >= 0; --i) {
430 | playlistView->selectionModel()->select(playlist->index(end + i), QItemSelectionModel::Select);
431 | }
432 | playlistView->scrollTo(playlist->index(end + items.length() - 1), QAbstractItemView::EnsureVisible);
433 | playlistView->selectionModel()->setCurrentIndex(playlist->index(end), QItemSelectionModel::NoUpdate);
434 | } else if (action == &remove) {
435 | playlist->remove(items);
436 | view->clearSelection();
437 | view->selectionModel()->setCurrentIndex(view->indexAt(pos), QItemSelectionModel::NoUpdate);
438 | } else if (action == &rename) {
439 | view->edit(items[0]);
440 | } else if (action == &exportTrack) {
441 | promptForExport();
442 | } else if (action == &exportChannels) {
443 | promptForExportChannels();
444 | }
445 | }
446 |
447 | void PlayerWindow::playlistDirty(bool dirty)
448 | {
449 | playlistIsDirty = dirty;
450 | exportPlaylistAction->setEnabled(playlist->rowCount() > 0);
451 | }
452 |
453 | QModelIndexList PlayerWindow::selectedIndexes() const
454 | {
455 | QModelIndexList items = songList->selectionModel()->selectedIndexes();
456 |
457 | if (items.isEmpty()) {
458 | for (const QModelIndex& idx : playlistView->selectionModel()->selectedIndexes()) {
459 | items << playlist->mapToSource(idx);
460 | }
461 | }
462 |
463 | return items;
464 | }
465 |
466 | void PlayerWindow::promptForExport()
467 | {
468 | promptForExport(selectedIndexes());
469 | }
470 |
471 | void PlayerWindow::promptForExportChannels()
472 | {
473 | promptForExport(selectedIndexes(), true);
474 | }
475 |
476 | void PlayerWindow::promptForExport(const QModelIndexList& items, bool split)
477 | {
478 | QSettings settings;
479 | QString lastExportPath = settings.value("lastExport").toString();
480 |
481 | if (items.length() == 1 && !split) {
482 | QModelIndex idx = items.first();
483 | QString name = idx.data(Qt::EditRole).toString();
484 | QString prefix = fixedNumber(idx.row(), 4);
485 | if (name.isEmpty()) {
486 | name = QStringLiteral("%1.wav").arg(prefix);
487 | } else {
488 | name = QStringLiteral("%1 - %2.wav").arg(prefix).arg(name);
489 | }
490 | QString path = QFileDialog::getSaveFileName(
491 | this,
492 | tr("Export track to file"),
493 | QDir(lastExportPath).absoluteFilePath(name),
494 | QStringLiteral("%1 (*.wav);;%2 (*)").arg(tr("Wave audio files")).arg(tr("All files"))
495 | );
496 | if (!path.isEmpty()) {
497 | settings.setValue("lastExport", QFileInfo(path).absolutePath());
498 | exportProgress->setRange(0, 0);
499 | exportProgress->setValue(0);
500 | progressPanel->show();
501 | player->exportToWave(path, idx.row());
502 | }
503 | } else {
504 | QString path = QFileDialog::getExistingDirectory(
505 | this,
506 | split ? tr("Export tracks into directory") : tr("Export channels into directory"),
507 | lastExportPath
508 | );
509 | if (!path.isEmpty()) {
510 | settings.setValue("lastExport", path);
511 | QList tracks;
512 | for (const QModelIndex& idx : items) {
513 | tracks << idx.row();
514 | }
515 | exportProgress->setRange(0, items.length());
516 | exportProgress->setValue(0);
517 | progressPanel->show();
518 | player->exportToWave(QDir(path), tracks, split);
519 | }
520 | }
521 | }
522 |
523 | void PlayerWindow::promptForExportAll()
524 | {
525 | QModelIndexList items;
526 | for (int i = 0; i < songs->rowCount(); i++) {
527 | items << songs->index(i, 0);
528 | }
529 | promptForExport(items);
530 | }
531 |
532 | void PlayerWindow::promptForExportPlaylist()
533 | {
534 | QModelIndexList items;
535 | for (int i = 0; i < playlist->rowCount(); i++) {
536 | items << playlist->mapToSource(playlist->index(i, 0));
537 | }
538 | promptForExport(items);
539 | }
540 |
541 | void PlayerWindow::exportStarted(const QString& path)
542 | {
543 | logMessage(tr("Exporting to %1...").arg(path));
544 | }
545 |
546 | void PlayerWindow::exportFinished(const QString& path)
547 | {
548 | logMessage(tr("Finished exporting %1.").arg(path));
549 | updateExportProgress();
550 | }
551 |
552 | void PlayerWindow::exportError(const QString& message)
553 | {
554 | logMessage(tr("Error while exporting: %1").arg(message));
555 | }
556 |
557 | void PlayerWindow::exportCancelled()
558 | {
559 | logMessage(tr("Export cancelled."));
560 | progressPanel->hide();
561 | }
562 |
563 | void PlayerWindow::playbackError(const QString& message)
564 | {
565 | logMessage(tr("Error while playing: %1").arg(message));
566 | }
567 |
568 | void PlayerWindow::logMessage(const QString& message)
569 | {
570 | log->appendPlainText(message);
571 | }
572 |
573 | void PlayerWindow::updateExportProgress()
574 | {
575 | int max = exportProgress->maximum();
576 | int val = exportProgress->value() + 1;
577 | if (val >= max) {
578 | progressPanel->hide();
579 | } else {
580 | exportProgress->setValue(val);
581 | }
582 | }
583 |
584 | void PlayerWindow::openPreferences()
585 | {
586 | PreferencesWindow* prefs = new PreferencesWindow(this);
587 | prefs->setAttribute(Qt::WA_DeleteOnClose);
588 | prefs->open();
589 | }
590 |
--------------------------------------------------------------------------------
/src/PlayerWindow.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include "PlayerContext.h"
7 | class TrackList;
8 | class SongModel;
9 | class PlaylistModel;
10 | class QAbstractItemModel;
11 | class QTreeView;
12 | class QLabel;
13 | class QPlainTextEdit;
14 | class QProgressBar;
15 | class VUMeter;
16 | class VUState;
17 | class SongTable;
18 | class Player;
19 | class PlayerControls;
20 | class RomView;
21 | class Rom;
22 |
23 | class PlayerWindow : public QMainWindow
24 | {
25 | Q_OBJECT
26 | public:
27 | PlayerWindow(Player* player, QWidget* parent = nullptr);
28 |
29 | public slots:
30 | void openRom();
31 | void openRom(const QString& path);
32 | void about();
33 |
34 | signals:
35 | void romUpdated(Rom*);
36 |
37 | protected:
38 | void closeEvent(QCloseEvent*);
39 |
40 | private slots:
41 | void selectSong(const QModelIndex& index);
42 | void updateVU(PlayerContext*, VUState* vu);
43 | void clearRecents();
44 | void openRecent(QAction* action);
45 | void songListMenu(const QPoint& pos);
46 | void playlistDirty(bool dirty);
47 | void clearOtherSelection(const QItemSelection& sel);
48 |
49 | void promptForExport();
50 | void promptForExportChannels();
51 | void promptForExportAll();
52 | void promptForExportPlaylist();
53 | void exportStarted(const QString& path);
54 | void exportFinished(const QString& path);
55 | void exportError(const QString& message);
56 | void exportCancelled();
57 | void playbackError(const QString& message);
58 |
59 | void openPreferences();
60 |
61 | private:
62 | QLayout* makeTop();
63 | QLayout* makeLeft();
64 | QLayout* makeRight();
65 | void makeMenu();
66 | QLabel* makeTitle();
67 | QTreeView* makeView(QAbstractItemModel* model);
68 |
69 | void fillRecents();
70 | void addRecent(const QString& path);
71 | void logMessage(const QString& message);
72 | void promptForExport(const QModelIndexList& items, bool split = false);
73 | void updateExportProgress();
74 | QModelIndexList selectedIndexes() const;
75 |
76 | VUMeter* masterVU;
77 | TrackList* trackList;
78 | QTreeView* songList;
79 | QTreeView* playlistView;
80 | RomView* romView;
81 | QPlainTextEdit* log;
82 | QWidget* progressPanel;
83 | QProgressBar* exportProgress;
84 |
85 | SongModel* songs;
86 | PlaylistModel* playlist;
87 | Player* player;
88 | PlayerControls* controls;
89 | QMenu* recentsMenu;
90 | QAction* saveAction;
91 | QAction* exportAction;
92 | QAction* exportChannelsAction;
93 | QAction* exportAllAction;
94 | QAction* exportPlaylistAction;
95 |
96 | bool playlistIsDirty;
97 | };
98 |
--------------------------------------------------------------------------------
/src/PlaylistModel.cpp:
--------------------------------------------------------------------------------
1 | #include "PlaylistModel.h"
2 | #include "SongModel.h"
3 | #include "ConfigManager.h"
4 | #include
5 | #include
6 |
7 | PlaylistModel::PlaylistModel(SongModel* source)
8 | : QAbstractProxyModel(source)
9 | {
10 | setSourceModel(source);
11 |
12 | QObject::connect(source, SIGNAL(modelReset()), this, SLOT(reload()));
13 | QObject::connect(source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(onDataChanged(QModelIndex,QModelIndex)));
14 | QObject::connect(source, SIGNAL(playlistDirty()), this, SIGNAL(playlistDirty()));
15 | }
16 |
17 | void PlaylistModel::reload()
18 | {
19 | beginResetModel();
20 | trackOrder.clear();
21 | trackIndex.clear();
22 | auto entries = ConfigManager::Instance().GetCfg().GetGameEntries();
23 | for (const auto& entry : entries) {
24 | trackIndex[entry.GetUID()] = trackOrder.length();
25 | trackOrder << entry.GetUID();
26 | }
27 | endResetModel();
28 | emit playlistDirty(false);
29 | }
30 |
31 | int PlaylistModel::rowCount(const QModelIndex& parent) const
32 | {
33 | if (parent.isValid()) {
34 | return 0;
35 | }
36 | return trackOrder.length();
37 | }
38 |
39 | int PlaylistModel::columnCount(const QModelIndex& parent) const
40 | {
41 | if (parent.isValid()) {
42 | return 0;
43 | }
44 | return sourceModel()->columnCount();
45 | }
46 |
47 | QModelIndex PlaylistModel::index(int row, int col, const QModelIndex& parent) const
48 | {
49 | if (row < 0 || row >= trackOrder.length() || col < 0 || col >= sourceModel()->columnCount() || parent.isValid()) {
50 | return QModelIndex();
51 | }
52 | return createIndex(row, col, trackOrder[row]);
53 | }
54 |
55 | QModelIndex PlaylistModel::parent(const QModelIndex&) const
56 | {
57 | return QModelIndex();
58 | }
59 |
60 | QModelIndex PlaylistModel::mapFromSource(const QModelIndex& idx) const
61 | {
62 | int pos = trackIndex.value(idx.row(), -1);
63 | if (pos < 0) {
64 | return QModelIndex();
65 | }
66 | return index(pos, idx.column());
67 | }
68 |
69 | QModelIndex PlaylistModel::mapToSource(const QModelIndex& idx) const
70 | {
71 | int row = idx.row();
72 | if (row < 0 || row >= trackOrder.length()) {
73 | return QModelIndex();
74 | }
75 | return sourceModel()->index(trackOrder[row], idx.column());
76 | }
77 |
78 | QVariant PlaylistModel::headerData(int section, Qt::Orientation orientation, int role) const
79 | {
80 | if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
81 | return tr("Playlist");
82 | }
83 | return sourceModel()->headerData(section, orientation, role);
84 | }
85 |
86 | Qt::DropActions PlaylistModel::supportedDragActions() const
87 | {
88 | return Qt::MoveAction;
89 | }
90 |
91 | Qt::DropActions PlaylistModel::supportedDropActions() const
92 | {
93 | return Qt::MoveAction | Qt::LinkAction;
94 | }
95 |
96 | QStringList PlaylistModel::mimeTypes() const
97 | {
98 | return QStringList() << "agbplay/tracklist";
99 | }
100 |
101 | QMimeData* PlaylistModel::mimeData(const QModelIndexList& idxs) const
102 | {
103 | if (idxs.isEmpty()) {
104 | return nullptr;
105 | }
106 | QMimeData* data = new QMimeData();
107 | QStringList content;
108 | for (const QModelIndex& idx : idxs) {
109 | content << QStringLiteral("@%1").arg(idx.row());
110 | }
111 | data->setData("agbplay/tracklist", content.join(",").toUtf8());
112 | return data;
113 | }
114 |
115 | bool PlaylistModel::canDropMimeData(const QMimeData* data, Qt::DropAction, int, int, const QModelIndex&) const
116 | {
117 | return data->formats().contains("agbplay/tracklist");
118 | }
119 |
120 | bool PlaylistModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int beforeRow, int, const QModelIndex&)
121 | {
122 | QStringList tracks = QString::fromUtf8(data->data("agbplay/tracklist")).split(",");
123 | int ct = tracks.length();
124 | int minPos = beforeRow, maxPos = beforeRow + ct;
125 | if (action == Qt::MoveAction) {
126 | QList toRemove;
127 | // First, collect the items that will be removed and the bounds that will be affected
128 | for (const QString& item : tracks) {
129 | int pos = item.section('@', 1, 1).toInt();
130 | toRemove << pos;
131 | if (pos <= beforeRow) {
132 | --beforeRow;
133 | }
134 | if (pos < minPos) {
135 | minPos = pos;
136 | }
137 | if (pos > maxPos) {
138 | maxPos = pos;
139 | }
140 | }
141 |
142 | // Create a list of indexes to operate on; the data will be updated later
143 | emit layoutAboutToBeChanged(QList(), QAbstractItemModel::VerticalSortHint);
144 | QModelIndexList layoutBefore, layoutAfter;
145 | for (int i = minPos; i <= maxPos; i++) {
146 | layoutBefore << index(i);
147 | }
148 |
149 | // Remove the items being moved, from last to first
150 | QList toRemoveSorted = toRemove;
151 | std::sort(toRemoveSorted.begin(), toRemoveSorted.end(), std::greater());
152 | for (int pos : toRemoveSorted) {
153 | layoutBefore.removeAt(pos - minPos);
154 | }
155 |
156 | // Insert the items being moved into the new locations
157 | for (int i = 0; i < ct; i++) {
158 | layoutBefore.insert(beforeRow + i - minPos, index(toRemove[i]));
159 | }
160 |
161 | // Update the data and any persistent model indexes
162 | for (int i = minPos; i <= maxPos; i++) {
163 | quintptr id = layoutBefore[i - minPos].internalId();
164 | layoutAfter << createIndex(i, 0, id);
165 | trackOrder[i] = int(id);
166 | }
167 | changePersistentIndexList(layoutBefore, layoutAfter);
168 | } else {
169 | // Insertion is much more simple
170 | QList toInsert;
171 | for (const QString& item : tracks) {
172 | toInsert << item.toInt();
173 | }
174 | beginInsertRows(QModelIndex(), beforeRow, beforeRow + ct - 1);
175 | for (int i = 0; i < ct; i++) {
176 | trackOrder.insert(beforeRow + i, toInsert[i]);
177 | }
178 | }
179 |
180 | // Update the mapping index
181 | trackIndex.clear();
182 | for (int i = 0; i < trackOrder.length(); i++) {
183 | trackIndex[trackOrder[i]] = i;
184 | }
185 |
186 | // Notify views of changes
187 | if (action == Qt::MoveAction) {
188 | emit layoutChanged(QList(), QAbstractItemModel::VerticalSortHint);
189 | } else {
190 | endInsertRows();
191 | }
192 | emit playlistDirty();
193 | return true;
194 | }
195 |
196 | void PlaylistModel::onDataChanged(const QModelIndex& start, const QModelIndex& end)
197 | {
198 | int min = trackOrder.length() - 1;
199 | int max = 0;
200 | int sRow = start.row();
201 | int eRow = end.row();
202 | if (eRow < sRow) {
203 | sRow = eRow;
204 | eRow = start.row();
205 | }
206 | for (int i = sRow; i <= eRow; i++) {
207 | int pos = trackIndex.value(i, -1);
208 | if (pos < 0) {
209 | continue;
210 | }
211 | if (pos > max) {
212 | max = pos;
213 | }
214 | if (pos < min) {
215 | min = pos;
216 | }
217 | }
218 | if (min <= max) {
219 | emit dataChanged(index(min, 0), index(max, columnCount() - 1));
220 | }
221 | }
222 |
223 | void PlaylistModel::save()
224 | {
225 | std::vector playlist;
226 |
227 | for (int i = 0; i < trackOrder.length(); i++) {
228 | QModelIndex idx = sourceModel()->index(trackOrder[i], 0);
229 | playlist.emplace_back(
230 | sourceModel()->data(idx, Qt::EditRole).toString().toStdString(),
231 | trackOrder[i]
232 | );
233 | }
234 |
235 | ConfigManager::Instance().GetCfg().GetGameEntries() = playlist;
236 | ConfigManager::Instance().Save();
237 | emit playlistDirty(false);
238 | }
239 |
240 | void PlaylistModel::append(const QModelIndexList& items)
241 | {
242 | beginInsertRows(QModelIndex(), trackOrder.length(), items.length());
243 | for (const QModelIndex& _idx : items) {
244 | QModelIndex idx = _idx.model() == this ? mapToSource(_idx) : _idx;
245 | trackIndex[idx.row()] = trackOrder.length();
246 | trackOrder << idx.row();
247 | }
248 | endInsertRows();
249 | emit playlistDirty();
250 | }
251 |
252 | void PlaylistModel::remove(const QModelIndexList& items)
253 | {
254 | for (const QModelIndex& idx : items) {
255 | if (idx.model() != this) {
256 | continue;
257 | }
258 | beginRemoveRows(QModelIndex(), idx.row(), 1);
259 | trackIndex.remove(int(idx.internalId()));
260 | trackOrder.removeAt(idx.row());
261 | endRemoveRows();
262 | }
263 | emit playlistDirty();
264 | }
265 |
--------------------------------------------------------------------------------
/src/PlaylistModel.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | class SongModel;
7 |
8 | class PlaylistModel : public QAbstractProxyModel
9 | {
10 | Q_OBJECT
11 | public:
12 | PlaylistModel(SongModel* source);
13 |
14 | int rowCount(const QModelIndex& parent = QModelIndex()) const;
15 | int columnCount(const QModelIndex& parent = QModelIndex()) const;
16 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
17 |
18 | QModelIndex index(int row, int col = 0, const QModelIndex& parent = QModelIndex()) const;
19 | QModelIndex parent(const QModelIndex& idx) const;
20 | QModelIndex mapFromSource(const QModelIndex& idx) const;
21 | QModelIndex mapToSource(const QModelIndex& idx) const;
22 |
23 | Qt::DropActions supportedDragActions() const;
24 | Qt::DropActions supportedDropActions() const;
25 | QStringList mimeTypes() const;
26 | QMimeData* mimeData(const QModelIndexList& idxs) const;
27 | bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const;
28 | bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
29 |
30 | void append(const QModelIndexList& items);
31 | void remove(const QModelIndexList& items);
32 |
33 | signals:
34 | void playlistDirty(bool dirty = true);
35 |
36 | public slots:
37 | void save();
38 |
39 | private slots:
40 | void reload();
41 | void onDataChanged(const QModelIndex& start, const QModelIndex& end);
42 |
43 | private:
44 | QList trackOrder;
45 | QHash trackIndex;
46 | };
47 |
--------------------------------------------------------------------------------
/src/PreferencesWindow.cpp:
--------------------------------------------------------------------------------
1 | #include "PreferencesWindow.h"
2 | #include "ConfigManager.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | PreferencesWindow::PreferencesWindow(QWidget* parent)
12 | : QDialog(parent)
13 | {
14 | setWindowTitle(tr("agbplay Preferences"));
15 |
16 | ConfigManager& cfg = ConfigManager::Instance();
17 | QGridLayout* layout = new QGridLayout(this);
18 | layout->setColumnStretch(1, 1);
19 |
20 | QLabel* lblCgbPolyphony = new QLabel(tr("&CGB Polyphony:"), this);
21 | layout->addWidget(lblCgbPolyphony, 0, 0);
22 | layout->addWidget(cgbPolyphony = new QComboBox(this), 0, 1, 1, 2);
23 | lblCgbPolyphony->setBuddy(cgbPolyphony);
24 | cgbPolyphony->addItem(tr("Strict (Default)"), int(CGBPolyphony::MONO_STRICT));
25 | cgbPolyphony->addItem(tr("Smooth"), int(CGBPolyphony::MONO_SMOOTH));
26 | cgbPolyphony->addItem(tr("Polyphonic"), int(CGBPolyphony::POLY));
27 | cgbPolyphony->setCurrentIndex(int(cfg.GetCgbPolyphony()));
28 |
29 | QLabel* lblMaxLoopsPlaylist = new QLabel(tr("&Loops during playback:"), this);
30 | layout->addWidget(lblMaxLoopsPlaylist, 1, 0);
31 | layout->addWidget(maxLoopsPlaylist = new QSpinBox(this), 1, 1, 1, 2);
32 | lblMaxLoopsPlaylist->setBuddy(maxLoopsPlaylist);
33 | maxLoopsPlaylist->setMinimum(1);
34 |
35 | loopInfinitely = new QCheckBox(tr("Loop &infinitely"), this);
36 | if (cfg.GetMaxLoopsPlaylist() < 0) {
37 | maxLoopsPlaylist->setValue(1);
38 | loopInfinitely->setChecked(true);
39 | maxLoopsPlaylist->setEnabled(false);
40 | } else {
41 | maxLoopsPlaylist->setValue(cfg.GetMaxLoopsPlaylist());
42 | }
43 | layout->addWidget(loopInfinitely, 2, 1, 1, 2);
44 |
45 | QLabel* lblMaxLoopsExport = new QLabel(tr("L&oops during export:"), this);
46 | layout->addWidget(lblMaxLoopsExport, 3, 0);
47 | layout->addWidget(maxLoopsExport = new QSpinBox(this), 3, 1, 1, 2);
48 | lblMaxLoopsExport->setBuddy(maxLoopsExport);
49 | maxLoopsExport->setValue(cfg.GetMaxLoopsExport());
50 | maxLoopsExport->setMinimum(1);
51 |
52 | QLabel* lblPadSecondsStart = new QLabel(tr("Add silence to &start of export:"), this);
53 | layout->addWidget(lblPadSecondsStart, 4, 0);
54 | layout->addWidget(padSecondsStart = new QDoubleSpinBox(this), 4, 1);
55 | layout->addWidget(new QLabel(tr("sec"), this), 4, 2);
56 | lblPadSecondsStart->setBuddy(padSecondsStart);
57 | padSecondsStart->setValue(cfg.GetPadSecondsStart());
58 | padSecondsStart->setMinimum(0);
59 |
60 | QLabel* lblPadSecondsEnd = new QLabel(tr("Add silence to &end of export:"), this);
61 | layout->addWidget(lblPadSecondsEnd, 5, 0);
62 | layout->addWidget(padSecondsEnd = new QDoubleSpinBox(this), 5, 1);
63 | layout->addWidget(new QLabel(tr("sec"), this), 5, 2);
64 | lblPadSecondsEnd->setBuddy(padSecondsEnd);
65 | padSecondsEnd->setValue(cfg.GetPadSecondsEnd());
66 | padSecondsEnd->setMinimum(0);
67 |
68 | QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
69 | layout->addWidget(buttons, 6, 0, 1, 3);
70 |
71 | QObject::connect(loopInfinitely, SIGNAL(clicked()), this, SLOT(updateEnabled()));
72 | QObject::connect(buttons, SIGNAL(accepted()), this, SLOT(save()));
73 | QObject::connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
74 | }
75 |
76 | void PreferencesWindow::save()
77 | {
78 | ConfigManager& cfg = ConfigManager::Instance();
79 |
80 | cfg.SetCgbPolyphony(CGBPolyphony(cgbPolyphony->currentData().toInt()));
81 | if (loopInfinitely->isChecked()) {
82 | cfg.SetMaxLoopsPlaylist(-1);
83 | } else {
84 | cfg.SetMaxLoopsPlaylist(maxLoopsPlaylist->value());
85 | }
86 | cfg.SetMaxLoopsExport(maxLoopsExport->value());
87 | cfg.SetPadSecondsStart(padSecondsStart->value());
88 | cfg.SetPadSecondsEnd(padSecondsEnd->value());
89 |
90 | cfg.Save();
91 | accept();
92 | }
93 |
94 | void PreferencesWindow::updateEnabled()
95 | {
96 | maxLoopsPlaylist->setEnabled(!loopInfinitely->isChecked());
97 | }
98 |
--------------------------------------------------------------------------------
/src/PreferencesWindow.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | class QComboBox;
5 | class QSpinBox;
6 | class QDoubleSpinBox;
7 | class QCheckBox;
8 |
9 | class PreferencesWindow : public QDialog
10 | {
11 | Q_OBJECT
12 | public:
13 | PreferencesWindow(QWidget* parent = nullptr);
14 |
15 | private slots:
16 | void updateEnabled();
17 | void save();
18 |
19 | private:
20 | QComboBox* cgbPolyphony;
21 | QSpinBox* maxLoopsPlaylist;
22 | QCheckBox* loopInfinitely;
23 | QSpinBox* maxLoopsExport;
24 | QDoubleSpinBox* padSecondsStart;
25 | QDoubleSpinBox* padSecondsEnd;
26 | };
27 |
--------------------------------------------------------------------------------
/src/RiffWriter.cpp:
--------------------------------------------------------------------------------
1 | #include "RiffWriter.h"
2 |
3 | template
4 | static void writeLE(QIODevice& file, T data)
5 | {
6 | char bytes[sizeof(T)];
7 | for (std::size_t i = 0; i < sizeof(T); i++) {
8 | bytes[i] = char(data & 0xFF);
9 | data = T(data >> 8);
10 | }
11 | file.write(bytes, sizeof(T));
12 | }
13 |
14 | RiffWriter::RiffWriter(uint32_t sampleRate, bool stereo, uint32_t size)
15 | : sampleRate(sampleRate), size(size), stereo(stereo), rewriteSize(!size)
16 | {
17 | // initializers only
18 | }
19 |
20 | RiffWriter::~RiffWriter()
21 | {
22 | close();
23 | }
24 |
25 | bool RiffWriter::open(const QString& filename)
26 | {
27 | file.setFileName(filename);
28 | bool ok = file.open(QIODevice::WriteOnly | QIODevice::Truncate);
29 | if (!ok) {
30 | return false;
31 | }
32 | file.write("RIFF", 4);
33 | writeLE(file, size ? size + 36 : 0xFFFFFFFF);
34 | file.write("WAVEfmt \x10\0\0\0\1\0", 14);
35 | writeLE(file, stereo ? 2 : 1);
36 | writeLE(file, sampleRate);
37 | writeLE(file, sampleRate * 2 * (stereo ? 2 : 1));
38 | writeLE(file, stereo ? 4 : 2);
39 | file.write("\x10\0data", 6);
40 | writeLE(file, size ? size : 0xFFFFFFFF);
41 | return true;
42 | }
43 |
44 | void RiffWriter::write(const uint8_t* data, size_t length)
45 | {
46 | if (rewriteSize) {
47 | size += std::uint32_t(length);
48 | }
49 | file.write(reinterpret_cast(data), length);
50 | }
51 |
52 | void RiffWriter::write(const std::vector& data)
53 | {
54 | std::size_t words = data.size();
55 | if (rewriteSize) {
56 | size += std::uint32_t(words * 2);
57 | }
58 | for (std::size_t i = 0; i < words; i++) {
59 | writeLE(file, data[i]);
60 | }
61 | }
62 |
63 | void RiffWriter::write(const std::vector& left, const std::vector& right)
64 | {
65 | std::size_t leftWords = left.size(), rightWords = right.size();
66 | std::size_t words = leftWords < rightWords ? rightWords : leftWords;
67 | if (rewriteSize) {
68 | size += std::uint32_t(words * 4);
69 | }
70 | for (std::size_t i = 0; i < words; i++) {
71 | writeLE(file, i < leftWords ? left[i] : 0);
72 | writeLE(file, i < rightWords ? right[i] : 0);
73 | }
74 | }
75 |
76 | void RiffWriter::close()
77 | {
78 | if (!file.isOpen()) {
79 | return;
80 | }
81 | if (rewriteSize) {
82 | bool ok = file.seek(4);
83 | if (ok) {
84 | writeLE(file, size + 36);
85 | file.seek(file.pos() + 32);
86 | writeLE(file, size);
87 | }
88 | }
89 | file.close();
90 | }
91 |
--------------------------------------------------------------------------------
/src/RiffWriter.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | class RiffWriter
9 | {
10 | public:
11 | RiffWriter(uint32_t sampleRate, bool stereo, uint32_t sizeInBytes = 0);
12 | ~RiffWriter();
13 |
14 | bool open(const QString& filename);
15 | void write(const uint8_t* data, size_t length);
16 | inline void write(const int8_t* data, size_t length)
17 | { write(reinterpret_cast(data), length); }
18 | inline void write(const std::vector& data)
19 | { write(data.data(), data.size()); }
20 | inline void write(const std::vector& data)
21 | { write(data.data(), data.size()); }
22 | void write(const std::vector& data);
23 | void write(const std::vector& left, const std::vector& right);
24 | void close();
25 |
26 | private:
27 | QFile file;
28 | uint32_t sampleRate, size;
29 | bool stereo, rewriteSize;
30 | };
31 |
--------------------------------------------------------------------------------
/src/RomView.cpp:
--------------------------------------------------------------------------------
1 | #include "RomView.h"
2 | #include "Rom.h"
3 | #include "SoundData.h"
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | RomView::RomView(QWidget* parent)
10 | : QWidget(parent)
11 | {
12 | QVBoxLayout* layout = new QVBoxLayout(this);
13 |
14 | romName = addLabel(tr("ROM Name:"));
15 | romCode = addLabel(tr("ROM Code:"));
16 |
17 | QGroupBox* box = new QGroupBox(tr("Songtable Offset:"), this);
18 | QVBoxLayout* boxLayout = new QVBoxLayout(box);
19 | tablePos = new QLabel(box);
20 | tableSelector = new QComboBox(box);
21 | boxLayout->setContentsMargins(0, 0, 0, 0);
22 | boxLayout->addWidget(tablePos);
23 | boxLayout->addWidget(tableSelector);
24 | tableSelector->hide();
25 | layout->addWidget(box, 0);
26 |
27 | numSongs = addLabel(tr("Number of Songs:"));
28 | layout->addStretch(1);
29 |
30 | QObject::connect(tableSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(onSongTableSelected()));
31 | }
32 |
33 | QLabel* RomView::addLabel(const QString& title)
34 | {
35 | QGroupBox* box = new QGroupBox(title, this);
36 | QVBoxLayout* boxLayout = new QVBoxLayout(box);
37 | QLabel* label = new QLabel(box);
38 | boxLayout->setContentsMargins(0, 0, 0, 0);
39 | boxLayout->addWidget(label);
40 | static_cast(layout())->addWidget(box, 0);
41 | return label;
42 | }
43 |
44 | void RomView::updateRom(Rom* rom)
45 | {
46 | if (!rom) {
47 | romName->setText("");
48 | romCode->setText("");
49 | } else {
50 | romName->setText(QString::fromStdString(rom->ReadString(0xA0, 12)));
51 | romCode->setText(QString::fromStdString(rom->GetROMCode()));
52 | }
53 | }
54 |
55 | void RomView::songTablesFound(const std::vector& addrs)
56 | {
57 | tableSelector->blockSignals(true);
58 | tableSelector->clear();
59 |
60 | for (quint32 addr : addrs) {
61 | tableSelector->addItem("0x" + QString::number(addr, 16), QVariant::fromValue(addr));
62 | }
63 |
64 | tableSelector->setVisible(addrs.size() > 1);
65 | tablePos->setVisible(addrs.size() <= 1);
66 | tableSelector->blockSignals(false);
67 | }
68 |
69 | void RomView::updateSongTable(SongTable* table)
70 | {
71 | if (!table) {
72 | tablePos->setText("");
73 | numSongs->setText("");
74 | tableSelector->hide();
75 | tablePos->show();
76 | } else {
77 | auto addr = table->GetSongTablePos();
78 | tablePos->setText("0x" + QString::number(addr, 16));
79 | int index = tableSelector->findData(QVariant::fromValue(addr));
80 | tableSelector->blockSignals(true);
81 | tableSelector->setCurrentIndex(index);
82 | tableSelector->blockSignals(false);
83 | numSongs->setText(QString::number(table->GetNumSongs()));
84 | }
85 | }
86 |
87 | void RomView::onSongTableSelected()
88 | {
89 | emit songTableSelected(tableSelector->currentData().value());
90 | }
91 |
--------------------------------------------------------------------------------
/src/RomView.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | class QLabel;
5 | class QComboBox;
6 | class Rom;
7 | class SongTable;
8 |
9 | class RomView : public QWidget
10 | {
11 | Q_OBJECT
12 | public:
13 | RomView(QWidget* parent = nullptr);
14 |
15 | public slots:
16 | void updateRom(Rom* rom);
17 | void songTablesFound(const std::vector& addrs);
18 | void updateSongTable(SongTable* table);
19 |
20 | signals:
21 | void songTableSelected(quint32 addr);
22 |
23 | private slots:
24 | void onSongTableSelected();
25 |
26 | private:
27 | QLabel* addLabel(const QString& title);
28 |
29 | QLabel* romName;
30 | QLabel* romCode;
31 | QLabel* tablePos;
32 | QComboBox* tableSelector;
33 | QLabel* numSongs;
34 | };
35 |
--------------------------------------------------------------------------------
/src/SongModel.cpp:
--------------------------------------------------------------------------------
1 | #include "SongModel.h"
2 | #include "SoundData.h"
3 | #include "UiUtils.h"
4 | #include "ConfigManager.h"
5 | #include "SongEntry.h"
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | SongModel::SongModel(QObject* parent)
13 | : QAbstractListModel(parent), songTable(nullptr), activeSong(-1), isPlaying(false), isPaused(false)
14 | {
15 | // initializers only
16 | }
17 |
18 | void SongModel::setSongTable(SongTable* table)
19 | {
20 | beginResetModel();
21 | activeSong = -1;
22 | songTable = table;
23 |
24 | titles.clear();
25 | std::size_t numSongs = songTable->GetNumSongs();
26 | for (std::size_t i = 0; i < numSongs; i++) {
27 | titles << QString();
28 | }
29 |
30 | auto entries = ConfigManager::Instance().GetCfg().GetGameEntries();
31 | for (const auto& entry : entries) {
32 | titles[entry.GetUID()] = QString::fromStdString(entry.GetName());
33 | }
34 |
35 | endResetModel();
36 | }
37 |
38 | int SongModel::rowCount(const QModelIndex& parent) const
39 | {
40 | if (!songTable || parent.isValid()) {
41 | return 0;
42 | }
43 | return int(songTable->GetNumSongs());
44 | }
45 |
46 | QVariant SongModel::data(const QModelIndex& index, int role) const
47 | {
48 | if (role == Qt::EditRole) {
49 | return titles[index.row()];
50 | } else if (role == Qt::DisplayRole) {
51 | return QStringLiteral("[%1] %2").arg(fixedNumber(index.row(), 4)).arg(titles[index.row()]);
52 | } else if (role == Qt::ForegroundRole) {
53 | if (activeSong == index.row()) {
54 | return qApp->style()->standardPalette().buttonText();
55 | }
56 | } else if (role == Qt::BackgroundRole) {
57 | if (activeSong == index.row()) {
58 | return qApp->style()->standardPalette().button();
59 | }
60 | } else if (role == Qt::DecorationRole) {
61 | if (activeSong == index.row()) {
62 | if (isPaused) {
63 | return qApp->style()->standardIcon(QStyle::SP_MediaPause);
64 | } else if (isPlaying) {
65 | return qApp->style()->standardIcon(QStyle::SP_MediaPlay);
66 | } else {
67 | return qApp->style()->standardIcon(QStyle::SP_MediaStop);
68 | }
69 | } else if (blankIcon.isNull()) {
70 | QPixmap px = qApp->style()->standardPixmap(QStyle::SP_MediaPlay);
71 | QImage blank(px.width(), px.height(), QImage::Format_ARGB32);
72 | blank.fill(0);
73 | blankIcon.addPixmap(QPixmap::fromImage(blank));
74 | }
75 | return blankIcon;
76 | }
77 | return QVariant();
78 | }
79 |
80 | bool SongModel::setData(const QModelIndex& index, const QVariant& value, int role)
81 | {
82 | if (role != Qt::EditRole) {
83 | return false;
84 | }
85 | titles[index.row()] = value.toString();
86 | emit playlistDirty(true);
87 | emit dataChanged(index, index);
88 | return true;
89 | }
90 |
91 | QVariant SongModel::headerData(int section, Qt::Orientation orientation, int role) const
92 | {
93 | if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
94 | return tr("Songs");
95 | }
96 | return QAbstractListModel::headerData(section, orientation, role);
97 | }
98 |
99 | std::uint32_t SongModel::songAddress(const QModelIndex& index) const
100 | {
101 | if (!songTable || !index.isValid() || index.parent().isValid()) {
102 | return 0;
103 | }
104 | return std::uint32_t(songTable->GetPosOfSong(std::uint16_t(index.row())));
105 | }
106 |
107 | int SongModel::findByAddress(quint32 addr) const
108 | {
109 | int ct = rowCount();
110 | for (int i = 0; i < ct; i++) {
111 | if (songTable->GetPosOfSong(std::uint16_t(i)) == addr) {
112 | return i;
113 | }
114 | }
115 | return -1;
116 | }
117 |
118 | void SongModel::songChanged(PlayerContext*, quint32 addr)
119 | {
120 | int oldActiveSong = activeSong;
121 | activeSong = findByAddress(addr);
122 | if (oldActiveSong == activeSong) {
123 | return;
124 | }
125 | if (oldActiveSong >= 0) {
126 | QModelIndex idx = index(oldActiveSong, 0);
127 | emit dataChanged(idx, idx);
128 | }
129 | if (activeSong >= 0) {
130 | QModelIndex idx = index(activeSong, 0);
131 | emit dataChanged(idx, idx);
132 | }
133 | }
134 |
135 | Qt::ItemFlags SongModel::flags(const QModelIndex& index) const
136 | {
137 | if (!index.isValid()) {
138 | return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled;
139 | }
140 | return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
141 | }
142 |
143 | Qt::DropActions SongModel::supportedDragActions() const
144 | {
145 | return Qt::LinkAction;
146 | }
147 |
148 | QMimeData* SongModel::mimeData(const QModelIndexList& idxs) const
149 | {
150 | if (idxs.isEmpty()) {
151 | return nullptr;
152 | }
153 | QMimeData* data = new QMimeData();
154 | QStringList content;
155 | for (const QModelIndex& idx : idxs) {
156 | content << QString::number(idx.row());
157 | }
158 | data->setData("agbplay/tracklist", content.join(",").toUtf8());
159 | return data;
160 | }
161 |
162 | void SongModel::stateChanged(bool isPlaying, bool isPaused)
163 | {
164 | this->isPlaying = isPlaying;
165 | this->isPaused = isPaused;
166 | emit dataChanged(index(activeSong, 0), index(activeSong, 0));
167 | }
168 |
--------------------------------------------------------------------------------
/src/SongModel.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | class SongTable;
7 | class PlayerContext;
8 |
9 | class SongModel : public QAbstractListModel
10 | {
11 | Q_OBJECT
12 | public:
13 | SongModel(QObject* parent = nullptr);
14 |
15 | int rowCount(const QModelIndex& parent = QModelIndex()) const;
16 | QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
17 | bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::DisplayRole);
18 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
19 | Qt::ItemFlags flags(const QModelIndex& idx) const;
20 | Qt::DropActions supportedDragActions() const;
21 | QMimeData* mimeData(const QModelIndexList& idxs) const;
22 |
23 | std::uint32_t songAddress(const QModelIndex& index) const;
24 |
25 | signals:
26 | void playlistDirty(bool dirty = true);
27 |
28 | public slots:
29 | void setSongTable(SongTable* table);
30 | void songChanged(PlayerContext*, quint32 addr);
31 | void stateChanged(bool isPlaying, bool isPaused);
32 |
33 | protected:
34 | int findByAddress(quint32 addr) const;
35 |
36 | private:
37 | SongTable* songTable;
38 | int activeSong;
39 | bool isPlaying, isPaused;
40 | QStringList titles;
41 | mutable QIcon blankIcon;
42 | };
43 |
--------------------------------------------------------------------------------
/src/TrackHeader.cpp:
--------------------------------------------------------------------------------
1 | #include "TrackHeader.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | TrackHeader::Label::Label(const QRect& rect, const QString& text, int pos)
9 | : rect(&rect), text(text), section(QStyleOptionHeader::Middle)
10 | {
11 | if (pos < 0) {
12 | section = QStyleOptionHeader::Beginning;
13 | } else if (pos > 0) {
14 | section = QStyleOptionHeader::End;
15 | }
16 | }
17 |
18 | TrackHeader::TrackHeader(QWidget* parent)
19 | : QWidget(parent)
20 | {
21 | QCheckBox muteCheck(tr("M"), this);
22 | lineHeight = muteCheck.sizeHint().height();
23 | mute = calcRect(muteCheck, tr("Mute"));
24 | lineHeight = mute.height();
25 |
26 | QStyleOptionHeader opt;
27 | opt.initFrom(this);
28 | opt.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal | QStyle::State_Active | QStyle::State_Enabled;
29 | opt.orientation = Qt::Horizontal;
30 | opt.section = 1;
31 | opt.text = "X";
32 | opt.position = QStyleOptionHeader::Middle;
33 | opt.rect = QRect(0, 0, 50, lineHeight);
34 | QSize headerSize = style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, opt.rect.size(), this);
35 | if (headerSize.height() > lineHeight) {
36 | lineHeight = headerSize.height();
37 | mute.setHeight(lineHeight);
38 | }
39 |
40 | trackNumber = calcRect(QLabel("00"), tr("Track"));
41 | solo = calcRect(QCheckBox(tr("S")), tr("Solo"));
42 | location = calcRect(QLabel("0x01234567"), tr("Location"));
43 | delay = calcRect(QLabel("W00"), tr("Delay"));
44 | program = calcRect(QLabel("127"), tr("Prog"), 1);
45 | pan = calcRect(QLabel("+127"), tr("Pan"), 1);
46 | volume = calcRect(QLabel("100"), tr("Vol"), 1);
47 | mod = calcRect(QLabel("100"), tr("Mod"), 1);
48 | pitch = calcRect(QLabel("+32767"), tr("Pitch"), 1);
49 |
50 | int muteWidth = mute.width();
51 | int soloWidth = solo.width();
52 | int progWidth = program.width();
53 | int panWidth = pan.width();
54 | int groupWidth = muteWidth > soloWidth ? muteWidth : soloWidth;
55 | if (progWidth > groupWidth) {
56 | groupWidth = progWidth;
57 | }
58 | if (panWidth > groupWidth) {
59 | groupWidth = panWidth;
60 | }
61 | mute.setWidth(groupWidth);
62 | solo.setWidth(groupWidth);
63 | program.setWidth(groupWidth);
64 | pan.setWidth(groupWidth);
65 |
66 | int locWidth = (location.width() & ~1) + 2;
67 | int volWidth = volume.width();
68 | int modWidth = mod.width();
69 | int halfWidth = modWidth > volWidth ? modWidth : volWidth;
70 | if (locWidth < halfWidth * 2) {
71 | locWidth = halfWidth * 2;
72 | } else {
73 | halfWidth = locWidth / 2;
74 | }
75 | location.setWidth(locWidth);
76 | volume.setWidth(halfWidth);
77 | mod.setWidth(halfWidth);
78 |
79 | int delayWidth = delay.width();
80 | int pitchWidth = pitch.width();
81 | if (delayWidth > pitchWidth) {
82 | pitch.setWidth(delayWidth);
83 | } else {
84 | delay.setWidth(pitchWidth);
85 | }
86 |
87 | mute.moveLeft(trackNumber.right() + 1);
88 | program.moveLeft(trackNumber.right() + 1);
89 | solo.moveLeft(mute.right() + 1);
90 | pan.moveLeft(mute.right() + 1);
91 | location.moveLeft(solo.right() + 1);
92 | volume.moveLeft(solo.right() + 1);
93 | mod.moveLeft(volume.right() + 1);
94 | delay.moveLeft(location.right() + 1);
95 | pitch.moveLeft(location.right() + 1);
96 |
97 | filler = QRect(0, program.top(), trackNumber.width(), delay.height());
98 |
99 | labels
100 | << Label(trackNumber, tr("Track"), -1)
101 | << Label(mute, tr("Mute"))
102 | << Label(solo, tr("Solo"))
103 | << Label(location, tr("Location"))
104 | << Label(delay, tr("Delay"), 1)
105 | << Label(filler, QString())
106 | << Label(program, tr("Prog"), -1)
107 | << Label(pan, tr("Pan"))
108 | << Label(volume, tr("Vol"))
109 | << Label(mod, tr("Mod"))
110 | << Label(pitch, tr("Pitch"), 1);
111 | }
112 |
113 | void TrackHeader::setTrackName(const QString& name)
114 | {
115 | trackName = name;
116 | update();
117 | }
118 |
119 | QRect TrackHeader::calcRect(const QWidget& widget, const QString& header, int line)
120 | {
121 | int w = widget.sizeHint().width();
122 | QLabel label(header, this);
123 | if (label.sizeHint().width() > w) {
124 | w = label.sizeHint().width();
125 | }
126 |
127 | QStyleOptionHeader opt;
128 | opt.initFrom(this);
129 | opt.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal | QStyle::State_Active | QStyle::State_Enabled;
130 | opt.orientation = Qt::Horizontal;
131 | opt.section = 1;
132 | opt.text = header;
133 | opt.position = QStyleOptionHeader::Middle;
134 | opt.rect = QRect(0, 0, w, lineHeight);
135 |
136 | QSize size = style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, opt.rect.size(), this);
137 | if (size.width() > w) {
138 | w = size.width();
139 | }
140 | w += style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing, nullptr, this);
141 |
142 | return QRect(0, line * lineHeight, w, lineHeight);
143 | }
144 |
145 | QSize TrackHeader::sizeHint() const
146 | {
147 | return QSize(pitch.right() + 1, pitch.bottom());
148 | }
149 |
150 | void TrackHeader::paintEvent(QPaintEvent*)
151 | {
152 | QStylePainter p(this);
153 | p.save();
154 |
155 | QStyleOptionHeader opt;
156 | opt.initFrom(this);
157 | opt.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal | QStyle::State_Active | QStyle::State_Enabled;
158 | opt.rect = rect();
159 | p.drawControl(QStyle::CE_Header, opt);
160 |
161 | opt.textAlignment = Qt::AlignCenter;
162 | for (const auto& label : labels) {
163 | opt.rect = label.rect->adjusted(0, 0, 0, -1);
164 | opt.text = label.text;
165 | opt.section = label.section;
166 | p.drawControl(label.text.isEmpty() ? QStyle::CE_HeaderEmptyArea : QStyle::CE_Header, opt);
167 | }
168 |
169 | p.restore();
170 | QRect titleRect(delay.topRight(), rect().bottomRight());
171 | p.setPen(palette().text().color());
172 | p.drawText(titleRect.adjusted(4, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, trackName);
173 | }
174 |
--------------------------------------------------------------------------------
/src/TrackHeader.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | class QLabel;
8 | class QCheckBox;
9 |
10 | class TrackHeader : public QWidget
11 | {
12 | Q_OBJECT
13 | public:
14 | TrackHeader(QWidget* parent = nullptr);
15 |
16 | void setTrackName(const QString& name);
17 |
18 | QRect trackNumber;
19 | QRect mute;
20 | QRect solo;
21 | QRect location;
22 | QRect delay;
23 | QRect program;
24 | QRect pan;
25 | QRect volume;
26 | QRect mod;
27 | QRect pitch;
28 |
29 | QSize sizeHint() const;
30 |
31 | protected:
32 | void paintEvent(QPaintEvent*);
33 |
34 | private:
35 | QRect calcRect(const QWidget& widget, const QString& header, int line = 0);
36 |
37 | QRect filler;
38 |
39 | struct Label {
40 | Label(const QRect& rect, const QString& text, int pos = 0);
41 | const QRect* rect;
42 | QString text;
43 | QStyleOptionHeader::SectionPosition section;
44 | };
45 | QList labels;
46 |
47 | int lineHeight;
48 | QString trackName;
49 | };
50 |
--------------------------------------------------------------------------------
/src/TrackList.cpp:
--------------------------------------------------------------------------------
1 | #include "TrackList.h"
2 | #include "TrackHeader.h"
3 | #include "TrackView.h"
4 | #include "PlayerContext.h"
5 | #include "VUMeter.h"
6 | #include "UiUtils.h"
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | TrackList::TrackList(QWidget* parent)
13 | : QScrollArea(parent)
14 | {
15 | base = new QWidget(this);
16 | setWidget(base);
17 | setWidgetResizable(true);
18 |
19 | trackLayout = new QVBoxLayout(base);
20 | trackLayout->setContentsMargins(0, 0, 0, 0);
21 | header = new TrackHeader(this);
22 | header->setTrackName("");
23 |
24 | // populate a dummy entry for geometry
25 | TrackView* v = new TrackView(header, 0, base);
26 | tracks << v;
27 | trackLayout->addWidget(v, 0);
28 | trackLayout->addStretch(1);
29 |
30 | header->setGeometry(1, 1, width() - 2, header->sizeHint().height());
31 | setViewportMargins(0, header->sizeHint().height() + 1, 0, 0);
32 |
33 | setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
34 | setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
35 | setSizeAdjustPolicy(QScrollArea::AdjustToContentsOnFirstShow);
36 | setMaximumWidth(v->maximumWidth() + verticalScrollBar()->sizeHint().width());
37 | }
38 |
39 | QSize TrackList::sizeHint() const
40 | {
41 | return QSize(
42 | base->sizeHint().width() + verticalScrollBar()->sizeHint().width() + 20,
43 | base->sizeHint().height() + 4
44 | );
45 | }
46 |
47 | void TrackList::showEvent(QShowEvent* e)
48 | {
49 | QScrollArea::showEvent(e);
50 | setMinimumWidth(sizeHint().width());
51 | setMinimumHeight(base->sizeHint().height() + 4);
52 | }
53 |
54 | void TrackList::resizeEvent(QResizeEvent* e)
55 | {
56 | QScrollArea::resizeEvent(e);
57 | header->resize(width() - 2, viewportMargins().top() - 1);
58 | }
59 |
60 | void TrackList::selectSong(PlayerContext* ctx, quint32 addr, const QString& title)
61 | {
62 | qDeleteAll(tracks);
63 | tracks.clear();
64 |
65 | if (ctx) {
66 | header->setTrackName(QStringLiteral("[%1] %2").arg(formatAddress(addr)).arg(title));
67 |
68 | int numTracks = int(ctx->seq.tracks.size());
69 | for (int i = 0; i < numTracks; i++) {
70 | TrackView* t = new TrackView(header, i, this);
71 | tracks << t;
72 | trackLayout->insertWidget(i, t);
73 | QObject::connect(t, SIGNAL(muteToggled(int,bool)), this, SLOT(onMuteToggled(int,bool)));
74 | QObject::connect(t, SIGNAL(soloToggled(int,bool)), this, SLOT(soloToggled(int,bool)));
75 | }
76 |
77 | update(ctx, nullptr);
78 | } else {
79 | header->setTrackName(QString());
80 | }
81 | }
82 |
83 | void TrackList::update(PlayerContext* ctx, VUState* vu)
84 | {
85 | int numTracks = tracks.size();
86 | if (vu) {
87 | for (int i = 0; i < numTracks; i++) {
88 | tracks[i]->update(ctx, vu->track[i].left, vu->track[i].right);
89 | }
90 | } else {
91 | for (int i = 0; i < numTracks; i++) {
92 | tracks[i]->update(ctx, 0, 0);
93 | }
94 | }
95 | }
96 |
97 | void TrackList::onMuteToggled(int track, bool on)
98 | {
99 | emit muteToggled(track, on);
100 | if (!on) {
101 | int numTracks = tracks.length();
102 | for (int i = 0; i < numTracks; i++) {
103 | tracks[i]->clearSolo();
104 | }
105 | }
106 | }
107 |
108 | void TrackList::soloToggled(int track, bool on)
109 | {
110 | int numTracks = tracks.length();
111 | for (int i = 0; i < numTracks; i++) {
112 | emit muteToggled(i, on ? (i != track) : false);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/TrackList.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | class QVBoxLayout;
7 | class TrackView;
8 | class TrackHeader;
9 | class PlayerContext;
10 | class VUState;
11 |
12 | class TrackList : public QScrollArea
13 | {
14 | Q_OBJECT
15 | public:
16 | TrackList(QWidget* parent = nullptr);
17 |
18 | QSize sizeHint() const;
19 |
20 | signals:
21 | void muteToggled(int track, bool on);
22 |
23 | public slots:
24 | void selectSong(PlayerContext* ctx, quint32 addr, const QString& title);
25 | void update(PlayerContext* ctx, VUState* vu);
26 |
27 | private slots:
28 | void onMuteToggled(int track, bool on);
29 | void soloToggled(int track, bool on);
30 |
31 | protected:
32 | void showEvent(QShowEvent*);
33 | void resizeEvent(QResizeEvent*);
34 |
35 | private:
36 | TrackHeader* header;
37 | QWidget* base;
38 | QVBoxLayout* trackLayout;
39 | QVector tracks;
40 | };
41 |
--------------------------------------------------------------------------------
/src/TrackView.cpp:
--------------------------------------------------------------------------------
1 | #include "TrackView.h"
2 | #include "TrackHeader.h"
3 | #include "PianoKeys.h"
4 | #include "VUMeter.h"
5 | #include "PlayerContext.h"
6 | #include "UiUtils.h"
7 | #include
8 | #include
9 | #include
10 |
11 | #define addLabel(name, text) \
12 | name = new QLabel(text, leftPanel); \
13 | name->setGeometry(header->name); \
14 | name->setAlignment(Qt::AlignCenter);
15 |
16 | TrackView::TrackView(TrackHeader* header, int index, QWidget* parent)
17 | : QWidget(parent), loudness(0.5f), trackIdx(index), muteUpdated(false)
18 | {
19 | QGridLayout* mainLayout = new QGridLayout(this);
20 | mainLayout->setContentsMargins(0, 0, 2, 0);
21 | mainLayout->setVerticalSpacing(1);
22 | mainLayout->setColumnStretch(0, 0);
23 | mainLayout->setColumnStretch(1, 1);
24 |
25 | leftPanel = new QWidget(this);
26 | keys = new PianoKeys(this);
27 | vu = new VUMeter(this);
28 |
29 | leftPanel->setFixedSize(header->sizeHint());
30 | mainLayout->addWidget(leftPanel, 0, 0, 2, 1);
31 | mainLayout->addWidget(keys, 0, 1);
32 | mainLayout->addWidget(vu, 1, 1);
33 |
34 | addLabel(trackNumber, "00");
35 | mute = new QCheckBox(TrackHeader::tr("M"), leftPanel);
36 | mute->setGeometry(header->mute);
37 | solo = new QCheckBox(TrackHeader::tr("S"), leftPanel);
38 | solo->setGeometry(header->solo);
39 | addLabel(location, "0x00000000");
40 | addLabel(delay, "W00");
41 | addLabel(program, "127");
42 | addLabel(pan, "+0");
43 | addLabel(volume, "100");
44 | addLabel(mod, "0");
45 | addLabel(pitch, "+0");
46 |
47 | trackNumber->setText(fixedNumber(index, 2));
48 |
49 | setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
50 | setMinimumWidth(sizeHint().width());
51 | setMaximumWidth(leftPanel->sizeHint().width() + keys->maximumSize().width());
52 |
53 | QObject::connect(mute, SIGNAL(toggled(bool)), vu, SLOT(setMute(bool)));
54 | QObject::connect(mute, SIGNAL(clicked(bool)), this, SLOT(setMute(bool)));
55 | QObject::connect(solo, SIGNAL(clicked(bool)), this, SLOT(setSolo(bool)));
56 | }
57 |
58 | int TrackView::headerWidth() const
59 | {
60 | return leftPanel->sizeHint().width();
61 | }
62 |
63 | void TrackView::resizeEvent(QResizeEvent* e)
64 | {
65 | QWidget::resizeEvent(e);
66 | vu->resize(PianoKeys::preferredWidth(keys->width()), vu->height());
67 | }
68 |
69 | QSize TrackView::sizeHint() const
70 | {
71 | return QSize(453 + leftPanel->sizeHint().width(), leftPanel->sizeHint().height());
72 | }
73 |
74 | void TrackView::update(PlayerContext* ctx, double left, double right)
75 | {
76 | const auto& track = ctx->seq.tracks[trackIdx];
77 | location->setText(formatAddress(track.pos));
78 | volume->setText(QString::number(track.vol));
79 | mod->setText(QString::number(track.mod));
80 | program->setText(QString::number(track.prog));
81 | pitch->setText(signedNumber(track.pitch));
82 | pan->setText(signedNumber(track.pan));
83 |
84 | int delayValue = std::max(0, int(track.delay));
85 | delay->setText("W" + fixedNumber(delayValue, 2));
86 |
87 | for (int i = 0; i < 128; i++) {
88 | keys->setNoteOn(i, track.activeNotes[i]);
89 | }
90 |
91 | vu->setLeft(left * 3);
92 | vu->setRight(right * 3);
93 | vu->setMute(track.muted);
94 | mute->setChecked(track.muted);
95 | if (track.muted) {
96 | solo->setChecked(false);
97 | }
98 | }
99 |
100 | void TrackView::setMute(bool on)
101 | {
102 | emit muteToggled(trackIdx, on);
103 | }
104 |
105 | void TrackView::setSolo(bool on)
106 | {
107 | emit soloToggled(trackIdx, on);
108 | }
109 |
110 | void TrackView::clearSolo()
111 | {
112 | solo->setChecked(false);
113 | }
114 |
--------------------------------------------------------------------------------
/src/TrackView.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include "LoudnessCalculator.h"
5 | class TrackHeader;
6 | class QLabel;
7 | class QCheckBox;
8 | class PianoKeys;
9 | class VUMeter;
10 | class PlayerContext;
11 |
12 | class TrackView : public QWidget
13 | {
14 | Q_OBJECT
15 | public:
16 | TrackView(TrackHeader* header, int index, QWidget* parent = nullptr);
17 |
18 | int headerWidth() const;
19 | QSize sizeHint() const;
20 |
21 | void update(PlayerContext* ctx, double left, double right);
22 | void clearSolo();
23 |
24 | signals:
25 | void muteToggled(int track, bool on);
26 | void soloToggled(int track, bool on);
27 |
28 | private slots:
29 | void setMute(bool);
30 | void setSolo(bool);
31 |
32 | protected:
33 | void resizeEvent(QResizeEvent*);
34 |
35 | private:
36 | LoudnessCalculator loudness;
37 |
38 | int trackIdx;
39 | QWidget* leftPanel;
40 | QLabel* trackNumber;
41 | QCheckBox* mute;
42 | QCheckBox* solo;
43 | QLabel* location;
44 | QLabel* delay;
45 | QLabel* program;
46 | QLabel* pan;
47 | QLabel* volume;
48 | QLabel* mod;
49 | QLabel* pitch;
50 | PianoKeys* keys;
51 | VUMeter* vu;
52 |
53 | bool muteUpdated;
54 | };
55 |
--------------------------------------------------------------------------------
/src/UiUtils.cpp:
--------------------------------------------------------------------------------
1 | #include "UiUtils.h"
2 |
3 | QString signedNumber(int number)
4 | {
5 | if (number <= 0) {
6 | return QString::number(number);
7 | } else {
8 | return "+" + QString::number(number);
9 | }
10 | }
11 |
12 | QString fixedNumber(int number, int digits)
13 | {
14 | return QString::number(number).rightJustified(digits, '0');
15 | }
16 |
17 | QString formatAddress(std::uint32_t addr)
18 | {
19 | return "0x" + QString::number(addr, 16).rightJustified(8, '0');
20 | }
21 |
--------------------------------------------------------------------------------
/src/UiUtils.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | QString signedNumber(int number);
7 | QString fixedNumber(int number, int digits);
8 | QString formatAddress(std::uint32_t addr);
9 |
10 | template
11 | inline QString formatAddress(T addr)
12 | {
13 | return formatAddress(std::uint32_t(addr));
14 | }
15 |
--------------------------------------------------------------------------------
/src/VUMeter.cpp:
--------------------------------------------------------------------------------
1 | #include "VUMeter.h"
2 | #include
3 | #include
4 |
5 | VUState::VUState()
6 | : masterLoudness(10.0f)
7 | {
8 | // initializers only
9 | }
10 |
11 | void VUState::setTrackCount(int numTracks)
12 | {
13 | loudness.clear();
14 | for (int i = 0; i < numTracks; i++) {
15 | loudness.emplace_back(5.0f);
16 | }
17 | track = std::vector(numTracks);
18 | }
19 |
20 | void VUState::reset()
21 | {
22 | masterLoudness.Reset();
23 | for (LoudnessCalculator& c : loudness) {
24 | c.Reset();
25 | }
26 | update();
27 | }
28 |
29 | void VUState::update()
30 | {
31 | masterLoudness.GetLoudness(master.left, master.right);
32 | std::size_t numTracks = loudness.size();
33 | for (std::size_t i = 0; i < numTracks; i++) {
34 | sample& level = track[i];
35 | loudness[i].GetLoudness(level.left, level.right);
36 | }
37 | }
38 |
39 | VUMeter::VUMeter(QWidget* parent)
40 | : QWidget(parent), leftLevel(0), rightLevel(0), muted(false), stereo(Qt::Horizontal)
41 | {
42 | // initializers only
43 | }
44 |
45 | void VUMeter::setStereoLayout(Qt::Orientation a)
46 | {
47 | stereo = a;
48 | update();
49 | }
50 |
51 | void VUMeter::setLeft(double v)
52 | {
53 | leftLevel = v > 1.0 ? 1.0 : v;
54 | update();
55 | }
56 |
57 | void VUMeter::setRight(double v)
58 | {
59 | rightLevel = v > 1.0 ? 1.0 : v;
60 | update();
61 | }
62 |
63 | void VUMeter::setMute(bool m)
64 | {
65 | if (m != muted) {
66 | muted = m;
67 | resizeEvent(nullptr);
68 | update();
69 | }
70 | }
71 |
72 | void VUMeter::paintEvent(QPaintEvent*)
73 | {
74 | QPainter p(this);
75 | int w = width();
76 | int h = height() - 2;
77 | p.fillRect(rect(), Qt::black);
78 |
79 | if (stereo == Qt::Horizontal) {
80 | int span = w / 2 - 6;
81 |
82 | double l = span * leftLevel;
83 | p.fillRect(span - l, 1, l, h, leftGradient);
84 |
85 | double r = span * rightLevel;
86 | p.fillRect(w - span, 1, r, h, rightGradient);
87 |
88 | int barWidth = w - span * 2;
89 | p.fillRect(span + 3, 1, barWidth - 7, h, Qt::green);
90 | } else {
91 | int barHeight = h / 2 - 1;
92 | p.fillRect(1, 1, w * leftLevel, barHeight, rightGradient);
93 | p.fillRect(1, h - barHeight + 1, w * rightLevel, barHeight, rightGradient);
94 | }
95 | }
96 |
97 | void VUMeter::resizeEvent(QResizeEvent*)
98 | {
99 | int v = muted ? 128 : 255;
100 | double w = width();
101 | double channelWidth = stereo == Qt::Horizontal ? w / 2 : 0;
102 |
103 | QLinearGradient left(0, 0, channelWidth, 0);
104 | left.setColorAt(0, QColor(v, 0, 0));
105 | left.setColorAt(0.25, QColor(v, v, 0));
106 | left.setColorAt(0.75, QColor(0, v, 0));
107 | left.setColorAt(1, QColor(0, v, 0));
108 | leftGradient = left;
109 |
110 | QLinearGradient right(channelWidth, 0, w, 0);
111 | right.setColorAt(1, QColor(v, 0, 0));
112 | right.setColorAt(0.25, QColor(0, v, 0));
113 | right.setColorAt(0.75, QColor(v, v, 0));
114 | right.setColorAt(0, QColor(0, v, 0));
115 | rightGradient = right;
116 | }
117 |
--------------------------------------------------------------------------------
/src/VUMeter.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include "LoudnessCalculator.h"
6 |
7 | struct VUState
8 | {
9 | VUState();
10 |
11 | void setTrackCount(int tracks);
12 | void reset();
13 | void update();
14 |
15 | sample master;
16 | std::vector track;
17 |
18 | LoudnessCalculator masterLoudness;
19 | std::vector loudness;
20 | };
21 |
22 | class VUMeter : public QWidget
23 | {
24 | Q_OBJECT
25 | public:
26 | VUMeter(QWidget* parent = nullptr);
27 |
28 | public slots:
29 | void setStereoLayout(Qt::Orientation a);
30 | void setMute(bool m);
31 | void setLeft(double v);
32 | void setRight(double v);
33 |
34 | protected:
35 | void paintEvent(QPaintEvent*);
36 | void resizeEvent(QResizeEvent*);
37 |
38 | private:
39 | double leftLevel, rightLevel;
40 | bool muted;
41 | Qt::Orientation stereo;
42 | QBrush leftGradient, rightGradient;
43 | };
44 |
45 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include "PlayerWindow.h"
10 | #include "Player.h"
11 | #include "Debug.h"
12 | #include "Xcept.h"
13 | #include "ConfigManager.h"
14 |
15 | #define STRINGIFY_(x) #x
16 | #define STRINGIFY(x) STRINGIFY_(x)
17 | #define AGBPLAY_VERSION_STRING STRINGIFY(AGBPLAY_VERSION)
18 |
19 | int main(int argc, char** argv)
20 | {
21 | QCoreApplication::setApplicationName("agbplay");
22 | QCoreApplication::setApplicationVersion(AGBPLAY_VERSION_STRING);
23 | QCoreApplication::setOrganizationName("ipatix");
24 | QCoreApplication::setOrganizationDomain("ipatix.agbplay");
25 |
26 | QApplication app(argc, argv);
27 | app.setWindowIcon(QIcon(":/logo.png"));
28 |
29 | if (!Debug::open("/dev/stderr") && !Debug::open(nullptr)) {
30 | QMessageBox::critical(nullptr, "agbplay-gui", PlayerWindow::tr("Debug Init failed"));
31 | return EXIT_FAILURE;
32 | }
33 |
34 | setlocale(LC_ALL, "");
35 | if (Pa_Initialize() != paNoError) {
36 | QMessageBox::critical(nullptr, "agbplay-gui", PlayerWindow::tr("Couldn't init portaudio"));
37 | return EXIT_FAILURE;
38 | }
39 |
40 | int result;
41 | /* scope */ {
42 | Player player;
43 | try {
44 | player.detectHostApi();
45 | } catch (std::exception& e) {
46 | QMessageBox::critical(nullptr, "agbplay-gui", e.what());
47 | return EXIT_FAILURE;
48 | }
49 |
50 | std::cout << "Loading Config..." << std::endl;
51 | ConfigManager::Instance().Load();
52 |
53 | PlayerWindow wgui(&player);
54 | wgui.show();
55 |
56 | QStringList args = app.arguments();
57 | if (args.length() > 1) {
58 | wgui.openRom(args[1]);
59 | }
60 |
61 | result = app.exec();
62 | }
63 |
64 | if (Pa_Terminate() != paNoError)
65 | std::cerr << "Error while terminating portaudio" << std::endl;
66 | Debug::close();
67 | return result;
68 | }
69 |
--------------------------------------------------------------------------------
/windows/Makefile:
--------------------------------------------------------------------------------
1 | QT_CONFIG_OPTS := -nomake examples -nomake tests -no-dbus -no-feature-testlib -no-feature-concurrent -no-feature-sql -no-feature-network
2 | QT_CONFIG_OPTS := $(QT_CONFIG_OPTS) -release -c++std c++17 -static -opengl desktop -opensource -confirm-license -prefix $(realpath .)
3 | PA_CONFIG_OPTS := --disable-alsa --disable-external-libs --disable-mpeg --without-jack --without-oss --with-winapi=wmme,wasapi
4 | PA_CONFIG_OPTS := $(PA_CONFIG_OPTS) --enable-static --disable-shared --disable-full-suite
5 |
6 | ifdef CROSS
7 | ifeq ($(CROSS),mingw32)
8 | XPLAT = i686-w64-mingw32-
9 | XPLAT_SUFFIX =
10 | PA_BUILD = mingw32
11 | PA_TARGET = i686-pc-mingw64
12 | else ifeq ($(CROSS),mingw64)
13 | XPLAT = x86_64-w64-mingw32-
14 | XPLAT_SUFFIX =
15 | PA_BUILD = mingw64
16 | PA_TARGET = x86_64-pc-mingw64
17 | else ifeq ($(CROSS),mingw32-posix)
18 | XPLAT = i686-w64-mingw32-
19 | XPLAT_SUFFIX = -posix
20 | PA_BUILD = mingw32
21 | PA_TARGET = i686-pc-mingw64
22 | else ifeq ($(CROSS),mingw64-posix)
23 | XPLAT = x86_64-w64-mingw32-
24 | XPLAT_SUFFIX = -posix
25 | PA_BUILD = mingw64
26 | PA_TARGET = x86_64-pc-mingw64
27 | else
28 | $(error Unknown CROSS, expected mingw32 or mingw64)
29 | endif
30 | QT_CONFIG_OPTS := $(QT_CONFIG_OPTS) -xplatform win32-g++ -device-option CROSS_COMPILE=$(XPLAT) --hostprefix=$(realpath .)/cross
31 | PA_CONFIG_OPTS := $(PA_CONFIG_OPTS) --build=$(PA_BUILD) CC=$(XPLAT)gcc$(XPLAT_SUFFIX) CXX=$(XPLAT)g++$(XPLAT_SUFFIX) --target=$(PA_TARGET) --host=$(PA_TARGET) --prefix=$(realpath .)
32 | CROSS_QMAKE := PKG_CONFIG=$(XPLAT)pkg-config QMAKE_CXX=$(XPLAT)g++$(XPLAT_SUFFIX) QMAKE_LINK=$(XPLAT)g++$(XPLAT_SUFFIX)
33 | else ifneq ($(OS),Windows_NT)
34 | $(error Cross-compilation requires CROSS=mingw32 or CROSS=mingw64)
35 | else
36 | QT_CONFIG_OPTS := $(QT_CONFIG_OPTS) -platform win32-g++
37 | PA_CONFIG_OPTS := $(PA_CONFIG_OPTS) --prefix=$(realpath .) CC=$(CC) CXX=$(CXX)
38 | CROSS_QMAKE :=
39 | endif
40 |
41 | all: agbplay-gui.exe
42 |
43 | qt-static: lib/libQt5Widgets.a
44 |
45 | qtbase/configure:
46 | git clone --depth=1 --single-branch --branch=5.15 https://github.com/qt/qtbase
47 |
48 | qtbase/.config.notes: qtbase/configure
49 | +cd qtbase && ./configure $(QT_CONFIG_OPTS)
50 |
51 | lib/libQt5Widgets.a: qtbase/.config.notes
52 | +$(MAKE) -C qtbase
53 |
54 | portaudio-static: lib/libportaudio.a
55 |
56 | portaudio/configure:
57 | git clone --depth=1 --single-branch --branch=v19.7.0 https://github.com/PortAudio/portaudio
58 |
59 | lib/libportaudio.a: portaudio/configure
60 | +cd portaudio && ./configure $(PA_CONFIG_OPTS)
61 | +$(MAKE) -C portaudio
62 | +$(MAKE) -C portaudio install
63 |
64 | Makefile.agbplay-gui: lib/libportaudio.a lib/libQt5Widgets.a ../agbplay-gui.pro
65 | mkdir -p .build
66 | ./qtbase/bin/qmake -o Makefile.agbplay-gui .. $(CROSS_QMAKE) PA_ROOT=$(CURDIR)
67 |
68 | agbplay-gui.exe: Makefile.agbplay-gui lib/libportaudio.a lib/libQt5Widgets.a FORCE
69 | mkdir -p .build
70 | +$(MAKE) -f Makefile.agbplay-gui
71 |
72 | clean:
73 | +$(MAKE) -f Makefile.agbplay-gui clean
74 |
75 | buildclean:
76 | -cd qtbase && git clean -dxf
77 | -cd qtbase && git clean -dXf
78 | -cd portaudio && git clean -dxf
79 | -cd portaudio && git clean -dXf
80 | -rm -rf .build bin lib include pkgconfig mkspecs cross doc plugins
81 | -rm -f Makefile.agbplay-gui
82 | -rm -f agbplay-gui.exe agbplay-gui_plugin_import.cpp agbplay-gui_resource.rc
83 |
84 | FORCE:
85 |
--------------------------------------------------------------------------------