├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── agbplay-gui.pro ├── boost └── math │ └── special_functions │ └── sinc.hpp ├── resources ├── about.html ├── agbplay.qrc └── logo.png ├── screenshot.png ├── src ├── AudioThread.cpp ├── AudioThread.h ├── ConfigManager.cpp ├── OS.cpp ├── PianoKeys.cpp ├── PianoKeys.h ├── Player.cpp ├── Player.h ├── PlayerControls.cpp ├── PlayerControls.h ├── PlayerWindow.cpp ├── PlayerWindow.h ├── PlaylistModel.cpp ├── PlaylistModel.h ├── PreferencesWindow.cpp ├── PreferencesWindow.h ├── RiffWriter.cpp ├── RiffWriter.h ├── RomView.cpp ├── RomView.h ├── SongModel.cpp ├── SongModel.h ├── TrackHeader.cpp ├── TrackHeader.h ├── TrackList.cpp ├── TrackList.h ├── TrackView.cpp ├── TrackView.h ├── UiUtils.cpp ├── UiUtils.h ├── VUMeter.cpp ├── VUMeter.h └── main.cpp └── windows └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | agbplay-gui 3 | *.wav 4 | *.exe 5 | *.o 6 | moc_* 7 | .qmake.stash 8 | /Makefile 9 | core 10 | Makefile.Debug 11 | Makefile.Release 12 | Makefile.agbplay-gui 13 | *plugin_import.cpp 14 | *resource.rc 15 | cross-qmake.sh 16 | windows/qtbase 17 | windows/portaudio 18 | windows/lib 19 | windows/include 20 | windows/bin 21 | windows/.build 22 | windows/cross 23 | windows/mkspecs 24 | windows/doc 25 | windows/plugins 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "agbplay"] 2 | path = agbplay 3 | url = https://github.com/ahigerd/agbplay 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # agbplay-gui 2 | 3 | **agbplay-gui** is a music player for Game Boy Advance ROMs that use the MusicPlayer2000 4 | (mp2k/m4a/"Sappy") sound engine. It is based on [agbplay](https://github.com/ipatix/agbplay) 5 | by ipatix. 6 | 7 | ![agbplay-gui screenshot](screenshot.png) 8 | 9 | ## Building 10 | 11 | agbplay-gui depends on the following libraries: 12 | 13 | * Qt 5 14 | * PortAudio 15 | 16 | A build script is provided for compiling on Windows using MSYS2. This script will download 17 | and build the required dependencies automatically. This script also supports cross 18 | compiling agbplay-gui from a non-Windows system with MinGW. 19 | 20 | First ensure that the agbplay submodule is checked out and up to date: 21 | 22 | * `git submodule update --init --recursive` 23 | 24 | To build on Windows using the automatic build script: 25 | 26 | * `make -C windows` 27 | 28 | To cross-compile a 32-bit binary using MinGW: 29 | 30 | * `make -C windows CROSS=mingw32` 31 | 32 | To cross-compile a 64-bit binary using MinGW: 33 | 34 | * `make -C windows CROSS=mingw64` 35 | 36 | To build using locally-installed libraries: 37 | 38 | * `qmake` 39 | * `make` 40 | 41 | ## License 42 | 43 | **agbplay-gui** is created by Adam Higerd. It is derived from agbplay by 44 | ipatix. Both agbplay and agbplay-gui are distributed under the terms of 45 | the LGPLv3. 46 | 47 | *This program is free software: you can redistribute it and/or modify it 48 | under the terms of the GNU Lesser General Public License as published by the 49 | Free Software Foundation, either version 3 of the License, or (at your 50 | option) any later version.* 51 | 52 | *This program is distributed in the hope that it will be useful, but 53 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 54 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 55 | License for more details.* 56 | 57 | *For more information about the GNU Lesser General Public License, see 58 | [https://www.gnu.org/licenses](https://www.gnu.org/licenses).* 59 | 60 | agbplay source code: [https://github.com/ipatix/agbplay](https://github.com/ipatix/agbplay) 61 | -------------------------------------------------------------------------------- /agbplay-gui.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | QT = core gui widgets 3 | CONFIG += c++17 4 | OBJECTS_DIR = .build 5 | MOC_DIR = .build 6 | RCC_DIR = .build 7 | INCLUDEPATH += $${_PRO_FILE_PWD_}/src $${_PRO_FILE_PWD_}/agbplay/src $${_PRO_FILE_PWD_} 8 | QMAKE_CXXFLAGS += -D_XOPEN_SOURCE=700 -Wall -Wextra -Wunreachable-code -Wno-conversion 9 | CONFIG += release 10 | CONFIG -= debug debug_and_release 11 | CONFIG += link_pkgconfig 12 | !isEmpty(PA_ROOT) { 13 | INCLUDEPATH += $$PA_ROOT/include 14 | LIBS += -L$$PA_ROOT/lib -lportaudio 15 | } 16 | else:packagesExist(portaudio-2.0) { 17 | PKGCONFIG += portaudio-2.0 18 | } 19 | else { 20 | !isEmpty(PA_INCLUDE): INCLUDEPATH += $$PA_INCLUDE 21 | !isEmpty(PA_LIB): LIBS += -L$$PA_LIB 22 | LIBS += -lportaudio 23 | } 24 | win32 { 25 | CONFIG += static 26 | QMAKE_LFLAGS += -static-libgcc -static-libstdc++ -static 27 | } 28 | 29 | RESOURCES += resources/agbplay.qrc 30 | 31 | GUI_CLASS += PianoKeys VUMeter TrackHeader TrackView TrackList 32 | GUI_CLASS += RomView PlayerWindow SongModel Player UiUtils 33 | GUI_CLASS += AudioThread PlayerControls PlaylistModel RiffWriter 34 | GUI_CLASS += PreferencesWindow 35 | for(F, GUI_CLASS) { 36 | HEADERS += src/$${F}.h 37 | SOURCES += src/$${F}.cpp 38 | } 39 | 40 | AGBPLAY += CGBChannel CGBPatterns Debug GameConfig PlayerContext 41 | AGBPLAY += SequenceReader SoundMixer ReverbEffect LoudnessCalculator 42 | AGBPLAY += SoundChannel Resampler Rom SoundData SongEntry Types Xcept 43 | AGBPLAY += Ringbuffer 44 | for(F, AGBPLAY) { 45 | HEADERS += agbplay/src/$${F}.h 46 | SOURCES += agbplay/src/$${F}.cpp 47 | } 48 | 49 | # files with the implementations replaced with Qt equivalents 50 | HEADERS += agbplay/src/ConfigManager.h agbplay/src/OS.h 51 | SOURCES += src/ConfigManager.cpp src/OS.cpp 52 | 53 | SOURCES += src/main.cpp 54 | 55 | VERSION = 1.1.0 56 | # If git commands can be run without errors, grab the commit hash 57 | system(git log -1 --pretty=format:) { 58 | BUILD_HASH = -$$system(git log -1 --pretty=format:%h) 59 | } 60 | else { 61 | BUILD_HASH = 62 | } 63 | 64 | DEFINES += AGBPLAY_VERSION=$${VERSION}$${BUILD_HASH} 65 | -------------------------------------------------------------------------------- /boost/math/special_functions/sinc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // this file exists to avoid needing to modify agbplay 4 | 5 | #include 6 | #include 7 | #include 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