├── .github └── workflows │ └── win32-i686.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── COPYING ├── Makefile ├── ctrmml.mak ├── doc └── screenshot.png ├── imgui.mak ├── libvgm.mak ├── readme.md └── src ├── audio_manager.cpp ├── audio_manager.h ├── config_window.cpp ├── config_window.h ├── dmf_importer.cpp ├── dmf_importer.h ├── editor_window.cpp ├── editor_window.h ├── emu_player.cpp ├── emu_player.h ├── main.cpp ├── main_window.cpp ├── main_window.h ├── miniz.c ├── song_manager.cpp ├── song_manager.h ├── track_info.cpp ├── track_info.h ├── track_list_window.cpp ├── track_list_window.h ├── track_view_window.cpp ├── track_view_window.h ├── unittest ├── main.cpp └── test_track_info.cpp ├── window.cpp ├── window.h └── window_type.h /.github/workflows/win32-i686.yml: -------------------------------------------------------------------------------- 1 | name: Win32 i686 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: windows-latest 13 | defaults: 14 | run: 15 | shell: msys2 {0} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: msys2/setup-msys2@v2 19 | with: 20 | msystem: MINGW32 21 | update: true 22 | install: base-devel git mingw-w64-i686-toolchain mingw-w64-i686-glfw mingw-w64-i686-cppunit mingw-w64-i686-cmake 23 | - name: build-libvgm 24 | run: | 25 | git clone -n https://github.com/valleybell/libvgm 26 | cd libvgm 27 | git checkout 5fe3883a 28 | mkdir build 29 | cd build 30 | cmake .. -G "MSYS Makefiles" -D AUDIODRV_LIBAO=OFF -D CMAKE_INSTALL_PREFIX=/mingw32 31 | make 32 | make install 33 | - name: init-submodules 34 | run: | 35 | git submodule update --init --recursive 36 | # cppunit doesn't play nice with github's win32 environment so this is skipped 37 | # for now 38 | # - name: run-unittests 39 | # run: | 40 | # make test RELEASE=1 41 | - name: make 42 | run: | 43 | make bin/mmlgui RELEASE=1 44 | - uses: actions/upload-artifact@v2 45 | with: 46 | name: binary 47 | path: bin/mmlgui.exe 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/* 2 | /obj/* 3 | doxygen 4 | *.vgm 5 | *.dll 6 | *.exe 7 | imgui.ini 8 | mmlgui 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "imgui"] 2 | path = imgui 3 | url = https://github.com/Flix01/imgui 4 | branch = imgui_with_addons 5 | ignore = dirty 6 | [submodule "ImGuiColorTextEdit"] 7 | path = ImGuiColorTextEdit 8 | url = https://github.com/superctr/ImGuiColorTextEdit 9 | branch = mmlgui 10 | [submodule "ctrmml"] 11 | path = ctrmml 12 | url = https://github.com/superctr/ctrmml 13 | [submodule "libvgm"] 14 | path = libvgm 15 | url = https://github.com/ValleyBell/libvgm 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(mmlgui LANGUAGES C CXX) 3 | 4 | find_package(PkgConfig REQUIRED) 5 | find_package(OpenGL REQUIRED) 6 | 7 | pkg_check_modules(GLFW3 REQUIRED glfw3) 8 | pkg_check_modules(CPPUNIT cppunit) 9 | 10 | if(MINGW) 11 | option(LINK_STATIC_LIBS "link with static runtime libraries (MinGW only)" ON) 12 | if(LINK_STATIC_LIBS) 13 | set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) 14 | set(CMAKE_CXX_STANDARD_LIBRARIES "-static-libgcc -static-libstdc++ -lwsock32 -lws2_32 ${CMAKE_CXX_STANDARD_LIBRARIES}") 15 | #This gets already set by libvgm... 16 | #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive") 17 | set(GLFW3_LIBRARIES -static ${GLFW3_STATIC_LIBRARIES} -dynamic) 18 | endif() 19 | else() 20 | option(LINK_STATIC_LIBS "link with static runtime libraries (MinGW only)" OFF) 21 | endif() 22 | 23 | add_subdirectory(ctrmml) 24 | add_subdirectory(libvgm) 25 | 26 | add_library(gui 27 | imgui/imgui.cpp 28 | imgui/imgui_demo.cpp 29 | imgui/imgui_draw.cpp 30 | imgui/imgui_widgets.cpp 31 | imgui/examples/imgui_impl_glfw.cpp 32 | imgui/examples/imgui_impl_opengl3.cpp 33 | imgui/examples/libs/gl3w/GL/gl3w.c 34 | imgui/addons/imguifilesystem/imguifilesystem.cpp 35 | ImGuiColorTextEdit/TextEditor.cpp) 36 | 37 | target_include_directories(gui PUBLIC 38 | imgui 39 | imgui/examples 40 | imgui/examples/libs/gl3w 41 | ImGuiColorTextEdit 42 | ${GLFW3_INCLUDE_DIRS}) 43 | 44 | target_link_libraries(gui PUBLIC OpenGL::GL ${GLFW3_LIBRARIES}) 45 | target_compile_definitions(gui PUBLIC -DIMGUI_IMPL_OPENGL_LOADER_GL3W) 46 | 47 | add_executable(mmlgui 48 | src/main.cpp 49 | src/window.cpp 50 | src/main_window.cpp 51 | src/editor_window.cpp 52 | src/song_manager.cpp 53 | src/track_info.cpp 54 | src/track_view_window.cpp 55 | src/track_list_window.cpp 56 | src/audio_manager.cpp 57 | src/emu_player.cpp 58 | src/config_window.cpp 59 | src/dmf_importer.cpp 60 | src/miniz.c) 61 | 62 | target_link_libraries(mmlgui PRIVATE ctrmml gui vgm-utils vgm-audio vgm-emu) 63 | target_compile_definitions(mmlgui PRIVATE -DLOCAL_LIBVGM) 64 | 65 | if(CPPUNIT_FOUND) 66 | add_executable(mmlgui_unittest 67 | src/track_info.cpp 68 | src/unittest/test_track_info.cpp 69 | src/unittest/main.cpp) 70 | target_link_libraries(mmlgui_unittest ctrmml) 71 | target_link_libraries(mmlgui_unittest ${CPPUNIT_LIBRARIES}) 72 | enable_testing() 73 | add_test(NAME run_mmlgui_unittest COMMAND mmlgui_unittest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 74 | endif() 75 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = src 2 | OBJ = obj 3 | BIN = bin 4 | 5 | CFLAGS = -Wall 6 | LDFLAGS = 7 | 8 | ifneq ($(RELEASE),1) 9 | ifeq ($(ASAN),1) 10 | CFLAGS += -fsanitize=address -O1 -fno-omit-frame-pointer 11 | LDFLAGS += -fsanitize=address 12 | endif 13 | CFLAGS += -g -DDEBUG 14 | else 15 | CFLAGS += -O2 16 | LDFLAGS += -s 17 | endif 18 | 19 | LDFLAGS_TEST = -lcppunit 20 | ifeq ($(OS),Windows_NT) 21 | LDFLAGS += -static-libgcc -static-libstdc++ -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic 22 | else 23 | UNAME_S := $(shell uname -s) 24 | endif 25 | 26 | include ctrmml.mak 27 | include imgui.mak 28 | include libvgm.mak 29 | 30 | $(OBJ)/%.o: $(SRC)/%.cpp 31 | @mkdir -p $(@D) 32 | $(CXX) $(CFLAGS) -MMD -c $< -o $@ 33 | 34 | $(OBJ)/%.o: $(SRC)/%.c 35 | @mkdir -p $(@D) 36 | $(CXX) $(CFLAGS) -MMD -c $< -o $@ 37 | 38 | #====================================================================== 39 | 40 | MMLGUI_BIN = $(BIN)/mmlgui 41 | UNITTEST_BIN = $(BIN)/unittest 42 | 43 | all: $(MMLGUI_BIN) test 44 | 45 | #====================================================================== 46 | # target mmlgui 47 | #====================================================================== 48 | MMLGUI_OBJS = \ 49 | $(IMGUI_OBJS) \ 50 | $(IMGUI_CTE_OBJS) \ 51 | $(OBJ)/main.o \ 52 | $(OBJ)/window.o \ 53 | $(OBJ)/main_window.o \ 54 | $(OBJ)/editor_window.o \ 55 | $(OBJ)/song_manager.o \ 56 | $(OBJ)/track_info.o \ 57 | $(OBJ)/track_view_window.o \ 58 | $(OBJ)/track_list_window.o \ 59 | $(OBJ)/audio_manager.o \ 60 | $(OBJ)/emu_player.o \ 61 | $(OBJ)/config_window.o \ 62 | $(OBJ)/miniz.o \ 63 | $(OBJ)/dmf_importer.o \ 64 | 65 | LDFLAGS_MMLGUI := $(LDFLAGS_IMGUI) $(LDFLAGS_CTRMML) $(LDFLAGS_LIBVGM) 66 | 67 | $(MMLGUI_BIN): $(MMLGUI_OBJS) $(LIBCTRMML_CHECK) 68 | @mkdir -p $(@D) 69 | $(CXX) $(MMLGUI_OBJS) $(LDFLAGS) $(LDFLAGS_MMLGUI) -o $@ 70 | #ifeq ($(OS),Windows_NT) 71 | # cp `which glfw3.dll` $(@D) 72 | #endif 73 | 74 | run: $(MMLGUI_BIN) 75 | $(MMLGUI_BIN) 76 | 77 | #====================================================================== 78 | # target unittest 79 | #====================================================================== 80 | UNITTEST_OBJS = \ 81 | $(OBJ)/track_info.o \ 82 | $(OBJ)/unittest/main.o \ 83 | $(OBJ)/unittest/test_track_info.o 84 | 85 | $(CTRMML_LIB)/lib$(LIBCTRMML).a: 86 | $(MAKE) -C $(CTRMML) lib 87 | 88 | $(UNITTEST_BIN): $(UNITTEST_OBJS) $(LIBCTRMML_CHECK) 89 | @mkdir -p $(@D) 90 | $(CXX) $(UNITTEST_OBJS) $(LDFLAGS) $(LDFLAGS_TEST) -o $@ 91 | 92 | test: $(UNITTEST_BIN) 93 | $(UNITTEST_BIN) 94 | 95 | clean: 96 | rm -rf $(OBJ) 97 | $(MAKE) -C $(CTRMML) clean 98 | rm -rf $(CTRMML_LIB) 99 | 100 | #====================================================================== 101 | 102 | .PHONY: all test run 103 | 104 | -include $(OBJ)/*.d $(OBJ)/unittest/*.d $(IMGUI_CTE_OBJ)/*.d $(IMGUI_OBJ)/*.d 105 | -------------------------------------------------------------------------------- /ctrmml.mak: -------------------------------------------------------------------------------- 1 | CTRMML = ctrmml 2 | CTRMML_SRC = $(CTRMML)/src 3 | CTRMML_LIB = $(CTRMML)/lib 4 | LIBCTRMML = ctrmml 5 | 6 | ifneq ($(RELEASE),1) 7 | LIBCTRMML := $(LIBCTRMML)_debug 8 | endif 9 | 10 | # ctrmml library is in a local submodule and only a static library can be built 11 | CFLAGS += -I$(CTRMML_SRC) 12 | LDFLAGS += -L$(CTRMML_LIB) -l$(LIBCTRMML) 13 | 14 | LIBCTRMML_CHECK := $(CTRMML_LIB)/lib$(LIBCTRMML).a -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superctr/mmlgui/e49f225ac2b2d46056b2c45a5d31c544227c4968/doc/screenshot.png -------------------------------------------------------------------------------- /imgui.mak: -------------------------------------------------------------------------------- 1 | IMGUI_CTE_SRC = ImGuiColorTextEdit 2 | IMGUI_CTE_OBJ = obj/$(IMGUI_CTE_SRC) 3 | IMGUI_SRC = imgui 4 | IMGUI_OBJ = obj/$(IMGUI_SRC) 5 | 6 | ifeq ($(RELEASE),1) 7 | CFLAGS += -DIMGUI_DISABLE_DEMO_WINDOWS=1 -DIMGUI_DISABLE_METRICS_WINDOW=1 8 | endif 9 | 10 | ##--------------------------------------------------------------------- 11 | ## BUILD FLAGS PER PLATFORM 12 | ##--------------------------------------------------------------------- 13 | CFLAGS += -I$(IMGUI_SRC) -I$(IMGUI_SRC)/examples -I$(IMGUI_SRC)/examples/libs/gl3w -DIMGUI_IMPL_OPENGL_LOADER_GL3W -I$(IMGUI_CTE_SRC) 14 | LDFLAGS_IMGUI = 15 | 16 | ifeq ($(UNAME_S), Linux) #LINUX 17 | ECHO_MESSAGE = "Linux" 18 | LDFLAGS_IMGUI += -lGL `pkg-config --static --libs glfw3` 19 | 20 | CFLAGS += `pkg-config --cflags glfw3` 21 | endif 22 | 23 | ifeq ($(UNAME_S), Darwin) #APPLE 24 | ECHO_MESSAGE = "Mac OS X" 25 | LDFLAGS_IMGUI += -framework OpenGL -framework Cocoa -framework IOKit -framework CoreVideo 26 | LDFLAGS_IMGUI += -L/usr/local/lib -L/opt/local/lib 27 | #LDFLAGS_IMGUI += -lglfw3 28 | LDFLAGS_IMGUI += -lglfw 29 | 30 | CFLAGS += -I/usr/local/include -I/opt/local/include 31 | endif 32 | 33 | ifeq ($(OS),Windows_NT) 34 | ECHO_MESSAGE = "MinGW" 35 | LDFLAGS_IMGUI += -Wl,-Bstatic -lglfw3 -Wl,-Bdynamic -lgdi32 -lopengl32 -limm32 36 | 37 | CFLAGS += `pkg-config --cflags glfw3` 38 | endif 39 | 40 | IMGUI_OBJS = \ 41 | $(IMGUI_OBJ)/imgui.o \ 42 | $(IMGUI_OBJ)/imgui_demo.o \ 43 | $(IMGUI_OBJ)/imgui_draw.o \ 44 | $(IMGUI_OBJ)/imgui_widgets.o \ 45 | $(IMGUI_OBJ)/examples/imgui_impl_glfw.o \ 46 | $(IMGUI_OBJ)/examples/imgui_impl_opengl3.o \ 47 | $(IMGUI_OBJ)/examples/libs/gl3w/GL/gl3w.o \ 48 | $(IMGUI_OBJ)/addons/imguifilesystem/imguifilesystem.o 49 | 50 | IMGUI_CTE_OBJS = \ 51 | $(IMGUI_CTE_OBJ)/TextEditor.o \ 52 | 53 | $(IMGUI_CTE_OBJ)/%.o: $(IMGUI_CTE_SRC)/%.cpp 54 | @mkdir -p $(@D) 55 | $(CXX) $(CFLAGS) -MMD -c $< -o $@ 56 | 57 | $(IMGUI_OBJ)/%.o: $(IMGUI_SRC)/%.cpp 58 | @mkdir -p $(@D) 59 | $(CXX) $(CFLAGS) -MMD -c $< -o $@ 60 | 61 | $(IMGUI_OBJ)/%.o: $(IMGUI_SRC)/%.c 62 | @mkdir -p $(@D) 63 | $(CC) $(CFLAGS) -MMD -c $< -o $@ 64 | -------------------------------------------------------------------------------- /libvgm.mak: -------------------------------------------------------------------------------- 1 | # libvgm should be installed on the system and we must tell the linker to 2 | # pick the statically build version (to avoid .so headaches) 3 | 4 | # Platform specific flags for libvgm 5 | ifeq ($(OS),Windows_NT) 6 | CFLAGS += -DAUDDRV_WINMM -DAUDDRV_DSOUND -DAUDDRV_XAUD2 7 | LDFLAGS_LIBVGM += -Wl,-Bstatic -lvgm-audio -lvgm-emu -lvgm-utils -Wl,-Bdynamic 8 | LDFLAGS_LIBVGM += -ldsound -luuid -lwinmm -lole32 9 | else 10 | CFLAGS += `pkg-config --with-path=/usr/local/lib/pkgconfig --cflags vgm-audio` 11 | LDFLAGS_LIBVGM += -Wl,-rpath,/usr/local/lib `pkg-config --with-path=/usr/local/lib/pkgconfig --libs vgm-audio vgm-emu` 12 | endif 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | mmlgui 2 | ====== 3 | 4 | ### [Latest automatic build can be found here](https://nightly.link/superctr/mmlgui/workflows/win32-i686/master/binary) 5 | 6 | MML (Music Macro Language) editor and compiler GUI. Powered by the [ctrmml](https://github.com/superctr/ctrmml) framework. 7 | 8 | ## Features 9 | 10 | - MML text editor with instant feedback 11 | - Supports Sega Mega Drive sound chips (YM2612, SN76489) 12 | - Visualization of MML tracks 13 | - Highlights currently playing notes in the editor window 14 | - VGM and MDS file export 15 | - Supports channel muting/isolation 16 | 17 | ### Future features / TODO 18 | 19 | - Configuration window 20 | - Support more sound chips 21 | - More track visualizations 22 | 23 | ## Using 24 | 25 | For Windows (or Wine) users, you can download the latest automatic build from [here](https://nightly.link/superctr/mmlgui/workflows/win32-i686/master/binary) 26 | 27 | Feel free to join the [Discord server](https://discord.com/invite/BPwM6PJv7T) 28 | if you have any questions or feedback. 29 | 30 | See [ctrmml/mml_ref.md](https://github.com/superctr/ctrmml/blob/master/mml_ref.md) for an MML reference. 31 | 32 | ![Screenshot](doc/screenshot.png) 33 | 34 | ## Compiling using CMake 35 | 36 | ### Prerequisites 37 | First, make sure all submodules have been cloned. If you didn't clone this repository 38 | with `git clone --recurse-submodules`, do this: 39 | 40 | git submodule update --init 41 | 42 | Make sure you have the following packages installed: (this list might not be 100% correct) 43 | 44 | glfw 45 | 46 | You also need to have `pkg-config` (or a compatible equivalent such as `pkgconf`) installed. 47 | 48 | Notice that there is no need to install libvgm for CMake builds, as it is built by the mmlgui target. 49 | 50 | #### Building 51 | 52 | mkdir build && cd build 53 | cmake .. 54 | cmake --build . 55 | ./mmlgui 56 | 57 | To build a debug or release version, add `-DCMAKE_BUILD_TYPE=Debug` or `-DCMAKE_BUILD_TYPE=Release` to the first 58 | cmake call. 59 | 60 | Note that once cmake has setup the build environment, it is only needed to call `cmake --build .` for successive builds. 61 | 62 | ## Compiling using the makefile 63 | The makefile is currently used for static linked builds for Windows as well as for address sanitizer builds. 64 | Once these features are integrated in the CMake build, the makefiles will most likely be removed. 65 | 66 | ### Prerequisites 67 | First, make sure all submodules have been cloned. If you didn't clone this repository 68 | with `git clone --recurse-submodules`, do this: 69 | 70 | git submodule update --init 71 | 72 | Make sure you have the following packages installed: (this list might not be 100% correct) 73 | 74 | glfw libvgm 75 | 76 | #### Installing libvgm 77 | 78 | If libvgm is not available on your system, you need to install it manually. Example: 79 | 80 | ##### Linux 81 | 82 | mkdir build && cd build 83 | cmake .. 84 | make 85 | sudo make install 86 | 87 | ##### Windows (MSYS2) 88 | The `CMAKE_INSTALL_PREFIX` should of course match where your MSYS2/MinGW install folder is. 89 | 90 | mkdir build && cd build 91 | cmake .. -G "MSYS Makefiles" -D AUDIODRV_LIBAO=OFF -D CMAKE_INSTALL_PREFIX=/mingw64 92 | make 93 | make install 94 | 95 | ### Building 96 | Once all prerequisites are installed, it should be as easy as 97 | 98 | make 99 | 100 | To build a release binary, add `RELEASE=1`. 101 | 102 | ### Special thanks 103 | 104 | - [libvgm](https://github.com/ValleyBell/libvgm) 105 | - [glfw](https://www.glfw.org/) 106 | - [Dear ImGui](https://github.com/ocornut/imgui) 107 | - [Dear ImGui addons branch](https://github.com/Flix01/imgui) 108 | - [ImGuiColorTextEditor](https://github.com/BalazsJako/ImGuiColorTextEdit) 109 | 110 | ### Copyright 111 | 112 | © 2020-2021 Ian Karlsson 113 | 114 | This program is free software; you can redistribute it and/or modify 115 | it under the terms of the GNU General Public License as published by 116 | the Free Software Foundation; either version 2 of the License, or 117 | (at your option) any later version. 118 | 119 | This program is distributed in the hope that it will be useful, 120 | but WITHOUT ANY WARRANTY; without even the implied warranty of 121 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 122 | GNU General Public License for more details. 123 | 124 | You should have received a copy of the GNU General Public License along 125 | with this program; if not, write to the Free Software Foundation, Inc., 126 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 127 | -------------------------------------------------------------------------------- /src/audio_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "audio_manager.h" 2 | 3 | // for debug output 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #if defined(LOCAL_LIBVGM) 10 | #include "audio/AudioStream.h" 11 | #include "audio/AudioStream_SpcDrvFuns.h" 12 | #else 13 | #include 14 | #include 15 | #endif 16 | 17 | //! First time initialization 18 | Audio_Manager::Audio_Manager() 19 | : driver_sig(-1) 20 | , driver_id(-1) 21 | , device_id(-1) 22 | , sample_rate(44100) 23 | , sample_size(4) 24 | , volume(1.0) 25 | , converted_volume(0x100) 26 | , streams() 27 | , window_handle(nullptr) 28 | , driver_handle(nullptr) 29 | , waiting_for_handle(false) 30 | , audio_initialized(false) 31 | , driver_opened(false) 32 | , device_opened(false) 33 | , driver_list() 34 | , device_list() 35 | { 36 | if (Audio_Init()) 37 | { 38 | fprintf(stderr, "Warning: audio initialization failed. Muting audio\n"); 39 | } 40 | else 41 | { 42 | audio_initialized = true; 43 | enumerate_drivers(); 44 | } 45 | } 46 | 47 | //! get the singleton instance of Audio_Manager 48 | Audio_Manager& Audio_Manager::get() 49 | { 50 | static Audio_Manager instance; 51 | return instance; 52 | } 53 | 54 | void Audio_Manager::set_driver(int new_driver_sig, int new_device_id) 55 | { 56 | std::string name = ""; 57 | 58 | if(!audio_initialized) 59 | return; 60 | 61 | // Reset device to default if we have already opened a device 62 | device_id = new_device_id; 63 | 64 | close_driver(); 65 | 66 | // Find a driver ID 67 | driver_sig = new_driver_sig; 68 | driver_id = -1; 69 | if(driver_sig != -1) 70 | { 71 | auto it = driver_list.find(driver_sig); 72 | 73 | if(it != driver_list.end()) 74 | { 75 | driver_id = it->second.first; 76 | name = it->second.second; 77 | open_driver(); 78 | } 79 | else 80 | { 81 | fprintf(stderr, "Warning: Driver id %02x unavailable, using default\n", driver_sig); 82 | } 83 | } 84 | 85 | if(driver_id == -1) 86 | { 87 | // pick the next one automatically if loading fails 88 | for(auto && i : driver_list) 89 | { 90 | driver_sig = i.first; 91 | driver_id = i.second.first; 92 | name = i.second.second; 93 | if(!open_driver()) 94 | break; 95 | } 96 | } 97 | 98 | if(driver_opened) 99 | { 100 | fprintf(stdout, "Loaded audio driver: '%s'\n", name.c_str()); 101 | enumerate_devices(); 102 | set_device(device_id); 103 | } 104 | else 105 | { 106 | if(driver_id == -1) 107 | fprintf(stderr, "Warning: No available drivers\n"); 108 | else 109 | fprintf(stderr, "Warning: Failed to open driver '%s'\n", name.c_str()); 110 | } 111 | } 112 | 113 | void Audio_Manager::set_device(int new_device_id) 114 | { 115 | std::string name = ""; 116 | 117 | if(!driver_opened) 118 | return; 119 | 120 | close_device(); 121 | 122 | // Find a driver ID 123 | device_id = -1; 124 | if(new_device_id != -1) 125 | { 126 | auto it = device_list.find(new_device_id); 127 | 128 | if(it != device_list.end()) 129 | { 130 | device_id = it->first; 131 | name = it->second; 132 | open_device(); 133 | } 134 | else 135 | { 136 | fprintf(stderr, "Warning: Device id %02x unavailable, using default\n", new_device_id); 137 | } 138 | } 139 | 140 | if(device_id == -1) 141 | { 142 | // pick the next one automatically if loading fails 143 | for(auto && i : device_list) 144 | { 145 | device_id = i.first; 146 | name = i.second; 147 | if(!open_device()) 148 | break; 149 | } 150 | } 151 | 152 | if(!device_opened) 153 | { 154 | if(device_id == -1) 155 | fprintf(stderr, "Warning: No available devices\n"); 156 | else 157 | fprintf(stderr, "Warning: Failed to open device '%s'\n", name.c_str()); 158 | } 159 | else 160 | { 161 | fprintf(stdout, "Loaded audio device: '%s'\n", name.c_str()); 162 | } 163 | } 164 | 165 | //! Get global volume 166 | bool Audio_Manager::get_audio_enabled() const 167 | { 168 | return audio_initialized && driver_opened && device_opened; 169 | } 170 | 171 | //! Set window handle (for APIs where this is required) 172 | void Audio_Manager::set_window_handle(void* new_handle) 173 | { 174 | window_handle = new_handle; 175 | if(waiting_for_handle) 176 | { 177 | if(!open_driver()) 178 | waiting_for_handle = false; 179 | } 180 | if(driver_opened && !device_opened) 181 | { 182 | open_device(); 183 | } 184 | } 185 | 186 | 187 | //! Set global sample rate. 188 | /*! 189 | * This function should re-initialize the streams with the new sample rate, if needed. 190 | * (Using Audio_Stream::stop_stream() and Audio_Stream::start_stream()) 191 | * 192 | * \return zero if successful, non-zero otherwise. 193 | */ 194 | int Audio_Manager::set_sample_rate(uint32_t new_sample_rate) 195 | { 196 | // TODO: reinit streams 197 | std::lock_guard lock(mutex); 198 | sample_rate = new_sample_rate; 199 | return 0; 200 | } 201 | 202 | //! Set global volume 203 | void Audio_Manager::set_volume(float new_volume) 204 | { 205 | volume = volume; 206 | converted_volume = volume * 256.0; 207 | } 208 | 209 | //! Get global volume 210 | float Audio_Manager::get_volume() const 211 | { 212 | return volume; 213 | } 214 | 215 | //! Add an audio stream 216 | int Audio_Manager::add_stream(std::shared_ptr stream) 217 | { 218 | std::lock_guard lock(mutex); 219 | stream->setup_stream(sample_rate); 220 | printf("Adding stream %s\n", typeid(*stream).name()); 221 | streams.push_back(stream); 222 | return 0; 223 | } 224 | 225 | //! Kill all streams and close audio system 226 | void Audio_Manager::clean_up() 227 | { 228 | close_driver(); 229 | if(audio_initialized) 230 | Audio_Deinit(); 231 | } 232 | 233 | //===================================================================== 234 | 235 | void Audio_Manager::enumerate_drivers() 236 | { 237 | driver_list.clear(); 238 | 239 | unsigned int driver_count = Audio_GetDriverCount(); 240 | if(!driver_count) 241 | { 242 | fprintf(stderr, "Warning: no audio drivers available. Muting audio\n"); 243 | } 244 | else 245 | { 246 | printf("Available audio drivers ...\n"); 247 | for(unsigned int i = 0; i < driver_count; i++) 248 | { 249 | AUDDRV_INFO* info; 250 | Audio_GetDriverInfo(i, &info); 251 | 252 | if(info->drvType == ADRVTYPE_OUT) 253 | { 254 | printf("%d = '%s' (type = %02x, sig = %02x)\n", i, info->drvName, info->drvType, info->drvSig); 255 | driver_list[info->drvSig] = std::make_pair(i, info->drvName); 256 | } 257 | } 258 | } 259 | } 260 | 261 | void Audio_Manager::enumerate_devices() 262 | { 263 | if(!driver_opened) 264 | return; 265 | 266 | device_list.clear(); 267 | 268 | const AUDIO_DEV_LIST* list = AudioDrv_GetDeviceList(driver_handle); 269 | if(!list->devCount) 270 | { 271 | fprintf(stderr, "Warning: no audio devices available. Muting audio\n"); 272 | } 273 | else 274 | { 275 | printf("Available audio devices ...\n"); 276 | for(unsigned int i = 0; i < list->devCount; i++) 277 | { 278 | AUDDRV_INFO* info; 279 | Audio_GetDriverInfo(i, &info); 280 | 281 | if(info->drvType) 282 | { 283 | printf("%d = '%s'\n", i, list->devNames[i]); 284 | device_list[i] = list->devNames[i]; 285 | } 286 | } 287 | } 288 | } 289 | 290 | //! Open a driver 291 | int Audio_Manager::open_driver() 292 | { 293 | AUDDRV_INFO* info; 294 | Audio_GetDriverInfo(driver_id, &info); 295 | 296 | #ifdef AUDDRV_DSOUND 297 | if(info->drvSig == ADRVSIG_DSOUND && !window_handle) 298 | { 299 | printf("Please give me a HWND first\n"); 300 | waiting_for_handle = true; 301 | return 0; 302 | } 303 | #endif 304 | 305 | uint8_t error_code; 306 | error_code = AudioDrv_Init(driver_id, &driver_handle); 307 | if(error_code) 308 | { 309 | printf("AudioDrv_Init error %02x\n", error_code); 310 | return -1; 311 | } 312 | 313 | #ifdef AUDDRV_DSOUND 314 | if(info->drvSig == ADRVSIG_DSOUND && window_handle) 315 | { 316 | void* dsoundDrv; 317 | dsoundDrv = AudioDrv_GetDrvData(driver_handle); 318 | DSound_SetHWnd(dsoundDrv, (HWND)window_handle); 319 | } 320 | #endif 321 | 322 | #ifdef AUDDRV_PULSE 323 | if(info->drvSig == ADRVSIG_PULSE) 324 | { 325 | void* pulseDrv; 326 | pulseDrv = AudioDrv_GetDrvData(driver_handle); 327 | Pulse_SetStreamDesc(pulseDrv, "mmlgui"); 328 | } 329 | #endif 330 | 331 | enumerate_devices(); 332 | driver_opened = true; 333 | return 0; 334 | } 335 | 336 | void Audio_Manager::close_driver() 337 | { 338 | if(device_opened) 339 | close_device(); 340 | if(driver_opened) 341 | AudioDrv_Deinit(&driver_handle); 342 | } 343 | 344 | int Audio_Manager::open_device() 345 | { 346 | if(device_opened) 347 | return -1; 348 | const std::lock_guard lock(mutex); 349 | auto opts = AudioDrv_GetOptions(driver_handle); 350 | opts->numChannels = 2; 351 | opts->numBitsPerSmpl = 16; 352 | sample_size = opts->numChannels * opts->numBitsPerSmpl / 8; 353 | AudioDrv_SetCallback(driver_handle, Audio_Manager::callback, NULL); 354 | int error_code = AudioDrv_Start(driver_handle, device_id); 355 | if(error_code) 356 | { 357 | printf("AudioDrv_Start error %02x\n", error_code); 358 | return -1; 359 | } 360 | device_opened = true; 361 | return 0; 362 | } 363 | 364 | void Audio_Manager::close_device() 365 | { 366 | // libvgm may halt if we lock our own mutex before calling this. (ALSA) 367 | AudioDrv_Stop(driver_handle); 368 | device_opened = false; 369 | } 370 | 371 | inline int16_t Audio_Manager::clip16(int32_t input) 372 | { 373 | if(input > 32767) 374 | input = 32767; 375 | else if(input < -32768) 376 | input = -32768; 377 | return input; 378 | } 379 | 380 | uint32_t Audio_Manager::callback(void* drv_struct, void* user_param, uint32_t buf_size, void* data) 381 | { 382 | Audio_Manager& am = Audio_Manager::get(); 383 | int sample_count = buf_size / am.sample_size; 384 | std::vector buffer(sample_count, {0, 0}); 385 | const std::lock_guard lock(am.mutex); 386 | 387 | // Input buffer 388 | for(auto stream = am.streams.begin(); stream != am.streams.end();) 389 | { 390 | Audio_Stream* s = stream->get(); 391 | s->get_sample(buffer.data(), sample_count, 2); 392 | if(s->get_finished()) 393 | { 394 | printf("Removing stream %s\n", typeid(*s).name()); 395 | s->stop_stream(); 396 | am.streams.erase(stream); 397 | } 398 | else 399 | { 400 | stream++; 401 | } 402 | } 403 | 404 | // Output buffer 405 | switch(am.sample_size) 406 | { 407 | case 4: 408 | { 409 | int16_t* sd = (int16_t*) data; 410 | for(int i = 0; i < sample_count; i ++) 411 | { 412 | int32_t l = buffer[i].L >> 8; 413 | int32_t r = buffer[i].R >> 8; 414 | *sd++ = clip16((l * am.converted_volume) >> 8); 415 | *sd++ = clip16((r * am.converted_volume) >> 8); 416 | } 417 | return sample_count * am.sample_size; 418 | } 419 | default: 420 | { 421 | return 0; 422 | } 423 | } 424 | } 425 | 426 | -------------------------------------------------------------------------------- /src/audio_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIO_MANAGER_H 2 | #define AUDIO_MANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #if defined(LOCAL_LIBVGM) 11 | #include "audio/AudioStream.h" 12 | #include "emu/Resampler.h" 13 | #else 14 | #include 15 | #include 16 | #endif 17 | 18 | //! Abstract class for audio stream control 19 | class Audio_Stream 20 | { 21 | public: 22 | inline Audio_Stream() 23 | : finished(false) 24 | {} 25 | 26 | inline virtual ~Audio_Stream() 27 | {} 28 | 29 | //! called by Audio_Manager when starting the stream. 30 | /*! 31 | * setup your resamplers and stuff here. 32 | */ 33 | virtual void setup_stream(uint32_t sample_rate) = 0; 34 | 35 | //! called by Audio_Manager during stream update. 36 | /*! 37 | * return zero to indicate that the stream should be stopped. 38 | */ 39 | virtual int get_sample(WAVE_32BS* output, int count, int channels) = 0; 40 | 41 | //! called by Audio_Manager when stopping the stream. 42 | /*! 43 | * resamplers should be cleaned up, but the playback may start again 44 | * so the "finished" state should not be updated here. 45 | */ 46 | virtual void stop_stream() = 0; 47 | 48 | //! get the "finished" flag status 49 | /*! 50 | * when set, the audio manager will stop mixing this stream and 51 | * destroy its pointer. 52 | */ 53 | inline void set_finished(bool flag) 54 | { 55 | finished = true; 56 | } 57 | 58 | //! get the "finished" flag status 59 | /*! 60 | * when set, the audio manager will stop mixing this stream and 61 | * destroy its pointer. 62 | */ 63 | inline bool get_finished() 64 | { 65 | return finished; 66 | } 67 | 68 | protected: 69 | bool finished; 70 | }; 71 | 72 | //! Audio manager class 73 | /*! 74 | * Please note that initialization/deinitialization of the libvgm audio library 75 | * (Audio_Init() and Audio_Deinit() should be done in the main function first. 76 | */ 77 | class Audio_Manager 78 | { 79 | public: 80 | // singleton guard 81 | Audio_Manager(Audio_Manager const&) = delete; 82 | void operator=(Audio_Manager const&) = delete; 83 | 84 | static Audio_Manager& get(); 85 | 86 | bool get_audio_enabled() const; 87 | 88 | void set_window_handle(void* new_handle); 89 | 90 | int set_sample_rate(uint32_t new_sample_rate); 91 | void set_volume(float new_volume); 92 | float get_volume() const; 93 | 94 | void set_driver(int new_driver_sig, int new_device_id = -1); 95 | inline int get_driver() const { return driver_sig; }; 96 | 97 | void set_device(int new_device_id); 98 | inline int get_device() const { return device_id; }; 99 | 100 | int add_stream(std::shared_ptr stream); 101 | 102 | const std::map>& get_driver_list() const { return driver_list; } 103 | const std::map& get_device_list() const { return device_list; } 104 | 105 | void clean_up(); 106 | 107 | private: 108 | Audio_Manager(); 109 | 110 | void enumerate_drivers(); 111 | void enumerate_devices(); 112 | 113 | int open_driver(); 114 | void close_driver(); 115 | 116 | int open_device(); 117 | void close_device(); 118 | 119 | static int16_t clip16(int32_t input); 120 | 121 | static uint32_t callback(void* drv_struct, void* user_param, uint32_t buf_size, void* data); 122 | 123 | int driver_sig; // Actual driver signature, -1 if not loaded 124 | int driver_id; // Actual driver id, -1 if not loaded 125 | int device_id; // Actual device id, -1 if not loaded 126 | 127 | uint32_t sample_rate; 128 | uint32_t sample_size; 129 | 130 | float volume; 131 | int32_t converted_volume; 132 | std::vector> streams; 133 | 134 | void* window_handle; 135 | void* driver_handle; 136 | 137 | bool waiting_for_handle; 138 | bool audio_initialized; 139 | bool driver_opened; 140 | bool device_opened; 141 | 142 | std::map> driver_list; 143 | std::map device_list; 144 | 145 | std::mutex mutex; 146 | }; 147 | 148 | #endif 149 | -------------------------------------------------------------------------------- /src/config_window.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui.h" 2 | #include "config_window.h" 3 | 4 | //===================================================================== 5 | Config_Window::Config_Window() 6 | { 7 | type = WT_CONFIG; 8 | } 9 | 10 | void Config_Window::display() 11 | { 12 | std::string window_id; 13 | window_id = "Configuration##" + std::to_string(id); 14 | 15 | ImGui::SetNextWindowSize(ImVec2(500,0), ImGuiCond_Always); 16 | ImGui::Begin(window_id.c_str(), &active, ImGuiWindowFlags_NoResize); 17 | ImGui::TextColored(ImVec4(250,0,0,255), "This window is currently a placeholder."); 18 | 19 | ImGui::BeginChild("tab_bar", ImVec2(0, 500), false); 20 | ImGui::PushItemWidth(ImGui::GetFontSize() * -12); 21 | 22 | if (ImGui::BeginTabBar("tabs", ImGuiTabBarFlags_None)) 23 | { 24 | if (ImGui::BeginTabItem("Audio")) 25 | { 26 | show_audio_tab(); 27 | ImGui::EndTabItem(); 28 | } 29 | if (ImGui::BeginTabItem("Emulator")) 30 | { 31 | show_emu_tab(); 32 | ImGui::EndTabItem(); 33 | } 34 | if (ImGui::BeginTabItem("Mixer")) 35 | { 36 | show_mixer_tab(); 37 | ImGui::EndTabItem(); 38 | } 39 | ImGui::EndTabBar(); 40 | } 41 | ImGui::PopItemWidth(); 42 | ImGui::EndChild(); 43 | 44 | show_confirm_buttons(); 45 | 46 | ImGui::End(); 47 | } 48 | 49 | void Config_Window::show_audio_tab() 50 | { 51 | if (ImGui::ListBoxHeader("Audio driver", 5)) 52 | { 53 | ImGui::Selectable("Placeholder", true); 54 | ImGui::Selectable("Placeholder", false); 55 | ImGui::ListBoxFooter(); 56 | } 57 | ImGui::Separator(); 58 | if (ImGui::ListBoxHeader("Audio device", 5)) 59 | { 60 | ImGui::Selectable("Placeholder", true); 61 | ImGui::Selectable("Placeholder", false); 62 | ImGui::Selectable("Placeholder", false); 63 | ImGui::Selectable("Placeholder", false); 64 | ImGui::Selectable("Placeholder", false); 65 | ImGui::Selectable("Placeholder", false); 66 | ImGui::Selectable("Placeholder", false); 67 | ImGui::Selectable("Placeholder", false); 68 | ImGui::ListBoxFooter(); 69 | } 70 | } 71 | 72 | void Config_Window::show_emu_tab() 73 | { 74 | if (ImGui::ListBoxHeader("SN76489 emulator", 5)) 75 | { 76 | ImGui::Selectable("Placeholder", true); 77 | ImGui::Selectable("Placeholder", false); 78 | ImGui::ListBoxFooter(); 79 | } 80 | ImGui::Separator(); 81 | if (ImGui::ListBoxHeader("YM2612 emulator", 5)) 82 | { 83 | ImGui::Selectable("Placeholder", true); 84 | ImGui::Selectable("Placeholder", false); 85 | ImGui::ListBoxFooter(); 86 | } 87 | } 88 | 89 | void Config_Window::show_mixer_tab() 90 | { 91 | static int global_vol = 100; 92 | static int sn_vol = 100; 93 | static int ym_vol = 100; 94 | ImGui::SliderInt("Global volume", &global_vol, 0, 100); 95 | ImGui::Separator(); 96 | ImGui::SliderInt("SN76489", &sn_vol, 0, 100); 97 | ImGui::SliderInt("YM2612", &ym_vol, 0, 100); 98 | } 99 | 100 | void Config_Window::show_confirm_buttons() 101 | { 102 | float content = ImGui::GetContentRegionMax().x; 103 | //float offset = content * 0.4f; // with progress bar 104 | float offset = content * 0.75f; // without progress bar 105 | float width = content - offset; 106 | float curpos = ImGui::GetCursorPos().x; 107 | if(offset < curpos) 108 | offset = curpos; 109 | ImGui::SetCursorPosX(offset); 110 | 111 | // PushNextItemWidth doesn't work for some stupid reason, width must be set manually 112 | auto size = ImVec2(content * 0.1f, 0); 113 | if(size.x < ImGui::GetFontSize() * 4.0f) 114 | size.x = ImGui::GetFontSize() * 4.0f; 115 | 116 | if(ImGui::Button("OK", size)) 117 | { 118 | active = false; 119 | } 120 | ImGui::SameLine(); 121 | if(ImGui::Button("Cancel", size)) 122 | { 123 | active = false; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/config_window.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_WINDOW_H 2 | #define CONFIG_WINDOW_H 3 | 4 | #include "window.h" 5 | 6 | //! Config window container 7 | class Config_Window : public Window 8 | { 9 | public: 10 | Config_Window(); 11 | void display() override; 12 | 13 | private: 14 | void show_audio_tab(); 15 | void show_emu_tab(); 16 | void show_mixer_tab(); 17 | void show_confirm_buttons(); 18 | }; 19 | 20 | #endif //CONFIG_WINDOW_H 21 | 22 | -------------------------------------------------------------------------------- /src/dmf_importer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "dmf_importer.h" 13 | #include "stringf.h" 14 | 15 | #define MINIZ_HEADER_FILE_ONLY 16 | #include "miniz.c" 17 | 18 | Dmf_Importer::Dmf_Importer(const char* filename) 19 | { 20 | if(std::ifstream is{filename, std::ios::binary | std::ios::ate}) 21 | { 22 | auto size = is.tellg(); 23 | std::vector dmfz(size, '\0'); 24 | is.seekg(0); 25 | if(is.read((char*)&dmfz[0],size)) 26 | { 27 | uLong dmfsize = 0x1000000; 28 | data.resize(dmfsize, 0); 29 | 30 | int res; 31 | res = uncompress((Byte*) data.data(), &dmfsize, (Byte*) dmfz.data(), dmfz.size()); 32 | if (res != Z_OK) 33 | { 34 | error_output = "File could not be decompressed\n"; 35 | } 36 | else 37 | { 38 | parse(); 39 | } 40 | } 41 | } 42 | else 43 | { 44 | error_output = "File could not be opened\n"; 45 | } 46 | } 47 | 48 | std::string Dmf_Importer::get_error() 49 | { 50 | return error_output; 51 | } 52 | 53 | std::string Dmf_Importer::get_mml() 54 | { 55 | return mml_output; 56 | } 57 | 58 | static inline uint32_t read_le32(uint8_t* data) 59 | { 60 | return data[0]|(data[1]<<8)|(data[2]<<16)|(data[3]<<24); 61 | } 62 | 63 | static inline std::string read_str(uint8_t* data, uint8_t length) 64 | { 65 | std::string out; 66 | for(int i = 0; i < length; i++) 67 | { 68 | out.push_back(*data++); 69 | } 70 | return out; 71 | } 72 | 73 | void Dmf_Importer::parse() 74 | { 75 | uint8_t tmp8; 76 | 77 | uint8_t* dmfptr = data.data(); 78 | 79 | dmfptr += 16; // skip header 80 | 81 | uint8_t version = *dmfptr++; 82 | if(version != 0x18) 83 | { 84 | error_output = stringf("Incompatible DMF version (found '0x%02x', expected '0x18')\nTry opening and saving the file in Deflemask legacy.",version); 85 | return; 86 | } 87 | 88 | uint8_t system = *dmfptr++; 89 | if((system & 0x3f) != 0x02) 90 | { 91 | error_output = "Sorry, the DMF must be a GENESIS module.\n"; 92 | return; 93 | } 94 | 95 | channel_count = 10; 96 | if(system & 0x40) 97 | channel_count += 3; 98 | 99 | // Skip song name 100 | tmp8 = *dmfptr++; 101 | dmfptr += tmp8; 102 | 103 | // Skip song author 104 | tmp8 = *dmfptr++; 105 | dmfptr += tmp8; 106 | 107 | // Skip highlight A/B, timebase, frame mode, custom HZ 108 | dmfptr += 10; 109 | 110 | pattern_rows = read_le32(dmfptr); 111 | dmfptr += 4; 112 | //printf("Number of pattern rows: %d (%08x)\n", dmf_pattern_rows, dmf_pattern_rows); 113 | 114 | matrix_rows = *dmfptr++; 115 | //printf("Number of pattern matrix rows: %d\n", dmf_matrix_rows); 116 | 117 | // Skip pattern matrix rows 118 | dmfptr += channel_count * matrix_rows; 119 | 120 | // Now read instruments 121 | instrument_count = *dmfptr++; 122 | 123 | for(int ins_counter = 0; ins_counter < instrument_count; ins_counter ++) 124 | { 125 | tmp8 = *dmfptr++; 126 | auto name = read_str(dmfptr, tmp8); 127 | dmfptr += tmp8; 128 | 129 | uint8_t type = *dmfptr++; 130 | 131 | if(type == 0) 132 | { 133 | dmfptr = parse_psg_instrument(dmfptr, ins_counter, name); 134 | } 135 | else if(type == 1) 136 | { 137 | dmfptr = parse_fm_instrument(dmfptr, ins_counter, name); 138 | } 139 | else 140 | { 141 | error_output = "Encountered an unknown instrument type.\n"; 142 | return; 143 | } 144 | } 145 | } 146 | 147 | #define ALG ptr[0] 148 | #define FB ptr[1] 149 | #define AR ptr[op+1] 150 | #define DR ptr[op+2] 151 | #define ML ptr[op+3] 152 | #define RR ptr[op+4] 153 | #define SL ptr[op+5] 154 | #define TL ptr[op+6] 155 | #define KS ptr[op+8] 156 | #define DT dt_table[ptr[op+9]] 157 | #define SR ptr[op+10] 158 | #define SSG (ptr[op+11] | ptr[op+0] * 100) 159 | 160 | uint8_t* Dmf_Importer::parse_fm_instrument(uint8_t* ptr, int id, std::string str) 161 | { 162 | uint8_t op_table[4] = {4, 28, 16, 40}; 163 | uint8_t dt_table[7] = {7, 6, 5, 0, 1, 2, 3}; 164 | 165 | mml_output += stringf("@%d fm %d %d ; %s\n", id, ALG, FB, str.c_str()); 166 | for(int opr=0; opr<4; opr++) 167 | { 168 | uint8_t op = op_table[opr]; 169 | mml_output += stringf(" %3d %3d %3d %3d %3d %3d %3d %3d %3d %3d \n", 170 | AR, DR, SR, RR, SL, TL, KS, ML, DT, SSG); 171 | } 172 | return ptr + 52; 173 | } 174 | 175 | uint8_t* Dmf_Importer::parse_psg_instrument(uint8_t* ptr, int id, std::string str) 176 | { 177 | uint8_t size; 178 | uint8_t loop; 179 | 180 | size = *ptr++; 181 | if(size) 182 | { 183 | mml_output += stringf("@%d psg ; %s", id, str.c_str()); 184 | loop = ptr[size * 4]; 185 | 186 | for(int i=0; i 5 | #include 6 | #include 7 | 8 | class Dmf_Importer 9 | { 10 | public: 11 | Dmf_Importer(const char* filename); 12 | 13 | std::string get_error(); 14 | std::string get_mml(); 15 | 16 | private: 17 | void parse(); 18 | uint8_t* parse_fm_instrument(uint8_t* ptr, int id, std::string str); 19 | uint8_t* parse_psg_instrument(uint8_t* ptr, int id, std::string str); 20 | 21 | std::string error_output; 22 | std::string mml_output; 23 | std::vector data; 24 | 25 | uint8_t channel_count; 26 | uint8_t pattern_rows; 27 | uint8_t matrix_rows; 28 | uint8_t instrument_count; 29 | 30 | }; 31 | #endif 32 | -------------------------------------------------------------------------------- /src/editor_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MML text editor 3 | 4 | todo 5 | 1. file i/o key shortcuts 6 | 2. syntax highlighting for MML 7 | 8 | bugs in the editor (to be fixed in my fork of ImGuiColorTextEdit) 9 | 1. highlighted area when doubleclicking doesn't take account punctuation 10 | (based on the code, This should hopefully automatically be fixed when 11 | MML highlighting is done) 12 | 2. data copied to clipboard from mmlgui has UNIX-linebreaks regardless of OS. 13 | This may break some windows tools (such as Notepad.exe) 14 | 15 | "bugs" in the imgui addons branch (maybe can be fixed locally and upstreamed?) 16 | 1. file dialog doesn't automatically focus the filename. 17 | 2. file dialog has no keyboard controls at all :/ 18 | */ 19 | 20 | #include "main_window.h" 21 | #include "editor_window.h" 22 | #include "track_view_window.h" 23 | #include "track_list_window.h" 24 | 25 | #include "dmf_importer.h" 26 | 27 | #include "imgui.h" 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | enum Flags 35 | { 36 | MODIFIED = 1<<0, 37 | FILENAME_SET = 1<<1, 38 | NEW = 1<<2, 39 | OPEN = 1<<3, 40 | SAVE = 1<<4, 41 | SAVE_AS = 1<<5, 42 | DIALOG = 1<<6, 43 | IGNORE_WARNING = 1<<7, 44 | RECOMPILE = 1<<8, 45 | EXPORT = 1<<9, 46 | IMPORT = 1<<10, 47 | }; 48 | 49 | Editor_Window::Editor_Window() 50 | : editor() 51 | , filename(default_filename) 52 | , flag(RECOMPILE) 53 | , line_pos(0) 54 | , cursor_pos(0) 55 | { 56 | type = WT_EDITOR; 57 | editor.SetColorizerEnable(false); // disable syntax highlighting for now 58 | song_manager = std::make_shared(); 59 | } 60 | 61 | void Editor_Window::display() 62 | { 63 | bool keep_open = true; 64 | 65 | if(test_flag(RECOMPILE)) 66 | { 67 | if(!song_manager->get_compile_in_progress()) 68 | { 69 | if(!song_manager->compile(editor.GetText(), filename)) 70 | clear_flag(RECOMPILE); 71 | } 72 | } 73 | 74 | std::string window_id; 75 | window_id = get_display_filename(); 76 | if(test_flag(MODIFIED)) 77 | window_id += "*"; 78 | window_id += "###Editor" + std::to_string(id); 79 | 80 | auto cpos = editor.GetCursorPosition(); 81 | ImGui::Begin(window_id.c_str(), &keep_open, /*ImGuiWindowFlags_HorizontalScrollbar |*/ ImGuiWindowFlags_MenuBar); 82 | ImGui::SetWindowSize(ImVec2(800, 600), ImGuiCond_Once); 83 | 84 | if (ImGui::BeginMenuBar()) 85 | { 86 | if (ImGui::BeginMenu("File")) 87 | { 88 | if (ImGui::MenuItem("New", "Ctrl+N")) 89 | set_flag(NEW); 90 | if (ImGui::MenuItem("Open...", "Ctrl+O")) 91 | set_flag(OPEN|DIALOG); 92 | if (ImGui::MenuItem("Save", "Ctrl+S", nullptr, test_flag(FILENAME_SET))) 93 | set_flag(SAVE|DIALOG); 94 | if (ImGui::MenuItem("Save As...", "Ctrl+Alt+S")) 95 | set_flag(SAVE|SAVE_AS|DIALOG); 96 | ImGui::Separator(); 97 | if (ImGui::MenuItem("Import patches from DMF...", nullptr, nullptr, !editor.IsReadOnly())) 98 | set_flag(IMPORT|DIALOG); 99 | show_export_menu(); 100 | ImGui::Separator(); 101 | if (ImGui::MenuItem("Close", "Ctrl+W")) 102 | keep_open = false; 103 | ImGui::EndMenu(); 104 | } 105 | 106 | if (ImGui::BeginMenu("Player")) 107 | { 108 | if (ImGui::MenuItem("Play", "F5")) 109 | play_song(); 110 | if (ImGui::MenuItem("Play from start of line", "F6")) 111 | play_from_line(); 112 | if (ImGui::MenuItem("Play from cursor", "F7")) 113 | play_from_cursor(); 114 | if (ImGui::MenuItem("Stop", "Escape or F8")) 115 | stop_song(); 116 | ImGui::EndMenu(); 117 | } 118 | 119 | if (ImGui::BeginMenu("Edit")) 120 | { 121 | bool ro = editor.IsReadOnly(); 122 | 123 | if (ImGui::MenuItem("Undo", "Ctrl+Z or Alt+Backspace", nullptr, !ro && editor.CanUndo())) 124 | editor.Undo(), set_flag(MODIFIED|RECOMPILE); 125 | if (ImGui::MenuItem("Redo", "Ctrl+Y", nullptr, !ro && editor.CanRedo())) 126 | editor.Redo(), set_flag(MODIFIED|RECOMPILE); 127 | 128 | ImGui::Separator(); 129 | 130 | if (ImGui::MenuItem("Cut", "Ctrl+X", nullptr, !ro && editor.HasSelection())) 131 | editor.Cut(), set_flag(MODIFIED|RECOMPILE); 132 | if (ImGui::MenuItem("Copy", "Ctrl+C", nullptr, editor.HasSelection())) 133 | editor.Copy(); 134 | if (ImGui::MenuItem("Delete", "Del", nullptr, !ro && editor.HasSelection())) 135 | editor.Delete(), set_flag(MODIFIED|RECOMPILE); 136 | 137 | GLFWerrorfun prev_error_callback = glfwSetErrorCallback(NULL); // disable clipboard error messages... 138 | 139 | if (ImGui::MenuItem("Paste", "Ctrl+V", nullptr, !ro && ImGui::GetClipboardText() != nullptr)) 140 | editor.Paste(), set_flag(MODIFIED|RECOMPILE); 141 | 142 | glfwSetErrorCallback(prev_error_callback); 143 | 144 | ImGui::Separator(); 145 | 146 | if (ImGui::MenuItem("Select All", "Ctrl+A", nullptr)) 147 | editor.SetSelection(TextEditor::Coordinates(), TextEditor::Coordinates(editor.GetTotalLines(), 0)); 148 | 149 | ImGui::Separator(); 150 | 151 | if (ImGui::MenuItem("Read-only mode", nullptr, &ro)) 152 | editor.SetReadOnly(ro); 153 | 154 | #ifdef DEBUG 155 | ImGui::Separator(); 156 | 157 | if (ImGui::MenuItem("Configuration...")) 158 | main_window.show_config_window(); 159 | #endif 160 | 161 | ImGui::EndMenu(); 162 | } 163 | 164 | if (ImGui::BeginMenu("View")) 165 | { 166 | if (ImGui::MenuItem("Track view...")) 167 | { 168 | children.push_back(std::make_shared(song_manager)); 169 | } 170 | if (ImGui::MenuItem("Track list...")) 171 | { 172 | children.push_back(std::make_shared(song_manager)); 173 | } 174 | ImGui::Separator(); 175 | if (ImGui::BeginMenu("Editor style")) 176 | { 177 | if (ImGui::MenuItem("Dark palette")) 178 | editor.SetPalette(TextEditor::GetDarkPalette()); 179 | if (ImGui::MenuItem("Light palette")) 180 | editor.SetPalette(TextEditor::GetLightPalette()); 181 | if (ImGui::MenuItem("Retro blue palette")) 182 | editor.SetPalette(TextEditor::GetRetroBluePalette()); 183 | ImGui::EndMenu(); 184 | } 185 | ImGui::EndMenu(); 186 | } 187 | 188 | if (ImGui::BeginMenu("Help")) 189 | { 190 | if (ImGui::MenuItem("About...")) 191 | { 192 | main_window.show_about_window(); 193 | } 194 | ImGui::EndMenu(); 195 | } 196 | 197 | ImGui::EndMenuBar(); 198 | } 199 | 200 | // focus on the text editor rather than the "frame" 201 | if (ImGui::IsWindowFocused()) 202 | ImGui::SetNextWindowFocus(); 203 | 204 | show_track_positions(); 205 | 206 | GLFWerrorfun prev_error_callback = glfwSetErrorCallback(NULL); // disable clipboard error messages... 207 | 208 | editor.Render("EditorArea", ImVec2(0, -ImGui::GetFrameHeight())); 209 | 210 | glfwSetErrorCallback(prev_error_callback); 211 | 212 | if(editor.IsTextChanged()) 213 | set_flag(MODIFIED|RECOMPILE); 214 | 215 | if (ImGui::IsWindowFocused() | editor.IsWindowFocused()) 216 | { 217 | ImGuiIO& io = ImGui::GetIO(); 218 | auto isOSX = io.ConfigMacOSXBehaviors; 219 | auto alt = io.KeyAlt; 220 | auto ctrl = io.KeyCtrl; 221 | auto shift = io.KeyShift; 222 | auto super = io.KeySuper; 223 | auto isShortcut = (isOSX ? (super && !ctrl) : (ctrl && !super)) && !alt && !shift; 224 | auto isAltShortcut = (isOSX ? (super && !ctrl) : (ctrl && !super)) && alt && !shift; 225 | 226 | if (isShortcut && ImGui::IsKeyPressed(GLFW_KEY_N)) 227 | set_flag(NEW); 228 | else if (isShortcut && ImGui::IsKeyPressed(GLFW_KEY_O)) 229 | set_flag(OPEN|DIALOG); 230 | else if (isShortcut && ImGui::IsKeyPressed(GLFW_KEY_S)) 231 | set_flag(SAVE|DIALOG); 232 | else if (isAltShortcut && ImGui::IsKeyPressed(GLFW_KEY_S)) 233 | set_flag(SAVE|SAVE_AS|DIALOG); 234 | else if (isShortcut && ImGui::IsKeyPressed(GLFW_KEY_W)) 235 | keep_open = false; 236 | else if(ImGui::IsKeyPressed(GLFW_KEY_ESCAPE) || ImGui::IsKeyPressed(GLFW_KEY_F8)) 237 | stop_song(); 238 | else if(ImGui::IsKeyPressed(GLFW_KEY_F5)) 239 | play_song(); 240 | else if(ImGui::IsKeyPressed(GLFW_KEY_F6)) 241 | play_from_line(); 242 | else if(ImGui::IsKeyPressed(GLFW_KEY_F7)) 243 | play_from_cursor(); 244 | 245 | song_manager->set_editor_position({cpos.mLine, cpos.mColumn}); 246 | line_pos = song_manager->get_song_pos_at_line(); 247 | cursor_pos = song_manager->get_song_pos_at_cursor(); 248 | } 249 | else 250 | { 251 | song_manager->set_editor_position({-1, -1}); 252 | } 253 | 254 | //ImGui::Spacing(); 255 | ImGui::AlignTextToFramePadding(); 256 | ImGui::Text("%6d:%-6d %6d line%c | %s ", cpos.mLine + 1, cpos.mColumn + 1, editor.GetTotalLines(), (editor.GetTotalLines() == 1) ? ' ' : 's', 257 | editor.IsOverwrite() ? "Ovr" : "Ins"); 258 | 259 | ImGui::SameLine(); 260 | ImGui::Text("| L:%5d C:%5d", line_pos, cursor_pos); 261 | 262 | //get_compile_result(); 263 | show_player_controls(); 264 | 265 | ImGui::End(); 266 | 267 | if(!keep_open) 268 | close_request_all(); 269 | 270 | if(player_error.size() && !modal_open) 271 | show_player_error(); 272 | 273 | if(get_close_request() == Window::CLOSE_IN_PROGRESS && !modal_open) 274 | show_close_warning(); 275 | 276 | if(get_close_request() == Window::CLOSE_OK) 277 | { 278 | active = false; 279 | song_manager->stop(); 280 | } 281 | else 282 | { 283 | handle_file_io(); 284 | } 285 | } 286 | 287 | void Editor_Window::close_request() 288 | { 289 | if(test_flag(MODIFIED)) 290 | close_req_state = Window::CLOSE_IN_PROGRESS; 291 | else 292 | close_req_state = Window::CLOSE_OK; 293 | } 294 | 295 | void Editor_Window::play_song(uint32_t position) 296 | { 297 | try 298 | { 299 | song_manager->play(position); 300 | } 301 | catch(InputError& except) 302 | { 303 | player_error = except.what(); 304 | } 305 | catch(std::exception& except) 306 | { 307 | player_error = "exception type: "; 308 | player_error += typeid(except).name(); 309 | player_error += "\nexception message: "; 310 | player_error += except.what(); 311 | } 312 | } 313 | 314 | void Editor_Window::stop_song() 315 | { 316 | song_manager->stop(); 317 | } 318 | 319 | void Editor_Window::play_from_cursor() 320 | { 321 | play_song(cursor_pos); 322 | } 323 | 324 | void Editor_Window::play_from_line() 325 | { 326 | play_song(line_pos); 327 | } 328 | 329 | void Editor_Window::show_player_error() 330 | { 331 | modal_open = 1; 332 | std::string modal_id; 333 | modal_id = get_display_filename() + "###modal"; 334 | if (!ImGui::IsPopupOpen(modal_id.c_str())) 335 | ImGui::OpenPopup(modal_id.c_str()); 336 | if(ImGui::BeginPopupModal(modal_id.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) 337 | { 338 | ImGui::Text("%s\n", player_error.c_str()); 339 | ImGui::Separator(); 340 | 341 | ImGui::SetItemDefaultFocus(); 342 | if (ImGui::Button("OK", ImVec2(120, 0))) 343 | { 344 | player_error = ""; 345 | ImGui::CloseCurrentPopup(); 346 | } 347 | ImGui::EndPopup(); 348 | } 349 | } 350 | 351 | void Editor_Window::show_close_warning() 352 | { 353 | modal_open = 1; 354 | std::string modal_id; 355 | modal_id = get_display_filename() + "###modal"; 356 | if (!ImGui::IsPopupOpen(modal_id.c_str())) 357 | ImGui::OpenPopup(modal_id.c_str()); 358 | if(ImGui::BeginPopupModal(modal_id.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) 359 | { 360 | ImGui::Text("You have unsaved changes. Close anyway?\n"); 361 | ImGui::Separator(); 362 | 363 | if (ImGui::Button("OK", ImVec2(120, 0))) 364 | { 365 | close_req_state = Window::CLOSE_OK; 366 | ImGui::CloseCurrentPopup(); 367 | } 368 | ImGui::SetItemDefaultFocus(); 369 | ImGui::SameLine(); 370 | if (ImGui::Button("Cancel", ImVec2(120, 0))) 371 | { 372 | close_req_state = Window::CLOSE_NOT_OK; 373 | ImGui::CloseCurrentPopup(); 374 | } 375 | ImGui::EndPopup(); 376 | } 377 | } 378 | 379 | void Editor_Window::show_warning() 380 | { 381 | modal_open = 1; 382 | std::string modal_id; 383 | modal_id = get_display_filename() + "###modal"; 384 | if (!ImGui::IsPopupOpen(modal_id.c_str())) 385 | ImGui::OpenPopup(modal_id.c_str()); 386 | if(ImGui::BeginPopupModal(modal_id.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) 387 | { 388 | ImGui::Text("You have unsaved changes. Continue anyway?\n"); 389 | ImGui::Separator(); 390 | 391 | if (ImGui::Button("OK", ImVec2(120, 0))) 392 | { 393 | set_flag(IGNORE_WARNING); 394 | ImGui::CloseCurrentPopup(); 395 | } 396 | ImGui::SetItemDefaultFocus(); 397 | ImGui::SameLine(); 398 | if (ImGui::Button("Cancel", ImVec2(120, 0))) 399 | { 400 | clear_flag(NEW|OPEN); 401 | ImGui::CloseCurrentPopup(); 402 | } 403 | ImGui::EndPopup(); 404 | } 405 | } 406 | 407 | void Editor_Window::handle_file_io() 408 | { 409 | // new file requested 410 | if(test_flag(NEW) && !modal_open) 411 | { 412 | if(test_flag(MODIFIED) && !test_flag(IGNORE_WARNING)) 413 | { 414 | show_warning(); 415 | } 416 | else 417 | { 418 | set_flag(RECOMPILE); 419 | clear_flag(MODIFIED|FILENAME_SET|NEW|OPEN|SAVE|DIALOG|IGNORE_WARNING); 420 | filename = default_filename; 421 | editor.SetText(""); 422 | editor.MoveTop(false); 423 | 424 | song_manager->reset_mute(); 425 | song_manager->stop(); 426 | } 427 | } 428 | // open dialog requested 429 | else if(test_flag(OPEN) && !modal_open) 430 | { 431 | if((flag & MODIFIED) && !(flag & IGNORE_WARNING)) 432 | { 433 | show_warning(); 434 | } 435 | else 436 | { 437 | modal_open = 1; 438 | fs.chooseFileDialog(test_flag(DIALOG), fs.getLastDirectory(), default_filter); 439 | clear_flag(DIALOG); 440 | if(strlen(fs.getChosenPath()) > 0) 441 | { 442 | if(load_file(fs.getChosenPath())) 443 | set_flag(DIALOG); // File couldn't be opened 444 | else 445 | clear_flag(OPEN|IGNORE_WARNING); 446 | } 447 | else if(fs.hasUserJustCancelledDialog()) 448 | { 449 | clear_flag(OPEN|IGNORE_WARNING); 450 | } 451 | } 452 | } 453 | // save dialog requested 454 | else if(test_flag(SAVE) && !modal_open) 455 | { 456 | if(test_flag(SAVE_AS) || !test_flag(FILENAME_SET)) 457 | { 458 | modal_open = 1; 459 | fs.saveFileDialog(test_flag(DIALOG), fs.getLastDirectory(), get_display_filename().c_str(), default_filter); 460 | clear_flag(DIALOG); 461 | if(strlen(fs.getChosenPath()) > 0) 462 | { 463 | if(save_file(fs.getChosenPath())) 464 | set_flag(DIALOG); // File couldn't be saved 465 | else 466 | clear_flag(SAVE|SAVE_AS); 467 | } 468 | else if(fs.hasUserJustCancelledDialog()) 469 | { 470 | clear_flag(SAVE|SAVE_AS); 471 | } 472 | } 473 | else 474 | { 475 | // TODO: show a message if file couldn't be saved ... 476 | clear_flag(DIALOG|SAVE); 477 | if(save_file(filename.c_str())) 478 | player_error = "File couldn't be saved."; 479 | } 480 | } 481 | // export dialog requested 482 | else if(test_flag(EXPORT) && !modal_open) 483 | { 484 | modal_open = 1; 485 | fs.saveFileDialog(test_flag(DIALOG), fs.getLastDirectory(), get_export_filename().c_str(), export_filter.c_str()); 486 | clear_flag(DIALOG); 487 | if(strlen(fs.getChosenPath()) > 0) 488 | { 489 | export_file(fs.getChosenPath()); 490 | clear_flag(EXPORT); 491 | } 492 | else if(fs.hasUserJustCancelledDialog()) 493 | { 494 | clear_flag(EXPORT); 495 | } 496 | } 497 | else if(test_flag(IMPORT) && !modal_open) 498 | { 499 | modal_open = 1; 500 | fs.chooseFileDialog(test_flag(DIALOG), fs.getLastDirectory(), ".dmf"); 501 | clear_flag(DIALOG); 502 | if(strlen(fs.getChosenPath()) > 0) 503 | { 504 | import_file(fs.getChosenPath()); 505 | clear_flag(IMPORT); 506 | } 507 | else if(fs.hasUserJustCancelledDialog()) 508 | { 509 | clear_flag(IMPORT); 510 | } 511 | } 512 | } 513 | 514 | int Editor_Window::load_file(const char* fn) 515 | { 516 | auto t = std::ifstream(fn); 517 | if (t.good()) 518 | { 519 | clear_flag(MODIFIED); 520 | set_flag(FILENAME_SET|RECOMPILE); 521 | filename = fn; 522 | std::string str((std::istreambuf_iterator(t)), std::istreambuf_iterator()); 523 | editor.SetText(str); 524 | editor.MoveTop(false); 525 | 526 | song_manager->stop(); 527 | song_manager->reset_mute(); 528 | return 0; 529 | } 530 | return -1; 531 | } 532 | 533 | int Editor_Window::import_file(const char* fn) 534 | { 535 | auto t = Dmf_Importer(fn); 536 | player_error += t.get_error(); 537 | if (!player_error.size()) 538 | { 539 | clear_flag(MODIFIED); 540 | set_flag(FILENAME_SET|RECOMPILE); 541 | editor.InsertText(t.get_mml()); 542 | return 0; 543 | } 544 | return -1; 545 | } 546 | 547 | int Editor_Window::save_file(const char* fn) 548 | { 549 | // If we don't set ios::binary, the runtime will 550 | // convert linebreaks to the OS native format. 551 | // I think it is OK to keep this behavior for now. 552 | auto t = std::ofstream(fn); 553 | if (t.good()) 554 | { 555 | clear_flag(MODIFIED); 556 | set_flag(FILENAME_SET|RECOMPILE); 557 | filename = fn; 558 | std::string str = editor.GetText(); 559 | t.write((char*)str.c_str(), str.size()); 560 | return 0; 561 | } 562 | return -1; 563 | } 564 | 565 | void Editor_Window::export_file(const char* fn) 566 | { 567 | try 568 | { 569 | auto song = song_manager->get_song(); 570 | auto bytes = song->get_platform()->get_export_data(*song, export_format); 571 | auto t = std::ofstream(fn, std::ios::binary); 572 | if(t.good()) 573 | t.write((char*)bytes.data(), bytes.size()); 574 | else 575 | player_error = "Cannot open file '" + std::string(fn) + "'"; 576 | } 577 | catch(InputError& except) 578 | { 579 | player_error = except.what(); 580 | } 581 | catch(std::exception& except) 582 | { 583 | player_error = "exception type: "; 584 | player_error += typeid(except).name(); 585 | player_error += "\nexception message: "; 586 | player_error += except.what(); 587 | } 588 | } 589 | 590 | std::string Editor_Window::get_display_filename() const 591 | { 592 | auto pos = filename.rfind("/"); 593 | if(pos != std::string::npos) 594 | return filename.substr(pos+1); 595 | else 596 | return filename; 597 | } 598 | 599 | std::string Editor_Window::get_export_filename() const 600 | { 601 | auto spos = filename.rfind("/"); 602 | auto epos = filename.rfind("."); 603 | if(spos != std::string::npos) 604 | return filename.substr(spos+1, epos-spos-1) + export_filter; 605 | else 606 | return filename.substr(0, epos) + export_filter; 607 | } 608 | 609 | // move this to top? 610 | void Editor_Window::show_player_controls() 611 | { 612 | auto result = song_manager->get_compile_result(); 613 | 614 | ImGui::SameLine(); 615 | float content = ImGui::GetContentRegionMax().x; 616 | //float offset = content * 0.4f; // with progress bar 617 | float offset = content * 0.75f; // without progress bar 618 | float width = content - offset; 619 | float curpos = ImGui::GetCursorPos().x; 620 | if(offset < curpos) 621 | offset = curpos; 622 | ImGui::SetCursorPosX(offset); 623 | 624 | // PushNextItemWidth doesn't work for some stupid reason, width must be set manually 625 | auto size = ImVec2(content * 0.1f, 0); 626 | if(size.x < ImGui::GetFontSize() * 4.0f) 627 | size.x = ImGui::GetFontSize() * 4.0f; 628 | 629 | // Handle play button (also indicates compile errors) 630 | if(result != Song_Manager::COMPILE_OK) 631 | { 632 | ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.6f); 633 | if(result == Song_Manager::COMPILE_NOT_DONE) 634 | { 635 | ImGui::Button("Wait", size); 636 | } 637 | else if(result == Song_Manager::COMPILE_ERROR) 638 | { 639 | ImGui::Button("Error", size); 640 | if (ImGui::IsItemHovered()) 641 | { 642 | ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); 643 | ImGui::BeginTooltip(); 644 | ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 645 | ImGui::TextUnformatted(song_manager->get_error_message().c_str()); 646 | ImGui::PopTextWrapPos(); 647 | ImGui::EndTooltip(); 648 | ImGui::PopStyleVar(); 649 | } 650 | } 651 | ImGui::PopStyleVar(); 652 | } 653 | else 654 | { 655 | if(ImGui::Button("Play", size)) 656 | play_song(); 657 | } 658 | // Handle stop button 659 | ImGui::SameLine(); 660 | if (ImGui::Button("Stop", size)) 661 | stop_song(); 662 | 663 | /* 664 | // Handle a progress bar. Just dummy for now 665 | float bar_test = width / content; 666 | ImGui::SameLine(); 667 | size = ImVec2(ImGui::GetContentRegionAvail().x, 0); 668 | ImGui::ProgressBar(bar_test, size); 669 | */ 670 | } 671 | 672 | void Editor_Window::get_compile_result() 673 | { 674 | ImGui::SameLine(); 675 | switch(song_manager->get_compile_result()) 676 | { 677 | default: 678 | case Song_Manager::COMPILE_NOT_DONE: 679 | ImGui::Text("Compiling"); 680 | break; 681 | case Song_Manager::COMPILE_OK: 682 | ImGui::Text("OK"); 683 | break; 684 | case Song_Manager::COMPILE_ERROR: 685 | ImGui::Text("Error"); 686 | if (ImGui::IsItemHovered()) 687 | { 688 | ImGui::BeginTooltip(); 689 | ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 690 | ImGui::TextUnformatted(song_manager->get_error_message().c_str()); 691 | ImGui::PopTextWrapPos(); 692 | ImGui::EndTooltip(); 693 | } 694 | break; 695 | } 696 | } 697 | 698 | std::string Editor_Window::dump_state() 699 | { 700 | std::string str = "filename: " + filename + "\n"; 701 | str += "modified: " + std::to_string(test_flag(MODIFIED)) + "\n"; 702 | str += "contents:\n" + editor.GetText() + "\nend contents\n"; 703 | return str; 704 | } 705 | 706 | void Editor_Window::show_track_positions() 707 | { 708 | std::map> highlights = {}; 709 | Song_Manager::Track_Map map = {}; 710 | unsigned int ticks = 0; 711 | 712 | auto tracks = song_manager->get_tracks(); 713 | if(tracks != nullptr) 714 | map = *tracks; 715 | 716 | auto player = song_manager->get_player(); 717 | if(player != nullptr && !player->get_finished()) 718 | ticks = player->get_driver()->get_player_ticks(); 719 | 720 | for(auto track_it = map.begin(); track_it != map.end(); track_it++) 721 | { 722 | auto& info = track_it->second; 723 | int offset = 0; 724 | 725 | // calculate offset to first loop 726 | if(ticks > info.length && info.loop_length) 727 | offset = ((ticks - info.loop_start) / info.loop_length) * info.loop_length; 728 | 729 | // calculate position 730 | auto it = info.events.lower_bound(ticks - offset); 731 | if(it != info.events.begin()) 732 | { 733 | --it; 734 | auto event = it->second; 735 | for(auto && i : event.references) 736 | { 737 | if(!i->get_filename().size()) 738 | { 739 | highlights[i->get_line()].insert(i->get_column()); 740 | } 741 | } 742 | } 743 | } 744 | editor.SetMmlHighlights(highlights); 745 | } 746 | 747 | void Editor_Window::show_export_menu() 748 | { 749 | auto format_list = song_manager->get_song()->get_platform()->get_export_formats(); 750 | unsigned int id = 0; 751 | for(auto&& i : format_list) 752 | { 753 | std::string item_title = "Export " + i.second + "..."; 754 | if (ImGui::MenuItem(item_title.c_str())) 755 | { 756 | set_flag(EXPORT|DIALOG); 757 | export_format = id; 758 | export_filter = "." + i.first; 759 | } 760 | id++; 761 | } 762 | } 763 | -------------------------------------------------------------------------------- /src/editor_window.h: -------------------------------------------------------------------------------- 1 | #ifndef EDITOR_WINDOW 2 | #define EDITOR_WINDOW 3 | 4 | #include 5 | #include 6 | 7 | #include "TextEditor.h" 8 | #include "addons/imguifilesystem/imguifilesystem.h" 9 | 10 | #include "window.h" 11 | #include "song_manager.h" 12 | 13 | class Editor_Window : public Window 14 | { 15 | public: 16 | Editor_Window(); 17 | 18 | void display() override; 19 | void close_request() override; 20 | std::string dump_state() override; 21 | 22 | void play_song(uint32_t position = 0); 23 | void stop_song(); 24 | 25 | void play_from_cursor(); 26 | void play_from_line(); 27 | 28 | private: 29 | const char* default_filename = "Untitled.mml"; 30 | const char* default_filter = ".mml;.muc;.txt"; 31 | 32 | void show_player_error(); 33 | void show_close_warning(); 34 | void show_warning(); 35 | void handle_file_io(); 36 | 37 | int load_file(const char* fn); 38 | int save_file(const char* fn); 39 | void export_file(const char* fn); 40 | int import_file(const char* fn); 41 | 42 | std::string get_display_filename() const; 43 | std::string get_export_filename() const; 44 | void show_player_controls(); 45 | void get_compile_result(); 46 | void show_track_positions(); 47 | 48 | void show_export_menu(); 49 | 50 | inline void set_flag(uint32_t data) { flag |= data; }; 51 | inline void clear_flag(uint32_t data) { flag &= ~data; }; 52 | inline bool test_flag(uint32_t data) { return flag & data; }; 53 | 54 | // Text editor state 55 | TextEditor editor; 56 | 57 | // File selection state 58 | ImGuiFs::Dialog fs; 59 | std::string filename; 60 | uint32_t flag; 61 | 62 | // Export state 63 | std::string export_filter; 64 | unsigned int export_format; 65 | 66 | // Song manager state 67 | std::shared_ptr song_manager; 68 | 69 | // Playback error string 70 | std::string player_error; 71 | 72 | // Song position 73 | uint32_t line_pos; 74 | uint32_t cursor_pos; 75 | }; 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /src/emu_player.cpp: -------------------------------------------------------------------------------- 1 | #include "emu_player.h" 2 | #include "platform/md.h" 3 | #include "input.h" 4 | 5 | #if defined(LOCAL_LIBVGM) 6 | #include "emu/EmuStructs.h" 7 | #include "emu/SoundEmu.h" 8 | #include "emu/SoundDevs.h" 9 | #include "emu/EmuCores.h" 10 | #include "emu/cores/sn764intf.h" 11 | #else 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #endif 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #define DEBUG_PRINT(fmt,...) 24 | 25 | using std::malloc; 26 | using std::free; 27 | 28 | Device_Wrapper::Device_Wrapper() 29 | : dev_init(false) 30 | , resmpl_init(false) 31 | , write_type(Device_Wrapper::NONE) 32 | , volume(0x100) 33 | , write_a8d8(nullptr) 34 | { 35 | } 36 | 37 | Device_Wrapper::~Device_Wrapper() 38 | { 39 | if(resmpl_init) 40 | { 41 | DEBUG_PRINT("deinit resampler\n"); 42 | // deinit 43 | Resmpl_Deinit(&resmpl); 44 | } 45 | if(dev_init) 46 | { 47 | DEBUG_PRINT("deinit emu\n"); 48 | // deinit 49 | SndEmu_Stop(&dev); 50 | } 51 | 52 | } 53 | 54 | void Device_Wrapper::set_default_volume(uint16_t vol) 55 | { 56 | volume = vol; 57 | } 58 | 59 | void Device_Wrapper::set_rate(uint32_t rate) 60 | { 61 | sample_rate = rate; 62 | 63 | if(resmpl_init) 64 | { 65 | DEBUG_PRINT("deinit resampler\n"); 66 | Resmpl_Deinit(&resmpl); 67 | resmpl_init = false; 68 | } 69 | 70 | if(dev_init) 71 | { 72 | DEBUG_PRINT("init resampler\n"); 73 | Resmpl_SetVals(&resmpl, 0xff, volume, sample_rate); 74 | Resmpl_DevConnect(&resmpl, &dev); 75 | Resmpl_Init(&resmpl); 76 | resmpl_init = true; 77 | } 78 | } 79 | 80 | void Device_Wrapper::init_sn76489(uint32_t freq, uint8_t lfsr_w, uint16_t lfsr_t) 81 | { 82 | DEBUG_PRINT("init emu (sn76489 @ %d Hz)\n", freq); 83 | DEV_GEN_CFG dev_cfg; 84 | SN76496_CFG sn_cfg; 85 | 86 | dev_cfg.emuCore = 0; 87 | dev_cfg.srMode = DEVRI_SRMODE_NATIVE; 88 | dev_cfg.flags = 0x00; 89 | dev_cfg.clock = freq; 90 | dev_cfg.smplRate = 44100; 91 | sn_cfg._genCfg = dev_cfg; 92 | sn_cfg.shiftRegWidth = lfsr_w; 93 | sn_cfg.noiseTaps = lfsr_t; 94 | sn_cfg.negate = 0; 95 | sn_cfg.stereo = 0; 96 | sn_cfg.clkDiv = 8; 97 | sn_cfg.segaPSG = 1; //??? 98 | sn_cfg.t6w28_tone = NULL; 99 | 100 | uint8_t status = SndEmu_Start(DEVID_SN76496, (DEV_GEN_CFG*)&sn_cfg, &dev); 101 | if(status) 102 | throw std::runtime_error("Device_Wrapper::init_sn76489"); 103 | 104 | SndEmu_GetDeviceFunc(dev.devDef, RWF_REGISTER | RWF_WRITE, DEVRW_A8D8, 0, (void**)&write_a8d8); 105 | write_type = Device_Wrapper::A8D8; 106 | 107 | dev.devDef->Reset(dev.dataPtr); 108 | 109 | dev_init = true; 110 | } 111 | 112 | void Device_Wrapper::init_ym2612(uint32_t freq) 113 | { 114 | DEBUG_PRINT("init emu (ym2612 @ %d Hz)\n", freq); 115 | DEV_GEN_CFG dev_cfg; 116 | 117 | dev_cfg.emuCore = 0; 118 | dev_cfg.srMode = DEVRI_SRMODE_NATIVE; 119 | dev_cfg.flags = 0x00; 120 | dev_cfg.clock = freq; 121 | dev_cfg.smplRate = 44100; 122 | 123 | uint8_t status = SndEmu_Start(DEVID_YM2612, (DEV_GEN_CFG*)&dev_cfg, &dev); 124 | if(status) 125 | throw std::runtime_error("Device_Wrapper::init_ym2612"); 126 | 127 | SndEmu_GetDeviceFunc(dev.devDef, RWF_REGISTER | RWF_WRITE, DEVRW_A8D8, 0, (void**)&write_a8d8); 128 | write_type = Device_Wrapper::P1A8D8; 129 | 130 | dev.devDef->Reset(dev.dataPtr); 131 | 132 | dev_init = true; 133 | } 134 | 135 | inline void Device_Wrapper::write(uint16_t addr, uint16_t data) 136 | { 137 | switch(write_type) 138 | { 139 | default: 140 | break; 141 | case Device_Wrapper::A8D8: 142 | write_a8d8(dev.dataPtr, addr, data); 143 | break; 144 | } 145 | } 146 | 147 | inline void Device_Wrapper::write(uint8_t port, uint16_t addr, uint16_t data) 148 | { 149 | switch(write_type) 150 | { 151 | default: 152 | write(addr, data); 153 | break; 154 | case Device_Wrapper::P1A8D8: 155 | write_a8d8(dev.dataPtr, (port << 1), addr); 156 | write_a8d8(dev.dataPtr, (port << 1) + 1, data); 157 | break; 158 | } 159 | } 160 | 161 | inline void Device_Wrapper::get_sample(WAVE_32BS* output, int count) 162 | { 163 | if(resmpl_init) 164 | Resmpl_Execute(&resmpl, count, output); 165 | } 166 | 167 | inline void Device_Wrapper::set_mute_mask(uint32_t mask) 168 | { 169 | if(dev_init) 170 | dev.devDef->SetMuteMask(dev.dataPtr, mask); 171 | } 172 | 173 | //===================================================================== 174 | 175 | Emu_Player::Emu_Player(std::shared_ptr song, uint32_t start_position) 176 | : sample_rate(1) 177 | , delta_time(0) 178 | , sample_delta(1) 179 | , play_time(0) 180 | , play_time2(0) 181 | , song(song) 182 | { 183 | 184 | driver = song->get_platform()->get_driver(1, (VGM_Interface*)this); 185 | driver.get()->play_song(*song.get()); 186 | if(start_position) 187 | driver.get()->skip_ticks(start_position); 188 | } 189 | 190 | Emu_Player::~Emu_Player() 191 | { 192 | } 193 | 194 | std::shared_ptr& Emu_Player::get_driver() 195 | { 196 | return driver; 197 | } 198 | 199 | void Emu_Player::set_mute_mask(const std::map& mask_map) 200 | { 201 | for(auto && i : mask_map) 202 | { 203 | auto dev = devices.find(i.first); 204 | if(dev != devices.end()) 205 | { 206 | dev->second.set_mute_mask(i.second); 207 | } 208 | } 209 | } 210 | 211 | //===================================================================== 212 | // Audio_Stream (front end) 213 | // Audio_Manager -> Emu_Player 214 | //===================================================================== 215 | 216 | void Emu_Player::setup_stream(uint32_t sample_rate) 217 | { 218 | if(sample_rate == 0) 219 | sample_rate = 1; 220 | this->sample_rate = sample_rate; 221 | sample_delta = 1.0 / sample_rate; 222 | delta_time = 0; 223 | printf("Emu_Player stream setup %d Hz, delta = %.8f, init = %.8f\n", sample_rate, sample_delta, delta_time); 224 | 225 | for(auto it = devices.begin(); it != devices.end(); it++) 226 | { 227 | it->second.set_rate(sample_rate); 228 | } 229 | } 230 | 231 | int Emu_Player::get_sample(WAVE_32BS* output, int count, int channels) 232 | { 233 | try 234 | { 235 | for(int i = 0; i < count; i++) 236 | { 237 | int max_steps = 100; 238 | delta_time += sample_delta; 239 | play_time += sample_delta; 240 | 241 | if(delta_time > 0) 242 | { 243 | //printf("\n%.8f,%.8f=%.8f ", play_time, play_time-play_time2, delta_time); 244 | play_time2 = play_time; 245 | } 246 | 247 | while(delta_time > 0) 248 | { 249 | double step = driver.get()->play_step(); 250 | delta_time -= step; 251 | 252 | //printf("-%.8f, ", step); 253 | if(!--max_steps) 254 | break; 255 | } 256 | 257 | // Update any dac streams 258 | for(auto && it = streams.begin(); it != streams.end(); it++) 259 | { 260 | if(it->second.active) 261 | { 262 | it->second.counter += it->second.freq; 263 | while(it->second.counter >= sample_rate) 264 | { 265 | devices[it->second.chip_id].write( 266 | it->second.port, 267 | it->second.reg, 268 | datablocks[it->second.db_id][it->second.position]); 269 | it->second.position ++; 270 | it->second.counter -= sample_rate; 271 | if(!--it->second.length) 272 | { 273 | it->second.active = false; 274 | } 275 | } 276 | } 277 | } 278 | 279 | // Get sample from sound chips 280 | for(auto && it = devices.begin(); it != devices.end(); it++) 281 | { 282 | it->second.get_sample(&output[i], 1); 283 | } 284 | 285 | if(!driver.get()->is_playing()) 286 | set_finished(true); 287 | } 288 | } 289 | catch(InputError& e) 290 | { 291 | handle_error(e.what()); 292 | } 293 | catch(std::exception& e) 294 | { 295 | handle_error(e.what()); 296 | } 297 | return count; 298 | } 299 | 300 | void Emu_Player::stop_stream() 301 | { 302 | printf("Emu_Player stream stop\n"); 303 | } 304 | 305 | void Emu_Player::handle_error(const char* str) 306 | { 307 | printf("Playback error: %s\n", str); 308 | delta_time = -1000; // Prevent error from reoccuring 309 | set_finished(true); 310 | } 311 | 312 | //===================================================================== 313 | // VGM_Interface (back end) 314 | // Driver -> Emu_Player 315 | //===================================================================== 316 | 317 | void Emu_Player::write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data) 318 | { 319 | switch(command) 320 | { 321 | case 0x50: 322 | devices[DEVID_SN76496].write(reg, data); 323 | break; 324 | case 0x52: 325 | devices[DEVID_YM2612].write(port, reg, data); 326 | default: 327 | break; 328 | } 329 | } 330 | 331 | void Emu_Player::dac_setup(uint8_t sid, uint8_t chip_id, uint32_t port, uint32_t reg, uint8_t db_id) 332 | { 333 | //printf("Emu_Player setup stream %02x = %02x,%02x,%02x,%02x\n", sid, chip_id, port, reg, db_id); 334 | streams[sid].chip_id = chip_id; 335 | streams[sid].port = port; 336 | streams[sid].reg = reg; 337 | streams[sid].db_id = db_id; 338 | streams[sid].active = false; 339 | } 340 | 341 | void Emu_Player::dac_start(uint8_t sid, uint32_t start, uint32_t length, uint32_t freq) 342 | { 343 | //printf("Emu_Player start stream %02x = %d,%d,%d\n", sid, start,length,freq); 344 | streams[sid].position = start; 345 | streams[sid].length = length; 346 | streams[sid].freq = freq; 347 | streams[sid].counter = sample_rate; 348 | streams[sid].active = true; 349 | } 350 | 351 | void Emu_Player::dac_stop(uint8_t sid) 352 | { 353 | //printf("Emu_Player stop stream %02x\n", sid); 354 | streams[sid].active = false; 355 | } 356 | 357 | //! Initialize sound chip. 358 | void Emu_Player::poke32(uint32_t offset, uint32_t data) 359 | { 360 | uint32_t clock = data & 0x3fffffff; 361 | bool dual_chip = data >> 30; 362 | bool extra_flag = data >> 31; 363 | switch(offset) 364 | { 365 | case 0x0c: 366 | devices[DEVID_SN76496].set_default_volume(0x80); 367 | devices[DEVID_SN76496].init_sn76489(clock); 368 | break; 369 | case 0x2c: 370 | devices[DEVID_YM2612].init_ym2612(clock); 371 | break; 372 | default: 373 | printf("Emu_Player poke %02x = %08x\n", offset, data); 374 | break; 375 | } 376 | } 377 | 378 | void Emu_Player::poke16(uint32_t offset, uint16_t data) 379 | { 380 | printf("Emu_Player poke %02x = %04x\n", offset, data); 381 | } 382 | 383 | void Emu_Player::poke8(uint32_t offset, uint8_t data) 384 | { 385 | printf("Emu_Player poke %02x = %02x\n", offset, data); 386 | } 387 | 388 | void Emu_Player::set_loop() 389 | { 390 | printf("Emu_Player set loop\n"); 391 | } 392 | 393 | void Emu_Player::stop() 394 | { 395 | printf("Emu_Player stop\n"); 396 | set_finished(true); 397 | } 398 | 399 | void Emu_Player::datablock(uint8_t dbtype, uint32_t dbsize, const uint8_t* db, uint32_t maxsize, uint32_t mask, 400 | uint32_t flags, uint32_t offset) 401 | { 402 | #if 0 403 | printf("Emu_Player datablock %02x = size %d, maxsize = %d, mask = %08x, flags = %08x, offset = %08x\n", 404 | dbtype, 405 | dbsize, maxsize, mask, flags, offset); 406 | #endif 407 | datablocks[dbtype].resize(maxsize); 408 | std::copy_n(db, dbsize, datablocks[dbtype].begin() + offset); 409 | } 410 | -------------------------------------------------------------------------------- /src/emu_player.h: -------------------------------------------------------------------------------- 1 | #ifndef EMU_INTERFACE_H 2 | #define EMU_INTERFACE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #if defined(LOCAL_LIBVGM) 9 | #include "emu/EmuStructs.h" 10 | #include "emu/SoundEmu.h" 11 | #include "emu/Resampler.h" 12 | #else 13 | #include 14 | #include 15 | #include 16 | #endif 17 | 18 | #include "audio_manager.h" 19 | #include "vgm.h" 20 | #include "driver.h" 21 | 22 | class Device_Wrapper 23 | { 24 | public: 25 | Device_Wrapper(); 26 | virtual ~Device_Wrapper(); 27 | 28 | void set_default_volume(uint16_t vol); 29 | 30 | void set_rate(uint32_t rate); 31 | 32 | void init_sn76489(uint32_t freq, 33 | uint8_t lfsr_w = 0x10, 34 | uint16_t lfsr_t = 0x09); 35 | 36 | void init_ym2612(uint32_t freq); 37 | 38 | void write(uint16_t addr, uint16_t data); 39 | void write(uint8_t port, uint16_t reg, uint16_t data); 40 | 41 | void get_sample(WAVE_32BS* output, int count); 42 | 43 | void set_mute_mask(uint32_t mask); 44 | 45 | private: 46 | DEV_INFO dev; 47 | RESMPL_STATE resmpl; 48 | 49 | bool dev_init; 50 | bool resmpl_init; 51 | 52 | enum { 53 | NONE = 0, 54 | A8D8, 55 | P1A8D8, 56 | } write_type; 57 | 58 | uint32_t sample_rate; 59 | uint16_t volume; 60 | DEVFUNC_WRITE_A8D8 write_a8d8; 61 | }; 62 | 63 | class Emu_Player 64 | : private VGM_Interface 65 | , public Audio_Stream 66 | { 67 | public: 68 | Emu_Player(std::shared_ptr song, uint32_t start_position = 0); 69 | virtual ~Emu_Player(); 70 | 71 | std::shared_ptr& get_driver(); 72 | 73 | void set_mute_mask(const std::map& mask_map); 74 | 75 | void setup_stream(uint32_t sample_rate); 76 | int get_sample(WAVE_32BS* output, int count, int channels); 77 | void stop_stream(); 78 | 79 | private: 80 | void handle_error(const char* str); 81 | void write(uint8_t command, uint16_t port, uint16_t reg, uint16_t data); 82 | void dac_setup(uint8_t sid, uint8_t chip_id, uint32_t port, uint32_t reg, uint8_t db_id); 83 | void dac_start(uint8_t sid, uint32_t start, uint32_t length, uint32_t freq); 84 | void dac_stop(uint8_t sid); 85 | void poke32(uint32_t offset, uint32_t data); 86 | void poke16(uint32_t offset, uint16_t data); 87 | void poke8(uint32_t offset, uint8_t data); 88 | void set_loop(); 89 | void stop(); 90 | void datablock( 91 | uint8_t dbtype, 92 | uint32_t dbsize, 93 | const uint8_t* db, 94 | uint32_t maxsize, 95 | uint32_t mask = 0xffffffff, 96 | uint32_t flags = 0, 97 | uint32_t offset = 0); 98 | 99 | struct Stream 100 | { 101 | uint8_t chip_id; 102 | uint8_t port; 103 | uint8_t reg; 104 | uint8_t db_id; 105 | bool active; 106 | uint32_t position; 107 | uint32_t length; 108 | uint32_t freq; 109 | int32_t counter; 110 | }; 111 | 112 | int sample_rate; 113 | float delta_time; 114 | float sample_delta; 115 | float play_time; 116 | float play_time2; 117 | 118 | std::map devices; 119 | std::map> datablocks; 120 | std::map streams; 121 | 122 | std::shared_ptr driver; 123 | std::shared_ptr song; 124 | }; 125 | 126 | #endif 127 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main_window.h" 2 | #include "audio_manager.h" 3 | 4 | // dear imgui: standalone example application for GLFW + OpenGL 3, using programmable pipeline 5 | // If you are new to dear imgui, see examples/README.txt and documentation at the top of imgui.cpp. 6 | // (GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.) 7 | 8 | #include "imgui.h" 9 | #include "imgui_impl_glfw.h" 10 | #include "imgui_impl_opengl3.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | // TODO : https://www.gnu.org/software/libc/manual/html_node/Backtraces.html 18 | //#ifdef defined(__GLIBC__) && !defined(__UCLIBC__) && !defined(__MUSL__) 19 | //#include 20 | //#endif 21 | 22 | // About Desktop OpenGL function loaders: 23 | // Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers. 24 | // Helper libraries are often used for this purpose! Here we are supporting a few common ones (gl3w, glew, glad). 25 | // You may use another loader/header of your choice (glext, glLoadGen, etc.), or chose to manually implement your own. 26 | #if defined(IMGUI_IMPL_OPENGL_LOADER_GL3W) 27 | #include // Initialize with gl3wInit() 28 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW) 29 | #include // Initialize with glewInit() 30 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD) 31 | #include // Initialize with gladLoadGL() 32 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2) 33 | #define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors. 34 | #include // Initialize with glbinding::Binding::initialize() 35 | #include 36 | using namespace gl; 37 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3) 38 | #define GLFW_INCLUDE_NONE // GLFW including OpenGL headers causes ambiguity or multiple definition errors. 39 | #include // Initialize with glbinding::initialize() 40 | #include 41 | using namespace gl; 42 | #else 43 | #include IMGUI_IMPL_OPENGL_LOADER_CUSTOM 44 | #endif 45 | 46 | // Include glfw3.h after our OpenGL definitions 47 | #include 48 | 49 | #ifdef _WIN32 50 | #define GLFW_EXPOSE_NATIVE_WIN32 51 | #include 52 | #endif 53 | 54 | // [Win32] Our example includes a copy of glfw3.lib pre-compiled with VS2010 to maximize ease of testing and compatibility with old VS compilers. 55 | // To link with VS2010-era libraries, VS2015+ requires linking with legacy_stdio_definitions.lib, which we do using this pragma. 56 | // Your own project should not be affected, as you are likely to link with a newer binary of GLFW that is adequate for your version of Visual Studio. 57 | #if defined(_MSC_VER) && (_MSC_VER >= 1900) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) 58 | #pragma comment(lib, "legacy_stdio_definitions") 59 | #endif 60 | 61 | // Our state 62 | Main_Window main_window; 63 | 64 | static void sig_handler(int signal) 65 | { 66 | // dump current editor state 67 | std::time_t time = std::time(nullptr); 68 | FILE* file = fopen("crash.log", "a"); 69 | fputs("========================================================================\n", file); 70 | fputs("Crash type : ", file); 71 | if(signal == SIGSEGV) 72 | fputs("SIGSEGV\n", file); 73 | else if(signal == SIGFPE) 74 | fputs("SIGFPE\n", file); 75 | else 76 | fprintf(file, "%d\n", signal); 77 | fprintf(file, "Crash time : %s\n", std::asctime(std::localtime(&time))); 78 | fputs(main_window.dump_state_all().c_str(), file); 79 | fputs("========================================================================\n", file); 80 | fclose(file); 81 | fprintf(stderr, "Saved dump to crash.log"); 82 | std::abort(); 83 | } 84 | 85 | static void glfw_error_callback(int error, const char* description) 86 | { 87 | fprintf(stderr, "Glfw Error %d: %s\n", error, description); 88 | } 89 | 90 | static void restyle_with_scale(float scale) 91 | { 92 | const float default_font_size = 13.0f; 93 | 94 | ImGuiStyle& current_style = ImGui::GetStyle(); 95 | ImGuiStyle new_style; 96 | std::copy(current_style.Colors, current_style.Colors + ImGuiCol_COUNT, new_style.Colors); 97 | new_style.ScaleAllSizes(scale); 98 | current_style = new_style; 99 | 100 | ImFontConfig font_config; 101 | font_config.SizePixels = default_font_size * scale; 102 | ImGui::GetIO().Fonts->AddFontDefault(&font_config); 103 | } 104 | 105 | int main(int argc, char* argv[]) 106 | { 107 | int driver_id = -1; 108 | int device_id = -1; 109 | float ui_scale = 1.0f; 110 | int carg = 1; 111 | while(carg < argc) 112 | { 113 | if(!std::strcmp(argv[carg], "--driver-id") && (argc > carg)) 114 | { 115 | driver_id = strtol(argv[++carg], NULL, 0); 116 | } 117 | if(!std::strcmp(argv[carg], "--device-id") && (argc > carg)) 118 | { 119 | device_id = strtol(argv[++carg], NULL, 0); 120 | } 121 | if(!std::strcmp(argv[carg], "--ui-scale") && (argc > carg)) 122 | { 123 | ui_scale = strtof(argv[++carg], NULL); 124 | } 125 | carg++; 126 | } 127 | 128 | // Setup window 129 | glfwSetErrorCallback(glfw_error_callback); 130 | if (!glfwInit()) 131 | return 1; 132 | 133 | glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); 134 | 135 | Audio_Manager::get().set_sample_rate(44100); 136 | 137 | // Decide GL+GLSL versions 138 | #if __APPLE__ 139 | // GL 3.2 + GLSL 150 140 | const char* glsl_version = "#version 150"; 141 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 142 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); 143 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only 144 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac 145 | #else 146 | // GL 3.0 + GLSL 130 147 | const char* glsl_version = "#version 130"; 148 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 149 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); 150 | //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only 151 | //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only 152 | #endif 153 | 154 | // Create window with graphics context 155 | GLFWwindow* window = glfwCreateWindow(1280, 720, "mmlgui", NULL, NULL); 156 | if (window == NULL) 157 | return 1; 158 | glfwMakeContextCurrent(window); 159 | glfwSwapInterval(1); // Enable vsync 160 | 161 | #ifdef _WIN32 162 | // libvgm DSound support may require the window handle 163 | Audio_Manager::get().set_window_handle(glfwGetWin32Window(window)); 164 | #endif 165 | Audio_Manager::get().set_driver(driver_id, device_id); 166 | 167 | // Initialize OpenGL loader 168 | #if defined(IMGUI_IMPL_OPENGL_LOADER_GL3W) 169 | bool err = gl3wInit() != 0; 170 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLEW) 171 | bool err = glewInit() != GLEW_OK; 172 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLAD) 173 | bool err = gladLoadGL() == 0; 174 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING2) 175 | bool err = false; 176 | glbinding::Binding::initialize(); 177 | #elif defined(IMGUI_IMPL_OPENGL_LOADER_GLBINDING3) 178 | bool err = false; 179 | glbinding::initialize([](const char* name) { return (glbinding::ProcAddress)glfwGetProcAddress(name); }); 180 | #else 181 | bool err = false; // If you use IMGUI_IMPL_OPENGL_LOADER_CUSTOM, your loader is likely to requires some form of initialization. 182 | #endif 183 | if (err) 184 | { 185 | fprintf(stderr, "Failed to initialize OpenGL loader!\n"); 186 | return 1; 187 | } 188 | 189 | // Setup Dear ImGui context 190 | IMGUI_CHECKVERSION(); 191 | ImGui::CreateContext(); 192 | ImGuiIO& io = ImGui::GetIO(); (void)io; 193 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 194 | //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 195 | 196 | // Setup Dear ImGui style 197 | ImGui::StyleColorsDark(); 198 | //ImGui::StyleColorsClassic(); 199 | 200 | // Setup Platform/Renderer bindings 201 | ImGui_ImplGlfw_InitForOpenGL(window, true); 202 | ImGui_ImplOpenGL3_Init(glsl_version); 203 | 204 | // Setup scaling 205 | float scale; 206 | glfwGetWindowContentScale(window, &scale, NULL); 207 | restyle_with_scale(scale * ui_scale); 208 | 209 | glfwSetWindowContentScaleCallback(window, [](GLFWwindow* window, float xscale, float yscale) { 210 | restyle_with_scale(xscale); 211 | }); 212 | 213 | // Load Fonts 214 | // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. 215 | // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. 216 | // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). 217 | // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. 218 | // - Read 'docs/FONTS.txt' for more instructions and details. 219 | // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! 220 | //io.Fonts->AddFontDefault(); 221 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); 222 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); 223 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); 224 | //io.Fonts->AddFontFromFileTTF("../../misc/fonts/ProggyTiny.ttf", 10.0f); 225 | //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese()); 226 | //IM_ASSERT(font != NULL); 227 | 228 | std::signal(SIGSEGV, sig_handler); 229 | std::signal(SIGFPE, sig_handler); 230 | 231 | ImVec4 clear_color = ImVec4(0.06f, 0.11f, 0.20f, 1.00f); 232 | 233 | // Main loop 234 | while (main_window.get_close_request() != Window::CLOSE_OK) 235 | { 236 | // Poll and handle events (inputs, window resize, etc.) 237 | // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. 238 | // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application. 239 | // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application. 240 | // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. 241 | glfwPollEvents(); 242 | 243 | // Start the Dear ImGui frame 244 | ImGui_ImplOpenGL3_NewFrame(); 245 | ImGui_ImplGlfw_NewFrame(); 246 | ImGui::NewFrame(); 247 | 248 | if(main_window.get_close_request() == Window::CLOSE_NOT_OK) 249 | { 250 | main_window.clear_close_request(); 251 | glfwSetWindowShouldClose(window, 0); 252 | } 253 | 254 | if(glfwWindowShouldClose(window)) 255 | main_window.close_request_all(); 256 | 257 | // run window manager 258 | Window::modal_open = false; 259 | main_window.display_all(); 260 | 261 | // Rendering 262 | ImGui::Render(); 263 | int display_w, display_h; 264 | glfwGetFramebufferSize(window, &display_w, &display_h); 265 | glViewport(0, 0, display_w, display_h); 266 | glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w); 267 | glClear(GL_COLOR_BUFFER_BIT); 268 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 269 | 270 | glfwSwapBuffers(window); 271 | } 272 | 273 | // Cleanup 274 | ImGui_ImplOpenGL3_Shutdown(); 275 | ImGui_ImplGlfw_Shutdown(); 276 | ImGui::DestroyContext(); 277 | Audio_Manager::get().clean_up(); 278 | 279 | glfwDestroyWindow(window); 280 | glfwTerminate(); 281 | 282 | return 0; 283 | } 284 | -------------------------------------------------------------------------------- /src/main_window.cpp: -------------------------------------------------------------------------------- 1 | #include "imgui.h" 2 | #include "main_window.h" 3 | #include "editor_window.h" 4 | #include "config_window.h" 5 | #include "audio_manager.h" 6 | 7 | #include 8 | #include 9 | 10 | //===================================================================== 11 | static const char* version_string = "v0.1"; 12 | 13 | static const char* gpl_string = 14 | "This program is free software; you can redistribute it and/or\n" 15 | "modify it under the terms of the GNU General Public License\n" 16 | // TODO: Need to clarify the license of libvgm first. 17 | //"as published by the Free Software Foundation; either version 2\n" 18 | //"of the License, or (at your option) any later version.\n" 19 | "version 2 as published by the Free Software Foundation.\n" 20 | "\n" 21 | "This program is distributed in the hope that it will be useful,\n" 22 | "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" 23 | "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" 24 | "GNU General Public License for more details.\n" 25 | "\n" 26 | "You should have received a copy of the GNU General Public License\n" 27 | "along with this program; if not, write to the Free Software\n" 28 | "Foundation Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n" 29 | "02110-1301, USA."; 30 | //===================================================================== 31 | 32 | #ifndef IMGUI_DISABLE_DEMO_WINDOWS 33 | static bool debug_imgui_demo_windows = false; 34 | #endif 35 | #ifndef IMGUI_DISABLE_METRICS_WINDOW 36 | static bool debug_imgui_metrics = false; 37 | #endif 38 | static bool debug_state_window = false; 39 | static bool debug_audio_window = false; 40 | static bool debug_ui_window = false; 41 | 42 | static void debug_menu() 43 | { 44 | #ifndef IMGUI_DISABLE_METRICS_WINDOW 45 | ImGui::MenuItem("ImGui metrics", NULL, &debug_imgui_metrics); 46 | #endif 47 | #ifndef IMGUI_DISABLE_DEMO_WINDOWS 48 | ImGui::MenuItem("ImGui demo windows", NULL, &debug_imgui_demo_windows); 49 | #endif 50 | ImGui::MenuItem("Select audio device", NULL, &debug_audio_window); 51 | ImGui::MenuItem("Display dump state", NULL, &debug_state_window); 52 | ImGui::MenuItem("UI settings", NULL, &debug_ui_window); 53 | if (ImGui::MenuItem("Quit")) 54 | { 55 | // if ctrl+shift was held, stimulate a segfault 56 | if(ImGui::GetIO().KeyCtrl && ImGui::GetIO().KeyAlt) 57 | *(int*)0 = 0; 58 | // if ctrl was held, raise a signal (usually that won't happen) 59 | if(ImGui::GetIO().KeyCtrl) 60 | std::raise(SIGFPE); 61 | // otherwise we quit normally 62 | else 63 | main_window.close_request_all(); 64 | } 65 | else if (ImGui::IsItemHovered()) 66 | ImGui::SetTooltip("hold ctrl to make a crash dump\n"); 67 | ImGui::EndMenu(); 68 | } 69 | 70 | static void debug_window() 71 | { 72 | #ifndef IMGUI_DISABLE_METRICS_WINDOW 73 | if(debug_imgui_metrics) 74 | ImGui::ShowMetricsWindow(&debug_imgui_metrics); 75 | #endif 76 | #ifndef IMGUI_DISABLE_DEMO_WINDOWS 77 | if(debug_imgui_demo_windows) 78 | ImGui::ShowDemoWindow(&debug_imgui_demo_windows); 79 | #endif 80 | if(debug_state_window) 81 | { 82 | std::string debug_state = main_window.dump_state_all(); 83 | ImGui::SetNextWindowPos(ImVec2(500, 400), ImGuiCond_Once); 84 | ImGui::Begin("Debug state", &debug_state_window); 85 | if (ImGui::Button("copy to clipboard")) 86 | ImGui::SetClipboardText(debug_state.c_str()); 87 | ImGui::BeginChild("debug_log", ImGui::GetContentRegionAvail(), false, ImGuiWindowFlags_HorizontalScrollbar); 88 | ImGui::TextUnformatted(debug_state.c_str(), debug_state.c_str()+debug_state.size()); 89 | ImGui::EndChild(); 90 | ImGui::End(); 91 | } 92 | if(debug_audio_window) 93 | { 94 | ImGui::Begin("Select Audio Device", &debug_audio_window, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); 95 | auto& am = Audio_Manager::get(); 96 | auto driver_list = am.get_driver_list(); 97 | auto driver = am.get_driver(); 98 | if (ImGui::ListBoxHeader("Audio driver", 5)) 99 | { 100 | if(ImGui::Selectable("Default", driver == -1)) 101 | { 102 | am.set_driver(-1, -1); 103 | } 104 | for(auto && i : driver_list) 105 | { 106 | if(ImGui::Selectable(i.second.second.c_str(), driver == i.first)) 107 | { 108 | am.set_driver(i.first, -1); 109 | } 110 | } 111 | ImGui::ListBoxFooter(); 112 | } 113 | 114 | auto device_list = am.get_device_list(); 115 | auto device = am.get_device(); 116 | if (ImGui::ListBoxHeader("Audio device", 5)) 117 | { 118 | if(ImGui::Selectable("Default", device == -1)) 119 | { 120 | am.set_device(-1); 121 | } 122 | for(auto && i : device_list) 123 | { 124 | if(ImGui::Selectable(i.second.c_str(), device == i.first)) 125 | { 126 | am.set_device(i.first); 127 | } 128 | } 129 | ImGui::ListBoxFooter(); 130 | } 131 | ImGui::End(); 132 | } 133 | if(debug_ui_window) 134 | { 135 | ImGui::Begin("UI settings", &debug_ui_window, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); 136 | 137 | ImGui::DragFloat("UI scaling", &ImGui::GetIO().FontGlobalScale, 0.005f, 0.3f, 2.0f, "%.2f"); 138 | 139 | ImGui::End(); 140 | } 141 | } 142 | 143 | //===================================================================== 144 | FPS_Overlay::FPS_Overlay() 145 | { 146 | type = WT_FPS_OVERLAY; 147 | } 148 | 149 | void FPS_Overlay::display() 150 | { 151 | const float DISTANCE = 10.0f; 152 | static int corner = 0; 153 | ImGuiIO& io = ImGui::GetIO(); 154 | if (corner != -1) 155 | { 156 | ImVec2 window_pos = ImVec2((corner & 1) ? io.DisplaySize.x - DISTANCE : DISTANCE, (corner & 2) ? io.DisplaySize.y - DISTANCE : DISTANCE); 157 | ImVec2 window_pos_pivot = ImVec2((corner & 1) ? 1.0f : 0.0f, (corner & 2) ? 1.0f : 0.0f); 158 | ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); 159 | } 160 | ImGui::SetNextWindowBgAlpha(0.35f); // Transparent background 161 | if (ImGui::Begin("overlay", &active, (corner != -1 ? ImGuiWindowFlags_NoMove : 0) | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) 162 | { 163 | ImGui::Text("mmlgui (%s)", version_string); 164 | ImGui::Separator(); 165 | ImGui::Text("FPS: %.2f", ImGui::GetIO().Framerate); 166 | if (ImGui::BeginPopupContextWindow()) 167 | { 168 | if (ImGui::BeginMenu("Debug")) 169 | debug_menu(); 170 | if (ImGui::BeginMenu("Overlay")) 171 | { 172 | if (ImGui::MenuItem("Custom", NULL, corner == -1)) corner = -1; 173 | if (ImGui::MenuItem("Top-left", NULL, corner == 0)) corner = 0; 174 | if (ImGui::MenuItem("Top-right", NULL, corner == 1)) corner = 1; 175 | if (ImGui::MenuItem("Bottom-left", NULL, corner == 2)) corner = 2; 176 | if (ImGui::MenuItem("Bottom-right", NULL, corner == 3)) corner = 3; 177 | if (ImGui::MenuItem("Close")) active = false; 178 | ImGui::EndMenu(); 179 | } 180 | ImGui::EndPopup(); 181 | } 182 | } 183 | ImGui::End(); 184 | } 185 | 186 | //===================================================================== 187 | About_Window::About_Window() 188 | { 189 | type = WT_ABOUT; 190 | } 191 | void About_Window::display() 192 | { 193 | std::string window_id; 194 | window_id = "About mmlgui##" + std::to_string(id); 195 | 196 | ImGui::Begin(window_id.c_str(), &active, ImGuiWindowFlags_AlwaysAutoResize); 197 | ImGui::Text("MML (Music Macro Language) editor and player."); 198 | ImGui::Text("%s", version_string); 199 | ImGui::Text("Copyright 2020-2021 Ian Karlsson."); 200 | ImGui::Separator(); 201 | ImGui::Text("Source code repository and issue reporting:"); 202 | ImGui::Text("https://github.com/superctr/mmlgui"); 203 | ImGui::Separator(); 204 | ImGui::BeginChild("credits", ImVec2(500, 300), false); 205 | ImGui::Text("%s", gpl_string); 206 | ImGui::Separator(); 207 | ImGui::Text("This program uses the following libraries:"); 208 | ImGui::BulletText("ctrmml\nBy Ian Karlsson\nGPLv2 or later\nhttps://github.com/superctr/ctrmml"); 209 | ImGui::BulletText("Dear ImGui\nBy Omar Cornut, et al.\nMIT license\nhttps://github.com/ocornut/imgui"); 210 | ImGui::BulletText("glfw\nBy Marcus Geelnard & Camilla Löwy\nzlib / libpng license\nhttps://glfw.org"); 211 | ImGui::BulletText("ImGuiColorTextEdit\nBy BalazsJako\nMIT license\nhttps://github.com/superctr/ImGuiColorTextEdit"); 212 | ImGui::BulletText("libvgm\nBy Valley Bell, et al.\nGPLv2\nhttps://github.com/ValleyBell/libvgm"); 213 | ImGui::EndChild(); 214 | ImGui::End(); 215 | } 216 | 217 | //===================================================================== 218 | Main_Window::Main_Window() 219 | : show_about(false) 220 | , show_config(false) 221 | { 222 | children.push_back(std::make_shared()); 223 | children.push_back(std::make_shared()); 224 | } 225 | 226 | void Main_Window::display() 227 | { 228 | if (ImGui::BeginPopupContextVoid()) 229 | { 230 | bool overlay_active = find_child(WT_FPS_OVERLAY) != children.end(); 231 | if (ImGui::MenuItem("Show FPS Overlay", nullptr, overlay_active)) 232 | { 233 | if(overlay_active) 234 | { 235 | find_child(WT_FPS_OVERLAY)->get()->close(); 236 | } 237 | else 238 | { 239 | children.push_back(std::make_shared()); 240 | } 241 | } 242 | if (ImGui::MenuItem("New MML...", nullptr, nullptr)) 243 | { 244 | children.push_back(std::make_shared()); 245 | } 246 | if (ImGui::MenuItem("About...", nullptr, nullptr)) 247 | { 248 | show_about_window(); 249 | } 250 | ImGui::EndPopup(); 251 | } 252 | if (show_about) 253 | { 254 | show_about = false; 255 | bool overlay_active = find_child(WT_ABOUT) != children.end(); 256 | if(!overlay_active) 257 | { 258 | children.push_back(std::make_shared()); 259 | } 260 | } 261 | if (show_config) 262 | { 263 | show_config = false; 264 | bool overlay_active = find_child(WT_CONFIG) != children.end(); 265 | if(!overlay_active) 266 | { 267 | children.push_back(std::make_shared()); 268 | } 269 | } 270 | debug_window(); 271 | } 272 | 273 | void Main_Window::show_about_window() 274 | { 275 | show_about = true; 276 | } 277 | 278 | void Main_Window::show_config_window() 279 | { 280 | show_config = true; 281 | } 282 | 283 | -------------------------------------------------------------------------------- /src/main_window.h: -------------------------------------------------------------------------------- 1 | #ifndef MAIN_WINDOW_H 2 | #define MAIN_WINDOW_H 3 | 4 | #include "window.h" 5 | 6 | //! FPS/version overlay 7 | class FPS_Overlay : public Window 8 | { 9 | public: 10 | FPS_Overlay(); 11 | void display() override; 12 | }; 13 | 14 | //! About window 15 | class About_Window : public Window 16 | { 17 | public: 18 | About_Window(); 19 | void display() override; 20 | }; 21 | 22 | //! Main window container 23 | class Main_Window : public Window 24 | { 25 | public: 26 | Main_Window(); 27 | void display() override; 28 | 29 | void show_about_window(); 30 | void show_config_window(); 31 | 32 | private: 33 | bool show_about; 34 | bool show_config; 35 | }; 36 | 37 | extern Main_Window main_window; 38 | 39 | #endif //MAIN_WINDOW_H 40 | -------------------------------------------------------------------------------- /src/song_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "song_manager.h" 2 | #include "track_info.h" 3 | #include "song.h" 4 | #include "input.h" 5 | #include "player.h" 6 | 7 | #include "mml_input.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | const int Song_Manager::max_channels = 16; 16 | 17 | // TODO : Use Song to get the correct map for each platform 18 | const std::map> Song_Manager::track_channel_table = { 19 | {0, {2, 1<<0}}, // YM2612 20 | {1, {2, 1<<1}}, 21 | {2, {2, 1<<2}}, 22 | {3, {2, 1<<3}}, 23 | {4, {2, 1<<4}}, 24 | {5, {2, (1<<5)|(1<<6)}}, 25 | 26 | {6, {0, 1<<0}}, // SN76489 27 | {7, {0, 1<<1}}, 28 | {8, {0, 1<<2}}, 29 | {9, {0, 1<<3}}, 30 | 31 | {10, {2, (1<<5)|(1<<6)}}, // PCM 2,3 32 | {11, {2, (1<<5)|(1<<6)}}, 33 | 34 | {12, {2, 1<<2}}, // FM3 Dummy 35 | {13, {2, 1<<2}}, 36 | {14, {2, 1<<2}}, 37 | {15, {2, 1<<2}}, 38 | }; 39 | 40 | //! constructs a Song_Manager 41 | Song_Manager::Song_Manager() 42 | : worker_ptr(nullptr) 43 | , worker_fired(false) 44 | , job_done(false) 45 | , job_successful(false) 46 | , song(nullptr) 47 | , player(nullptr) 48 | , editor_position({-1, -1}) 49 | , editor_jump_hack(false) 50 | , song_pos_at_line(0) 51 | , song_pos_at_cursor(0) 52 | { 53 | reset_mute(); 54 | } 55 | 56 | //! Song_Manager destructor 57 | Song_Manager::~Song_Manager() 58 | { 59 | if(worker_ptr && worker_ptr->joinable()) 60 | { 61 | { 62 | // kill worker thread 63 | std::lock_guard guard(mutex); 64 | worker_fired = true; 65 | } 66 | condition_variable.notify_one(); 67 | 68 | if(!job_done) 69 | { 70 | // detach the compile thread in order to not crash the entire application 71 | std::cerr << "Song_Manager killed during ongoing compile job. Compiler stuck?\n"; 72 | worker_ptr->detach(); 73 | } 74 | else 75 | { 76 | // wait for compile thread to end normally 77 | worker_ptr->join(); 78 | } 79 | } 80 | } 81 | 82 | //! Get compile result 83 | Song_Manager::Compile_Result Song_Manager::get_compile_result() 84 | { 85 | std::lock_guard guard(mutex); 86 | if(!job_done || !worker_ptr) 87 | return COMPILE_NOT_DONE; 88 | else if(job_successful) 89 | return COMPILE_OK; 90 | else 91 | return COMPILE_ERROR; 92 | } 93 | 94 | //! Get compile in progress 95 | bool Song_Manager::get_compile_in_progress() 96 | { 97 | std::lock_guard guard(mutex); 98 | if(!worker_ptr) 99 | return false; 100 | else 101 | return !job_done; 102 | } 103 | 104 | //! Compile from a buffer 105 | /* 106 | * \return non-zero if compile thread was busy. 107 | * \return zero if compile was successfully started 108 | */ 109 | int Song_Manager::compile(const std::string& buffer, const std::string& filename) 110 | { 111 | { 112 | std::lock_guard guard(mutex); 113 | if(job_done || !worker_ptr) 114 | { 115 | if(!worker_ptr) 116 | worker_ptr = std::make_unique(&Song_Manager::worker, this); 117 | 118 | job_buffer = buffer; 119 | job_filename = filename; 120 | job_done = 0; 121 | job_successful = 0; 122 | } 123 | else 124 | { 125 | return -1; 126 | } 127 | } 128 | condition_variable.notify_one(); 129 | return 0; 130 | } 131 | 132 | //! Start song playback. 133 | /*! 134 | * \exception InputError Song playback errors, for example missing samples or bad data. 135 | * \exception std::exception Should always be catched to avoid data loss. 136 | */ 137 | void Song_Manager::play(uint32_t start_position) 138 | { 139 | stop(); 140 | 141 | Audio_Manager& am = Audio_Manager::get(); 142 | player = std::make_shared(get_song(), start_position); 143 | player->set_mute_mask(mute_mask); 144 | am.add_stream(std::static_pointer_cast(player)); 145 | } 146 | 147 | //! Stop song playback 148 | void Song_Manager::stop() 149 | { 150 | if(player.get() != nullptr) 151 | { 152 | player->set_finished(true); 153 | } 154 | } 155 | 156 | //! Get song data 157 | std::shared_ptr Song_Manager::get_song() 158 | { 159 | std::lock_guard guard(mutex); 160 | return song; 161 | } 162 | 163 | //! Get player 164 | std::shared_ptr Song_Manager::get_player() 165 | { 166 | return player; 167 | } 168 | 169 | //! Get track info data 170 | std::shared_ptr> Song_Manager::get_tracks() 171 | { 172 | std::lock_guard guard(mutex); 173 | return tracks; 174 | } 175 | 176 | //! Get line info 177 | std::shared_ptr Song_Manager::get_lines() 178 | { 179 | std::lock_guard guard(mutex); 180 | return lines; 181 | } 182 | 183 | //! Get error message 184 | std::string Song_Manager::get_error_message() 185 | { 186 | std::lock_guard guard(mutex); 187 | return error_message; 188 | } 189 | 190 | //! Check if event is a note or subroutine call 191 | static inline bool is_note_or_jump(Event::Type type) 192 | { 193 | if(type == Event::NOTE || type == Event::TIE || type == Event::REST || type == Event::JUMP) 194 | return true; 195 | else 196 | return false; 197 | } 198 | 199 | //! Check if event is a loop end 200 | static inline bool is_loop_event(Event::Type type) 201 | { 202 | if(type == Event::LOOP_START || type == Event::LOOP_BREAK || type == Event::LOOP_END) 203 | return true; 204 | else 205 | return false; 206 | } 207 | 208 | //! Get the length of a loop section. 209 | static unsigned int get_loop_length(Song& song, Track& track, unsigned int position, InputRef*& refptr, unsigned int max_recursion) 210 | { 211 | // This function is a hack only needed in the case where a subroutine ends with a loop section. 212 | // Until I properly count the length of subroutines again, this will be necessary. 213 | // Example: passport.mml:177 214 | int depth = 0; 215 | int count = track.get_event(position).param - 1; 216 | int start_time = 0; 217 | int end_time = track.get_event(position).play_time; 218 | int break_time = 0; 219 | refptr = nullptr; 220 | while(position-- > 0) 221 | { 222 | auto event = track.get_event(position); 223 | start_time = event.play_time; 224 | if(is_note_or_jump(event.type) && refptr == nullptr) 225 | { 226 | refptr = event.reference.get(); 227 | } 228 | else if(event.type == Event::LOOP_END) 229 | { 230 | if(refptr == nullptr) 231 | get_loop_length(song, track, position, refptr, max_recursion - 1); 232 | depth++; 233 | } 234 | else if(event.type == Event::LOOP_BREAK && !depth) 235 | { 236 | break_time = end_time - event.play_time; 237 | refptr = nullptr; 238 | } 239 | else if(event.type == Event::LOOP_START) 240 | { 241 | if(depth) 242 | depth--; 243 | else 244 | break; 245 | } 246 | } 247 | unsigned int result = (end_time - start_time) * count - break_time; 248 | return result; 249 | } 250 | 251 | //! Get the length of a subroutine. 252 | static unsigned int get_subroutine_length(Song& song, unsigned int param, unsigned int max_recursion) 253 | { 254 | try 255 | { 256 | Track& track = song.get_track(param); 257 | InputRef* dummy; 258 | if(track.get_event_count()) 259 | { 260 | auto event = track.get_event(track.get_event_count() - 1); 261 | uint32_t end_time; 262 | if(event.type == Event::JUMP && max_recursion != 0) 263 | end_time = event.play_time + get_subroutine_length(song, event.param, max_recursion - 1); 264 | else if(event.type == Event::LOOP_END && max_recursion != 0) 265 | end_time = event.play_time + get_loop_length(song, track, track.get_event_count() - 1, dummy, max_recursion - 1); 266 | else 267 | end_time = event.play_time + event.on_time + event.off_time; 268 | return end_time - track.get_event(0).play_time; 269 | } 270 | } 271 | catch(std::exception &e) 272 | { 273 | } 274 | return 0; 275 | } 276 | 277 | //! Set the current editor position, and find any events adjacent to the editor cursor. 278 | /*! 279 | * Call this function from the UI thread. 280 | * 281 | * Cursor should be displayed if an InputRef* from editor_refs can be also found in the Track_Info array (tracks). 282 | * 283 | * This approach is not perfect, there are a few things that need to be looked into. 284 | */ 285 | void Song_Manager::set_editor_position(const Editor_Position& d) 286 | { 287 | editor_position = d; 288 | 289 | song_pos_at_cursor = UINT_MAX; 290 | song_pos_at_line = UINT_MAX; 291 | 292 | editor_refs.clear(); 293 | editor_jump_hack = false; 294 | 295 | if(d.line != -1 && get_compile_result() == COMPILE_OK) 296 | { 297 | // Take ownership of the song and track info pointers. 298 | auto song = get_song(); 299 | auto lines = get_lines(); 300 | auto& line_map = (*lines.get())[d.line]; 301 | 302 | for(auto && i : line_map) 303 | { 304 | Track& track = song->get_track(i.first); 305 | unsigned int position = i.second; 306 | unsigned int event_count = track.get_event_count(); 307 | 308 | // Disable subroutine cursor hack if we are editing a line containing a subroutine. 309 | if(i.first > max_channels) 310 | editor_jump_hack = true; 311 | 312 | // Find the reference to the note adjacent to the note, and the song playtime at the 313 | // beginning of the line and at the cursor. 314 | if(event_count > 0) 315 | { 316 | InputRef* refptr = nullptr; 317 | 318 | // Backtrack until we find an event adjacent to the cursor 319 | while(position-- > 0) 320 | { 321 | auto ref = track.get_event(position).reference; 322 | if(ref != nullptr) 323 | { 324 | int line = ref->get_line(), column = ref->get_column(); 325 | if((line < d.line) || (line == d.line && column < d.column)) 326 | break; 327 | } 328 | } 329 | 330 | // Select the adjacent note / rest / tie event right of the cursor 331 | while(++position < event_count) 332 | { 333 | auto event = track.get_event(position); 334 | 335 | if(is_note_or_jump(event.type)) 336 | { 337 | refptr = event.reference.get(); 338 | if(event.play_time < song_pos_at_cursor) 339 | song_pos_at_cursor = event.play_time; 340 | break; 341 | } 342 | else if(is_loop_event(event.type)) 343 | { 344 | break; 345 | } 346 | } 347 | 348 | // Nothing was found, select the adjacent event left of cursor. 349 | // Also scan for the first event at the line. 350 | bool passed_line = false; 351 | while((!passed_line || refptr == nullptr) && position-- > 0) 352 | { 353 | auto event = track.get_event(position); 354 | uint32_t length = event.on_time + event.off_time; 355 | InputRef* loop_refptr; 356 | 357 | if(event.type == Event::JUMP) 358 | length = get_subroutine_length(*song, event.param, 10); 359 | else if(event.type == Event::LOOP_END) 360 | length = get_loop_length(*song, track, position, loop_refptr, 10); 361 | 362 | auto ref = track.get_event(position).reference; 363 | if(ref != nullptr) 364 | { 365 | passed_line = ((signed)ref->get_line() < d.line); 366 | if(!passed_line && event.play_time < song_pos_at_line) 367 | song_pos_at_line = event.play_time; 368 | } 369 | 370 | if(refptr == nullptr && length != 0 && is_note_or_jump(event.type)) 371 | { 372 | refptr = event.reference.get(); 373 | if((event.play_time + length) < song_pos_at_cursor) 374 | song_pos_at_cursor = event.play_time + length; 375 | } 376 | else if(refptr == nullptr && length != 0 && event.type == Event::LOOP_END) 377 | { 378 | refptr = loop_refptr; 379 | if((event.play_time + length) < song_pos_at_cursor) 380 | song_pos_at_cursor = event.play_time + length; 381 | } 382 | } 383 | 384 | if(song_pos_at_cursor < song_pos_at_line) 385 | song_pos_at_line = song_pos_at_cursor; 386 | 387 | if(refptr != nullptr) 388 | editor_refs.insert(refptr); 389 | } 390 | } 391 | } 392 | if(song_pos_at_cursor == UINT_MAX) 393 | song_pos_at_cursor = 0; 394 | if(song_pos_at_line == UINT_MAX) 395 | song_pos_at_line = 0; 396 | } 397 | 398 | //! worker thread 399 | void Song_Manager::worker() 400 | { 401 | std::unique_lock lock(mutex); 402 | while(!worker_fired) 403 | { 404 | if(!job_done) 405 | compile_job(lock, job_buffer, job_filename); 406 | condition_variable.wait(lock); 407 | } 408 | } 409 | 410 | //! Compile job 411 | void Song_Manager::compile_job(std::unique_lock& lock, std::string buffer, std::string filename) 412 | { 413 | lock.unlock(); 414 | 415 | bool successful = false; 416 | std::shared_ptr ref = nullptr; 417 | std::shared_ptr temp_song = nullptr; 418 | std::shared_ptr temp_tracks = nullptr; 419 | std::shared_ptr temp_lines = nullptr; 420 | std::string str; 421 | std::string message; 422 | int line = 0; 423 | 424 | try 425 | { 426 | temp_song = std::make_shared(); 427 | temp_tracks = std::make_shared(); 428 | temp_lines = std::make_shared(); 429 | 430 | int path_break = filename.find_last_of("/\\"); 431 | if(path_break != -1) 432 | temp_song->add_tag("include_path", filename.substr(0, path_break + 1)); 433 | 434 | MML_Input input = MML_Input(temp_song.get()); 435 | 436 | // Read MML input line by line 437 | std::stringstream stream(buffer); 438 | for(; std::getline(stream, str);) 439 | { 440 | input.read_line(tabs_to_spaces(str), line); 441 | temp_lines.get()->insert({line, input.get_track_map()}); 442 | line++; 443 | } 444 | 445 | // Generate track note lists. 446 | for(auto it = temp_song->get_track_map().begin(); it != temp_song->get_track_map().end(); it++) 447 | { 448 | // TODO: Max track count should be decided based on the target platform. 449 | if(it->first < max_channels) 450 | temp_tracks->emplace_hint(temp_tracks->end(), 451 | std::make_pair(it->first, Track_Info_Generator(*temp_song, it->second))); 452 | } 453 | 454 | successful = true; 455 | message = ""; 456 | } 457 | catch (InputError& error) 458 | { 459 | ref = error.get_reference(); 460 | message = error.what(); 461 | } 462 | catch (std::exception& except) 463 | { 464 | ref = std::make_shared("", str, line, 0); 465 | message = "Exception: " + std::string(except.what()); 466 | } 467 | 468 | lock.lock(); 469 | job_done = true; 470 | job_successful = successful; 471 | song = temp_song; 472 | tracks = temp_tracks; 473 | lines = temp_lines; 474 | error_message = message; 475 | error_reference = ref; 476 | } 477 | 478 | //! Convert all tabs to spaces in a string. 479 | /*! 480 | * Currently tabstop is hardcoded to 4 to match the editor. 481 | */ 482 | std::string Song_Manager::tabs_to_spaces(const std::string& str) const 483 | { 484 | const unsigned int tabstop = 4; 485 | std::string out = ""; 486 | for(char i : str) 487 | { 488 | if(i == '\t') 489 | { 490 | do 491 | { 492 | out.push_back(' '); 493 | } 494 | while(out.size() % tabstop != 0); 495 | } 496 | else 497 | { 498 | out.push_back(i); 499 | } 500 | } 501 | return out; 502 | } 503 | 504 | std::pair Song_Manager::get_channel(uint16_t track) const 505 | { 506 | auto search = track_channel_table.find(track); 507 | if(search != track_channel_table.end()) 508 | { 509 | return search->second; 510 | } 511 | else 512 | { 513 | return {-1, 0}; 514 | } 515 | } 516 | 517 | void Song_Manager::toggle_mute(uint16_t track_id) 518 | { 519 | auto channel = get_channel(track_id); 520 | if(channel.first >= 0) 521 | { 522 | if(get_mute(track_id)) 523 | { 524 | mute_mask[channel.first] &= ~channel.second; 525 | } 526 | else 527 | { 528 | mute_mask[channel.first] |= channel.second; 529 | } 530 | } 531 | update_mute(); 532 | } 533 | 534 | void Song_Manager::toggle_solo(uint16_t track_id) 535 | { 536 | auto channel = get_channel(track_id); 537 | if(channel.first >= 0) 538 | { 539 | if(get_solo(track_id)) 540 | { 541 | reset_mute(); 542 | } 543 | else 544 | { 545 | for(auto && i : mute_mask) 546 | { 547 | if(i.first == channel.first) 548 | i.second = ~channel.second; 549 | else if(i.first != channel.first) 550 | i.second = 0xffffffff; 551 | } 552 | update_mute(); 553 | } 554 | } 555 | } 556 | 557 | bool Song_Manager::get_mute(uint16_t track_id) const 558 | { 559 | auto channel = get_channel(track_id); 560 | auto it = mute_mask.find(channel.first); 561 | 562 | if(channel.first >= 0 && it != mute_mask.end() && it->second & channel.second) 563 | return true; 564 | else 565 | return false; 566 | } 567 | 568 | bool Song_Manager::get_solo(uint16_t track_id) const 569 | { 570 | auto channel = get_channel(track_id); 571 | bool is_solo = false; 572 | if(channel.first >= 0) 573 | { 574 | is_solo = true; 575 | for(auto && i : mute_mask) 576 | { 577 | if(i.first == channel.first && i.second != (uint32_t)~channel.second) 578 | is_solo = false; 579 | else if(i.first != channel.first && i.second != 0xffffffff) 580 | is_solo = false; 581 | } 582 | } 583 | return is_solo; 584 | } 585 | 586 | void Song_Manager::reset_mute() 587 | { 588 | mute_mask[0x00] = 0; 589 | mute_mask[0x02] = 0; 590 | update_mute(); 591 | } 592 | 593 | void Song_Manager::update_mute() 594 | { 595 | if(player) 596 | player->set_mute_mask(mute_mask); 597 | } 598 | -------------------------------------------------------------------------------- /src/song_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef SONG_MANAGER_H 2 | #define SONG_MANAGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "core.h" 13 | #include "song.h" 14 | #include "input.h" 15 | #include "mml_input.h" 16 | 17 | #include "audio_manager.h" 18 | #include "emu_player.h" 19 | 20 | struct Track_Info; 21 | 22 | class Song_Manager 23 | { 24 | public: 25 | enum Compile_Result 26 | { 27 | COMPILE_NOT_DONE = -1, // also used to indicate that a compile is in progress 28 | COMPILE_OK = 0, 29 | COMPILE_ERROR = 1 30 | }; 31 | 32 | typedef std::map Track_Map; 33 | typedef std::map Line_Map; 34 | typedef std::set Ref_Ptr_Set; 35 | 36 | typedef struct 37 | { 38 | int line; 39 | int column; 40 | } Editor_Position; 41 | 42 | Song_Manager(); 43 | virtual ~Song_Manager(); 44 | 45 | Compile_Result get_compile_result(); 46 | bool get_compile_in_progress(); 47 | 48 | int compile(const std::string& buffer, const std::string& filename); 49 | void play(uint32_t start_position = 0); 50 | void stop(); 51 | 52 | std::shared_ptr get_song(); 53 | std::shared_ptr get_player(); 54 | std::shared_ptr get_tracks(); 55 | std::shared_ptr get_lines(); 56 | std::string get_error_message(); 57 | 58 | void set_editor_position(const Editor_Position& d); 59 | 60 | //! Get the current editor position. Used to display cursors. 61 | inline const Editor_Position& get_editor_position() const { return editor_position; } 62 | 63 | //! Get a set of references pointers at the editor position. Note that the pointers may be invalid. 64 | inline const Ref_Ptr_Set& get_editor_refs() const { return editor_refs; } 65 | 66 | //! Return true if editor position points to a subroutine. Used to enable a hack. 67 | inline bool get_editor_subroutine() const { return editor_jump_hack; } 68 | 69 | //! Get the song playtime at the beginning of line pointed at by the editor. 70 | inline uint32_t get_song_pos_at_line() const { return song_pos_at_line; } 71 | 72 | //! Get the song playtime at the cursor pointed at by the editor. 73 | inline uint32_t get_song_pos_at_cursor() const { return song_pos_at_cursor; } 74 | 75 | std::pair get_channel(uint16_t track) const; 76 | 77 | void toggle_mute(uint16_t track_id); 78 | void toggle_solo(uint16_t track_id); 79 | 80 | bool get_mute(uint16_t track_id) const; 81 | bool get_solo(uint16_t track_id) const; 82 | 83 | void reset_mute(); 84 | 85 | private: 86 | void worker(); 87 | void compile_job(std::unique_lock& lock, std::string buffer, std::string filename); 88 | std::string tabs_to_spaces(const std::string& str) const; 89 | void update_mute(); 90 | 91 | // song status 92 | const static int max_channels; 93 | 94 | // worker state 95 | std::mutex mutex; 96 | std::condition_variable condition_variable; 97 | std::unique_ptr worker_ptr; 98 | 99 | // worker status 100 | bool worker_fired; // set to 1 to kill worker thread 101 | bool job_done; // set to 0 to begin compile 102 | bool job_successful; 103 | 104 | // worker input 105 | std::string job_buffer; 106 | std::string job_filename; 107 | 108 | // worker output 109 | std::shared_ptr song; 110 | std::shared_ptr tracks; 111 | std::shared_ptr lines; 112 | std::string error_message; 113 | std::shared_ptr error_reference; 114 | 115 | // playback state 116 | std::shared_ptr player; 117 | 118 | // editor state 119 | Editor_Position editor_position; 120 | Ref_Ptr_Set editor_refs; 121 | bool editor_jump_hack; 122 | 123 | unsigned int song_pos_at_line; 124 | unsigned int song_pos_at_cursor; 125 | 126 | // muting 127 | std::map mute_mask; // Chip_id, channel_id 128 | const static std::map> track_channel_table; // Track to channel ID table 129 | }; 130 | 131 | #endif 132 | -------------------------------------------------------------------------------- /src/track_info.cpp: -------------------------------------------------------------------------------- 1 | #include "track_info.h" 2 | #include "track.h" 3 | 4 | //! Generate Track_Info 5 | /*! 6 | * \exception InputError if any validation errors occur. These should be displayed to the user. 7 | */ 8 | Track_Info_Generator::Track_Info_Generator(Song& song, Track& track) 9 | : Player(song, track) 10 | , Track_Info() 11 | , slur_flag(0) 12 | { 13 | loop_start = -1; 14 | loop_length = 0; 15 | length = 0; 16 | 17 | // step all the way to the end 18 | while(is_enabled()) 19 | step_event(); 20 | 21 | length = get_play_time(); 22 | if(loop_start >= 0) 23 | loop_length = length - loop_start; 24 | } 25 | 26 | void Track_Info_Generator::write_event() 27 | { 28 | // Insert event 29 | if((event.type == Event::NOTE || event.type == Event::TIE || event.type == Event::REST) && 30 | (on_time || off_time) ) 31 | { 32 | // Fill the ext_event 33 | Track_Info::Ext_Event ext; 34 | ext.note = event.param; 35 | ext.on_time = on_time; 36 | ext.is_tie = (event.type == Event::TIE); 37 | ext.is_slur = slur_flag; 38 | ext.off_time = off_time; 39 | ext.volume = get_var(Event::VOL_FINE); 40 | ext.coarse_volume_flag = coarse_volume_flag(); 41 | ext.instrument = get_var(Event::INS); 42 | ext.transpose = get_var(Event::TRANSPOSE); 43 | ext.pitch_envelope = get_var(Event::PITCH_ENVELOPE); 44 | ext.portamento = get_var(Event::PORTAMENTO); 45 | ext.references = get_references(); 46 | if(get_var(Event::DRUM_MODE)) 47 | ext.references.push_back(reference); 48 | events.emplace_hint(events.end(), std::pair(get_play_time(), ext)); 49 | 50 | slur_flag = false; 51 | } 52 | // Record timestamp of segno event. 53 | else if(event.type == Event::SEGNO) 54 | loop_start = get_play_time(); 55 | // Set the slur flag 56 | else if(event.type == Event::SLUR) 57 | slur_flag = true; 58 | } 59 | 60 | bool Track_Info_Generator::loop_hook() 61 | { 62 | // do not loop 63 | return 0; 64 | } 65 | -------------------------------------------------------------------------------- /src/track_info.h: -------------------------------------------------------------------------------- 1 | #ifndef TRACK_INFO_H 2 | #define TRACK_INFO_H 3 | 4 | #include 5 | #include 6 | 7 | #include "player.h" 8 | 9 | struct Track_Info 10 | { 11 | //! Extended event struct 12 | struct Ext_Event 13 | { 14 | uint16_t note; 15 | uint16_t on_time; 16 | bool is_tie; 17 | bool is_slur; 18 | 19 | uint16_t off_time; 20 | 21 | bool coarse_volume_flag; 22 | uint16_t volume; 23 | uint16_t instrument; 24 | int16_t transpose; 25 | uint16_t pitch_envelope; 26 | uint16_t portamento; 27 | 28 | std::vector> references; 29 | }; 30 | 31 | std::map events; 32 | 33 | int loop_start; // -1 for no loop 34 | unsigned int loop_length; 35 | unsigned int length; 36 | }; 37 | 38 | class Track_Info_Generator : public Player, public Track_Info 39 | { 40 | public: 41 | Track_Info_Generator(Song& song, Track& track); 42 | 43 | private: 44 | void write_event() override; 45 | bool loop_hook() override; 46 | 47 | bool slur_flag; 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /src/track_list_window.cpp: -------------------------------------------------------------------------------- 1 | #include "track_list_window.h" 2 | #include "track_info.h" 3 | #include "song.h" 4 | 5 | Track_List_Window::Track_List_Window(std::shared_ptr song_mgr) 6 | : song_manager(song_mgr) 7 | { 8 | } 9 | 10 | void Track_List_Window::display() 11 | { 12 | // Draw window 13 | std::string window_id; 14 | window_id = "Track List##" + std::to_string(id); 15 | 16 | ImGui::Begin(window_id.c_str(), &active); 17 | ImGui::SetWindowSize(ImVec2(200, 300), ImGuiCond_Once); 18 | 19 | ImGui::Columns(3, "tracklist"); 20 | ImGui::Separator(); 21 | ImGui::Text("Name"); ImGui::NextColumn(); 22 | ImGui::Text("Length"); ImGui::NextColumn(); 23 | ImGui::Text("Loop"); ImGui::NextColumn(); 24 | ImGui::Separator(); 25 | 26 | Song_Manager::Track_Map& map = *song_manager->get_tracks(); 27 | 28 | for(auto&& i : map) 29 | { 30 | bool pop_text = false; 31 | int id = i.first; 32 | 33 | if(song_manager->get_mute(id)) 34 | { 35 | ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); 36 | pop_text = true; 37 | } 38 | 39 | std::string str = ""; 40 | if(id < 'Z'-'A') 41 | str.push_back(id + 'A'); 42 | else 43 | str = std::to_string(id); 44 | 45 | if(ImGui::Selectable(str.c_str(), false, ImGuiSelectableFlags_SpanAllColumns|ImGuiSelectableFlags_AllowDoubleClick)) 46 | { 47 | song_manager->toggle_mute(id); 48 | if(ImGui::IsMouseDoubleClicked(0)) 49 | { 50 | song_manager->toggle_solo(id); 51 | } 52 | } 53 | ImGui::NextColumn(); 54 | ImGui::Text("%5d", i.second.length); 55 | ImGui::NextColumn(); 56 | ImGui::Text("%5d", i.second.loop_length); 57 | ImGui::NextColumn(); 58 | 59 | if(pop_text) 60 | { 61 | ImGui::PopStyleColor(); 62 | } 63 | } 64 | ImGui::Columns(1); 65 | ImGui::Separator(); 66 | 67 | ImGui::End(); 68 | } 69 | -------------------------------------------------------------------------------- /src/track_list_window.h: -------------------------------------------------------------------------------- 1 | #ifndef TRACK_LIST_WINDOW_H 2 | #define TRACK_LIST_WINDOW_H 3 | 4 | #include 5 | #include 6 | 7 | #include "imgui.h" 8 | 9 | #include "window.h" 10 | #include "song_manager.h" 11 | 12 | // todo: just get the Track_Info struct. 13 | // I don't want to bring all of ctrmml in the global namespace here 14 | #include "track_info.h" 15 | 16 | class Track_List_Window : public Window 17 | { 18 | public: 19 | Track_List_Window(std::shared_ptr song_mgr); 20 | 21 | void display() override; 22 | 23 | private: 24 | std::shared_ptr song_manager; 25 | }; 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/track_view_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Track view window 3 | 4 | currently implemented: 5 | - drag to y-zoom 6 | - note labels (simple) 7 | todo: 8 | - outlined note borders (?) 9 | - not sure if i want this anymore, the current "flat" design looks consistent 10 | - always draw left/right border 11 | - if note is tie or slur, don't draw top border 12 | - buffer the bottom border 13 | - note is tie or slur, don't draw bottom border of last note or top border of this note 14 | - otherwise, draw top border of this note 15 | - if there is a rest, draw bottom border of last note 16 | - channel name indicator 17 | - should be displayed at top 18 | - single click fast to mute 19 | - double click to solo 20 | - drag to x-scroll 21 | - tooltips 22 | - show note information 23 | - doubleclick a note/rest to open reference in text editor 24 | - handle time signature / realign measure lines.. 25 | - scroll wheel to y-zoom 26 | */ 27 | 28 | #include "track_view_window.h" 29 | #include "track_info.h" 30 | #include "song.h" 31 | 32 | #include 33 | #include 34 | 35 | // max objects drawn per object per frame 36 | const int Track_View_Window::max_objs_per_column = 200; 37 | 38 | //time signature (currently fixed) 39 | const unsigned int Track_View_Window::measure_beat_count = 4; 40 | const unsigned int Track_View_Window::measure_beat_value = 4; 41 | 42 | // minimum scroll position 43 | const double Track_View_Window::x_min = 0.0; 44 | const double Track_View_Window::y_min = 0.0; 45 | 46 | // scroll inertia 47 | const double Track_View_Window::inertia_threshold = 1.0; 48 | const double Track_View_Window::inertia_acceleration = 0.95; 49 | 50 | // height of header 51 | const double Track_View_Window::track_header_height = 20.0; 52 | // width of columns 53 | const double Track_View_Window::ruler_width = 25.0; 54 | const double Track_View_Window::track_width = 25.0; 55 | const double Track_View_Window::padding_width = 5.0; 56 | 57 | Track_View_Window::Track_View_Window(std::shared_ptr song_mgr) 58 | : x_pos(0.0) 59 | , y_pos(0.0) 60 | , x_scale(1.0) 61 | , y_scale(1.0) 62 | , y_scale_log(1.0) 63 | , x_scroll(0.0) 64 | , y_scroll(0.0) 65 | , y_follow(true) 66 | , y_editor(0) 67 | , y_player(0) 68 | , y_user(0.0) 69 | , song_manager(song_mgr) 70 | , dragging(false) 71 | { 72 | } 73 | 74 | void Track_View_Window::display() 75 | { 76 | // Draw window 77 | std::string window_id; 78 | window_id = "Track View##" + std::to_string(id); 79 | 80 | ImGui::Begin(window_id.c_str(), &active); 81 | ImGui::SetWindowSize(ImVec2(550, 600), ImGuiCond_Once); 82 | 83 | ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.2); 84 | 85 | if(y_player) 86 | ImGui::InputScalar("Time", ImGuiDataType_U32, &y_player, NULL, NULL, "%d.00", ImGuiInputTextFlags_ReadOnly); 87 | else 88 | ImGui::InputDouble("Time", &y_user, 1.0f, 1.0f, "%.2f"); 89 | ImGui::SameLine(); 90 | ImGui::InputDouble("Scale", &y_scale_log, 0.01f, 0.1f, "%.2f"); 91 | ImGui::SameLine(); 92 | ImGui::Checkbox("Follow", &y_follow); 93 | 94 | ImGui::PopItemWidth(); 95 | 96 | canvas_pos = ImGui::GetCursorScreenPos(); 97 | canvas_size = ImGui::GetContentRegionAvail(); 98 | if (canvas_size.x < 50.0f) canvas_size.x = 50.0f; 99 | if (canvas_size.y < 50.0f) canvas_size.y = 50.0f; 100 | 101 | update_position(); 102 | handle_input(); 103 | 104 | draw_list = ImGui::GetWindowDrawList(); 105 | cursor_list.clear(); 106 | 107 | // draw background 108 | draw_list->AddRectFilled( 109 | canvas_pos, 110 | ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), 111 | IM_COL32(0, 0, 0, 255)); 112 | 113 | // draw ruler and beat lines 114 | draw_list->PushClipRect( 115 | canvas_pos, 116 | ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), 117 | true); 118 | draw_ruler(); 119 | draw_list->PopClipRect(); 120 | 121 | // draw track events 122 | draw_list->PushClipRect( 123 | ImVec2(canvas_pos.x + ruler_width, canvas_pos.y), 124 | ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), 125 | true); 126 | draw_tracks(); 127 | draw_track_header(); 128 | draw_list->PopClipRect(); 129 | 130 | // draw cursor 131 | draw_list->PushClipRect( 132 | canvas_pos, 133 | ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), 134 | true); 135 | draw_cursors(); 136 | draw_list->PopClipRect(); 137 | 138 | ImGui::End(); 139 | } 140 | 141 | //! Update position in follow mode 142 | void Track_View_Window::update_position() 143 | { 144 | y_scale = std::pow(y_scale_log, 2); 145 | 146 | // Get player position 147 | auto player = song_manager->get_player(); 148 | if(player != nullptr && !player->get_finished()) 149 | y_player = player->get_driver()->get_player_ticks(); 150 | else 151 | y_player = 0; 152 | 153 | if(y_follow) 154 | { 155 | if(y_player) 156 | { 157 | // Set scroll position to player position in follow mode 158 | y_pos = y_player - (canvas_size.y / 2.0) / y_scale; 159 | y_scroll = 0; 160 | dragging = false; 161 | } 162 | else if ( song_manager->get_editor_position().line >= 0 163 | && y_editor != song_manager->get_song_pos_at_cursor() 164 | && !song_manager->get_compile_in_progress()) 165 | { 166 | // Set scroll position to editor position in follow mode 167 | y_editor = song_manager->get_song_pos_at_cursor(); 168 | y_user = y_editor - (canvas_size.y / 2.0) / y_scale; 169 | if(y_user < 0) 170 | y_user = 0; 171 | y_scroll = 0; 172 | dragging = false; 173 | } 174 | } 175 | } 176 | 177 | //! Handle user input 178 | void Track_View_Window::handle_input() 179 | { 180 | // handle controls 181 | ImGuiIO& io = ImGui::GetIO(); 182 | ImGui::InvisibleButton("canvas", canvas_size); 183 | 184 | if(!(y_follow && y_player)) 185 | { 186 | if(dragging) 187 | { 188 | if(!ImGui::IsMouseDown(0)) 189 | dragging = false; 190 | else 191 | y_scroll = io.MouseDelta.y; 192 | } 193 | if(ImGui::IsItemHovered()) 194 | { 195 | if(!dragging && ImGui::IsMouseClicked(0)) 196 | dragging = true; 197 | y_scroll += io.MouseWheel * 5; 198 | } 199 | 200 | // handle scrolling 201 | if(std::abs(y_scroll) >= inertia_threshold) 202 | { 203 | y_user -= y_scroll / y_scale; 204 | y_scroll *= inertia_acceleration; 205 | if(y_user < y_min) 206 | y_user = y_min; 207 | } 208 | y_pos = y_user - track_header_height / y_scale; 209 | } 210 | } 211 | 212 | 213 | //! Draw measure and beat ruler 214 | void Track_View_Window::draw_ruler() 215 | { 216 | int yp = y_pos; 217 | 218 | // calculate time signature 219 | unsigned int whole = song_manager->get_song()->get_ppqn() * 4; 220 | unsigned int beat_len = whole / measure_beat_value; 221 | //unsigned int measure_len = measure_beat_value * measure_beat_count; 222 | 223 | // calculate current beat 224 | int beat = y_pos / beat_len; 225 | 226 | // start from the beginning of the beat (above clip area if necessary) 227 | double y = 0.0 - std::fmod(y_pos, beat_len) * y_scale; 228 | 229 | // special case for negative position 230 | if(y_pos < 0) 231 | { 232 | y = -y_pos * y_scale; 233 | yp = 0; 234 | beat = 0; 235 | } 236 | 237 | double ruler_width = 25.0; 238 | 239 | // draw ruler face 240 | draw_list->AddRectFilled( 241 | canvas_pos, 242 | ImVec2( 243 | canvas_pos.x + std::floor(ruler_width), 244 | canvas_pos.y + canvas_size.y), 245 | IM_COL32(85, 85, 85, 255)); 246 | 247 | // draw beats 248 | for(int i=0; iCalcTextSizeA( 258 | ImGui::GetFontSize(), 259 | ruler_width, 260 | ruler_width, 261 | str.c_str()); 262 | 263 | draw_list->AddText( 264 | ImGui::GetFont(), 265 | ImGui::GetFontSize(), 266 | ImVec2( 267 | canvas_pos.x + ruler_width/2 - size.x/2, 268 | canvas_pos.y + y - size.y/2), 269 | IM_COL32(255, 255, 255, 255), 270 | str.c_str()); 271 | 272 | // draw a "bright" line 273 | draw_list->AddRectFilled( 274 | ImVec2( 275 | canvas_pos.x + std::floor(ruler_width), 276 | canvas_pos.y + std::floor(y)), 277 | ImVec2( 278 | canvas_pos.x + canvas_size.x, 279 | canvas_pos.y + std::floor(y)+1), 280 | IM_COL32(85, 85, 85, 255)); 281 | } 282 | else 283 | { 284 | // draw a dark line 285 | draw_list->AddRectFilled( 286 | ImVec2( 287 | canvas_pos.x + std::floor(ruler_width), 288 | canvas_pos.y + std::floor(y)), 289 | ImVec2( 290 | canvas_pos.x + canvas_size.x, 291 | canvas_pos.y + std::floor(y)+1), 292 | IM_COL32(35, 35, 35, 255)); 293 | } 294 | beat++; 295 | y += beat_len * y_scale; 296 | if(y > canvas_size.y) 297 | break; 298 | } 299 | } 300 | 301 | //! Draw the track header 302 | void Track_View_Window::draw_track_header() 303 | { 304 | double x1 = canvas_pos.x + ruler_width; 305 | double x2 = canvas_pos.x + canvas_size.x; 306 | double y1 = canvas_pos.y; 307 | double y2 = canvas_pos.y + track_header_height; 308 | ImU32 fill_color = IM_COL32(65, 65, 65, 180); 309 | 310 | draw_list->AddRectFilled( 311 | ImVec2(x1,y1), 312 | ImVec2(x2,y2), 313 | fill_color); 314 | 315 | Song_Manager::Track_Map& map = *song_manager->get_tracks(); 316 | 317 | double x = std::floor(ruler_width * 2.0); 318 | 319 | for(auto track_it = map.begin(); track_it != map.end(); track_it++) 320 | { 321 | x1 = canvas_pos.x + std::floor(x); 322 | x2 = canvas_pos.x + std::floor(x + track_width); 323 | 324 | int id = track_it->first; 325 | 326 | static const double margin = 2.0; 327 | double max_width = track_width - margin * 2; 328 | 329 | std::string str = ""; 330 | if(id < 'Z'-'A') 331 | str.push_back(id + 'A'); 332 | else 333 | str = std::to_string(id); 334 | 335 | ImFont* font = ImGui::GetFont(); 336 | ImVec2 size = font->CalcTextSizeA(font->FontSize, max_width, max_width, str.c_str()); 337 | 338 | double y_offset = (track_header_height - font->FontSize) / 2.0; 339 | 340 | draw_list->AddText( 341 | font, 342 | font->FontSize, 343 | ImVec2( 344 | x1 + track_width/2 - size.x/2, 345 | y1 + y_offset), 346 | song_manager->get_mute(id) 347 | ? IM_COL32(128, 128, 128, 128) 348 | : IM_COL32(255, 255, 255, 255), 349 | str.c_str()); 350 | 351 | if(ImGui::IsItemHovered() && ImGui::IsMouseHoveringRect(ImVec2(x1,y1),ImVec2(x2,y2))) 352 | { 353 | if(ImGui::IsMouseClicked(0)) 354 | song_manager->toggle_mute(id); 355 | if(ImGui::IsMouseDoubleClicked(0)) 356 | song_manager->toggle_solo(id); 357 | } 358 | 359 | x += std::floor(track_width + padding_width); 360 | } 361 | } 362 | 363 | //! Draw the tracks 364 | void Track_View_Window::draw_tracks() 365 | { 366 | auto map = song_manager->get_tracks(); 367 | 368 | double x = std::floor(ruler_width * 2.0); 369 | 370 | int y_off = 0; 371 | double yr = y_pos * y_scale; 372 | 373 | for(auto track_it = map->begin(); track_it != map->end(); track_it++) 374 | { 375 | auto& info = track_it->second; 376 | 377 | // calculate offset to first loop 378 | if(y_pos > info.length && info.loop_length) 379 | y_off = (((int)y_pos - info.loop_start) / info.loop_length) * info.loop_length; 380 | else 381 | y_off = 0; 382 | 383 | // calculate position 384 | auto it = info.events.lower_bound(y_pos - y_off); 385 | double y = (it->first + y_off) * y_scale - yr; 386 | 387 | border_complete = true; 388 | last_ref = nullptr; 389 | 390 | // draw the previous event if we can 391 | if(it != info.events.begin()) 392 | { 393 | --it; 394 | y = (it->first + y_off) * y_scale - yr; 395 | y = draw_event(x, y, it->first, it->second); 396 | ++it; 397 | } 398 | 399 | // draw each event 400 | for(int i=0; i canvas_size.y) 411 | { 412 | if(it->second.on_time) 413 | { 414 | double x1 = canvas_pos.x + std::floor(x); 415 | double x2 = canvas_pos.x + std::floor(x + track_width); 416 | double y1 = canvas_pos.y + std::floor(y); 417 | draw_event_border(x1, x2, y1, it->second); 418 | } 419 | break; 420 | } 421 | y = draw_event(x, y, it->first, it->second); 422 | it++; 423 | } 424 | 425 | x += std::floor(track_width + padding_width); 426 | } 427 | } 428 | 429 | //! Draw a single event 430 | double Track_View_Window::draw_event(double x, double y, int position, const Track_Info::Ext_Event& event) 431 | { 432 | // calculate coordinates 433 | double x1 = canvas_pos.x + std::floor(x); 434 | double x2 = canvas_pos.x + std::floor(x + track_width); 435 | double y1 = canvas_pos.y + std::floor(y); 436 | double y2 = canvas_pos.y + std::floor(y + event.on_time * y_scale); 437 | double y2a = canvas_pos.y + std::floor(y + (event.on_time + event.off_time) * y_scale); 438 | ImU32 fill_color = IM_COL32(195, 0, 0, 255); 439 | 440 | //testing 441 | if(ImGui::GetIO().MousePos.y >= canvas_pos.y + track_header_height 442 | && ImGui::IsMouseHoveringRect(ImVec2(x1,y1),ImVec2(x2,y2a)) 443 | && ImGui::IsItemHovered()) 444 | { 445 | fill_color = IM_COL32(235, 40, 40, 255); 446 | hover_event(position, event); 447 | } 448 | 449 | // draw the note 450 | if(event.on_time) 451 | { 452 | draw_event_border(x1, x2, y1, event); 453 | 454 | draw_list->AddRectFilled( 455 | ImVec2(x1,y1), 456 | ImVec2(x2,y2), 457 | fill_color); 458 | 459 | // draw note text 460 | ImFont* font = ImGui::GetFont(); 461 | if(!event.is_tie && std::floor(event.on_time * y_scale) > font->FontSize) 462 | { 463 | static const double margin = 2.0; 464 | std::string str = get_note_name(event.note + event.transpose); 465 | double max_width = track_width - margin * 2; 466 | ImVec2 size = font->CalcTextSizeA(font->FontSize, max_width, max_width, str.c_str()); 467 | 468 | draw_list->AddText( 469 | font, 470 | font->FontSize, 471 | ImVec2( 472 | x1 + track_width/2 - size.x/2, 473 | y1 + margin), 474 | IM_COL32(255, 255, 255, 255), 475 | str.c_str()); 476 | } 477 | 478 | } 479 | 480 | // draw the gap 481 | if(event.off_time) 482 | { 483 | border_complete = true; 484 | } 485 | 486 | // add editor cursor 487 | auto editor_refs = song_manager->get_editor_refs(); 488 | int index = 0; 489 | for(auto&& ref : event.references) 490 | { 491 | if(editor_refs.count(ref.get())) 492 | { 493 | auto editor_pos = song_manager->get_editor_position(); 494 | double cursor_y = y1; 495 | 496 | // Set flag if we have already displayed a cursor for the current ref, and we are in a subroutine call. 497 | bool jump_hack = !song_manager->get_editor_subroutine() && (event.references.size() > 1) && last_ref == ref.get(); 498 | last_ref = ref.get(); 499 | 500 | if( (int)ref->get_line() < editor_pos.line 501 | || ((int)ref->get_line() == editor_pos.line && (int)ref->get_column() < editor_pos.column)) 502 | { 503 | cursor_y = y2a; 504 | if(jump_hack && cursor_list.size()) 505 | cursor_list[cursor_list.size() - 1] = ImVec2(std::floor(x1 - padding_width / 2), cursor_y); 506 | } 507 | 508 | if(!jump_hack) 509 | cursor_list.push_back(ImVec2(std::floor(x1 - padding_width / 2), cursor_y)); 510 | } 511 | index++; 512 | } 513 | 514 | return y + (event.on_time + event.off_time) * y_scale; 515 | 516 | } 517 | 518 | void Track_View_Window::draw_event_border(double x1, double x2, double y, const Track_Info::Ext_Event& event) 519 | { 520 | int border_width = y_scale * 0.55; 521 | 522 | // draw a border before the note if we're not a slur or tie 523 | if(!event.is_tie && !event.is_slur && border_width) 524 | { 525 | draw_list->AddRectFilled( 526 | ImVec2(x1,y-border_width), 527 | ImVec2(x2,y), 528 | IM_COL32(0, 0, 0, 255)); 529 | } 530 | } 531 | 532 | //! Handle mouse hovering 533 | void Track_View_Window::hover_event(int position, const Track_Info::Ext_Event& event) 534 | { 535 | if((hover_obj != &event) || dragging) 536 | { 537 | hover_obj = &event; 538 | hover_time = 0; 539 | } 540 | else 541 | { 542 | hover_time++; 543 | if(hover_time > 20) 544 | { 545 | ImGui::BeginTooltip(); 546 | ImGui::Text("@%d %c%d", 547 | event.instrument, 548 | (event.coarse_volume_flag) ? 'v' : 'V', 549 | event.volume); 550 | if(event.transpose) 551 | { 552 | ImGui::SameLine(); 553 | ImGui::Text("k%d", event.transpose); 554 | } 555 | if(event.pitch_envelope) 556 | { 557 | ImGui::SameLine(); 558 | ImGui::Text("M%d", event.pitch_envelope); 559 | } 560 | if(event.portamento) 561 | { 562 | ImGui::SameLine(); 563 | ImGui::Text("G%d", event.portamento); 564 | } 565 | ImGui::SameLine(); 566 | if(event.on_time && !event.is_tie) 567 | { 568 | ImGui::Text("o%d%s", 569 | event.note/12, 570 | get_note_name(event.note).c_str()); 571 | ImGui::Text("t: %d-%d", position, position+event.on_time - 1); 572 | } 573 | else if(event.is_tie) 574 | { 575 | ImGui::Text(";tie"); 576 | if(event.on_time) 577 | ImGui::Text("t: %d-%d", position, position+event.on_time - 1); 578 | else 579 | ImGui::Text("t: %d", position); 580 | } 581 | else if(event.off_time) 582 | { 583 | ImGui::Text(";rest"); 584 | ImGui::Text("t: %d", position); 585 | } 586 | ImGui::EndTooltip(); 587 | } 588 | } 589 | } 590 | 591 | //! Get the note name 592 | std::string Track_View_Window::get_note_name(uint16_t note) const 593 | { 594 | const char* semitones[12] = { "c", "c+", "d", "d+", "e", "f", "f+", "g", "g+", "a", "a+", "b" }; 595 | std::string str = semitones[note % 12]; 596 | // TODO: add octave if we have enough space 597 | 598 | return str; 599 | } 600 | 601 | void Track_View_Window::draw_cursors() 602 | { 603 | if(y_player) 604 | { 605 | // draw player position 606 | double y = (y_player - y_pos) * y_scale; 607 | if(y > 0 && y < canvas_size.y) 608 | { 609 | draw_list->AddRectFilled( 610 | ImVec2( 611 | canvas_pos.x + std::floor(ruler_width), 612 | canvas_pos.y + std::floor(y)), 613 | ImVec2( 614 | canvas_pos.x + canvas_size.x, 615 | canvas_pos.y + std::floor(y)+1), 616 | IM_COL32(0, 200, 0, 255)); 617 | } 618 | } 619 | // Draw editor cursors. 620 | for(auto && i : cursor_list) 621 | { 622 | draw_list->AddRectFilled( 623 | i, 624 | ImVec2( 625 | i.x + track_width + padding_width, 626 | i.y + 1), 627 | IM_COL32(255, 255, 255, 255)); 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /src/track_view_window.h: -------------------------------------------------------------------------------- 1 | #ifndef TRACK_VIEW_WINDOW_H 2 | #define TRACK_VIEW_WINDOW_H 3 | 4 | #include 5 | #include 6 | 7 | #include "imgui.h" 8 | 9 | #include "window.h" 10 | #include "song_manager.h" 11 | 12 | // todo: just get the Track_Info struct. 13 | // I don't want to bring all of ctrmml in the global namespace here 14 | #include "track_info.h" 15 | 16 | class Track_View_Window : public Window 17 | { 18 | public: 19 | Track_View_Window(std::shared_ptr song_mgr); 20 | 21 | void display() override; 22 | 23 | private: 24 | const static int max_objs_per_column; 25 | const static unsigned int measure_beat_count; //time signature (currently fixed) 26 | const static unsigned int measure_beat_value; 27 | 28 | const static double x_min; 29 | const static double y_min; 30 | const static double inertia_threshold; 31 | const static double inertia_acceleration; 32 | 33 | const static double track_header_height; 34 | const static double ruler_width; 35 | const static double track_width; 36 | const static double padding_width; 37 | 38 | void draw_ruler(); 39 | 40 | void draw_track_header(); 41 | void draw_tracks(); 42 | void draw_cursors(); 43 | 44 | double draw_event(double x, double y, int position, const Track_Info::Ext_Event& event); 45 | void draw_event_border(double x1, double x2, double y, const Track_Info::Ext_Event& event); 46 | 47 | void hover_event(int position, const Track_Info::Ext_Event& event); 48 | 49 | std::string get_note_name(uint16_t note) const; 50 | 51 | void update_position(); 52 | void handle_input(); 53 | 54 | double x_pos; 55 | double y_pos; 56 | 57 | double x_scale; 58 | double y_scale; 59 | double y_scale_log; 60 | 61 | double x_scroll; 62 | double y_scroll; 63 | 64 | bool y_follow; // If set, follow song position. 65 | 66 | uint32_t y_editor; // last editor Y position 67 | uint32_t y_player; // Player Y position 68 | double y_user; // User Y position 69 | 70 | std::shared_ptr song_manager; 71 | 72 | // scrolling state 73 | bool hovered; 74 | bool dragging; 75 | 76 | // tooltip state 77 | int hover_time; 78 | const Track_Info::Ext_Event* hover_obj; 79 | 80 | // drawing stuff 81 | ImVec2 canvas_pos; 82 | ImVec2 canvas_size; 83 | ImDrawList* draw_list; 84 | std::vector cursor_list; 85 | const InputRef* last_ref; 86 | 87 | // buffered drawing the bottom border of tied notes 88 | bool border_complete; 89 | ImVec2 border_pos; 90 | }; 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /src/unittest/main.cpp: -------------------------------------------------------------------------------- 1 | // http://cppunit.sourceforge.net/doc/cvs/cppunit_cookbook.html 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char **argv) 9 | { 10 | CppUnit::TextTestRunner runner; 11 | CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(); 12 | CppUnit::BriefTestProgressListener listener; 13 | runner.eventManager().addListener(&listener); 14 | runner.addTest(registry.makeTest()); 15 | bool run_ok = runner.run("", false,true,false); 16 | return !run_ok; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/unittest/test_track_info.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "../track_info.h" 5 | #include "song.h" 6 | #include "input.h" 7 | #include "mml_input.h" 8 | 9 | class Track_Info_Test : public CppUnit::TestFixture 10 | { 11 | CPPUNIT_TEST_SUITE(Track_Info_Test); 12 | CPPUNIT_TEST(test_generator); 13 | CPPUNIT_TEST(test_drum_mode); 14 | CPPUNIT_TEST_SUITE_END(); 15 | private: 16 | Song *song; 17 | MML_Input *mml_input; 18 | public: 19 | void setUp() 20 | { 21 | song = new Song(); 22 | mml_input = new MML_Input(song); 23 | } 24 | void tearDown() 25 | { 26 | delete mml_input; 27 | delete song; 28 | } 29 | void test_generator() 30 | { 31 | mml_input->read_line("A c1 r2 c2 @2 v12 ^4 &c8"); 32 | Track_Info info = Track_Info_Generator(*song, song->get_track(0)); 33 | auto& track_map = info.events; 34 | auto it = track_map.begin(); 35 | CPPUNIT_ASSERT_EQUAL((int)0, it->first); 36 | CPPUNIT_ASSERT_EQUAL((uint16_t)96, it->second.on_time); 37 | it++; 38 | CPPUNIT_ASSERT_EQUAL((int)96, it->first); 39 | CPPUNIT_ASSERT_EQUAL((uint16_t)48, it->second.off_time); 40 | it++; 41 | CPPUNIT_ASSERT_EQUAL((int)144, it->first); 42 | CPPUNIT_ASSERT_EQUAL((uint16_t)48, it->second.on_time); 43 | it++; 44 | // the two volume/instrument commands should not be in the list 45 | CPPUNIT_ASSERT_EQUAL((int)192, it->first); 46 | CPPUNIT_ASSERT_EQUAL(true, it->second.is_tie); 47 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, it->second.on_time); 48 | it++; 49 | CPPUNIT_ASSERT_EQUAL((int)216, it->first); 50 | CPPUNIT_ASSERT_EQUAL(true, it->second.is_slur); 51 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, it->second.on_time); 52 | it++; 53 | CPPUNIT_ASSERT(it == track_map.end()); 54 | } 55 | void test_drum_mode() 56 | { 57 | mml_input->read_line("*30 @30c ;D30a"); 58 | mml_input->read_line("*31 @31c ;D30b"); 59 | mml_input->read_line("*32 @32c ;D30c"); 60 | mml_input->read_line("A l16 D30 ab8c4"); 61 | Track_Info info = Track_Info_Generator(*song, song->get_track(0)); 62 | auto& track_map = info.events; 63 | auto it = track_map.begin(); 64 | CPPUNIT_ASSERT_EQUAL((int)0, it->first); 65 | 66 | CPPUNIT_ASSERT_EQUAL((uint16_t)6, it->second.on_time); 67 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, it->second.off_time); 68 | it++; 69 | CPPUNIT_ASSERT_EQUAL((int)6, it->first); 70 | CPPUNIT_ASSERT_EQUAL((uint16_t)12, it->second.on_time); 71 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, it->second.off_time); 72 | it++; 73 | CPPUNIT_ASSERT_EQUAL((int)18, it->first); 74 | CPPUNIT_ASSERT_EQUAL((uint16_t)24, it->second.on_time); 75 | CPPUNIT_ASSERT_EQUAL((uint16_t)0, it->second.off_time); 76 | it++; 77 | CPPUNIT_ASSERT(it == track_map.end()); 78 | } 79 | }; 80 | 81 | CPPUNIT_TEST_SUITE_REGISTRATION(Track_Info_Test); 82 | 83 | -------------------------------------------------------------------------------- /src/window.cpp: -------------------------------------------------------------------------------- 1 | #include "window.h" 2 | #include 3 | #include 4 | 5 | uint32_t Window::id_counter = 0; // next window's ID 6 | bool Window::modal_open = 0; // indicates if a modal is open, since imgui can 7 | // only display one modal at a time, and will softlock 8 | // if another modal is opened. 9 | 10 | Window::Window(Window* parent) 11 | : active(true) 12 | , parent(parent) 13 | , close_req_state(Window::NO_CLOSE_REQUEST) 14 | { 15 | id = id_counter++; 16 | children.clear(); 17 | } 18 | 19 | Window::~Window() 20 | { 21 | } 22 | 23 | //! Display window including child windows 24 | bool Window::display_all() 25 | { 26 | display(); 27 | for(auto i = children.begin(); i != children.end(); ) 28 | { 29 | bool child_active = i->get()->display_all(); 30 | if(!child_active) 31 | children.erase(i); 32 | else 33 | ++i; 34 | } 35 | return active; 36 | } 37 | 38 | //! Tell window to close. 39 | void Window::close() 40 | { 41 | active = false; 42 | } 43 | 44 | //! Find child window with matching type 45 | std::vector>::iterator Window::find_child(Window_Type type) 46 | { 47 | for(auto i = children.begin(); i != children.end(); i++) 48 | { 49 | if(i->get()->active && i->get()->type == type) 50 | return i; 51 | } 52 | return children.end(); 53 | } 54 | 55 | //! Request windows to close. 56 | void Window::close_request_all() 57 | { 58 | if(close_req_state != Window::CLOSE_IN_PROGRESS) 59 | { 60 | for(auto i = children.begin(); i != children.end(); i++) 61 | { 62 | if(i->get()->active) 63 | i->get()->close_request_all(); 64 | } 65 | close_request(); 66 | } 67 | } 68 | 69 | //! Request window to close. 70 | void Window::close_request() 71 | { 72 | close_req_state = Window::CLOSE_OK; 73 | } 74 | 75 | //! Get the status of close request. 76 | Window::Close_Request_State Window::get_close_request() 77 | { 78 | for(auto i = children.begin(); i != children.end(); i++) 79 | { 80 | if(i->get()->active) 81 | { 82 | auto child_request = i->get()->get_close_request(); 83 | if(child_request == Window::CLOSE_IN_PROGRESS) 84 | return Window::CLOSE_IN_PROGRESS; 85 | else if(child_request == Window::CLOSE_NOT_OK) 86 | return Window::CLOSE_NOT_OK; 87 | } 88 | } 89 | return close_req_state; 90 | } 91 | 92 | //! Clear the close request status 93 | void Window::clear_close_request() 94 | { 95 | for(auto i = children.begin(); i != children.end(); i++) 96 | { 97 | if(i->get()->active) 98 | { 99 | i->get()->clear_close_request(); 100 | } 101 | } 102 | close_req_state = Window::NO_CLOSE_REQUEST; 103 | } 104 | 105 | //! Dump state of this window and all children 106 | std::string Window::dump_state_all() 107 | { 108 | std::string str; 109 | for(auto i = children.begin(); i != children.end(); i++) 110 | { 111 | if(i->get()->active) 112 | str += "child " + std::string(typeid(*i->get()).name()) + ":\n" + i->get()->dump_state_all(); 113 | } 114 | return str + dump_state(); 115 | } 116 | 117 | //! Dump window state (for debugging) 118 | std::string Window::dump_state() 119 | { 120 | return ""; 121 | } 122 | -------------------------------------------------------------------------------- /src/window.h: -------------------------------------------------------------------------------- 1 | #ifndef WINDOW_H 2 | #define WINDOW_H 3 | 4 | #include "window_type.h" 5 | #include 6 | #include 7 | #include 8 | 9 | //! Abstract window class 10 | class Window 11 | { 12 | public: 13 | enum Close_Request_State 14 | { 15 | NO_CLOSE_REQUEST = 0, 16 | CLOSE_NOT_OK = 1, 17 | CLOSE_IN_PROGRESS = 2, 18 | CLOSE_OK = 3, 19 | }; 20 | 21 | private: 22 | static uint32_t id_counter; 23 | 24 | protected: 25 | uint32_t id; 26 | Window_Type type; 27 | bool active; 28 | class Window* parent; 29 | std::vector> children; 30 | Close_Request_State close_req_state; 31 | 32 | std::vector>::iterator find_child(Window_Type type); 33 | 34 | public: 35 | static bool modal_open; 36 | 37 | Window(class Window* parent = nullptr); 38 | virtual ~Window(); 39 | 40 | bool display_all(); 41 | virtual void display() = 0; 42 | virtual void close(); 43 | 44 | void close_request_all(); 45 | virtual void close_request(); 46 | virtual Close_Request_State get_close_request(); 47 | void clear_close_request(); 48 | 49 | virtual std::string dump_state(); 50 | std::string dump_state_all(); 51 | }; 52 | 53 | #endif //WINDOW_H -------------------------------------------------------------------------------- /src/window_type.h: -------------------------------------------------------------------------------- 1 | #ifndef WINDOW_TYPE_H 2 | #define WINDOW_TYPE_H 3 | 4 | enum Window_Type 5 | { 6 | WT_DEFAULT = 0, 7 | WT_FPS_OVERLAY, 8 | WT_EDITOR, 9 | WT_CONFIG, 10 | WT_ABOUT 11 | }; 12 | 13 | #endif 14 | --------------------------------------------------------------------------------