├── .gitignore ├── .gitmodules ├── LICENSE.CLAP ├── LICENSE.md ├── Makefile ├── README.VST3 ├── README.md ├── buildvs.cmd ├── config.mak ├── gui ├── channelwidget.cpp ├── channelwidget.h ├── gui.pro └── main.cpp ├── mp2k-clef-win64.vst3 ├── msvc32.mak ├── plugins ├── Makefile └── clefplugin.cpp └── src ├── Makefile ├── instrumentdata.cpp ├── instrumentdata.h ├── main.cpp ├── romfile.cpp ├── romfile.h ├── songdata.cpp ├── songdata.h ├── songtable.cpp └── songtable.h /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary files 2 | *.tmp 3 | *.sw* 4 | 5 | # audio files 6 | *.mp3 7 | *.m4a 8 | *.wav 9 | *.m3u 10 | 11 | # build temporaries 12 | gui/.qmake.stash 13 | Makefile.d 14 | *.o 15 | *.obj 16 | *.exp 17 | *.lib 18 | build 19 | build_win 20 | *_plugin_import.cpp 21 | core 22 | 23 | # build outputs 24 | depends.mak 25 | include 26 | mp2k-clef 27 | mp2k-clef_d 28 | *.dll 29 | *.a 30 | *.so 31 | *.exe 32 | *_gui 33 | *_gui_d 34 | *.clap 35 | *.vst3 36 | gui/Makefile* 37 | 38 | # external dependencies 39 | plugins/foobar2000 40 | plugins/pfc 41 | plugins/libPPUI 42 | 43 | # game files 44 | *.gba 45 | *.cheats 46 | *.sav 47 | *.ss* 48 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libclef"] 2 | path = libclef 3 | url = https://github.com/ahigerd/libclef 4 | [submodule "clap"] 5 | path = plugins/clap 6 | url = https://github.com/free-audio/clap.git 7 | -------------------------------------------------------------------------------- /LICENSE.CLAP: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexandre BIQUE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | mp2k-clef is copyright (c) 2021-2024 Adam Higerd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | This project is based upon libclef, copyright (c) 2020-2024 Adam Higerd. 21 | libclef is distributed under the same terms as above. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | _default: cli 2 | 3 | include config.mak 4 | 5 | cli: $(PLUGIN_NAME)$(EXE) 6 | 7 | debug: $(PLUGIN_NAME)_d$(EXE) 8 | 9 | gui: $(PLUGIN_NAME)_gui$(EXE) 10 | 11 | guidebug: $(PLUGIN_NAME)_gui_d$(EXE) 12 | 13 | all: cli plugins gui 14 | 15 | plugins: audacious 16 | 17 | audacious: aud_$(PLUGIN_NAME).$(DLL) 18 | 19 | winamp: in_$(PLUGIN_NAME).dll 20 | 21 | foobar: foo_input_$(PLUGIN_NAME).dll 22 | 23 | clap: $(PLUGIN_NAME).clap 24 | 25 | clapdebug: $(PLUGIN_NAME)_d.clap 26 | 27 | libclef/src: 28 | git submodule update --init --recursive 29 | 30 | ifeq ($(CROSS),msvc) 31 | depends.mak: libclef/src $(wildcard src/*.h src/*.cpp src/*/*.h src/*/*.cpp plugins/*.cpp libclef/src/*.cpp libclef/src/*/*.h libclef/src/*/*.cpp libclef/src/*/*.h) 32 | $(WINE) cmd /c buildvs.cmd depends 33 | 34 | foo_input_$(PLUGIN_NAME).$(DLL) in_$(PLUGIN_NAME).$(DLL) aud_$(PLUGIN_NAME).$(DLL) clean: FORCE depends.mak 35 | MAKEFLAGS= $(WINE) nmake /f msvc32.mak $@ 36 | else 37 | libclef/$(BUILDPATH)/libclef.a libclef/$(BUILDPATH)/libclef_d.a: libclef/src $(wildcard libclef/src/*.cpp libclef/*/*.h libclef/src/*/*.cpp libclef/*/*/*.h) 38 | $(MAKE) -C libclef $(BUILDPATH)/$(notdir $@) 39 | 40 | $(BUILDPATH)/Makefile.d: $(wildcard src/*.cpp src/*/*.cpp src/*.h src/*/*.h) Makefile src/Makefile config.mak 41 | $(MAKE) -C src ../$@ 42 | 43 | $(PLUGIN_NAME)$(EXE): src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef.a 44 | $(MAKE) -C src ../$@ 45 | 46 | $(PLUGIN_NAME)_d$(EXE): src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef_d.a 47 | $(MAKE) -C src ../$@ 48 | 49 | $(BUILDPATH)/lib$(PLUGIN_NAME).a: src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef.a 50 | $(MAKE) -C src ../$@ 51 | 52 | $(BUILDPATH)/lib$(PLUGIN_NAME)_d.a: src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef_d.a 53 | $(MAKE) -C src ../$@ 54 | 55 | gui/Makefile: gui/gui.pro libclef/gui/gui.pri Makefile config.mak 56 | cd gui && $(QMAKE) BUILDPATH=../$(BUILDPATH) PLUGIN_NAME=$(PLUGIN_NAME) CLEF_LDFLAGS="$(LDFLAGS_R)" 57 | 58 | gui/Makefile.debug: gui/gui.pro libclef/gui/gui.pri Makefile config.mak 59 | cd gui && $(QMAKE) -o Makefile.debug BUILD_DEBUG=1 BUILDPATH=../$(BUILDPATH) PLUGIN_NAME=$(PLUGIN_NAME) CLEF_LDFLAGS="$(LDFLAGS_D)" 60 | 61 | $(PLUGIN_NAME)_gui$(EXE): src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef.a gui/Makefile $(BUILDPATH)/lib$(PLUGIN_NAME).a 62 | $(MAKE) -C gui 63 | 64 | $(PLUGIN_NAME)_gui_d$(EXE): src/Makefile $(BUILDPATH)/Makefile.d config.mak libclef/$(BUILDPATH)/libclef_d.a gui/Makefile.debug $(BUILDPATH)/lib$(PLUGIN_NAME)_d.a 65 | $(MAKE) -C gui -f Makefile.debug 66 | 67 | aud_$(PLUGIN_NAME).$(DLL): $(PLUGIN_NAME)$(EXE) libclef/$(BUILDPATH)/libclef.a plugins/Makefile config.mak plugins/clefplugin.cpp 68 | $(MAKE) -C plugins ../$@ 69 | 70 | aud_$(PLUGIN_NAME)_d.$(DLL): libclef/$(BUILDPATH)/libclef_d.a $(PLUGIN_NAME)_d$(EXE) plugins/Makefile config.mak plugins/clefplugin.cpp 71 | $(MAKE) -C plugins ../$@ 72 | 73 | ifeq ($(OS),Windows_NT) 74 | in_$(PLUGIN_NAME).$(DLL): $(PLUGIN_NAME)$(EXE) libclef/$(BUILDPATH)/libclef.a plugins/Makefile config.mak plugins/clefplugin.cpp 75 | $(MAKE) -C plugins ../$@ 76 | 77 | in_$(PLUGIN_NAME)_d.$(DLL): $(PLUGIN_NAME)_d$(EXE) libclef/$(BUILDPATH)/libclef_d.a plugins/Makefile config.mak plugins/clefplugin.cpp 78 | $(MAKE) -C plugins ../$@ 79 | else 80 | in_$(PLUGIN_NAME).dll in_$(PLUGIN_NAME)_d.dll: FORCE 81 | $(MAKE) CROSS=mingw $@ 82 | endif 83 | 84 | $(PLUGIN_NAME).clap: $(PLUGIN_NAME)$(EXE) libclef/$(BUILDPATH)/libclef.a plugins/Makefile config.mak plugins/clefplugin.cpp 85 | $(MAKE) -C plugins ../$@ 86 | 87 | $(PLUGIN_NAME)_d.clap: $(PLUGIN_NAME)_d$(EXE) libclef/$(BUILDPATH)/libclef_d.a plugins/Makefile config.mak plugins/clefplugin.cpp 88 | $(MAKE) -C plugins ../$@ 89 | 90 | guiclean: FORCE 91 | -[ -f gui/Makefile ] && $(MAKE) -C gui distclean 92 | -[ -f gui/Makefile.debug ] && $(MAKE) -C gui -f Makefile.debug distclean 93 | 94 | clean: guiclean FORCE 95 | -rm -f $(BUILDPATH)/*.o $(BUILDPATH)/*/*.o $(BUILDPATH)/Makefile.d 96 | -rm -f $(PLUGIN_NAME)$(EXE) $(PLUGIN_NAME)_d$(EXE) $(PLUGIN_NAME)_gui$(EXE) $(PLUGIN_NAME)_gui_d$(EXE) *.$(DLL) 97 | -$(MAKE) -C libclef clean 98 | endif 99 | 100 | FORCE: 101 | -------------------------------------------------------------------------------- /README.VST3: -------------------------------------------------------------------------------- 1 | The VST3 plugin is a wrapper around the CLAP plugin. 2 | 3 | To use the VST3 plugin, put the win64 CLAP plugin in the canonical system path: 4 | C:\Program Files\Common Files\CLAP\ 5 | 6 | The VST3 plugin can be installed wherever your DAW searches for them. The standard 7 | path for VST3 plugins is: 8 | C:\Program Files\Common Files\VST3\ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mp2k-clef 2 | ========= 3 | 4 | mp2k-clef is a player for MusicPlayer2000 tracks from Game Boy Advance games. 5 | 6 | Building 7 | -------- 8 | To build on POSIX platforms or MinGW using GNU Make, simply run `make`. The following make 9 | targets are recognized: 10 | 11 | * `cli`: builds the command-line tool. (default) 12 | * `gui`: builds the graphical interface. 13 | * `plugins`: builds all player plugins supported by the current platform. 14 | * `all`: builds the command-line tool and all plugins supported by the current platform. 15 | * `debug`: builds a debug version of the command-line tool. 16 | * `guidebug`: builds a debug version of the graphical interface. 17 | * `audacious`: builds just the Audacious player plugin, if supported. 18 | * `winamp`: builds just the Winamp player plugin, if supported. 19 | * `foobar`: builds just the Foobar2000 player plugin, if supported. 20 | * `clap`: builds the CLAP instrument plugin, if supported. 21 | * `clapdebug`: builds a debug version of the CLAP instrument plugin, if supported. 22 | * `aud_mp2k-clef_d.dll`: builds a debug version of the Audacious plugin, if supported. 23 | * `in_mp2k-clef_d.dll`: builds a debug version of the Winamp plugin, if supported. 24 | 25 | The following make variables are also recognized: 26 | 27 | * `CROSS=mingw`: If building on Linux, use MinGW to build 32-bit Windows binaries. 28 | * `CROSS=mingw64`: If building on Linux, use MinGW to build 64-bit Windows binaries. 29 | * `CROSS=msvc`: Use Microsoft Visual C++ to build Windows binaries, using Wine if the current 30 | platform is not Windows. (Required to build the Foobar2000 plugin.) 31 | * `WINE=[command]`: Sets the command used to run Wine. (Default: `wine`) 32 | * `QMAKE=[command]`: Sets the command used to invoke qmake for GUI builds. (Default: `qmake`) 33 | 34 | To build using Microsoft Visual C++ on Windows without using GNU Make, run `buildvs.cmd`, 35 | optionally with one or more build targets. The following build targets are supported: 36 | 37 | * `cli`: builds the command-line tool. (default) 38 | * `plugins`: builds the Winamp and Foobar2000 plugins. 39 | * `all`: builds the command-line tool and the Winamp and Foobar2000 plugins. 40 | * `winamp`: builds just the Winamp plugin. 41 | * `foobar`: builds just the Foobar2000 plugin. 42 | 43 | Separate debug builds are not supported with Microsoft Visual C++, but the build flags may be 44 | edited in `msvc.mak`. 45 | 46 | License 47 | ------- 48 | mp2k-clef is copyright (c) 2021-2024 Adam Higerd and distributed under the terms of the 49 | [MIT license](LICENSE.md). 50 | 51 | This project is based upon libclef, copyright (c) 2020-2024 Adam Higerd and distributed 52 | under the terms of the [MIT license](LICENSE.md). 53 | 54 | [CLAP](https://cleveraudio.org/) is an open-source audio plugin format. The 55 | [CLAP SDK](https://github.com/free-audio/clap) is copyright (c) 2021 Alexander BIQUE 56 | and distributed under the terms of the [MIT license](LICENSE.CLAP). 57 | -------------------------------------------------------------------------------- /buildvs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo. 2> depends.tmp 3 | for /r %%f in (*.cpp) do ( 4 | SET "_o=%%f" 5 | call cmd /c if "%%_o:main.cpp=%%"=="%%_o%%" echo "mp2k-clef.exe" "in_mp2k-clef.dll" "aud_mp2k-clef.dll": "%%_o:~0,-4%%.obj">>depends.tmp 6 | call cmd /c if "%%_o:main.cpp=%%"=="%%_o%%" echo "foo_input_mp2k-clef.dll": "%%_o:~0,-4%%.obj">>depends.tmp 7 | echo.>>depends.tmp 8 | ) 9 | type depends.tmp | find \src\ > depends.mak 10 | del depends.tmp 11 | if [%*] NEQ [depends] nmake /f msvc32.mak %* 12 | -------------------------------------------------------------------------------- /config.mak: -------------------------------------------------------------------------------- 1 | PLUGIN_NAME = mp2k-clef 2 | -include libclef/config.mak 3 | -include ../libclef/config.mak 4 | -------------------------------------------------------------------------------- /gui/channelwidget.cpp: -------------------------------------------------------------------------------- 1 | #include "channelwidget.h" 2 | #include "vumeter.h" 3 | #include "seq/sequenceevent.h" 4 | #include "synth/synthcontext.h" 5 | #include "synth/channel.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | ChannelWidget::ChannelWidget(QWidget* parent) 12 | : QGroupBox(tr("Channels"), parent), context(nullptr), lastUpdate(0) 13 | { 14 | new QGridLayout(this); 15 | } 16 | 17 | void ChannelWidget::contextUpdated(SynthContext* context) 18 | { 19 | qDeleteAll(channels); 20 | channels.clear(); 21 | this->context = context; 22 | lastUpdate = 0; 23 | if (!context) { 24 | return; 25 | } 26 | QGridLayout* grid = static_cast(layout()); 27 | int index = 0; 28 | for (const auto& channel : context->channels) { 29 | ChannelCheckBox* cb = new ChannelCheckBox(index, channel.get(), this); 30 | channels << cb; 31 | grid->addWidget(cb, index / 4, index % 4); 32 | index++; 33 | } 34 | } 35 | 36 | void ChannelWidget::updateMeters() 37 | { 38 | if (!context) { 39 | return; 40 | } 41 | if (channels.size() != int(context->channels.size())) { 42 | contextUpdated(context); 43 | } 44 | double timestamp = context->currentTime(); 45 | if (std::abs(timestamp - lastUpdate) > (1.0 / 30.0)) { 46 | lastUpdate = timestamp; 47 | } else if (timestamp < lastUpdate + (1.0 / 120.0)) { 48 | return; 49 | } 50 | for (ChannelCheckBox* cb : channels) { 51 | cb->updateMeter(lastUpdate); 52 | } 53 | lastUpdate = timestamp; 54 | } 55 | 56 | void ChannelWidget::unmuteAll() 57 | { 58 | for (auto channel : channels) { 59 | channel->setActive(true); 60 | } 61 | } 62 | 63 | void ChannelWidget::setSolo(ChannelCheckBox* solo) 64 | { 65 | for (auto channel : channels) { 66 | channel->setActive(channel == solo); 67 | } 68 | } 69 | 70 | ChannelCheckBox::ChannelCheckBox(int index, Channel* channel, ChannelWidget* parent) 71 | : QWidget(parent), channel(channel) 72 | { 73 | QHBoxLayout* layout = new QHBoxLayout(this); 74 | layout->setContentsMargins(0, 0, 0, 0); 75 | layout->setSpacing(0); 76 | 77 | chk = new QCheckBox(QString::number(index + 1), this); 78 | chk->setFixedWidth(50); 79 | chk->setChecked(true); 80 | layout->addWidget(chk, 0); 81 | 82 | vu = new VUMeter(this); 83 | vu->setScaleMode(QAudio::LinearVolumeScale); 84 | vu->setChannels(1); 85 | layout->addWidget(vu, 1); 86 | 87 | QObject::connect(chk, SIGNAL(toggled(bool)), this, SLOT(setActive(bool))); 88 | 89 | QAction* solo = new QAction(tr("Solo"), this); 90 | QObject::connect(solo, SIGNAL(triggered()), this, SLOT(setSolo())); 91 | addAction(solo); 92 | 93 | QAction* unmute = new QAction(tr("Unmute All"), this); 94 | QObject::connect(unmute, SIGNAL(triggered()), parent, SLOT(unmuteAll())); 95 | addAction(unmute); 96 | 97 | setContextMenuPolicy(Qt::ActionsContextMenu); 98 | } 99 | 100 | void ChannelCheckBox::setActive(bool checked) 101 | { 102 | channel->mute = !checked; 103 | if (chk->isChecked() != checked) { 104 | chk->setChecked(checked); 105 | } 106 | } 107 | 108 | void ChannelCheckBox::updateMeter(double timestamp) 109 | { 110 | double level = channel->mute ? 0 : channel->notes.size() * channel->gain->valueAt(timestamp); 111 | vu->setLevel(0, level > 4.0 ? 1.0 : level / 4.0); 112 | } 113 | 114 | void ChannelCheckBox::setSolo() 115 | { 116 | static_cast(parent())->setSolo(this); 117 | } 118 | -------------------------------------------------------------------------------- /gui/channelwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef D2W_CHANNELWIDGET_H 2 | #define D2W_CHANNELWIDGET_H 3 | 4 | #include 5 | class QCheckBox; 6 | class Channel; 7 | class SynthContext; 8 | class VUMeter; 9 | class ChannelWidget; 10 | 11 | class ChannelCheckBox : public QWidget 12 | { 13 | Q_OBJECT 14 | public: 15 | ChannelCheckBox(int index, Channel* channel, ChannelWidget* parent); 16 | 17 | QCheckBox* chk; 18 | VUMeter* vu; 19 | Channel* channel; 20 | 21 | void updateMeter(double timestamp); 22 | 23 | public slots: 24 | void setActive(bool muted); 25 | void setSolo(); 26 | }; 27 | 28 | class ChannelWidget : public QGroupBox 29 | { 30 | Q_OBJECT 31 | public: 32 | ChannelWidget(QWidget* parent); 33 | 34 | void setSolo(ChannelCheckBox* channel); 35 | 36 | public slots: 37 | void contextUpdated(SynthContext* context); 38 | void updateMeters(); 39 | void unmuteAll(); 40 | 41 | private: 42 | SynthContext* context; 43 | QList channels; 44 | double lastUpdate; 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /gui/gui.pro: -------------------------------------------------------------------------------- 1 | isEmpty(BUILDPATH) { 2 | error("BUILDPATH must be set") 3 | } 4 | BUILDPATH = $$absolute_path($$BUILDPATH) 5 | include($$BUILDPATH/../libclef/gui/gui.pri) 6 | 7 | HEADERS += channelwidget.h 8 | SOURCES += channelwidget.cpp 9 | 10 | SOURCES += main.cpp ../plugins/clefplugin.cpp 11 | -------------------------------------------------------------------------------- /gui/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mainwindow.h" 3 | #include "clefcontext.h" 4 | #include "channelwidget.h" 5 | #include "playercontrols.h" 6 | #include "synth/synthcontext.h" 7 | #include "plugin/baseplugin.h" 8 | #include 9 | 10 | class GbampWindow : public MainWindow 11 | { 12 | public: 13 | GbampWindow(ClefPluginBase* plugin) : MainWindow(plugin) 14 | { 15 | resize(400, 200); 16 | } 17 | 18 | QWidget* createPluginWidget(QWidget* parent) 19 | { 20 | ChannelWidget* cw = new ChannelWidget(parent); 21 | QObject::connect(controls, SIGNAL(bufferUpdated()), cw, SLOT(updateMeters())); 22 | return cw; 23 | } 24 | }; 25 | 26 | int main(int argc, char** argv) 27 | { 28 | ClefContext ctx; 29 | ClefPluginBase* plugin = Clef::makePlugin(&ctx); 30 | 31 | QCoreApplication::setApplicationName(QString::fromStdString(plugin->pluginName())); 32 | QCoreApplication::setApplicationVersion(QString::fromStdString(plugin->version())); 33 | QCoreApplication::setOrganizationName("libclef"); 34 | QCoreApplication::setOrganizationDomain("libclef" + QString::fromStdString(plugin->pluginShortName())); 35 | QApplication app(argc, argv); 36 | 37 | GbampWindow mw(plugin); 38 | mw.show(); 39 | if (app.arguments().length() > 1) { 40 | mw.openFile(app.arguments()[1], true); 41 | } 42 | 43 | return app.exec(); 44 | } 45 | -------------------------------------------------------------------------------- /mp2k-clef-win64.vst3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahigerd/mp2k-clef/4ba820270d5e5c5917a879646093e75b9e3859ff/mp2k-clef-win64.vst3 -------------------------------------------------------------------------------- /msvc32.mak: -------------------------------------------------------------------------------- 1 | PLUGIN_NAME = sample 2 | 3 | cli: "$(PLUGIN_NAME).exe" 4 | 5 | all: cli plugins 6 | 7 | plugins: winamp foobar 8 | 9 | audacious: "aud_$(PLUGIN_NAME).dll" 10 | 11 | winamp: "in_$(PLUGIN_NAME).dll" 12 | 13 | foobar: "foo_input_$(PLUGIN_NAME).dll" 14 | 15 | libclef\src: 16 | git submodules update --init --recursive 17 | 18 | !include depends.mak 19 | 20 | .cpp.obj: 21 | $(CPP) /std:c++latest /DUNICODE /D_UNICODE /DNDEBUG /O2 /EHsc /I src /I libclef\src /I plugins\foobar2000 /I plugins /c /Fo$@ $< 22 | 23 | plugins\foobarplugin.obj: plugins\clefplugin.cpp libclef\src\plugins\foobarplugin.h FORCE 24 | $(CPP) /std:c++latest /DUNICODE /D_UNICODE /DNDEBUG /DBUILD_FOOBAR /O2 /EHsc /I src /I libclef\src /I plugins\foobar2000 /I plugins /c /Fo$@ plugins\clefplugin.cpp 25 | 26 | plugins\audaciousplugin.obj: plugins\clefplugin.cpp libclef\src\plugins\audaciousplugin.h FORCE 27 | $(CPP) /std:c++latest /DUNICODE /D_UNICODE /DNDEBUG /DBUILD_AUDACIOUS /O2 /EHsc /I src /I libclef\src /c /Fo$@ plugins\clefplugin.cpp 28 | 29 | plugins\winampplugin.obj: plugins\clefplugin.cpp libclef\src\plugins\winampplugin.h FORCE 30 | $(CPP) /std:c++latest /DUNICODE /D_UNICODE /DNDEBUG /DBUILD_WINAMP /O2 /EHsc /I src /I libclef\src /c /Fo$@ plugins\clefplugin.cpp 31 | 32 | "$(PLUGIN_NAME).exe": src\main.obj 33 | link.exe /subsystem:console /out:$@ $** 34 | 35 | "aud_$(PLUGIN_NAME).dll": plugins\audaciousplugin.obj 36 | link.exe /dll user32.lib /out:$@ $** 37 | 38 | "in_$(PLUGIN_NAME).dll": plugins\winampplugin.obj 39 | link.exe /dll user32.lib /out:$@ $** 40 | 41 | "foo_input_$(PLUGIN_NAME).dll": plugins\foobarplugin.obj plugins\foobar2000\SDK\input.obj plugins\foobar2000\SDK\guids.obj \ 42 | plugins\foobar2000\SDK\file_info.obj plugins\foobar2000\SDK\file_info_impl.obj plugins\foobar2000\SDK\replaygain_info.obj \ 43 | plugins\foobar2000\SDK\console.obj plugins\foobar2000\SDK\service.obj plugins\foobar2000\SDK\cfg_var.obj \ 44 | plugins\foobar2000\SDK\abort_callback.obj plugins\foobar2000\SDK\audio_chunk.obj plugins\foobar2000\SDK\audio_chunk_channel_config.obj \ 45 | plugins\foobar2000\SDK\filesystem.obj plugins\foobar2000\SDK\filesystem_helper.obj plugins\foobar2000\SDK\componentversion.obj \ 46 | plugins\foobar2000\SDK\main_thread_callback.obj plugins\foobar2000\SDK\utility.obj plugins\foobar2000\SDK\playable_location.obj \ 47 | plugins\pfc\string_base.obj plugins\pfc\other.obj plugins\pfc\string_conv.obj plugins\pfc\bit_array.obj \ 48 | plugins\pfc\utf8.obj plugins\pfc\sort.obj plugins\pfc\win-objects.obj plugins\pfc\guid.obj plugins\pfc\audio_math.obj \ 49 | plugins\pfc\timers.obj plugins\pfc\audio_sample.obj plugins\pfc\pathUtils.obj plugins\pfc\stringNew.obj \ 50 | plugins\foobar2000\foobar2000_component_client\component_client.obj 51 | link.exe /dll user32.lib ole32.lib shell32.lib plugins\foobar2000\shared\shared.lib /out:$@ $** 52 | 53 | clean: FORCE 54 | -cmd /c for /r %%f in (*.obj,*.dll,*.exp,*.exe) do del /q %%f 55 | -del /q depends.tmp 56 | 57 | FORCE: 58 | -------------------------------------------------------------------------------- /plugins/Makefile: -------------------------------------------------------------------------------- 1 | ROOTPATH := ../ 2 | include ../config.mak 3 | 4 | OBJS_R = $(filter-out ../$(BUILDPATH)/gui/% ../$(BUILDPATH)/gui_d/% %_d.o ../$(BUILDPATH)/main.o,$(wildcard ../$(BUILDPATH)/*.o ../$(BUILDPATH)/*/*.o)) 5 | OBJS_D = $(filter-out ../$(BUILDPATH)/gui/% ../$(BUILDPATH)/gui_d/% ../$(BUILDPATH)/main_d.o,$(wildcard ../$(BUILDPATH)/*_d.o ../$(BUILDPATH)/*/*_d.o)) 6 | 7 | ../aud_$(PLUGIN_NAME).$(DLL): $(OBJS_R) ../libclef/$(BUILDPATH)/libclef.a $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp Makefile 8 | $(CXX) -shared -o $@ $(CXXFLAGS_R) -DBUILD_AUDACIOUS clefplugin.cpp $(shell pkg-config --cflags --libs audacious) $(OBJS_R) $(LDFLAGS_R) 9 | strip $@ 10 | 11 | ../aud_$(PLUGIN_NAME)_d.$(DLL): $(OBJS_D) ../libclef/$(BUILDPATH)/libclef_d.$(DLL) $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp Makefile 12 | $(CXX) -shared -o $@ $(CXXFLAGS_D) -DBUILD_AUDACIOUS clefplugin.cpp $(shell pkg-config --cflags --libs audacious) $(OBJS_D) $(LDFLAGS_D) 13 | 14 | ../in_$(PLUGIN_NAME).$(DLL): $(OBJS_R) ../libclef/$(BUILDPATH)/libclef.a $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp Makefile 15 | $(CXX) -shared -o $@ $(CXXFLAGS_R) -DBUILD_WINAMP clefplugin.cpp $(OBJS_R) $(LDFLAGS_R) 16 | strip $@ 17 | 18 | ../in_$(PLUGIN_NAME)_d.$(DLL): $(OBJS_D) ../libclef/$(BUILDPATH)/libclef_d.$(DLL) $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp Makefile 19 | $(CXX) -shared -o $@ $(CXXFLAGS_D) -DBUILD_WINAMP clefplugin.cpp $(OBJS_D) $(LDFLAGS_D) 20 | 21 | ../$(PLUGIN_NAME).clap: $(OBJS_R) ../libclef/$(BUILDPATH)/libclef.a $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp ../libclef/src/plugin/clapplugin.cpp Makefile 22 | $(CXX) -shared -o $@ $(CXXFLAGS_R) -DBUILD_CLAP -Iclap/include clefplugin.cpp ../libclef/src/plugin/clapplugin.cpp $(OBJS_R) $(LDFLAGS_R) 23 | 24 | ../$(PLUGIN_NAME)_d.clap: $(OBJS_D) ../libclef/$(BUILDPATH)/libclef_d.a $(wildcard ../libclef/src/plugins/*.h) clefplugin.cpp ../libclef/src/plugin/clapplugin.cpp Makefile 25 | $(CXX) -shared -o $@ $(CXXFLAGS_D) -DBUILD_CLAP -Iclap/include clefplugin.cpp ../libclef/src/plugin/clapplugin.cpp $(OBJS_D) $(LDFLAGS_D) 26 | 27 | FORCE: 28 | -------------------------------------------------------------------------------- /plugins/clefplugin.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin/baseplugin.h" 2 | #include "codec/sampledata.h" 3 | #include "romfile.h" 4 | #include "songtable.h" 5 | #include "songdata.h" 6 | #include 7 | #include 8 | #include 9 | 10 | #ifdef BUILD_CLAP 11 | #include "plugin/clapplugin.h" 12 | #endif 13 | 14 | // In the functions below, openFile() is provided by the plugin interface. Use this 15 | // instead of standard library functions to open additional files in order to use 16 | // the host's virtual filesystem. 17 | 18 | static SynthContext* openBySubsong(ClefContext* ctx, std::unique_ptr& rom, std::unique_ptr& songData, const std::string& filename, std::istream& file) 19 | { 20 | SynthContext* synth = nullptr; 21 | try { 22 | synth = new SynthContext(ctx, 32768); 23 | size_t qpos = filename.rfind('?'); 24 | std::string baseFile = filename.substr(0, qpos); 25 | bool alreadyLoaded = rom && rom->filename == baseFile; 26 | if (!alreadyLoaded) { 27 | rom.reset(new ROMFile(ctx)); 28 | } 29 | if (file) { 30 | rom->load(synth, file, baseFile); 31 | } else { 32 | auto newFile(ctx->openFile(baseFile)); 33 | rom->load(synth, *newFile, baseFile); 34 | } 35 | 36 | if (ctx->isDawPlugin) { 37 | SongTable st = rom->findSongTable(-1); 38 | int numSongs = st.songs.size(); 39 | for (int index = 0; index < numSongs; index++) { 40 | // initialize instruments 41 | try { 42 | st.songFromTable(index); 43 | } catch (...) { 44 | // ignore 45 | } 46 | } 47 | } else { 48 | std::string subsong; 49 | if (qpos != std::string::npos) { 50 | subsong = filename.substr(qpos + 1); 51 | } 52 | if (subsong.substr(0, 2) == "0x") { 53 | uint32_t addr = std::stoi(subsong, nullptr, 0); 54 | songData.reset(new SongData(rom.get(), addr)); 55 | } else { 56 | SongTable st = rom->findSongTable(-1); 57 | int index = std::stoi(subsong.empty() ? "0" : subsong); 58 | do { 59 | try { 60 | songData.reset(st.songFromTable(index)); 61 | break; 62 | } catch (std::exception& e) { 63 | ++index; 64 | if (index >= st.songs.size()) { 65 | throw; 66 | } 67 | } 68 | } while (!songData); 69 | } 70 | 71 | for (int i = 0; i < songData->numTracks(); i++) { 72 | synth->addChannel(songData->getTrack(i)); 73 | } 74 | } 75 | 76 | return synth; 77 | } catch (std::exception& e) { 78 | std::cerr << "error in openBySubsong " << e.what() << std::endl; 79 | delete synth; 80 | throw; 81 | } 82 | } 83 | 84 | static std::map durationCache; 85 | static std::map> subsongCache; 86 | 87 | struct ClefPluginInfo { 88 | CLEF_PLUGIN_STATIC_FIELDS 89 | #ifdef BUILD_CLAP 90 | using ClapPlugin = ClefClapPlugin; 91 | #endif 92 | 93 | std::unique_ptr rom; 94 | std::unique_ptr songData; 95 | 96 | static bool isPlayable(ClefContext*, const std::string&, std::istream&) { 97 | // Implementations should check to see if the file is supported. 98 | // Return false or throw an exception to report failure. 99 | return true; 100 | } 101 | 102 | static int sampleRate(ClefContext* ctx, const std::string& filename, std::istream& file) { 103 | // Implementations should return the sample rate of the file. 104 | // This can be hard-coded if the plugin always uses the same sample rate. 105 | return 32768; 106 | } 107 | 108 | static double length(ClefContext* ctx, const std::string& filename, std::istream& file) { 109 | // Implementations should return the length of the file in seconds. 110 | auto iter = durationCache.find(filename); 111 | if (iter != durationCache.end()) { 112 | return iter->second; 113 | } 114 | size_t qpos = filename.rfind('?'); 115 | std::string base = filename.substr(0, qpos); 116 | std::unique_ptr lengthRom(new ROMFile(ctx)); 117 | lengthRom->load(nullptr, file, base); 118 | SongTable st = lengthRom->findAllSongs(); 119 | std::vector subsongs; 120 | bool first = true; 121 | for (uint32_t song : st.songs) { 122 | std::ostringstream ss; 123 | ss << base << "?0x" << std::hex << std::setw(6) << std::setfill('0') << song; 124 | std::string subsong = ss.str(); 125 | 126 | std::unique_ptr lengthSong; 127 | double length = 0; 128 | try { 129 | std::unique_ptr synth(openBySubsong(ctx, lengthRom, lengthSong, subsong, file)); 130 | if (synth) { 131 | length = synth->maximumTime(); 132 | subsongs.push_back(subsong); 133 | } 134 | } catch (...) { 135 | // ignore 136 | } 137 | durationCache[subsong] = length; 138 | if (first && filename != subsong) { 139 | durationCache[filename] = length; 140 | } 141 | } 142 | subsongCache[base] = subsongs; 143 | return durationCache[filename]; 144 | } 145 | 146 | static TagMap readTags(ClefContext* ctx, const std::string& filename, std::istream& file) { 147 | // Implementations should read the tags from the file. 148 | // If the file format does not support embedded tags, consider 149 | // inheriting from TagsM3UMixin and removing this function. 150 | return TagMap(); 151 | } 152 | 153 | static std::vector getSubsongs(ClefContext* clef, const std::string& filename, std::istream& file) 154 | { 155 | std::string base = filename.substr(0, filename.rfind('?')); 156 | 157 | auto iter = subsongCache.find(base); 158 | if (iter != subsongCache.end()) { 159 | return iter->second; 160 | } 161 | subsongCache[base] = std::vector(); 162 | length(clef, base, file); 163 | return subsongCache.at(base); 164 | } 165 | 166 | SynthContext* prepare(ClefContext* ctx, const std::string& filename, std::istream& file) { 167 | // Prepare to play the file. Load any necessary data into memory and store any 168 | // applicable state in members on this plugin object. 169 | 170 | // Be sure to call this to clear the sample cache: 171 | ctx->purgeSamples(); 172 | rom.reset(nullptr); 173 | 174 | return openBySubsong(ctx, rom, songData, filename, file); 175 | } 176 | 177 | void release() { 178 | // Release any retained state allocated in prepare(). 179 | songData.reset(nullptr); 180 | rom.reset(nullptr); 181 | } 182 | }; 183 | 184 | const std::string ClefPluginInfo::version = "0.0.1"; 185 | const std::string ClefPluginInfo::pluginName = "mp2k-clef"; 186 | const std::string ClefPluginInfo::pluginShortName = "mp2k-clef"; 187 | const std::string ClefPluginInfo::author = "Adam Higerd"; 188 | const std::string ClefPluginInfo::url = "https://bitbucket.org/ahigerd/mp2k-clef"; 189 | ConstPairList ClefPluginInfo::extensions = { { "gba", "GBA ROM images (*.gba)" } }; 190 | const std::string ClefPluginInfo::about = 191 | "mp2k-clef copyright (C) 2020-2023 Adam Higerd\n" 192 | "Distributed under the MIT license."; 193 | 194 | CLEF_PLUGIN(ClefPluginInfo); 195 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | ROOTPATH := ../ 2 | include ../config.mak 3 | 4 | SOURCES = $(filter-out main.cpp,$(wildcard *.cpp */*.cpp)) 5 | OBJS_R = $(patsubst %.cpp, ../$(BUILDPATH)/%.o, $(SOURCES)) 6 | OBJS_D = $(patsubst %.cpp, ../$(BUILDPATH)/%_d.o, $(SOURCES)) 7 | MAIN_R = ../$(BUILDPATH)/main.o 8 | MAIN_D = ../$(BUILDPATH)/main_d.o 9 | 10 | ../$(PLUGIN_NAME)$(EXE): ../libclef/$(BUILDPATH)/libclef.a $(OBJS_R) $(MAIN_R) 11 | $(CXX) -o $@ $(MAIN_R) $(OBJS_R) $(LDFLAGS_R) 12 | strip $@ 13 | 14 | ../$(PLUGIN_NAME)_d$(EXE): ../libclef/$(BUILDPATH)/libclef_d.a $(OBJS_D) $(MAIN_D) 15 | $(CXX) -o $@ $(MAIN_D) $(OBJS_D) $(LDFLAGS_D) 16 | 17 | ../$(BUILDPATH)/lib$(PLUGIN_NAME).a: ../libclef/$(BUILDPATH)/libclef.a $(filter-out ../$(BUILDPATH)/main.o,$(OBJS_R)) 18 | rm -f $@ 19 | gcc-ar -rc $@ $(OBJS_R) 20 | 21 | ../$(BUILDPATH)/lib$(PLUGIN_NAME)_d.a: ../libclef/$(BUILDPATH)/libclef_d.a $(filter-out ../$(BUILDPATH)/main_d.o,$(OBJS_R)) 22 | rm -f $@ 23 | gcc-ar -rc $@ $(OBJS_D) 24 | 25 | ../$(BUILDPATH)/%.o: %.cpp Makefile ../config.mak 26 | @mkdir -p $(dir $@) 27 | $(CXX) $(CXXFLAGS_R) -c -o $@ $< 28 | 29 | ../$(BUILDPATH)/%_d.o: %.cpp Makefile ../config.mak 30 | @mkdir -p $(dir $@) 31 | $(CXX) $(CXXFLAGS_D) -c -o $@ $< 32 | 33 | ../$(BUILDPATH)/Makefile.d: main.cpp $(SOURCES) $(wildcard *.h */*.h) Makefile ../config.mak 34 | @mkdir -p $(dir $@) 35 | $(CXX) $(CXXFLAGS) -MT ../$(BUILDPATH)/main.o -MM -MF - main.cpp > $@ 36 | $(CXX) $(CXXFLAGS) -MT ../$(BUILDPATH)/main_d.o -MM -MF - main.cpp >> $@ 37 | $(foreach src, $(SOURCES), $(CXX) $(CXXFLAGS) -MT $(patsubst %.cpp, ../$(BUILDPATH)/%.o, $(src)) -MM -MF - $(src) >> $@;) 38 | $(foreach src, $(SOURCES), $(CXX) $(CXXFLAGS) -MT $(patsubst %.cpp, ../$(BUILDPATH)/%_d.o, $(src)) -MM -MF - $(src) >> $@;) 39 | 40 | include ../$(BUILDPATH)/Makefile.d 41 | 42 | FORCE: 43 | -------------------------------------------------------------------------------- /src/instrumentdata.cpp: -------------------------------------------------------------------------------- 1 | #include "instrumentdata.h" 2 | #include "romfile.h" 3 | #include "utility.h" 4 | #include "clefcontext.h" 5 | #include "codec/sampledata.h" 6 | #include "codec/pcmcodec.h" 7 | #include "seq/sequenceevent.h" 8 | #include "synth/synthcontext.h" 9 | #include "synth/oscillator.h" 10 | #include "synth/sampler.h" 11 | #include "riffwriter.h" 12 | #include 13 | #include 14 | 15 | /* 16 | class SweepNode : public AudioNode 17 | { 18 | public: 19 | SweepNode(const SynthContext* ctx, AudioNode* source, uint8_t sweep) 20 | : AudioNode(ctx), source(source), lastTime(0), active(true) 21 | { 22 | freq = source->param(BaseOscillator::Frequency).get(); 23 | if (!freq) { 24 | active = false; 25 | source = nullptr; 26 | return; 27 | } 28 | sweepShift = sweep & 0x7; 29 | sweepNegate = sweep & 0x8; 30 | sweepPeriod = (sweep >> 4) & 0x7; 31 | if (!sweepShift || !sweepPeriod) { 32 | sweepShift = 0; 33 | sweepPeriod = 0; 34 | } 35 | } 36 | 37 | bool isActive() const { 38 | return active; 39 | } 40 | 41 | protected: 42 | int16_t generateSample(double time, int channel) { 43 | int16_t sample = source->getSample(time, channel); 44 | if (!sweepShift) { 45 | return sample; 46 | } 47 | double delta = (time - lastTime) * 128.0; 48 | if (delta >= sweepPeriod) { 49 | int16_t timer = 4194304.0 / freq->valueAt(time); 50 | int16_t adj = timer >> sweepShift; 51 | timer = sweepNegate ? timer - adj : timer + adj; 52 | if (timer <= 0) { 53 | sweepShift = 0; 54 | } else if (timer >= 2048) { 55 | active = false; 56 | } else { 57 | freq->setConstant(4194304.0 / timer); 58 | } 59 | delta -= sweepPeriod; 60 | lastTime = time - delta; 61 | } 62 | return sample; 63 | } 64 | 65 | AudioNode* source; 66 | AudioParam* freq; 67 | 68 | double lastTime; 69 | uint8_t sweepPeriod; 70 | uint8_t sweepShift; 71 | bool sweepNegate : 1; 72 | bool active : 1; 73 | bool squelch : 1; 74 | }; 75 | */ 76 | 77 | MpInstrument* MpInstrument::load(const ROMFile* rom, uint32_t addr, bool isSplit) 78 | { 79 | if (addr == 0x80808080) { 80 | return nullptr; 81 | } 82 | uint8_t type = rom->read(addr); 83 | if (type == Square1) { 84 | if (rom->read(addr) == 0x0000000200003c01ULL && rom->read(addr + 8) == 0x000f0000) { 85 | // unused instrument 86 | return nullptr; 87 | } 88 | } 89 | 90 | uint8_t normType = type; 91 | if (normType < 0x10) { 92 | normType &= 0x7; 93 | } 94 | try { 95 | switch (normType) { 96 | case GBSample: 97 | case Sample: 98 | case FixedSample: 99 | return new SampleInstrument(rom, addr); 100 | case Square1: 101 | case Square2: 102 | case Noise: 103 | return new PSGInstrument(rom, addr); 104 | case KeySplit: 105 | case Percussion: 106 | if (!isSplit) { 107 | return new SplitInstrument(rom, addr); 108 | } 109 | default: 110 | //std::cerr << "0x" << std::hex << addr << ": Unknown instrument type " << std::dec << (int)type << std::endl; 111 | return nullptr; 112 | } 113 | } catch (ROMFile::BadAccess& e) { 114 | //std::cerr << "Bad pointer loading " << int(type) << " instrument at 0x" << std::hex << addr << std::dec << std::endl; 115 | return nullptr; 116 | } catch (std::exception& e) { 117 | //std::cerr << "Error loading " << int(type) << " instrument at 0x" << std::hex << addr << std::dec << ": " << e.what() << std::endl; 118 | return nullptr; 119 | } 120 | } 121 | 122 | MpInstrument::MpInstrument(const ROMFile* rom, uint32_t addr) 123 | : rom(rom), addr(addr), type(Type(rom->read(addr))), forcePan(false), pan(0), gate(0) 124 | { 125 | if (type & 0x7) { 126 | type = Type(type & 0x7); 127 | attack = (rom->read(addr + 8) & 0x7) / 7.0; 128 | decay = rom->read(addr + 9) / 60.0; 129 | sustain = rom->read(addr + 10) / 15.0; 130 | release = rom->read(addr + 11) / 60.0; 131 | gate = rom->read(addr + 2) / 255.0; 132 | } else if (type == 0 || type == 8) { 133 | attack = (255 - rom->read(addr + 8)) / 60.0; 134 | decay = rom->read(addr + 9) / 256.0; 135 | sustain = rom->read(addr + 10) / 255.0; 136 | release = rom->read(addr + 11) / 256.0; 137 | } 138 | } 139 | 140 | Channel::Note* MpInstrument::addEnvelope(Channel* channel, Channel::Note* note, double factor) const 141 | { 142 | double startGain = 1.0; 143 | double eAttack = 1.0; 144 | if (attack) { 145 | startGain = 1.0 - (60 * attack) / 255; 146 | eAttack = attack * factor; 147 | } 148 | 149 | bool expDecay = (type & 0x7) == 0; 150 | double eDecay = decay * factor; 151 | double eRelease = release * factor; 152 | if (expDecay) { 153 | // fitted using gradient descent 154 | // TODO: use DiscreteEnvelope 155 | static const double COEF = 64.9707; 156 | static const double ADJ = 1.4875; 157 | eDecay = eDecay ? std::log(eDecay) * COEF - ADJ : 0; 158 | eRelease = eRelease ? std::log(eRelease) * COEF - ADJ : 0; 159 | } 160 | 161 | Envelope* env = new Envelope(channel->ctx, eAttack, 0, eDecay, sustain, 0, eRelease); 162 | env->expAttack = false; 163 | env->expDecay = expDecay; 164 | env->param(Envelope::StartGain)->setConstant(startGain); 165 | env->connect(note->source); 166 | note->source.reset(env); 167 | return note; 168 | } 169 | 170 | bool MpInstrument::operator==(const MpInstrument* other) const 171 | { 172 | if (!other) { 173 | return false; 174 | } 175 | if (other == this) { 176 | // reflexive identity 177 | return true; 178 | } 179 | if (other->type != type || other->attack != attack || other->decay != decay || 180 | other->sustain != sustain || other->release != release || other->gate != gate) { 181 | return false; 182 | } 183 | if (type == Sample || type == GBSample || type == FixedSample) { 184 | const SampleInstrument* s1 = static_cast(this); 185 | const SampleInstrument* s2 = static_cast(other); 186 | return s1->sample == s2->sample; 187 | } else if (type == Square1 || type == Square2 || type == Noise) { 188 | const PSGInstrument* s1 = static_cast(this); 189 | const PSGInstrument* s2 = static_cast(other); 190 | return s1->mode == s2->mode && s1->sweep == s2->sweep; 191 | } else if (type == KeySplit || type == Percussion) { 192 | const SplitInstrument* s1 = static_cast(this); 193 | const SplitInstrument* s2 = static_cast(other); 194 | return s1->splits == s2->splits; 195 | } 196 | return false; 197 | } 198 | 199 | SampleInstrument::SampleInstrument(const ROMFile* rom, uint32_t addr) 200 | : MpInstrument(rom, addr) 201 | { 202 | uint32_t sampleAddr = rom->readPointer(addr + 4); 203 | uint32_t sampleStart = sampleAddr; 204 | uint64_t sampleID = (uint64_t(type) << 32) | sampleAddr; 205 | sample = rom->context()->getSample(sampleID); 206 | if (!sample) { 207 | int sampleBits = 4; 208 | int sampleLen = 16; 209 | int loopStart = 0; 210 | int loopEnd = 32; 211 | double sampleRate = rom->sampleRate; 212 | if (type == GBSample) { 213 | sampleRate = 4186.0; 214 | } else { 215 | pan = rom->read(addr + 3) ^ 0x80; 216 | forcePan = !(pan & 0x80); 217 | if (pan == 127) { 218 | // adjust full right panning to make centering easier 219 | pan = 128; 220 | } 221 | sampleBits = 8; 222 | sampleLen = rom->read(sampleAddr + 12); 223 | if (rom->read(sampleAddr + 2)) { 224 | loopStart = rom->read(sampleAddr + 8); 225 | loopEnd = sampleLen; 226 | } else { 227 | loopEnd = 0; 228 | } 229 | if (type == Sample) { 230 | sampleRate = rom->read(sampleAddr + 4) / 1024.0; 231 | } 232 | sampleStart = sampleAddr + 16; 233 | } 234 | if (sampleStart + sampleLen > rom->rom.size()) { 235 | throw ROMFile::BadAccess(sampleStart + sampleLen); 236 | } 237 | sample = PcmCodec(rom->context(), type == GBSample ? 4 : 8).decodeRange(rom->rom.begin() + sampleStart, rom->rom.begin() + sampleStart + sampleLen, sampleID); 238 | sample->sampleRate = sampleRate; 239 | sample->loopStart = loopStart; 240 | sample->loopEnd = loopEnd; 241 | 242 | /* 243 | std::ostringstream fnss; 244 | fnss << "dump/sample-" << std::hex << (sampleAddr) << ".wav"; 245 | RiffWriter dump(sampleRate, false); 246 | dump.open(fnss.str()); 247 | dump.write(sample->channels[0]); 248 | dump.close(); 249 | */ 250 | } 251 | } 252 | 253 | std::string SampleInstrument::displayName() const 254 | { 255 | std::ostringstream ss; 256 | if (type == GBSample) { 257 | ss << "Waveform"; 258 | } else { 259 | ss << "Sample"; 260 | } 261 | uint32_t sampleAddr = (sample->sampleID & 0xFFFFFFFF); 262 | ss << " (0x" << std::hex << sampleAddr << ")"; 263 | return ss.str(); 264 | } 265 | 266 | BaseNoteEvent* SampleInstrument::makeEvent(double, uint8_t key, uint8_t vel, double len) const 267 | { 268 | if (key & 0x80) return nullptr; 269 | InstrumentNoteEvent* event = new InstrumentNoteEvent; 270 | event->duration = len; 271 | event->pitch = key; 272 | // TODO: velocity accuracy 273 | event->volume = (vel / 127.0); 274 | return event; 275 | } 276 | 277 | Channel::Note* SampleInstrument::noteEvent(Channel* channel, std::shared_ptr event) 278 | { 279 | static const double cFreq = 261.6256; 280 | double ratio = 1.0; 281 | if (type != FixedSample) { 282 | double freq = noteToFreq(int8_t(static_cast(event.get())->pitch)); 283 | ratio = freq / cFreq; 284 | } 285 | std::shared_ptr node(new Sampler(channel->ctx, sample, ratio)); 286 | node->param(AudioNode::Gain)->setConstant(event->volume); 287 | node->param(AudioNode::Pan)->setConstant(event->pan); 288 | double duration = event->duration; 289 | if (!duration) { 290 | duration = sample->duration(); 291 | } 292 | Channel::Note* note = channel->allocNote(event, node, duration); 293 | return addEnvelope(channel, note, 1.0); 294 | } 295 | 296 | PSGInstrument::PSGInstrument(const ROMFile* rom, uint32_t addr) 297 | : MpInstrument(rom, addr), sweep(0) 298 | { 299 | if (type == Square1) { 300 | sweep = rom->read(addr + 3); 301 | } 302 | mode = rom->read(addr + 4); 303 | bool err = rom->read(addr + 5) || rom->read(addr + 6) || rom->read(addr + 7); 304 | if (type == Noise) { 305 | err = err || mode > 1; 306 | } else { 307 | err = err || mode > 3; 308 | } 309 | if (err) { 310 | throw std::runtime_error("invalid PSG instrument type " + std::to_string(mode)); 311 | } 312 | } 313 | 314 | std::string PSGInstrument::displayName() const 315 | { 316 | std::ostringstream ss; 317 | if (type == Square1 || type == Square2) { 318 | ss << "Square "; 319 | if (mode == 0) { 320 | ss << "(12.5%)"; 321 | } else if (mode == 1) { 322 | ss << "(25%)"; 323 | } else if (mode == 2) { 324 | ss << "(50%)"; 325 | } else if (mode == 3) { 326 | ss << "(75%)"; 327 | } 328 | } else if (type == Noise) { 329 | ss << "Noise (type " << int(mode) << ")"; 330 | } else { 331 | ss << "PSG (type " << int(type) << ")"; 332 | } 333 | return ss.str(); 334 | } 335 | 336 | BaseNoteEvent* PSGInstrument::makeEvent(double volume, uint8_t key, uint8_t vel, double len) const 337 | { 338 | InstrumentNoteEvent* event = new InstrumentNoteEvent; 339 | event->duration = (gate && len > gate) ? gate : len; 340 | event->pitch = key; 341 | event->volume = (vel / 127.0); 342 | return event; 343 | } 344 | 345 | Channel::Note* PSGInstrument::noteEvent(Channel* channel, std::shared_ptr event) 346 | { 347 | BaseOscillator::WaveformPreset waveformID = BaseOscillator::Square50; 348 | if (type == Noise) { 349 | if (mode & 1) { 350 | waveformID = BaseOscillator::GBNoise127; 351 | } else { 352 | waveformID = BaseOscillator::GBNoise; 353 | } 354 | } else if (mode == 0) { 355 | waveformID = BaseOscillator::Square125; 356 | } else if (mode == 1) { 357 | waveformID = BaseOscillator::Square25; 358 | /* 359 | } else if (mode == 2) { 360 | waveformID = BaseOscillator::Square50; 361 | */ 362 | } else if (mode == 3) { 363 | waveformID = BaseOscillator::Square75; 364 | } 365 | double pitch = static_cast(event.get())->pitch; 366 | double freq; 367 | if (type == Noise) { 368 | constexpr double ln_8 = std::log(8.0); 369 | constexpr double ln_2 = std::log(2.0); 370 | if (pitch < 76) { 371 | freq = 4096 * fastExp((pitch - 60) / 12, ln_8); 372 | } else if (pitch < 78) { 373 | freq = 65536 * fastExp((pitch - 76) / 2, ln_2); 374 | } else if (pitch < 80) { 375 | freq = 131072 * fastExp(pitch - 78, ln_2); 376 | } else { 377 | freq = 524288; 378 | } 379 | if (freq < 4.5714) { 380 | freq = 4.5714; 381 | } 382 | // TODO: Why is the *8 necessary to pull this into the right range? 383 | freq = 8 * 262144.0 / freq; 384 | } else { 385 | freq = noteToFreq(pitch); 386 | } 387 | // TODO: velocity accuracy 388 | double volume = event->volume * 0.3; 389 | std::shared_ptr node(BaseOscillator::create( 390 | channel->ctx, 391 | waveformID, 392 | freq, 393 | volume, 394 | event->pan 395 | )); 396 | Channel::Note* note = channel->allocNote(event, node, event->duration); 397 | /* 398 | if (sweep) { 399 | node = new SweepNode(ctx, node, sweep); 400 | } 401 | */ 402 | return addEnvelope(channel, note, volume); 403 | } 404 | 405 | SplitInstrument::SplitInstrument(const ROMFile* rom, uint32_t addr) 406 | : MpInstrument(rom, addr) 407 | { 408 | attack = -1; 409 | uint32_t splitAddr = rom->readPointer(rom->baseAddr | (addr + 4)); 410 | if (type == Percussion) { 411 | for (int i = 0; i < 128; i++) { 412 | splits.emplace_back(load(rom, splitAddr + 12 * i, true)); 413 | } 414 | } else { 415 | uint32_t tableAddr = rom->readPointer(rom->baseAddr | (addr + 8)); 416 | for (int i = 0; i < 128; i++) { 417 | int n = rom->read(tableAddr + i); 418 | splits.emplace_back(load(rom, splitAddr + 12 * n, true)); 419 | } 420 | } 421 | } 422 | 423 | std::string SplitInstrument::displayName() const 424 | { 425 | std::ostringstream ss; 426 | if (type == KeySplit) { 427 | ss << "Split"; 428 | } else { 429 | ss << "Percussion"; 430 | } 431 | ss << " (0x" << std::hex << addr << ")"; 432 | return ss.str(); 433 | } 434 | 435 | BaseNoteEvent* SplitInstrument::makeEvent(double volume, uint8_t key, uint8_t vel, double len) const 436 | { 437 | auto split = splits.at(key).get(); 438 | if (!split) { 439 | return nullptr; 440 | } 441 | if (type == Percussion) { 442 | uint8_t baseKey = (*rom)[addr + 1]; 443 | if (baseKey > 0) { 444 | key = baseKey; 445 | } 446 | } 447 | BaseNoteEvent* event = split->makeEvent(volume, key, vel, len); 448 | if (split->forcePan && split->pan != 64) { 449 | event->pan = split->pan; 450 | } 451 | return event; 452 | } 453 | 454 | Channel::Note* SplitInstrument::noteEvent(Channel* channel, std::shared_ptr event) 455 | { 456 | auto split = splits.at(size_t(static_cast(event.get())->pitch)); 457 | if (!split) { 458 | return nullptr; 459 | } 460 | return split->noteEvent(channel, event); 461 | } 462 | 463 | InstrumentData::InstrumentData(const ROMFile* rom, uint32_t addr) 464 | { 465 | for (int i = 0; i < 128; i++) { 466 | instruments[i] = 0; 467 | } 468 | SynthContext* synth = rom->synthContext(); 469 | for (int instId = 0; addr < rom->rom.size() && instId < 128; addr += 12, instId++) { 470 | MpInstrument* inst = synth ? static_cast(synth->getInstrument(addr)) : nullptr; 471 | if (inst) { 472 | instruments[instId] = addr; 473 | continue; 474 | } 475 | inst = MpInstrument::load(rom, addr); 476 | if (!inst) { 477 | //std::cout << instId << ": unknown/bad instrument @ 0x" << std::hex << addr << std::endl; 478 | instruments[instId] = 0; 479 | continue; 480 | } 481 | //std::cerr << instId << ": Loaded 0x" << std::hex << addr << " of type " << std::dec << inst->type << std::endl; 482 | MpInstrument* dupe = findDupe(synth, *inst); 483 | if (dupe) { 484 | instruments[instId] = dupe->addr; 485 | delete inst; 486 | } else if (synth) { 487 | instruments[instId] = addr; 488 | synth->registerInstrument(addr, std::unique_ptr(inst)); 489 | } else { 490 | instruments[instId] = instId; 491 | } 492 | } 493 | } 494 | 495 | MpInstrument* InstrumentData::findDupe(SynthContext* synth, const MpInstrument& inst) const 496 | { 497 | if (!synth) { 498 | return nullptr; 499 | } 500 | int numInsts = synth->numInstruments(); 501 | for (int i = 0; i < numInsts; i++) { 502 | uint64_t otherID = synth->instrumentID(i); 503 | MpInstrument* other = static_cast(synth->getInstrument(otherID)); 504 | if (inst == other) { 505 | return other; 506 | } 507 | } 508 | return nullptr; 509 | } 510 | 511 | void MpInstrument::showParsed(std::ostream& out, std::string indent) const 512 | { 513 | out << indent << displayName() << ":" << std::endl; 514 | out << indent << " Base address: 0x" << std::hex << addr << std::dec << std::endl; 515 | if (attack >= 0) { 516 | out << indent << " A=" << int(rom->read(addr + 8)) << " (" << attack << ")" << std::endl; 517 | out << indent << " D=" << int(rom->read(addr + 9)) << " (" << decay << ")" << std::endl; 518 | out << indent << " S=" << int(rom->read(addr + 10)) << " (" << sustain << ")" << std::endl; 519 | out << indent << " R=" << int(rom->read(addr + 11)) << " (" << release << ")" << std::endl; 520 | } 521 | } 522 | 523 | void SplitInstrument::showParsed(std::ostream& out, std::string) const 524 | { 525 | out << displayName() << ":" << std::endl; 526 | out << " Base address: 0x" << std::hex << addr << std::dec << std::endl; 527 | uint32_t splitAddr = rom->readPointer(rom->baseAddr | (addr + 4)); 528 | out << " Instruments: 0x" << std::hex << splitAddr << std::dec << std::endl; 529 | if (type == KeySplit) { 530 | uint32_t tableAddr = rom->readPointer(rom->baseAddr | (addr + 8)); 531 | out << " Split table: 0x" << std::hex << tableAddr << std::dec << std::endl; 532 | } 533 | int note = 0; 534 | for (const auto& split : splits) { 535 | if (split.get()) { 536 | out << " " << note << ": " << split->displayName() << std::endl; 537 | } 538 | note++; 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /src/instrumentdata.h: -------------------------------------------------------------------------------- 1 | #ifndef GBAMP2WAV_INSTRUMENTDATA_H 2 | #define GBAMP2WAV_INSTRUMENTDATA_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "synth/iinstrument.h" 9 | class ROMFile; 10 | class SampleData; 11 | struct BaseNoteEvent; 12 | class SynthContext; 13 | 14 | class MpInstrument: public IInstrument { 15 | public: 16 | static MpInstrument* load(const ROMFile* rom, uint32_t addr, bool isSplit = false); 17 | MpInstrument(const ROMFile* rom, uint32_t addr); 18 | virtual ~MpInstrument() {} 19 | 20 | const ROMFile* rom; 21 | uint32_t addr; 22 | 23 | enum Type { 24 | Sample = 0, 25 | Square1 = 1, 26 | Square2 = 2, 27 | GBSample = 3, 28 | Noise = 4, 29 | FixedSample = 8, 30 | KeySplit = 0x40, 31 | Percussion = 0x80, 32 | }; 33 | Type type; 34 | 35 | double attack, decay, sustain, release; 36 | bool forcePan; 37 | uint8_t pan; 38 | double gate; 39 | virtual BaseNoteEvent* makeEvent(double volume, uint8_t key, uint8_t vel, double len) const = 0; 40 | virtual Channel::Note* noteEvent(Channel* channel, std::shared_ptr event) = 0; 41 | 42 | inline bool operator==(const MpInstrument& other) const { return *this == &other; } 43 | bool operator==(const MpInstrument* other) const; 44 | 45 | virtual void showParsed(std::ostream& out, std::string indent = std::string()) const; 46 | 47 | protected: 48 | Channel::Note* addEnvelope(Channel* channel, Channel::Note* event, double factor) const; 49 | }; 50 | 51 | class SampleInstrument : public MpInstrument { 52 | public: 53 | SampleInstrument(const ROMFile* rom, uint32_t addr); 54 | 55 | SampleData* sample; 56 | 57 | virtual BaseNoteEvent* makeEvent(double volume, uint8_t key, uint8_t vel, double len) const; 58 | virtual Channel::Note* noteEvent(Channel* channel, std::shared_ptr event); 59 | virtual std::string displayName() const; 60 | 61 | //virtual void showParsed(std::ostream& out, std::string indent = std::string()) const; 62 | }; 63 | 64 | class PSGInstrument : public MpInstrument { 65 | public: 66 | PSGInstrument(const ROMFile* rom, uint32_t addr); 67 | 68 | uint8_t mode, sweep; 69 | 70 | virtual BaseNoteEvent* makeEvent(double volume, uint8_t key, uint8_t vel, double len) const; 71 | virtual Channel::Note* noteEvent(Channel* channel, std::shared_ptr event); 72 | virtual std::string displayName() const; 73 | 74 | //virtual void showParsed(std::ostream& out, std::string indent = std::string()) const; 75 | }; 76 | 77 | class SplitInstrument : public MpInstrument { 78 | public: 79 | SplitInstrument(const ROMFile* rom, uint32_t addr); 80 | 81 | std::vector> splits; 82 | 83 | virtual BaseNoteEvent* makeEvent(double volume, uint8_t key, uint8_t vel, double len) const; 84 | virtual Channel::Note* noteEvent(Channel* channel, std::shared_ptr event); 85 | virtual std::string displayName() const; 86 | 87 | virtual void showParsed(std::ostream& out, std::string indent = std::string()) const; 88 | }; 89 | 90 | class InstrumentData { 91 | public: 92 | InstrumentData(const ROMFile* rom, uint32_t addr); 93 | 94 | uint32_t instruments[128]; 95 | 96 | private: 97 | MpInstrument* findDupe(SynthContext* synth, const MpInstrument& inst) const; 98 | }; 99 | 100 | #endif 101 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "romfile.h" 2 | #include "songtable.h" 3 | #include "songdata.h" 4 | #include "instrumentdata.h" 5 | #include "utility.h" 6 | #include "clefcontext.h" 7 | #include "synth/synthcontext.h" 8 | #include "riffwriter.h" 9 | #include "commandargs.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | static int scanSongTables(const ROMFile& rom, bool doValidate) 19 | { 20 | std::vector sts = rom.findSongTables(); 21 | if (sts.empty()) { 22 | std::cerr << "No song tables found." << std::endl; 23 | return 1; 24 | } 25 | 26 | for (const auto& st : rom.findSongTables()) { 27 | std::cerr << "Song table @ 0x" << std::hex << st.tableStart << std::dec << ": " << st.songs.size() << " songs (table size " << ((st.tableEnd - st.tableStart) / 8) << ")" << std::endl; 28 | for (uint32_t addr = st.tableStart; addr < st.tableEnd; addr += 8) { 29 | uint32_t song = rom.readPointer(addr); 30 | if (!doValidate && std::find(st.songs.begin(), st.songs.end(), song) == st.songs.end()) { 31 | continue; 32 | } 33 | int idx = (addr - st.tableStart) / 8; 34 | std::cerr << "\t" << std::setfill(' ') << std::setw(4) << idx << " Song @ 0x" << std::hex << std::setw(8) << std::setfill('0') << song << std::dec << " - "; 35 | try { 36 | std::unique_ptr sd(st.songAt(song)); 37 | std::cerr << sd->numTracks() << " tracks" << std::endl; 38 | } catch (std::exception& e) { 39 | std::cerr << e.what() << std::endl; 40 | } catch (...) { 41 | std::cerr << "unknown error" << std::endl; 42 | } 43 | } 44 | } 45 | return 0; 46 | } 47 | 48 | static int scanAllSongs(const ROMFile& rom) 49 | { 50 | SongTable st = rom.findAllSongs(); 51 | if (st.songs.empty()) { 52 | std::cerr << "No songs found." << std::endl; 53 | return 1; 54 | } 55 | 56 | for (uint32_t song : st.songs) { 57 | std::unique_ptr sd(st.songAt(song)); 58 | std::cerr << "Song @ 0x" << std::hex << song << std::dec << " - " << sd->numTracks() << " tracks" << std::endl; 59 | std::cerr << std::endl; 60 | } 61 | return 0; 62 | } 63 | 64 | int main(int argc, char** argv) 65 | { 66 | CommandArgs args({ 67 | { "help", "h", "", "Show this help text" }, 68 | { "output", "o", "filename", "Specify the output filename" }, 69 | { "scan", "s", "", "Scan for song tables" }, 70 | { "scan-songs", "S", "", "Scan for songs, even without tables" }, 71 | { "validate", "V", "", "Validate songs when scanning" }, 72 | { "table", "t", "location", "Use a specific song table" }, 73 | { "parse", "p", "", "Output parsed sequence data instead of audio" }, 74 | { "instruments", "i", "", "Output parsed instrument data instead of audio" }, 75 | { "multiboot", "m", "", "Treat the input file as a multiboot image instead of a ROM" }, 76 | { "mute", "", "channels", "Comma-separated list of channels to mute" }, 77 | { "solo", "", "channels", "Comma-separated list of channels to solo" }, 78 | { "preamp", "", "gain", "Adjust all channel volumes before mixing (default 1.0)" }, 79 | { "", "", "input", "Path to the input file" }, 80 | { "", "", "song", "Song index or sequence offset" }, 81 | }); 82 | 83 | std::string argError = args.parse(argc, argv); 84 | if (!argError.empty()) { 85 | std::cerr << argError << std::endl; 86 | return 1; 87 | } 88 | 89 | if (args.hasKey("help") || args.positional().empty()) { 90 | std::cerr << args.usageText(argv[0]) << std::endl; 91 | return 0; 92 | } 93 | 94 | std::string src = args.positional()[0]; 95 | 96 | ClefContext clef; 97 | SynthContext ctx(&clef, 32768); 98 | ROMFile rom(&clef); 99 | rom.load(&ctx, src, args.hasKey("multiboot")); 100 | 101 | if (args.hasKey("scan")) { 102 | return scanSongTables(rom, args.hasKey("validate")); 103 | } 104 | 105 | if (args.hasKey("scan-songs")) { 106 | return scanAllSongs(rom); 107 | } 108 | 109 | if (args.positional().size() < 2) { 110 | std::cerr << args.usageText(argv[0]) << std::endl; 111 | return 1; 112 | } 113 | 114 | std::string songSelection = args.positional()[1]; 115 | 116 | SongTable songTable; 117 | bool byAddr = songSelection.substr(0, 2) == "0x"; 118 | if (args.hasKey("table")) { 119 | std::string tbl(args.getString("table")); 120 | uint32_t songTableAddr = 0; 121 | if (tbl.size() > 2 && tbl[1] == 'x') { 122 | songTableAddr = args.getInt("table"); 123 | songTable = rom.findSongTable(1, songTableAddr); 124 | if (songTable.songs.empty()) { 125 | std::cerr << "Song table " << args.getInt("table") << " not found" << std::endl; 126 | return 1; 127 | } 128 | } else { 129 | int songTableIdx = args.getInt("table"); 130 | do { 131 | songTable = rom.findSongTable(1, songTableAddr); 132 | if (songTable.songs.empty()) { 133 | std::cerr << "Song table " << args.getInt("table") << " not found" << std::endl; 134 | return 1; 135 | } 136 | songTableIdx--; 137 | songTableAddr = songTable.tableEnd; 138 | } while (songTableIdx > 0); 139 | } 140 | } else if (byAddr) { 141 | songTable = rom.findAllSongs(); 142 | } else { 143 | songTable = rom.findSongTable(-1); 144 | if (songTable.songs.empty()) { 145 | std::cerr << "No song table found" << std::endl; 146 | return 1; 147 | } 148 | } 149 | 150 | std::unique_ptr sd; 151 | try { 152 | if (byAddr) { 153 | uint32_t addr = 0; 154 | addr = std::stoi(songSelection, nullptr, 16); 155 | sd.reset(songTable.songAt(addr)); 156 | } else { 157 | uint32_t index = ~0; 158 | index = std::stoi(songSelection); 159 | sd.reset(songTable.songFromTable(index)); 160 | } 161 | if (!sd) { 162 | std::cerr << "Could not load song " << songSelection << std::endl; 163 | return 1; 164 | } 165 | } catch (std::exception& e) { 166 | std::cerr << "An error occurred while loading song " << songSelection << std::endl; 167 | std::cerr << "\t" << e.what() << std::endl; 168 | return 1; 169 | } 170 | 171 | std::string filename = args.getString("output"); 172 | if (args.hasKey("parse")) { 173 | if (filename.empty()) { 174 | sd->showParsed(std::cout); 175 | } else { 176 | std::ofstream out(filename); 177 | sd->showParsed(out); 178 | } 179 | return 0; 180 | } else if (args.hasKey("instruments")) { 181 | std::ofstream out; 182 | if (!filename.empty()) { 183 | out.open(filename); 184 | } 185 | std::ostream& oout = filename.empty() ? std::cout : out; 186 | for (int i = 0; i < 128; i++) { 187 | auto inst = sd->getInstrument(i); 188 | if (inst) { 189 | oout << "Instrument " << i << " - "; 190 | inst->showParsed(oout); 191 | oout << std::endl; 192 | } 193 | } 194 | return 0; 195 | } 196 | 197 | bool mute[16] = { 198 | false, false, false, false, false, false, false, false, 199 | false, false, false, false, false, false, false, false, 200 | }; 201 | bool solo = args.hasKey("solo"); 202 | if (solo && args.hasKey("mute")) { 203 | std::cerr << "Only one of --mute and --solo may be specified." << std::endl; 204 | return 1; 205 | } else if (solo || args.hasKey("mute")) { 206 | std::string chans(solo ? args.getString("solo") : args.getString("mute")); 207 | const char* chanPtr = chans.c_str(); 208 | const char* chanEnd = chanPtr + chans.size(); 209 | while (chanPtr < chanEnd) { 210 | const char* nextPtr; 211 | // Old C function is const-incorrect, but it's the best tool for the job. 212 | int chan = std::strtol(chanPtr, const_cast(&nextPtr), 0); 213 | bool error = (nextPtr <= chanPtr || chan < 0 || chan > 15); 214 | error = error || !(*nextPtr == ',' || *nextPtr == '\0'); 215 | if (error) { 216 | std::cerr << "Invalid " << (solo ? "solo" : "mute") << " channel list: " << chans << std::endl; 217 | return 1; 218 | } 219 | mute[chan] = true; 220 | chanPtr = nextPtr; 221 | } 222 | } 223 | 224 | 225 | for (int i = 0; i < sd->numTracks(); i++) { 226 | TrackData* td = static_cast(sd->getTrack(i)); 227 | if (args.hasKey("preamp")) { 228 | td->preamp = args.getFloat("preamp"); 229 | std::cerr << i << " " << td->preamp << std::endl; 230 | } 231 | ctx.addChannel(td); 232 | ctx.channels[i]->mute = (mute[i] != solo); 233 | } 234 | 235 | if (filename.empty()) { 236 | std::ostringstream fnss; 237 | fnss << src << "." << songSelection << ".wav"; 238 | filename = fnss.str(); 239 | } 240 | std::cerr << "Writing " << (int(ctx.maximumTime() * 10) * .1) << " seconds to \"" << filename << "\"..." << std::endl; 241 | RiffWriter riff(ctx.sampleRate, true); 242 | riff.open(filename); 243 | ctx.save(&riff); 244 | riff.close(); 245 | return 0; 246 | } 247 | -------------------------------------------------------------------------------- /src/romfile.cpp: -------------------------------------------------------------------------------- 1 | #include "romfile.h" 2 | #include "songtable.h" 3 | #include 4 | #include 5 | #include 6 | 7 | std::string ROMFile::BadAccess::message(uint32_t addr) 8 | { 9 | std::ostringstream ss; 10 | ss << "Address out of range: 0x" << std::hex << addr; 11 | return ss.str(); 12 | } 13 | 14 | ROMFile::BadAccess::BadAccess(uint32_t addr) 15 | : std::out_of_range(ROMFile::BadAccess::message(addr)), addr(addr) 16 | { 17 | // initializers only 18 | } 19 | 20 | ROMFile::ROMFile(ClefContext* ctx) 21 | : sampleRate(13379), ctx(ctx) 22 | { 23 | // initializers only 24 | } 25 | 26 | void ROMFile::load(SynthContext* synth, const std::string& path, bool multiboot) 27 | { 28 | std::ifstream f(path); 29 | load(synth, f, path, multiboot); 30 | } 31 | 32 | void ROMFile::load(SynthContext* synth, std::istream& f, const std::string& path, bool multiboot) 33 | { 34 | this->synth = synth; 35 | this->multiboot = multiboot; 36 | if (multiboot) { 37 | baseAddr = 0x02000000; 38 | headerSize = 0xC0; 39 | } else { 40 | baseAddr = 0x08000000; 41 | headerSize = 0x200; 42 | } 43 | if (path == filename) { 44 | return; 45 | } 46 | uint8_t buffer[1024]; 47 | while (f) { 48 | f.read(reinterpret_cast(buffer), sizeof(buffer)); 49 | rom.insert(rom.end(), buffer, buffer + f.gcount()); 50 | } 51 | filename = path; 52 | } 53 | 54 | uint32_t ROMFile::cleanPointer(uint32_t addr, uint32_t size, bool align) const 55 | { 56 | uint32_t mask = align ? 0xFE000003 : 0xFE000000; 57 | if ((addr & mask) != baseAddr) return BAD_PTR; 58 | addr &= 0x01FFFFFF; 59 | if (addr < headerSize || addr > rom.size() - size) return BAD_PTR; 60 | return addr; 61 | } 62 | 63 | uint32_t ROMFile::cleanDeref(uint32_t addr, uint32_t size, bool alignTarget, bool alignPointer) const 64 | { 65 | addr = cleanPointer(addr | baseAddr, 4, alignPointer); 66 | if (addr == BAD_PTR) return BAD_PTR; 67 | return cleanPointer(parseInt(rom, addr), size, alignTarget); 68 | } 69 | 70 | SongTable ROMFile::findSongTable(int minSongs, uint32_t offset) const 71 | { 72 | size_t size = rom.size() - 8; 73 | SongTable result(this); 74 | uint32_t tableStart = 0; 75 | std::vector songs; 76 | offset -= 4; 77 | int badCount = 0; 78 | while ((offset += 4) < size) { 79 | uint32_t addr = cleanDeref(offset, 12); 80 | if (addr != BAD_PTR && !checkSong(addr, false)) { 81 | addr = BAD_PTR; 82 | } 83 | if (addr == BAD_PTR) { 84 | if (tableStart) { 85 | if (songs.size() > result.songs.size()) { 86 | result.tableStart = tableStart; 87 | result.tableEnd = offset; 88 | result.songs = songs; 89 | if (minSongs >= 0 && result.songs.size() > minSongs) { 90 | return result; 91 | } 92 | } 93 | //songs.clear(); 94 | } 95 | tableStart = 0; 96 | continue; 97 | } 98 | if (!tableStart) { 99 | tableStart = offset; 100 | } 101 | if (std::find(songs.begin(), songs.end(), addr) == songs.end() && checkSong(addr)) { 102 | // Song is valid and is not a duplicate 103 | songs.push_back(addr); 104 | } 105 | offset += 4; 106 | } 107 | if (songs.size() > result.songs.size()) { 108 | result.tableStart = tableStart; 109 | result.tableEnd = offset; 110 | result.songs = songs; 111 | } 112 | return result; 113 | } 114 | 115 | std::vector ROMFile::findSongTables(uint32_t offset) const 116 | { 117 | std::vector tables; 118 | size_t size = rom.size() - 8; 119 | while (offset < size) { 120 | SongTable table = findSongTable(0, offset); 121 | if (!table.songs.size()) { 122 | // Didn't find (another) song table 123 | break; 124 | } 125 | tables.push_back(table); 126 | offset = table.tableEnd; 127 | } 128 | return tables; 129 | } 130 | 131 | SongTable ROMFile::findAllSongs() const 132 | { 133 | SongTable result(this); 134 | int size = rom.size() - 12; 135 | for (int offset = headerSize; offset < size; offset += 4) { 136 | if (checkSong(offset)) { 137 | result.songs.push_back(offset); 138 | offset += 4; 139 | } 140 | } 141 | return result; 142 | } 143 | 144 | bool ROMFile::checkSong(uint32_t addr, bool deep) const 145 | { 146 | try { 147 | uint8_t numTracks = rom[addr]; 148 | if (!numTracks) return !deep; 149 | uint32_t end = addr + 8 + numTracks * 4; 150 | if (/*(deep && !rom[addr + 1]) || */ end >= rom.size()) { 151 | return false; 152 | } 153 | for (int p = addr + 4; p < end; p += 4) { 154 | uint32_t data = cleanDeref(p, 12, false); 155 | if (data == BAD_PTR) return false; 156 | if (!deep) continue; 157 | if (p == addr + 4) { 158 | // tone data 159 | if (rom[data + 2]) return false; 160 | uint8_t inst = rom[data]; 161 | if (inst > 12 && inst != 16 && inst != 32 && inst != 64 && inst != 128) return false; 162 | uint32_t wave = parseInt(rom, data + 4); 163 | if (inst & 0x7) { 164 | if (inst & 0x7 == 4) { 165 | if (wave > 1) return false; 166 | } else if (inst & 0x7 != 3) { 167 | if (wave > 3) return false; 168 | } 169 | if (rom[data + 8] > 7) return false; 170 | if (rom[data + 9] > 7) return false; 171 | if (rom[data + 10] > 15) return false; 172 | if (rom[data + 11] > 7) return false; 173 | } 174 | if (inst == 0 || inst == 8 || inst == 3 || inst == 11 || inst == 16 || inst == 32) { 175 | wave = cleanPointer(wave, 16); 176 | if (wave == BAD_PTR) return false; 177 | if (inst & 0x7 == 0) { 178 | if (rom[wave] || rom[wave + 1] || rom[wave + 2] || rom[wave + 3] & ~0x40) return false; 179 | } 180 | } else if (inst == 64 || inst == 128) { 181 | if (rom[data + 1] || rom[data + 2] || rom[data + 3]) return false; 182 | if (cleanPointer(wave, 16) == BAD_PTR) return false; 183 | if (inst == 64) { 184 | if (cleanDeref(data + 8, 128) == BAD_PTR) return false; 185 | } else { 186 | if (parseInt(rom, data + 8)) return false; 187 | } 188 | } 189 | } else { 190 | // track data 191 | uint8_t cmd = rom[data]; 192 | // track has no events 193 | if (deep && cmd == 0xB1) return false; 194 | // track starts with running status 195 | if (cmd < 0x80) return false; 196 | // track starts with unknown command 197 | if (cmd == 0xC8 || cmd == 0xC9 || cmd == 0xCA || cmd == 0xCB || cmd == 0xCC) return false; 198 | } 199 | } 200 | return true; 201 | } catch (...) { 202 | return false; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/romfile.h: -------------------------------------------------------------------------------- 1 | #ifndef GBAMP2WAV_ROMFILE_H 2 | #define GBAMP2WAV_ROMFILE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "utility.h" 9 | class ClefContext; 10 | class SynthContext; 11 | class SongTable; 12 | 13 | class ROMFile { 14 | public: 15 | static constexpr uint32_t BAD_PTR = 0xDEADBEEF; 16 | 17 | class BadAccess : public std::out_of_range { 18 | public: 19 | static std::string message(uint32_t addr); 20 | BadAccess(uint32_t addr); 21 | const uint32_t addr; 22 | }; 23 | 24 | ROMFile(ClefContext* ctx); 25 | ROMFile(const ROMFile& other) = delete; 26 | ROMFile(ROMFile&& other) = delete; 27 | ROMFile& operator=(const ROMFile& other) = delete; 28 | ROMFile& operator=(ROMFile&& other) = delete; 29 | 30 | void load(SynthContext* synth, const std::string& path, bool multiboot = false); 31 | void load(SynthContext* synth, std::istream& stream, const std::string& path, bool multiboot = false); 32 | 33 | inline ClefContext* context() const { return ctx; } 34 | inline SynthContext* synthContext() const { return synth; } 35 | 36 | SongTable findSongTable(int minSongs = -1, uint32_t offset = 0x200) const; 37 | std::vector findSongTables(uint32_t offset = 0x200) const; 38 | SongTable findAllSongs() const; 39 | bool checkSong(uint32_t addr, bool deep = true) const; 40 | 41 | std::string filename; 42 | std::vector rom; 43 | uint32_t sampleRate; 44 | uint32_t baseAddr; 45 | uint32_t headerSize; 46 | bool multiboot; 47 | 48 | inline uint8_t operator[](uint32_t addr) const { return read(addr); } 49 | template inline T read(uint32_t addr) const { 50 | uint32_t cleaned = cleanPointer(addr | baseAddr, sizeof(T), false); 51 | if (cleaned == BAD_PTR) throw BadAccess(addr); 52 | return parseInt(rom, cleaned); 53 | } 54 | inline uint32_t readPointer(uint32_t addr, bool align = true) const { 55 | uint32_t cleaned = cleanDeref(addr, 4, align, false); 56 | if (cleaned == BAD_PTR) throw BadAccess(addr); 57 | return cleaned; 58 | } 59 | template inline T deref(uint32_t addr) const { 60 | uint32_t cleaned = cleanDeref(addr, sizeof(T), (sizeof(T) & 3) > 0); 61 | if (cleaned == BAD_PTR) throw BadAccess(addr); 62 | return parseInt(rom, cleaned); 63 | } 64 | 65 | private: 66 | uint32_t cleanPointer(uint32_t addr, uint32_t size = 4, bool align = true) const; 67 | uint32_t cleanDeref(uint32_t addr, uint32_t size = 4, bool align = true, bool alignPointer = true) const; 68 | 69 | ClefContext* ctx; 70 | SynthContext* synth; 71 | }; 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /src/songdata.cpp: -------------------------------------------------------------------------------- 1 | #include "songdata.h" 2 | #include "romfile.h" 3 | #include "synth/audionode.h" 4 | #include "synth/synthcontext.h" 5 | #include 6 | #include 7 | #include 8 | 9 | static const uint8_t noteLength[50] = { 10 | 0, 0xFF, 11 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 12 | 17, 18, 19, 20, 21, 22, 23, 24, 28, 30, 32, 36, 40, 42, 44, 48, 13 | 52, 54, 56, 60, 64, 66, 68, 72, 76, 78, 80, 84, 88, 90, 92, 96 14 | }; 15 | 16 | namespace EventType { 17 | enum Opcode { 18 | FINE = 0xB1, 19 | GOTO, 20 | PATT, 21 | PEND, 22 | REPT, 23 | STOP, 24 | MEMACC = 0xB9, 25 | PRIO, 26 | TEMPO, 27 | KEYSH, 28 | VOICE, 29 | VOL, 30 | PAN, 31 | BEND, 32 | BENDR, 33 | LFOS, 34 | LFODL, 35 | MOD, 36 | MODT, 37 | TUNE = 0xC8, 38 | XCMD = 0xCD, 39 | EOT, 40 | TIE, 41 | }; 42 | 43 | // starts at 0xB1 44 | static std::string names[] = { 45 | "FINE", "GOTO", "PATT", "PEND", "REPT", "STOP", "", "", "MEMACC", "PRIO", 46 | "TEMPO", "KEYSH", "VOICE", "VOL", "PAN", "BEND", "BENDR", "LFOS", "LFODL", 47 | "MOD", "MODT", "", "", "TUNE", "", "", "", "", "XCMD", "EOT", "TIE", 48 | }; 49 | 50 | static std::string name(uint8_t opcode) 51 | { 52 | if (opcode < 0x80) return ""; 53 | if (opcode >= 0xD0) return "NOTE"; 54 | if (opcode <= 0xB0) return "REST"; 55 | return names[opcode - 0xB1]; 56 | } 57 | 58 | static int size(uint8_t opcode) 59 | { 60 | // return 0 = running status 61 | // return -1 = variable length 62 | switch (opcode) { 63 | case FINE: 64 | case STOP: 65 | case PEND: return 1; 66 | case GOTO: 67 | case PATT: return 5; 68 | case REPT: return 6; 69 | case MEMACC: 70 | case EOT: 71 | case TIE: return -1; 72 | } 73 | if (opcode < 0x80) { 74 | return 0; 75 | } else if (opcode < FINE) { 76 | return 1; 77 | } else if (opcode >= PRIO && opcode <= XCMD) { 78 | return 2; 79 | } else { 80 | return -1; 81 | } 82 | } 83 | } 84 | 85 | std::string RawEvent::render() const { 86 | std::ostringstream ss; 87 | ss << "0x" << std::hex << addr << " " << std::dec; 88 | if (opcode < 0x80) { 89 | ss << " "; 90 | } else { 91 | ss << std::setw(6) << EventType::name(opcode); 92 | } 93 | bool first = true; 94 | if (opcode >= 0x80 && opcode <= 0xB0) { 95 | ss << " " << (opcode - 0x80); 96 | first = false; 97 | } else if (opcode >= 0xD0) { 98 | ss << " " << (opcode - 0xCF); 99 | first = false; 100 | } 101 | for (uint32_t arg : args) { 102 | if (first) { 103 | first = false; 104 | ss << " "; 105 | } else { 106 | ss << ", "; 107 | } 108 | if (arg > 0xFF) { 109 | ss << std::hex << "0x" << arg << std::dec; 110 | } else { 111 | ss << arg; 112 | } 113 | } 114 | return ss.str(); 115 | } 116 | 117 | TrackData::TrackData(SongData* song, int index, uint32_t addr, MpInstrument* defaultInst) 118 | : trackIndex(index), song(song), addr(addr), hasLoop(true), preamp(1.0), playIndex(0), playTime(0), secPerTick(1.0 / 75.0), 119 | lengthCache(-1), currentInstrument(defaultInst), bendRange(2), transpose(0), tuning(0), stopped(false) 120 | { 121 | std::unordered_map addrToIndex; 122 | std::unordered_map indexToAddr; 123 | const ROMFile& r = *song->rom; 124 | uint32_t pos = addr; 125 | uint8_t running = 0; 126 | uint8_t instrument = 0, noteVel = 127, noteLen = 96; 127 | RawEvent raw; 128 | int8_t noteKey = 60; 129 | uint32_t argAddr, argPos; 130 | uint32_t timestamp = 0; 131 | uint32_t returnAddr = 0; 132 | uint32_t repeatAddr = 0; 133 | uint8_t repeatCount = 1; 134 | int ts = 0; 135 | while (true) { 136 | raw.addr = pos; 137 | raw.opcode = r[pos]; 138 | raw.args.clear(); 139 | int eventSize = EventType::size(raw.opcode); 140 | uint32_t argOffset = 1; 141 | if (eventSize == 0) { 142 | // running status 143 | raw.opcode = running; 144 | eventSize = EventType::size(raw.opcode) - 1; 145 | argOffset = 0; 146 | } 147 | if (eventSize < 0) { 148 | if (raw.opcode == EventType::MEMACC) { 149 | if (r[pos + 1] > 5) { 150 | eventSize = 8; 151 | } else { 152 | eventSize = 4; 153 | } 154 | } else { 155 | eventSize += 2; 156 | int maxSize; 157 | switch (raw.opcode) { 158 | case EventType::EOT: 159 | maxSize = argOffset; 160 | break; 161 | case EventType::TIE: 162 | maxSize = argOffset + 1; 163 | break; 164 | default: 165 | maxSize = argOffset + 2; 166 | } 167 | while (r[pos + eventSize] < 0x80 && eventSize <= maxSize) { 168 | eventSize++; 169 | } 170 | } 171 | } 172 | if (raw.opcode == EventType::MEMACC) { 173 | for (int i = 1; i < 4; i++) { 174 | raw.args.push_back(r[pos + i]); 175 | } 176 | if (eventSize > 4) { 177 | raw.args.push_back(r.read(pos + 4)); 178 | } 179 | } else if (raw.opcode == EventType::GOTO || raw.opcode == EventType::PATT) { 180 | raw.args.push_back(r.readPointer(pos + 1, false)); 181 | } else if (raw.opcode == EventType::REPT) { 182 | raw.args.push_back(r[pos + 1]); 183 | raw.args.push_back(r.readPointer(pos + 2, false)); 184 | } else if (eventSize > argOffset) { 185 | for (int i = argOffset; i < eventSize; i++) { 186 | raw.args.push_back(r[pos + i]); 187 | } 188 | } 189 | rawEvents.push_back(raw); 190 | pos += eventSize; 191 | Mp2kEvent ev; 192 | ev.raw = raw; 193 | ev.effAddr = (uint64_t(returnAddr) << 32) | raw.addr; 194 | ev.duration = 0; 195 | size_t index = events.size(); 196 | addrToIndex[ev.effAddr] = index; 197 | if (!indexToAddr.count(index)) { 198 | indexToAddr[index] = ev.effAddr; 199 | } 200 | if (raw.opcode < 0xB1) { 201 | ev.type = Mp2kEvent::Rest; 202 | ev.duration = noteLength[raw.opcode - 0x81 + 2]; 203 | ts += ev.duration; 204 | events.push_back(ev); 205 | } else if (raw.opcode >= 0xCE) { // EOT / TIE / NOTE 206 | running = raw.opcode; 207 | uint8_t noteLen = noteLength[raw.opcode - 0xCE]; 208 | int numArgs = raw.args.size(); 209 | if (numArgs > 0) noteKey = raw.args[0]; 210 | if (numArgs > 1) noteVel = raw.args[1]; 211 | if (numArgs > 2) noteLen += raw.args[2]; 212 | ev.type = Mp2kEvent::Note; 213 | ev.param = noteKey; 214 | if (raw.opcode == EventType::EOT) { 215 | ev.value = 0; 216 | ev.duration = 0xFF; 217 | } else { 218 | ev.value = noteVel; 219 | ev.duration = noteLen; 220 | } 221 | events.push_back(ev); 222 | } else switch (raw.opcode) { 223 | using namespace EventType; 224 | case FINE: 225 | case STOP: 226 | ev.type = Mp2kEvent::Stop; 227 | events.push_back(ev); 228 | return; 229 | case GOTO: 230 | { 231 | uint64_t effAddr = (uint64_t(returnAddr) << 32) | raw.args[0]; 232 | if (addrToIndex.count(effAddr)) { 233 | // Jump to an address we've already seen 234 | hasLoop = true; 235 | ev.type = Mp2kEvent::Goto; 236 | ev.value = addrToIndex.at(effAddr); 237 | events.push_back(ev); 238 | return; 239 | } 240 | } 241 | // Skip decoding forward to the goto target 242 | pos = raw.args[0]; 243 | break; 244 | case PATT: 245 | if (returnAddr) { 246 | throw std::runtime_error("nested pattern detected"); 247 | } 248 | repeatCount = 1; 249 | returnAddr = pos; 250 | pos = raw.args[0]; 251 | break; 252 | case PEND: 253 | if (!returnAddr) { 254 | // PEND without a call stack is ignored 255 | continue; 256 | } 257 | repeatCount--; 258 | if (repeatCount > 0) { 259 | pos = repeatAddr; 260 | } else { 261 | pos = returnAddr; 262 | returnAddr = 0; 263 | } 264 | break; 265 | case REPT: 266 | if (returnAddr) { 267 | throw std::runtime_error("nested pattern detected"); 268 | } 269 | repeatCount = raw.args[0]; 270 | if (repeatCount > 0) { 271 | repeatAddr = raw.args[1]; 272 | returnAddr = pos; 273 | pos = repeatAddr; 274 | } 275 | break; 276 | case PRIO: 277 | // ignore 278 | break; 279 | case VOICE: 280 | case VOL: 281 | case PAN: 282 | case BEND: 283 | case BENDR: 284 | case MOD: 285 | case TUNE: 286 | running = raw.opcode; 287 | // fallthrough; 288 | case TEMPO: 289 | case KEYSH: 290 | case LFOS: 291 | case LFODL: 292 | case MODT: 293 | ev.type = Mp2kEvent::Param; 294 | ev.param = raw.opcode; 295 | ev.value = raw.args[0]; 296 | events.push_back(ev); 297 | break; 298 | case XCMD: 299 | // TODO 300 | case 0xB9: // Unknown 301 | case 0xCB: // Unknown 302 | case 0xCC: // Unknown 303 | // ??? 304 | //break; 305 | //std::cerr << "unknown " << (int)raw.opcode << std::endl; 306 | break; 307 | default: 308 | //std::cerr << "XXX " << std::hex << (int)raw.opcode << std::endl; 309 | throw std::runtime_error("unknown MP2K command"); 310 | } 311 | } 312 | } 313 | 314 | TrackData::~TrackData() 315 | { 316 | } 317 | 318 | void TrackData::internalReset() 319 | { 320 | playIndex = 0; 321 | playTime = 0; 322 | secPerTick = 1.0 / 60.0; 323 | } 324 | 325 | double TrackData::length() const 326 | { 327 | if (lengthCache < 0) { 328 | double spt = 1.0 / 60.0; 329 | double lastEnd = 0; 330 | double time = 0; 331 | int lastIndex = events.size(); 332 | for (int index = 0; index < lastIndex; index++) { 333 | const Mp2kEvent& ev = events[index]; 334 | if (ev.type == Mp2kEvent::Rest) { 335 | time += ev.duration * spt; 336 | spt = song->tickLengthAt(time); 337 | } else if (ev.type == Mp2kEvent::Note && ev.duration != 0xFF) { 338 | // TODO: release trails 339 | double end = time + ev.duration * spt; 340 | if (end > lastEnd) { 341 | lastEnd = end; 342 | } 343 | } else if (ev.type == Mp2kEvent::Stop || ev.type == Mp2kEvent::Goto) { 344 | // Since the sequences are pre-processed, a goto event is a loop point 345 | break; 346 | } 347 | } 348 | lengthCache = (time > lastEnd ? time : lastEnd) + 1; 349 | } 350 | return lengthCache; 351 | } 352 | 353 | bool TrackData::isFinished() const 354 | { 355 | return stopped || playIndex >= events.size() || playTime > length(); 356 | } 357 | 358 | double SongData::tickLengthAt(double timestamp) const 359 | { 360 | for (int i = tempos.size() - 1; i >= 0; --i) { 361 | if (tempos[i].first <= timestamp) { 362 | return tempos[i].second; 363 | } 364 | } 365 | return 1.0 / 60.0; 366 | } 367 | 368 | std::shared_ptr TrackData::readNextEvent() 369 | { 370 | bool didGoto = false; 371 | while (!isFinished() && !pendingEvents.size()) { 372 | //std::cerr << trackIndex << ":" << playIndex << "@" << playTime << "\t" << events[playIndex].raw.render() << std::endl; 373 | const Mp2kEvent& event = events[playIndex++]; 374 | secPerTick = song->tickLengthAt(playTime); 375 | double duration = event.duration == 0xFF ? -1 : event.duration * secPerTick; 376 | if (event.type == Mp2kEvent::Stop) { 377 | stopped = true; 378 | } else if (event.type == Mp2kEvent::Rest) { 379 | playTime += duration; 380 | } else if (event.type == Mp2kEvent::Goto) { 381 | if (didGoto) { 382 | // loop never produces an event: abort 383 | playIndex = events.size(); 384 | //std::cerr << "abort" << std::endl; 385 | } else { 386 | playIndex = event.value; 387 | //std::cerr << "goto" << std::endl; 388 | didGoto = true; 389 | } 390 | } else if (event.type == Mp2kEvent::Param) { 391 | switch (event.param) { 392 | using namespace EventType; 393 | case TEMPO: 394 | // simplification of 1.0 / (value / 75.0 * 60.0) 395 | secPerTick = 0.8 * 1.6 / event.value; // TODO: fix base rate 396 | song->tempos.emplace_back(playTime, secPerTick); 397 | break; 398 | case KEYSH: 399 | transpose = event.value; 400 | break; 401 | case TUNE: 402 | tuning = (int(event.value) - 64) / 64.0; 403 | break; 404 | case VOICE: 405 | { 406 | int instID = int(event.value); 407 | currentInstrument = song->getInstrument(instID); 408 | std::cerr << trackIndex << ": Using instrument " << std::dec << instID << " (" << (currentInstrument ? (int)currentInstrument->type : -1) << ") " << std::endl; 409 | if (currentInstrument) { 410 | pendingEvents.emplace_back(new ChannelEvent('inst', uint64_t(currentInstrument->addr))); 411 | releaseTime = currentInstrument->release; 412 | } 413 | } 414 | break; 415 | case PAN: 416 | pendingEvents.emplace_back(new ChannelEvent(AudioNode::Pan, event.value / 128.0)); 417 | pendingEvents.back()->timestamp = playTime; 418 | break; 419 | case VOL: 420 | pendingEvents.emplace_back(new ChannelEvent(AudioNode::Gain, 2 * preamp * event.value / 127.0)); 421 | pendingEvents.back()->timestamp = playTime; 422 | volume = preamp * event.value / 127.0; 423 | break; 424 | case BENDR: 425 | bendRange = event.value; 426 | break; 427 | case BEND: 428 | { 429 | double bend = noteToFreq(69 + bendRange * (event.value - 64.0) / 64.0) / 440.0; 430 | for (const auto& it : activeNotes) { 431 | ModulatorEvent* modEvent = new ModulatorEvent(it.second.playbackID, 'bend', bend); 432 | modEvent->timestamp = playTime; 433 | pendingEvents.emplace_back(modEvent); 434 | } 435 | } 436 | break; 437 | default: 438 | // TODO 439 | std::cerr << "unknown param " << std::hex << (int)event.param << std::dec << std::endl; 440 | break; 441 | } 442 | } else if (event.type == Mp2kEvent::Note && currentInstrument) { 443 | uint8_t note = event.param + transpose; 444 | // PSG instruments are mutually exclusive 445 | bool psg = currentInstrument->type & 0x7; 446 | uint8_t noteID = psg ? 0x80 + currentInstrument->type : note; 447 | auto& active = psg ? song->activePsg : activeNotes; 448 | auto iter = active.find(noteID); 449 | if (iter != active.end() && (psg || iter->second.releaseTime == 0 || iter->second.endTime > playTime)) { 450 | KillEvent* killEvent = new KillEvent(iter->second.playbackID, playTime); 451 | if (event.value > 0 || iter->second.endTime == 0 || iter->second.endTime > playTime) { 452 | // note replaced with another note or past its release time 453 | killEvent->immediate = true; 454 | active.erase(iter); 455 | } else if (!iter->second.released) { 456 | iter->second.released = true; 457 | iter->second.endTime = playTime + (iter->second.endTime - iter->second.releaseTime); 458 | iter->second.releaseTime = playTime; 459 | } 460 | pendingEvents.emplace_back(killEvent); 461 | } 462 | if (duration != 0) { 463 | BaseNoteEvent* noteEvent = currentInstrument->makeEvent(volume, note + tuning, event.value, duration); 464 | if (noteEvent) { 465 | noteEvent->timestamp = playTime; 466 | double noteReleaseTime = duration >= 0 ? playTime + duration : 0; 467 | double endTime = noteReleaseTime + releaseTime; 468 | active[noteID] = (ActiveNote){ 469 | noteEvent->playbackID, 470 | noteReleaseTime, 471 | endTime, 472 | false, 473 | }; 474 | if (psg) { 475 | activeNotes[noteID] = active[noteID]; 476 | } 477 | pendingEvents.emplace_back(noteEvent); 478 | } 479 | } 480 | } else if (event.type == Mp2kEvent::Note) { 481 | std::cerr << "note without valid instrument" << std::endl; 482 | } 483 | } 484 | if (isFinished()) { 485 | while (activeNotes.size()) { 486 | auto iter = activeNotes.begin(); 487 | KillEvent* killEvent = new KillEvent(iter->second.playbackID, playTime); 488 | killEvent->immediate = true; 489 | pendingEvents.emplace_back(killEvent); 490 | activeNotes.erase(iter); 491 | } 492 | } 493 | if (pendingEvents.size()) { 494 | auto result = pendingEvents.at(0); 495 | pendingEvents.erase(pendingEvents.begin()); 496 | return result; 497 | } 498 | return nullptr; 499 | } 500 | 501 | void TrackData::showParsed(std::ostream& out) 502 | { 503 | out << "Track " << trackIndex << ":" << std::endl; 504 | for (const RawEvent& raw : rawEvents) { 505 | out << "\t" << raw.render() << std::endl; 506 | } 507 | } 508 | 509 | SongData::SongData(const ROMFile* rom, uint32_t addr) 510 | : BaseSequence(rom->context()), rom(rom), addr(addr), hasLoop(false), instruments(rom, rom->readPointer(addr + 4)) 511 | { 512 | int numTracks = rom->read(addr); 513 | MpInstrument* defaultInst = nullptr; 514 | if (rom->synthContext()) { 515 | int defaultInstId = rom->synthContext()->instrumentID(0); 516 | defaultInst = static_cast(rom->synthContext()->getInstrument(defaultInstId)); 517 | } 518 | for (int i = 0; i < numTracks; i++) { 519 | TrackData* track = new TrackData(this, i, rom->readPointer(addr + 8 + i * 4, false), defaultInst); 520 | addTrack(track); 521 | if (track->hasLoop) { 522 | hasLoop = true; 523 | } 524 | } 525 | } 526 | 527 | bool SongData::canLoop() const 528 | { 529 | return false; 530 | } 531 | 532 | MpInstrument* SongData::getInstrument(uint8_t id) const 533 | { 534 | if (id >= 128) { 535 | return nullptr; 536 | } 537 | uint32_t addr = instruments.instruments[id]; 538 | if (!addr) { 539 | return nullptr; 540 | } 541 | return static_cast(rom->synthContext()->getInstrument(addr)); 542 | } 543 | 544 | void SongData::showParsed(std::ostream& out) 545 | { 546 | for (const auto& track : tracks) { 547 | track->showParsed(out); 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/songdata.h: -------------------------------------------------------------------------------- 1 | #ifndef GBAMP2WAV_SONGDATA_H 2 | #define GBAMP2WAV_SONGDATA_H 3 | 4 | #include "seq/isequence.h" 5 | #include "seq/itrack.h" 6 | #include "instrumentdata.h" 7 | #include 8 | class ROMFile; 9 | class SongData; 10 | class SynthContext; 11 | 12 | struct RawEvent { 13 | uint32_t addr; 14 | uint8_t opcode; 15 | std::vector args; 16 | 17 | std::string render() const; 18 | }; 19 | 20 | struct Mp2kEvent { 21 | enum Type { 22 | Note, 23 | Rest, 24 | Param, 25 | Goto, 26 | Stop, 27 | }; 28 | 29 | uint64_t effAddr; 30 | Type type; 31 | uint16_t value; 32 | uint8_t param; 33 | uint8_t duration; 34 | RawEvent raw; 35 | }; 36 | 37 | class TrackData : public ITrack { 38 | public: 39 | struct ActiveNote { 40 | uint64_t playbackID; 41 | double releaseTime, endTime; 42 | bool released; 43 | }; 44 | 45 | TrackData(SongData* song, int index, uint32_t addr, MpInstrument* defaultInst); 46 | ~TrackData(); 47 | 48 | virtual bool isFinished() const; 49 | 50 | virtual double length() const; 51 | 52 | int trackIndex; 53 | SongData* const song; 54 | const uint32_t addr; 55 | bool hasLoop; 56 | double preamp; 57 | 58 | void showParsed(std::ostream& out); 59 | 60 | protected: 61 | virtual std::shared_ptr readNextEvent(); 62 | virtual void internalReset(); 63 | 64 | size_t playIndex; 65 | double playTime; 66 | double secPerTick; 67 | mutable double lengthCache; 68 | MpInstrument* currentInstrument; 69 | std::vector rawEvents; 70 | std::vector events; 71 | std::vector> pendingEvents; 72 | std::unordered_map activeNotes; 73 | double bendRange; 74 | double releaseTime; 75 | uint8_t transpose; 76 | double tuning; 77 | double volume; 78 | bool stopped : 1; 79 | }; 80 | 81 | class SongData : public BaseSequence { 82 | public: 83 | SongData(const ROMFile* rom, uint32_t addr); 84 | SongData(const SongData& other) = delete; 85 | SongData(SongData&& other) = delete; 86 | SongData& operator=(const SongData& other) = delete; 87 | SongData& operator=(SongData&& other) = delete; 88 | 89 | virtual bool canLoop() const; 90 | MpInstrument* getInstrument(uint8_t id) const; 91 | 92 | const ROMFile* const rom; 93 | const uint32_t addr; 94 | InstrumentData instruments; 95 | 96 | void showParsed(std::ostream& out); 97 | 98 | std::vector> tempos; 99 | std::unordered_map activePsg; 100 | 101 | double tickLengthAt(double timestamp) const; 102 | 103 | private: 104 | bool hasLoop; 105 | }; 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /src/songtable.cpp: -------------------------------------------------------------------------------- 1 | #include "songtable.h" 2 | #include "songdata.h" 3 | #include "romfile.h" 4 | 5 | SongTable::SongTable() 6 | : rom(nullptr), tableStart(0), tableEnd(0) 7 | { 8 | // initializers only 9 | } 10 | 11 | SongTable::SongTable(const ROMFile* rom) 12 | : rom(rom), tableStart(0), tableEnd(0) 13 | { 14 | // initializers only 15 | } 16 | 17 | SongData* SongTable::songAt(uint32_t addr) const 18 | { 19 | return new SongData(rom, addr); 20 | } 21 | 22 | SongData* SongTable::song(size_t index) const 23 | { 24 | return songAt(songs.at(index)); 25 | } 26 | 27 | SongData* SongTable::songFromTable(size_t index) const 28 | { 29 | uint32_t ptr = tableStart + 8 * index; 30 | if (ptr >= tableEnd) { 31 | throw std::out_of_range("song index out of range"); 32 | } 33 | return songAt(rom->readPointer(ptr)); 34 | } 35 | -------------------------------------------------------------------------------- /src/songtable.h: -------------------------------------------------------------------------------- 1 | #ifndef GBAMP2WAV_SONGTABLE_H 2 | #define GBAMP2WAV_SONGTABLE_H 3 | 4 | #include 5 | #include 6 | class ROMFile; 7 | class SongData; 8 | 9 | using std::size_t; 10 | 11 | class SongTable { 12 | public: 13 | SongTable(); 14 | SongTable(const ROMFile* rom); 15 | SongTable(const SongTable& other) = default; 16 | SongTable(SongTable&& other) = default; 17 | SongTable& operator=(const SongTable& other) = default; 18 | SongTable& operator=(SongTable&& other) = default; 19 | 20 | const ROMFile* rom; 21 | uint32_t tableStart, tableEnd; 22 | std::vector songs; 23 | 24 | SongData* songAt(uint32_t addr) const; 25 | SongData* song(size_t index) const; 26 | SongData* songFromTable(size_t index) const; 27 | }; 28 | 29 | #endif 30 | --------------------------------------------------------------------------------