├── .clang-tidy ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── gh-pages.yml ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── cmake ├── modules │ ├── FindZSTR.cmake │ ├── FindZlibStatic.cmake │ └── Findgcem.cmake └── toolchains │ └── wasm32-emscripten-vcpkg.cmake ├── console ├── CMakeLists.txt └── console.cpp ├── include ├── core │ ├── config_types.h │ ├── conversion_options.h │ ├── data.h │ ├── effects.h │ ├── factory.h │ ├── generated_data.h │ ├── global_options.h │ ├── module.h │ ├── module_base.h │ ├── note.h │ ├── options.h │ ├── state.h │ └── status.h ├── dmf2mod.h ├── modules │ ├── debug.h │ ├── dmf.h │ └── mod.h ├── utils │ ├── hash.h │ ├── stream_reader.h │ └── utils.h ├── version.h └── version.h.in ├── src ├── CMakeLists.txt ├── core │ ├── conversion_options.cpp │ ├── factory.cpp │ ├── global_options.cpp │ ├── module.cpp │ ├── options.cpp │ └── status.cpp ├── dmf2mod.cpp ├── modules │ ├── debug.cpp │ ├── dmf.cpp │ └── mod.cpp └── utils │ └── utils.cpp ├── vcpkg.json └── webapp ├── CMakeLists.txt ├── pre.js ├── ui ├── index.html └── webapp.js └── webapp.cpp /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: > 3 | bugprone-macro-parentheses, 4 | bugprone-macro-repeated-side-effects, 5 | modernize-avoid-bind, 6 | modernize-avoid-c-arrays, 7 | modernize-concat-nested-namespaces, 8 | modernize-deprecated-headers, 9 | modernize-deprecated-ios-base-aliases, 10 | modernize-loop-convert, 11 | modernize-make-shared, 12 | modernize-make-unique, 13 | modernize-pass-by-value, 14 | modernize-raw-string-literal, 15 | modernize-redundant-void-arg, 16 | modernize-replace-auto-ptr, 17 | modernize-replace-random-shuffle, 18 | modernize-return-braced-init-list, 19 | modernize-shrink-to-fit, 20 | modernize-unary-static-assert, 21 | modernize-use-auto, 22 | modernize-use-bool-literals, 23 | modernize-use-default-member-init, 24 | modernize-use-emplace, 25 | modernize-use-equals-default, 26 | modernize-use-equals-delete, 27 | modernize-use-nodiscard, 28 | modernize-use-noexcept, 29 | modernize-use-nullptr, 30 | modernize-use-override, 31 | modernize-use-trailing-return-type, 32 | modernize-use-transparent-functors, 33 | modernize-use-uncaught-exceptions, 34 | modernize-use-using, 35 | performance-trivially-destructible, 36 | readability-braces-around-statements, 37 | readability-const-return-type, 38 | readability-identifier-naming, 39 | readability-misleading-indentation, 40 | readability-simplify-boolean-expr 41 | WarningsAsErrors: '' 42 | HeaderFilterRegex: '' # don't show errors from headers 43 | AnalyzeTemporaryDtors: false 44 | FormatStyle: none 45 | User: user 46 | CheckOptions: 47 | - key: readability-identifier-naming.ClassCase 48 | value: CamelCase 49 | - key: readability-identifier-naming.EnumCase 50 | value: CamelCase 51 | - key: readability-identifier-naming.TypedefCase 52 | value: CamelCase 53 | - key: readability-identifier-naming.UnionCase 54 | value: CamelCase 55 | - key: readability-identifier-naming.StructCase 56 | value: CamelCase 57 | - key: readability-identifier-naming.UnionCase 58 | value: CamelCase 59 | # not yet working, as it currently applies both for static and object members 60 | # - key: readability-identifier-naming.MemberPrefix 61 | # value: 'm_' 62 | # currently only working for local static variables: 63 | #- key: readability-identifier-naming.StaticVariablePrefix 64 | # value: 's_' 65 | # not yet working 66 | # - key: readability-identifier-naming.VariableCase 67 | # value: camelBack 68 | #- key: readability-identifier-naming.FunctionCase 69 | # value: camelBack 70 | ... 71 | 72 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{c,cpp,cc,h,hpp,hh}] 9 | indent_style = tab 10 | 11 | [*.{js,html}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [CMakeLists.txt] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tell GitHub Linguist to ignore these 3rd party directories: 2 | extern/** linguist-vendored 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build-windows: 5 | name: windows-x86_64 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Configure 10 | run: cmake --preset console 11 | - name: Build 12 | run: cmake --build --preset console 13 | - name: Run 14 | run: .\build\console\Release\dmf2mod.exe 15 | build-linux: 16 | name: linux-x86_64 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Configure 21 | run: cmake --preset console 22 | - name: Build 23 | run: cmake --build --preset console 24 | - name: Run 25 | run: ./build/console/dmf2mod 26 | build-macos: 27 | name: macos-arm64 28 | runs-on: macOS-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Configure 32 | run: cmake --preset console 33 | - name: Build 34 | run: cmake --build --preset console 35 | - name: Run 36 | run: ./build/console/dmf2mod 37 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | on: [push, pull_request] 3 | jobs: 4 | deploy: 5 | runs-on: ubuntu-latest 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Setup Emscripten 11 | uses: mymindstorm/setup-emsdk@v14 12 | with: 13 | version: 'latest' 14 | actions-cache-folder: 'emsdk-cache' 15 | - name: Verify Emscripten 16 | run: emcc -v 17 | - name: Configure 18 | run: cmake --preset web-app 19 | - name: Build 20 | run: cmake --build --preset web-app 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v4 23 | if: ${{ github.ref == 'refs/heads/main' }} 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./build/web-app/html 27 | exclude_assets: '*.txt,CMakeFiles,*.cmake,Makefile' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom ignores 2 | docs/ 3 | test/ 4 | .vscode/ 5 | include/version.h 6 | vcpkg_installed/ 7 | 8 | ##### Windows 9 | # Windows thumbnail cache files 10 | Thumbs.db 11 | Thumbs.db:encryptable 12 | ehthumbs.db 13 | ehthumbs_vista.db 14 | 15 | # Dump file 16 | *.stackdump 17 | 18 | # Folder config file 19 | [Dd]esktop.ini 20 | 21 | # Recycle Bin used on file shares 22 | $RECYCLE.BIN/ 23 | 24 | # Windows Installer files 25 | *.cab 26 | *.msi 27 | *.msix 28 | *.msm 29 | *.msp 30 | 31 | # Windows shortcuts 32 | *.lnk 33 | 34 | ##### Linux 35 | *~ 36 | 37 | # temporary files which can be created if a process still has a handle open of a deleted file 38 | .fuse_hidden* 39 | 40 | # KDE directory preferences 41 | .directory 42 | 43 | # Linux trash folder which might appear on any partition or disk 44 | .Trash-* 45 | 46 | # .nfs files are created when an open file is removed but is still being accessed 47 | .nfs* 48 | 49 | ##### MacOS 50 | # General 51 | .DS_Store 52 | .AppleDouble 53 | .LSOverride 54 | 55 | # Thumbnails 56 | ._* 57 | 58 | # Files that might appear in the root of a volume 59 | .DocumentRevisions-V100 60 | .fseventsd 61 | .Spotlight-V100 62 | .TemporaryItems 63 | .Trashes 64 | .VolumeIcon.icns 65 | .com.apple.timemachine.donotpresent 66 | 67 | # Directories potentially created on remote AFP share 68 | .AppleDB 69 | .AppleDesktop 70 | Network Trash Folder 71 | Temporary Items 72 | .apdisk 73 | 74 | ##### Android 75 | # Built application files 76 | *.apk 77 | *.ap_ 78 | *.aab 79 | 80 | # Files for the ART/Dalvik VM 81 | *.dex 82 | 83 | # Java class files 84 | *.class 85 | 86 | # Generated files 87 | bin/ 88 | gen/ 89 | out/ 90 | # Uncomment the following line in case you need and you don't have the release build type files in your app 91 | # release/ 92 | 93 | # Gradle files 94 | .gradle/ 95 | build/ 96 | 97 | # Local configuration file (sdk path, etc) 98 | local.properties 99 | 100 | # Proguard folder generated by Eclipse 101 | proguard/ 102 | 103 | # Log Files 104 | *.log 105 | 106 | # Android Studio Navigation editor temp files 107 | .navigation/ 108 | 109 | # Android Studio captures folder 110 | captures/ 111 | 112 | # IntelliJ 113 | *.iml 114 | .idea/workspace.xml 115 | .idea/tasks.xml 116 | .idea/gradle.xml 117 | .idea/assetWizardSettings.xml 118 | .idea/dictionaries 119 | .idea/libraries 120 | # Android Studio 3 in .gitignore file. 121 | .idea/caches 122 | .idea/modules.xml 123 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 124 | .idea/navEditor.xml 125 | 126 | # Keystore files 127 | # Uncomment the following lines if you do not want to check your keystore files in. 128 | #*.jks 129 | #*.keystore 130 | 131 | # External native build folder generated in Android Studio 2.2 and later 132 | .externalNativeBuild 133 | 134 | # Google Services (e.g. APIs or Firebase) 135 | # google-services.json 136 | 137 | # Freeline 138 | freeline.py 139 | freeline/ 140 | freeline_project_description.json 141 | 142 | # fastlane 143 | fastlane/report.xml 144 | fastlane/Preview.html 145 | fastlane/screenshots 146 | fastlane/test_output 147 | fastlane/readme.md 148 | 149 | # Version control 150 | vcs.xml 151 | 152 | # lint 153 | lint/intermediates/ 154 | lint/generated/ 155 | lint/outputs/ 156 | lint/tmp/ 157 | # lint/reports/ 158 | 159 | ##### Backup 160 | *.bak 161 | *.gho 162 | *.ori 163 | *.orig 164 | *.tmp 165 | 166 | ##### GPG 167 | secring.* 168 | 169 | ##### Dropbox 170 | # Dropbox settings and caches 171 | .dropbox 172 | .dropbox.attr 173 | .dropbox.cache 174 | 175 | ##### SynopsysVCS 176 | # Waveform formats 177 | *.vcd 178 | *.vpd 179 | *.evcd 180 | *.fsdb 181 | 182 | # Default name of the simulation executable. A different name can be 183 | # specified with this switch (the associated daidir database name is 184 | # also taken from here): -o / 185 | simv 186 | 187 | # Generated for Verilog and VHDL top configs 188 | simv.daidir/ 189 | simv.db.dir/ 190 | 191 | # Infrastructure necessary to co-simulate SystemC models with 192 | # Verilog/VHDL models. An alternate directory may be specified with this 193 | # switch: -Mdir= 194 | csrc/ 195 | 196 | # Log file - the following switch allows to specify the file that will be 197 | # used to write all messages from simulation: -l 198 | *.log 199 | 200 | # Coverage results (generated with urg) and database location. The 201 | # following switch can also be used: urg -dir .vdb 202 | simv.vdb/ 203 | urgReport/ 204 | 205 | # DVE and UCLI related files. 206 | DVEfiles/ 207 | ucli.key 208 | 209 | # When the design is elaborated for DirectC, the following file is created 210 | # with declarations for C/C++ functions. 211 | vc_hdrs.h 212 | 213 | ##### SVN 214 | .svn/ 215 | 216 | ##### Mercurial 217 | .hg/ 218 | .hgignore 219 | .hgsigs 220 | .hgsub 221 | .hgsubstate 222 | .hgtags 223 | 224 | ##### Bazaar 225 | .bzr/ 226 | .bzrignore 227 | 228 | ##### CVS 229 | /CVS/* 230 | **/CVS/* 231 | .cvsignore 232 | */.cvsignore 233 | 234 | ##### TortoiseGit 235 | # Project-level settings 236 | /.tgitconfig 237 | 238 | ##### PuTTY 239 | # Private key 240 | *.ppk 241 | 242 | ##### Vim 243 | # Swap 244 | [._]*.s[a-v][a-z] 245 | !*.svg # comment out if you don't need vector files 246 | [._]*.sw[a-p] 247 | [._]s[a-rt-v][a-z] 248 | [._]ss[a-gi-z] 249 | [._]sw[a-p] 250 | 251 | # Session 252 | Session.vim 253 | Sessionx.vim 254 | 255 | # Temporary 256 | .netrwhist 257 | *~ 258 | # Auto-generated tag files 259 | tags 260 | # Persistent undo 261 | [._]*.un~ 262 | 263 | ##### Emacs 264 | # -*- mode: gitignore; -*- 265 | *~ 266 | \#*\# 267 | /.emacs.desktop 268 | /.emacs.desktop.lock 269 | *.elc 270 | auto-save-list 271 | tramp 272 | .\#* 273 | 274 | # Org-mode 275 | .org-id-locations 276 | *_archive 277 | 278 | # flymake-mode 279 | *_flymake.* 280 | 281 | # eshell files 282 | /eshell/history 283 | /eshell/lastdir 284 | 285 | # elpa packages 286 | /elpa/ 287 | 288 | # reftex files 289 | *.rel 290 | 291 | # AUCTeX auto folder 292 | /auto/ 293 | 294 | # cask packages 295 | .cask/ 296 | dist/ 297 | 298 | # Flycheck 299 | flycheck_*.el 300 | 301 | # server auth directory 302 | /server/ 303 | 304 | # projectiles files 305 | .projectile 306 | 307 | # directory configuration 308 | .dir-locals.el 309 | 310 | # network security 311 | /network-security.data 312 | 313 | ##### SublimeText 314 | # Cache files for Sublime Text 315 | *.tmlanguage.cache 316 | *.tmPreferences.cache 317 | *.stTheme.cache 318 | 319 | # Workspace files are user-specific 320 | *.sublime-workspace 321 | 322 | # Project files should be checked into the repository, unless a significant 323 | # proportion of contributors will probably not be using Sublime Text 324 | # *.sublime-project 325 | 326 | # SFTP configuration file 327 | sftp-config.json 328 | sftp-config-alt*.json 329 | 330 | # Package control specific files 331 | Package Control.last-run 332 | Package Control.ca-list 333 | Package Control.ca-bundle 334 | Package Control.system-ca-bundle 335 | Package Control.cache/ 336 | Package Control.ca-certs/ 337 | Package Control.merged-ca-bundle 338 | Package Control.user-ca-bundle 339 | oscrypto-ca-bundle.crt 340 | bh_unicode_properties.cache 341 | 342 | # Sublime-github package stores a github token in this file 343 | # https://packagecontrol.io/packages/sublime-github 344 | GitHub.sublime-settings 345 | 346 | ##### Notepad++ 347 | # Notepad++ backups # 348 | *.bak 349 | 350 | ##### TextMate 351 | *.tmproj 352 | *.tmproject 353 | tmtags 354 | 355 | ##### VisualStudioCode 356 | .vscode/* 357 | !.vscode/settings.json 358 | !.vscode/tasks.json 359 | !.vscode/launch.json 360 | !.vscode/extensions.json 361 | *.code-workspace 362 | 363 | # Local History for Visual Studio Code 364 | .history/ 365 | 366 | ##### NetBeans 367 | **/nbproject/private/ 368 | **/nbproject/Makefile-*.mk 369 | **/nbproject/Package-*.bash 370 | build/ 371 | nbbuild/ 372 | dist/ 373 | nbdist/ 374 | .nb-gradle/ 375 | 376 | ##### JetBrains 377 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 378 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 379 | 380 | # User-specific stuff 381 | .idea/**/workspace.xml 382 | .idea/**/tasks.xml 383 | .idea/**/usage.statistics.xml 384 | .idea/**/dictionaries 385 | .idea/**/shelf 386 | 387 | # Generated files 388 | .idea/**/contentModel.xml 389 | 390 | # Sensitive or high-churn files 391 | .idea/**/dataSources/ 392 | .idea/**/dataSources.ids 393 | .idea/**/dataSources.local.xml 394 | .idea/**/sqlDataSources.xml 395 | .idea/**/dynamic.xml 396 | .idea/**/uiDesigner.xml 397 | .idea/**/dbnavigator.xml 398 | 399 | # Gradle 400 | .idea/**/gradle.xml 401 | .idea/**/libraries 402 | 403 | # Gradle and Maven with auto-import 404 | # When using Gradle or Maven with auto-import, you should exclude module files, 405 | # since they will be recreated, and may cause churn. Uncomment if using 406 | # auto-import. 407 | # .idea/artifacts 408 | # .idea/compiler.xml 409 | # .idea/jarRepositories.xml 410 | # .idea/modules.xml 411 | # .idea/*.iml 412 | # .idea/modules 413 | # *.iml 414 | # *.ipr 415 | 416 | # CMake 417 | cmake-build-*/ 418 | 419 | # Mongo Explorer plugin 420 | .idea/**/mongoSettings.xml 421 | 422 | # File-based project format 423 | *.iws 424 | 425 | # IntelliJ 426 | out/ 427 | 428 | # mpeltonen/sbt-idea plugin 429 | .idea_modules/ 430 | 431 | # JIRA plugin 432 | atlassian-ide-plugin.xml 433 | 434 | # Cursive Clojure plugin 435 | .idea/replstate.xml 436 | 437 | # Crashlytics plugin (for Android Studio and IntelliJ) 438 | com_crashlytics_export_strings.xml 439 | crashlytics.properties 440 | crashlytics-build.properties 441 | fabric.properties 442 | 443 | # Editor-based Rest Client 444 | .idea/httpRequests 445 | 446 | # Android studio 3.1+ serialized cache file 447 | .idea/caches/build_file_checksums.ser 448 | 449 | ##### Eclipse 450 | .metadata 451 | bin/ 452 | tmp/ 453 | *.tmp 454 | *.bak 455 | *.swp 456 | *~.nib 457 | local.properties 458 | .settings/ 459 | .loadpath 460 | .recommenders 461 | 462 | # External tool builders 463 | .externalToolBuilders/ 464 | 465 | # Locally stored "Eclipse launch configurations" 466 | *.launch 467 | 468 | # PyDev specific (Python IDE for Eclipse) 469 | *.pydevproject 470 | 471 | # CDT-specific (C/C++ Development Tooling) 472 | .cproject 473 | 474 | # CDT- autotools 475 | .autotools 476 | 477 | # Java annotation processor (APT) 478 | .factorypath 479 | 480 | # PDT-specific (PHP Development Tools) 481 | .buildpath 482 | 483 | # sbteclipse plugin 484 | .target 485 | 486 | # Tern plugin 487 | .tern-project 488 | 489 | # TeXlipse plugin 490 | .texlipse 491 | 492 | # STS (Spring Tool Suite) 493 | .springBeans 494 | 495 | # Code Recommenders 496 | .recommenders/ 497 | 498 | # Annotation Processing 499 | .apt_generated/ 500 | .apt_generated_test/ 501 | 502 | # Scala IDE specific (Scala & Java development for Eclipse) 503 | .cache-main 504 | .scala_dependencies 505 | .worksheet 506 | 507 | # Uncomment this line if you wish to ignore the project description file. 508 | # Typically, this file would be tracked if it contains build/dependency configurations: 509 | #.project 510 | 511 | ##### Qt 512 | # C++ objects and libs 513 | *.slo 514 | *.lo 515 | *.o 516 | *.a 517 | *.la 518 | *.lai 519 | *.so 520 | *.so.* 521 | *.dll 522 | *.dylib 523 | 524 | # Qt-es 525 | object_script.*.Release 526 | object_script.*.Debug 527 | *_plugin_import.cpp 528 | /.qmake.cache 529 | /.qmake.stash 530 | *.pro.user 531 | *.pro.user.* 532 | *.qbs.user 533 | *.qbs.user.* 534 | *.moc 535 | moc_*.cpp 536 | moc_*.h 537 | qrc_*.cpp 538 | ui_*.h 539 | *.qmlc 540 | *.jsc 541 | Makefile* 542 | *build-* 543 | *.qm 544 | *.prl 545 | 546 | # Qt unit tests 547 | target_wrapper.* 548 | 549 | # QtCreator 550 | *.autosave 551 | 552 | # QtCreator Qml 553 | *.qmlproject.user 554 | *.qmlproject.user.* 555 | 556 | # QtCreator CMake 557 | CMakeLists.txt.user* 558 | 559 | # QtCreator 4.8< compilation database 560 | compile_commands.json 561 | 562 | # QtCreator local machine specific files for imported projects 563 | *creator.user* 564 | 565 | ##### VisualStudio 566 | ##### VisualStudio 567 | ## Ignore Visual Studio temporary files, build results, and 568 | ## files generated by popular Visual Studio add-ons. 569 | ## 570 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 571 | 572 | # User-specific files 573 | *.rsuser 574 | *.suo 575 | *.user 576 | *.userosscache 577 | *.sln.docstates 578 | 579 | # User-specific files (MonoDevelop/Xamarin Studio) 580 | *.userprefs 581 | 582 | # Mono auto generated files 583 | mono_crash.* 584 | 585 | # Build results 586 | [Dd]ebug/ 587 | [Dd]ebugPublic/ 588 | [Rr]elease/ 589 | [Rr]eleases/ 590 | x64/ 591 | x86/ 592 | [Ww][Ii][Nn]32/ 593 | [Aa][Rr][Mm]/ 594 | [Aa][Rr][Mm]64/ 595 | bld/ 596 | [Bb]in/ 597 | [Oo]bj/ 598 | [Ll]og/ 599 | [Ll]ogs/ 600 | 601 | # Visual Studio 2015/2017 cache/options directory 602 | .vs/ 603 | # Uncomment if you have tasks that create the project's static files in wwwroot 604 | #wwwroot/ 605 | 606 | # Visual Studio 2017 auto generated files 607 | Generated\ Files/ 608 | 609 | # MSTest test Results 610 | [Tt]est[Rr]esult*/ 611 | [Bb]uild[Ll]og.* 612 | 613 | # NUnit 614 | *.VisualState.xml 615 | TestResult.xml 616 | nunit-*.xml 617 | 618 | # Build Results of an ATL Project 619 | [Dd]ebugPS/ 620 | [Rr]eleasePS/ 621 | dlldata.c 622 | 623 | # Benchmark Results 624 | BenchmarkDotNet.Artifacts/ 625 | 626 | # .NET Core 627 | project.lock.json 628 | project.fragment.lock.json 629 | artifacts/ 630 | 631 | # ASP.NET Scaffolding 632 | ScaffoldingReadMe.txt 633 | 634 | # StyleCop 635 | StyleCopReport.xml 636 | 637 | # Files built by Visual Studio 638 | *_i.c 639 | *_p.c 640 | *_h.h 641 | *.ilk 642 | *.meta 643 | *.obj 644 | *.iobj 645 | *.pch 646 | *.pdb 647 | *.ipdb 648 | *.pgc 649 | *.pgd 650 | *.rsp 651 | *.sbr 652 | *.tlb 653 | *.tli 654 | *.tlh 655 | *.tmp 656 | *.tmp_proj 657 | *_wpftmp.csproj 658 | *.log 659 | *.vspscc 660 | *.vssscc 661 | .builds 662 | *.pidb 663 | *.svclog 664 | *.scc 665 | 666 | # Chutzpah Test files 667 | _Chutzpah* 668 | 669 | # Visual C++ cache files 670 | ipch/ 671 | *.aps 672 | *.ncb 673 | *.opendb 674 | *.opensdf 675 | *.sdf 676 | *.cachefile 677 | *.VC.db 678 | *.VC.VC.opendb 679 | 680 | # Visual Studio profiler 681 | *.psess 682 | *.vsp 683 | *.vspx 684 | *.sap 685 | 686 | # Visual Studio Trace Files 687 | *.e2e 688 | 689 | # TFS 2012 Local Workspace 690 | $tf/ 691 | 692 | # Guidance Automation Toolkit 693 | *.gpState 694 | 695 | # ReSharper is a .NET coding add-in 696 | _ReSharper*/ 697 | *.[Rr]e[Ss]harper 698 | *.DotSettings.user 699 | 700 | # TeamCity is a build add-in 701 | _TeamCity* 702 | 703 | # DotCover is a Code Coverage Tool 704 | *.dotCover 705 | 706 | # AxoCover is a Code Coverage Tool 707 | .axoCover/* 708 | !.axoCover/settings.json 709 | 710 | # Coverlet is a free, cross platform Code Coverage Tool 711 | coverage*[.json, .xml, .info] 712 | 713 | # Visual Studio code coverage results 714 | *.coverage 715 | *.coveragexml 716 | 717 | # NCrunch 718 | _NCrunch_* 719 | .*crunch*.local.xml 720 | nCrunchTemp_* 721 | 722 | # MightyMoose 723 | *.mm.* 724 | AutoTest.Net/ 725 | 726 | # Web workbench (sass) 727 | .sass-cache/ 728 | 729 | # Installshield output folder 730 | [Ee]xpress/ 731 | 732 | # DocProject is a documentation generator add-in 733 | DocProject/buildhelp/ 734 | DocProject/Help/*.HxT 735 | DocProject/Help/*.HxC 736 | DocProject/Help/*.hhc 737 | DocProject/Help/*.hhk 738 | DocProject/Help/*.hhp 739 | DocProject/Help/Html2 740 | DocProject/Help/html 741 | 742 | # Click-Once directory 743 | publish/ 744 | 745 | # Publish Web Output 746 | *.[Pp]ublish.xml 747 | *.azurePubxml 748 | # Note: Comment the next line if you want to checkin your web deploy settings, 749 | # but database connection strings (with potential passwords) will be unencrypted 750 | *.pubxml 751 | *.publishproj 752 | 753 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 754 | # checkin your Azure Web App publish settings, but sensitive information contained 755 | # in these scripts will be unencrypted 756 | PublishScripts/ 757 | 758 | # NuGet Packages 759 | *.nupkg 760 | # NuGet Symbol Packages 761 | *.snupkg 762 | # The packages folder can be ignored because of Package Restore 763 | **/[Pp]ackages/* 764 | # except build/, which is used as an MSBuild target. 765 | !**/[Pp]ackages/build/ 766 | # Uncomment if necessary however generally it will be regenerated when needed 767 | #!**/[Pp]ackages/repositories.config 768 | # NuGet v3's project.json files produces more ignorable files 769 | *.nuget.props 770 | *.nuget.targets 771 | 772 | # Microsoft Azure Build Output 773 | csx/ 774 | *.build.csdef 775 | 776 | # Microsoft Azure Emulator 777 | ecf/ 778 | rcf/ 779 | 780 | # Windows Store app package directories and files 781 | AppPackages/ 782 | BundleArtifacts/ 783 | Package.StoreAssociation.xml 784 | _pkginfo.txt 785 | *.appx 786 | *.appxbundle 787 | *.appxupload 788 | 789 | # Visual Studio cache files 790 | # files ending in .cache can be ignored 791 | *.[Cc]ache 792 | # but keep track of directories ending in .cache 793 | !?*.[Cc]ache/ 794 | 795 | # Others 796 | ClientBin/ 797 | ~$* 798 | *~ 799 | *.dbmdl 800 | *.dbproj.schemaview 801 | *.jfm 802 | *.pfx 803 | *.publishsettings 804 | orleans.codegen.cs 805 | 806 | # Including strong name files can present a security risk 807 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 808 | #*.snk 809 | 810 | # Since there are multiple workflows, uncomment next line to ignore bower_components 811 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 812 | #bower_components/ 813 | 814 | # RIA/Silverlight projects 815 | Generated_Code/ 816 | 817 | # Backup & report files from converting an old project file 818 | # to a newer Visual Studio version. Backup files are not needed, 819 | # because we have git ;-) 820 | _UpgradeReport_Files/ 821 | Backup*/ 822 | UpgradeLog*.XML 823 | UpgradeLog*.htm 824 | ServiceFabricBackup/ 825 | *.rptproj.bak 826 | 827 | # SQL Server files 828 | *.mdf 829 | *.ldf 830 | *.ndf 831 | 832 | # Business Intelligence projects 833 | *.rdl.data 834 | *.bim.layout 835 | *.bim_*.settings 836 | *.rptproj.rsuser 837 | *- [Bb]ackup.rdl 838 | *- [Bb]ackup ([0-9]).rdl 839 | *- [Bb]ackup ([0-9][0-9]).rdl 840 | 841 | # Microsoft Fakes 842 | FakesAssemblies/ 843 | 844 | # GhostDoc plugin setting file 845 | *.GhostDoc.xml 846 | 847 | # Node.js Tools for Visual Studio 848 | .ntvs_analysis.dat 849 | node_modules/ 850 | 851 | # Visual Studio 6 build log 852 | *.plg 853 | 854 | # Visual Studio 6 workspace options file 855 | *.opt 856 | 857 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 858 | *.vbw 859 | 860 | # Visual Studio LightSwitch build output 861 | **/*.HTMLClient/GeneratedArtifacts 862 | **/*.DesktopClient/GeneratedArtifacts 863 | **/*.DesktopClient/ModelManifest.xml 864 | **/*.Server/GeneratedArtifacts 865 | **/*.Server/ModelManifest.xml 866 | _Pvt_Extensions 867 | 868 | # Paket dependency manager 869 | .paket/paket.exe 870 | paket-files/ 871 | 872 | # FAKE - F# Make 873 | .fake/ 874 | 875 | # CodeRush personal settings 876 | .cr/personal 877 | 878 | # Python Tools for Visual Studio (PTVS) 879 | __pycache__/ 880 | *.pyc 881 | 882 | # Cake - Uncomment if you are using it 883 | # tools/** 884 | # !tools/packages.config 885 | 886 | # Tabs Studio 887 | *.tss 888 | 889 | # Telerik's JustMock configuration file 890 | *.jmconfig 891 | 892 | # BizTalk build output 893 | *.btp.cs 894 | *.btm.cs 895 | *.odx.cs 896 | *.xsd.cs 897 | 898 | # OpenCover UI analysis results 899 | OpenCover/ 900 | 901 | # Azure Stream Analytics local run output 902 | ASALocalRun/ 903 | 904 | # MSBuild Binary and Structured Log 905 | *.binlog 906 | 907 | # NVidia Nsight GPU debugger configuration file 908 | *.nvuser 909 | 910 | # MFractors (Xamarin productivity tool) working folder 911 | .mfractor/ 912 | 913 | # Local History for Visual Studio 914 | .localhistory/ 915 | 916 | # BeatPulse healthcheck temp database 917 | healthchecksdb 918 | 919 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 920 | MigrationBackup/ 921 | 922 | # Ionide (cross platform F# VS Code tools) working folder 923 | .ionide/ 924 | 925 | # Fody - auto-generated XML schema 926 | FodyWeavers.xsd 927 | 928 | ##### Gradle 929 | .gradle 930 | **/build/ 931 | !src/**/build/ 932 | 933 | # Ignore Gradle GUI config 934 | gradle-app.setting 935 | 936 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 937 | !gradle-wrapper.jar 938 | 939 | # Cache of project 940 | .gradletasknamecache 941 | 942 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 943 | # gradle/wrapper/gradle-wrapper.properties 944 | 945 | ##### CMake 946 | CMakeLists.txt.user 947 | CMakeCache.txt 948 | CMakeFiles 949 | CMakeScripts 950 | Testing 951 | Makefile 952 | cmake_install.cmake 953 | install_manifest.txt 954 | compile_commands.json 955 | CTestTestfile.cmake 956 | _deps 957 | 958 | ##### C++ 959 | # Prerequisites 960 | *.d 961 | 962 | # Compiled Object files 963 | *.slo 964 | *.lo 965 | *.o 966 | *.obj 967 | 968 | # Precompiled Headers 969 | *.gch 970 | *.pch 971 | 972 | # Compiled Dynamic libraries 973 | *.so 974 | *.dylib 975 | *.dll 976 | 977 | # Fortran module files 978 | *.mod 979 | *.smod 980 | 981 | # Compiled Static libraries 982 | *.lai 983 | *.la 984 | *.a 985 | *.lib 986 | 987 | # Executables 988 | *.exe 989 | *.out 990 | *.app 991 | 992 | # C/C++ binary extension file 993 | *.bin 994 | 995 | ##### C 996 | # Prerequisites 997 | *.d 998 | 999 | # Object files 1000 | *.o 1001 | *.ko 1002 | *.obj 1003 | *.elf 1004 | 1005 | # Linker output 1006 | *.ilk 1007 | *.map 1008 | *.exp 1009 | 1010 | # Precompiled Headers 1011 | *.gch 1012 | *.pch 1013 | 1014 | # Libraries 1015 | *.lib 1016 | *.a 1017 | *.la 1018 | *.lo 1019 | 1020 | # Shared objects (inc. Windows DLLs) 1021 | *.dll 1022 | *.so 1023 | *.so.* 1024 | *.dylib 1025 | 1026 | # Executables 1027 | *.exe 1028 | *.out 1029 | *.app 1030 | *.i*86 1031 | *.x86_64 1032 | *.hex 1033 | 1034 | # Debug files 1035 | *.dSYM/ 1036 | *.su 1037 | *.idb 1038 | *.pdb 1039 | 1040 | # Kernel Module Compile Results 1041 | *.mod* 1042 | *.cmd 1043 | .tmp_versions/ 1044 | modules.order 1045 | Module.symvers 1046 | Mkfile.old 1047 | dkms.conf 1048 | 1049 | # Raspberry Pi Pico Object file 1050 | *.uf2 1051 | # Raspberry Pi Pico disassembler file 1052 | *.dis 1053 | 1054 | # Custom unignore 1055 | !/extern/zlib/win32 1056 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | project(dmf2mod VERSION "0.2.0") 4 | set(PROJECT_VERSION_STAGE "alpha") 5 | if(PROJECT_VERSION_STAGE) 6 | set(PROJECT_VERSION "${PROJECT_VERSION}-${PROJECT_VERSION_STAGE}") 7 | endif() 8 | 9 | set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules" ${CMAKE_MODULE_PATH}) 10 | 11 | enable_language(CXX) 12 | set(CMAKE_CXX_STANDARD 17) 13 | set(CMAKE_CXX_STANDARD_REQUIRED True) 14 | 15 | ###################### 16 | ## Set build target ## 17 | ###################### 18 | 19 | set(BUILD_TARGET "console" CACHE STRING "Choose build type: library, console, or web-app") 20 | set_property(CACHE BUILD_TARGET PROPERTY STRINGS "library" "console" "web-app") 21 | 22 | if(BUILD_TARGET STREQUAL "library") 23 | if(DEFINED EMSCRIPTEN) 24 | message(FATAL_ERROR "Cannot build library using Emscripten") 25 | endif() 26 | message(STATUS "Building dmf2mod library") 27 | elseif(BUILD_TARGET STREQUAL "console") 28 | set(BUILD_CONSOLE 1) 29 | if(DEFINED EMSCRIPTEN) 30 | message(FATAL_ERROR "Cannot build command-line application using Emscripten") 31 | endif() 32 | message(STATUS "Building dmf2mod command-line application") 33 | elseif(BUILD_TARGET STREQUAL "web-app") 34 | set(BUILD_WEBAPP 1) 35 | if(NOT DEFINED EMSCRIPTEN) 36 | message(FATAL_ERROR "Please use the wasm32-emscripten-vcpkg.cmake toolchain file") 37 | endif() 38 | message(STATUS "Building dmf2mod web app") 39 | else() 40 | message(FATAL_ERROR "Invalid BUILD_TARGET: ${BUILD_TARGET}") 41 | endif() 42 | 43 | ############################# 44 | ## Set build configuration ## 45 | ############################# 46 | 47 | # Set default build type if none was specified 48 | set(DEFAULT_BUILD_TYPE "Release") 49 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 50 | message(STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified") 51 | set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build" FORCE) 52 | # Set the possible values of build type for cmake-gui 53 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Asan" "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 54 | endif() 55 | 56 | # Set up address sanitizer 57 | if(CMAKE_BUILD_TYPE STREQUAL "Asan") 58 | message(STATUS "Using address sanitizer") 59 | add_compile_options(${CMAKE_CXX_FLAGS_DEBUG}) 60 | if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 61 | add_compile_options(/fsanitize=address) 62 | add_link_options(/fsanitize=address) 63 | else() 64 | add_compile_options(-fsanitize=address) 65 | add_link_options(-fsanitize=address) 66 | endif() 67 | endif() 68 | 69 | # Display default configuration-specific flags (debugging and optimization flags) 70 | message(STATUS "Debug flags: ${CMAKE_CXX_FLAGS_DEBUG}") 71 | message(STATUS "Release flags: ${CMAKE_CXX_FLAGS_RELEASE}") 72 | 73 | ########################### 74 | ## Static analysis setup ## 75 | ########################### 76 | 77 | option(ENABLE_CPPCHECK "Enable testing with cppcheck" FALSE) 78 | 79 | # Set up Cppcheck static code analysis 80 | if(ENABLE_CPPCHECK) 81 | find_program(CPPCHECK cppcheck) 82 | if(CPPCHECK) 83 | set(CMAKE_CXX_CPPCHECK 84 | ${CPPCHECK} 85 | --suppress=syntaxError 86 | --enable=all 87 | --inconclusive) 88 | else() 89 | message(SEND_ERROR "cppcheck requested but executable not found") 90 | endif() 91 | endif() 92 | 93 | ################## 94 | ## Dependencies ## 95 | ################## 96 | 97 | find_package(gcem REQUIRED) 98 | find_package(ZSTR REQUIRED) 99 | 100 | ########### 101 | ## Build ## 102 | ########### 103 | 104 | set(DMF2MOD_ROOT ${PROJECT_SOURCE_DIR}) 105 | 106 | add_subdirectory(src) 107 | 108 | # Configure version 109 | configure_file(include/version.h.in include/version.h) 110 | 111 | # Build web app 112 | if(BUILD_WEBAPP) 113 | add_subdirectory(webapp) 114 | return() 115 | endif() 116 | 117 | # DMF2MOD library build 118 | 119 | # Compiler-specific warnings 120 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 121 | set(WARNING_FLAGS -Wall -Wno-unknown-pragmas -Werror -Wno-error=cpp -Wno-error=unused-variable) 122 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") 123 | set(WARNING_FLAGS -Wall -Wno-unknown-pragmas "-Wno-\#warnings" -Werror -Wno-error=unused-variable) 124 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 125 | set(WARNING_FLAGS /W3) #/WX 126 | set(OTHER_FLAGS /utf-8 /permissive-) 127 | endif() 128 | 129 | add_library(${PROJECT_NAME} STATIC ${DMF2MOD_SOURCES}) 130 | 131 | target_link_libraries(${PROJECT_NAME} PRIVATE gcem zstr::zstr) 132 | target_include_directories(${PROJECT_NAME} PUBLIC ${DMF2MOD_ROOT}/include) 133 | target_compile_options(${PROJECT_NAME} PRIVATE ${WARNING_FLAGS} ${OTHER_FLAGS}) 134 | 135 | #set_target_properties(${PROJECT_NAME} PROPERTIES 136 | # ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) 137 | 138 | if(BUILD_CONSOLE) 139 | add_subdirectory(console) 140 | endif() 141 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 10, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 24, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "console", 11 | "description": "dmf2mod command-line application", 12 | "binaryDir": "build/console", 13 | "cacheVariables": { 14 | "BUILD_TARGET": "console", 15 | "CMAKE_BUILD_TYPE": "Release" 16 | } 17 | }, 18 | { 19 | "name": "console-ninja", 20 | "inherits": "console", 21 | "generator": "Ninja" 22 | }, 23 | { 24 | "name": "web-app", 25 | "description": "dmf2mod web app using Emscripten with vcpkg", 26 | "toolchainFile": "${sourceDir}/cmake/toolchains/wasm32-emscripten-vcpkg.cmake", 27 | "binaryDir": "build/web-app", 28 | "cacheVariables": { 29 | "BUILD_TARGET": "web-app", 30 | "CMAKE_BUILD_TYPE": "Release" 31 | } 32 | } 33 | ], 34 | "buildPresets": [ 35 | { 36 | "name": "console", 37 | "configurePreset": "console", 38 | "configuration": "Release" 39 | }, 40 | { 41 | "name": "console-ninja", 42 | "configurePreset": "console" 43 | }, 44 | { 45 | "name": "web-app", 46 | "configurePreset": "web-app", 47 | "configuration": "Release" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dalton Messmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dmf2mod [![Build Status](https://github.com/messmerd/dmf2mod/workflows/build/badge.svg)](https://github.com/messmerd/dmf2mod/actions?query=workflow%3Abuild) 2 | 3 | A cross-platform utility for converting Deflemask's DMF files to other trackers' module files. 4 | 5 | ⇨ *Check it out [in your browser](https://messmerd.github.io/dmf2mod)!* 6 | 7 | ## Currently supported conversions 8 | 9 | - DMF (Game Boy only) ⇨ MOD 10 | 11 | Conversion to XM and other module formats may be added in the future. 12 | 13 | ## Build 14 | 15 | ### Command-line application 16 | 17 | Linux and macOS: 18 | 19 | ```bash 20 | cmake -S. -Bbin/Release 21 | cmake --build ./bin/Release 22 | ``` 23 | 24 | Windows: 25 | 26 | ```bash 27 | cmake -S. -Bbin 28 | cmake --build .\bin --config Release 29 | ``` 30 | 31 | #### Web application 32 | 33 | ```bash 34 | emcmake cmake -S. -Bbin/webapp 35 | emmake make --dir=bin/webapp 36 | ``` 37 | 38 | Requires the Emscripten SDK. 39 | 40 | ## Usage 41 | 42 | ```text 43 | dmf2mod output.[ext] input.dmf [options] 44 | dmf2mod [ext] input.dmf [options] 45 | dmf2mod [option] 46 | ``` 47 | 48 | Options: 49 | 50 | ```text 51 | -f, --force Overwrite output file. 52 | --help [module type] Displays the help message. Provide module type (i.e. mod) for module-specific options. 53 | --verbose Print debug info to console in addition to errors and/or warnings. 54 | -v, --version Display the dmf2mod version. 55 | ``` 56 | 57 | Options when converting to MOD: 58 | 59 | ```text 60 | --arp Allow arpeggio effects 61 | --port Allow portamento up/down effects 62 | --port2note Allow portamento to note effects 63 | --vib Allow vibrato effects 64 | --tempo=[accuracy, compat] Prioritize tempo accuracy or compatibility with effects (Default: accuracy) 65 | ``` 66 | 67 | ## DMF⇨MOD Conversions 68 | 69 | Because of the severe limitations of the MOD format compared to DMF, there are several restrictions when converting DMF files to MOD. For example, DMF files must use the Game Boy system and patterns must have 64 or fewer rows. 70 | 71 | The range of notes that ProTracker can play is less than half that of Deflemask, and because of this, dmf2mod must occasionally downsample wavetables to play higher frequency notes in MOD. Sometimes a square wave or wavetable will use two or more samples in MOD - each of which covers a separate note range. While this allows the limited ProTracker to play any note that can be played by Deflemask, it unfortunately can cause issues with portamento effects around the boundaries of these note ranges. 72 | 73 | The MOD format is severely limited by only one effects column per channel, and this problem is exacerbated considering that the MOD format implements volume changes as effects. So to make the most of the situation, dmf2mod uses a priority system to determine which effect should be used for each MOD effects slot. Each type of nonessential effect (besides volume) has its own command-line option to enable its usage and provide the user with more control over the conversion process. By default, all these effects are disabled. 74 | 75 | The behavior of portamento and vibrato effects depends on the speed of the MOD module. When the speed is at the default value of 6, these effects play at the correct rate. Otherwise they may play at the wrong rate or not at all. To ensure the proper initial speed value is used, use the `--tempo=compat` command-line option. This has the downside of limiting the tempo of the module to 16-127.5 BPM. By default, the `--tempo=accuracy` option is used, which enables the full tempo range of ~3.1 BPM to 765 BPM but cannot guarantee the correct behavior of effects - especially portamentos. 76 | 77 | Deflemask instruments are unsupported, so if you want to change the volume of a channel or the WAVE channel's wavetable for example, your DMF module will need to use only the built-in commands in Deflemask's pattern editor. 78 | 79 | Currently, dmf2mod converts notes, volume changes, initial tempo, and the following Deflemask effects: 80 | 81 | - **0xy** - Arpeggio (`--arp`) 82 | - **1xx** - Portamento Up (`--port`) 83 | - **2xx** - Portamento Down (`--port`) 84 | - **3xx** - Portamento to Note (`--port2note`) 85 | - **4xy** - Vibrato (`--vib`) 86 | - **Bxx** - Position Jump 87 | - **Dxx** - Pattern Break 88 | - **10xx** - Set WAVE 89 | - **12xx** - Set Duty Cycle 90 | 91 | Effects 10xx and 12xx are implemented by changing the sample in MOD rather than as a MOD effect. As such, they do not count towards the 1 effect per channel limit. 92 | 93 | SQ1, SQ2, and WAVE channels are supported. 94 | 95 | In later updates, I may add: 96 | 97 | - Systems besides Game Boy 98 | - Support for patterns with over 64 rows 99 | - More effects 100 | - Tempo changes? 101 | - Channel master volume? 102 | - Noise channel? 103 | - ... 104 | 105 | Instruments will not receive support due to MOD limitations. The noise channel may receive support in the future if it is feasible and can be made to sound decent. 106 | 107 | ______ 108 | Created by Dalton Messmer . 109 | -------------------------------------------------------------------------------- /cmake/modules/FindZSTR.cmake: -------------------------------------------------------------------------------- 1 | # FindZSTR.cmake 2 | 3 | find_package(ZlibStatic) 4 | 5 | if(DEFINED VCPKG_TOOLCHAIN) 6 | # Use vcpkg when available 7 | find_path(ZSTR_INCLUDE_DIRS "zstr.hpp") 8 | 9 | # Manually create the CMake target 10 | if(ZSTR_INCLUDE_DIRS) 11 | add_library(zstr::zstr INTERFACE IMPORTED) 12 | target_include_directories(zstr::zstr INTERFACE "${ZSTR_INCLUDE_DIRS}") 13 | target_link_libraries(zstr::zstr INTERFACE ZLIB::ZLIB) 14 | target_compile_features(zstr::zstr INTERFACE cxx_std_17) 15 | endif() 16 | else() 17 | message(STATUS "Downloading zstr using FetchContent") 18 | 19 | include(FetchContent) 20 | FetchContent_Declare(ZStrGitRepo 21 | GIT_REPOSITORY "https://github.com/mateidavid/zstr" 22 | GIT_TAG "master" 23 | ) 24 | FetchContent_MakeAvailable(ZStrGitRepo) 25 | 26 | if(TARGET zstr::zstr) 27 | set(ZSTR_FOUND TRUE) 28 | get_target_property(ZSTR_INCLUDE_DIRS zstr::zstr INTERFACE_INCLUDE_DIRECTORIES) 29 | endif() 30 | endif() 31 | 32 | include(FindPackageHandleStandardArgs) 33 | find_package_handle_standard_args(ZSTR REQUIRED_VARS ZSTR_INCLUDE_DIRS) 34 | -------------------------------------------------------------------------------- /cmake/modules/FindZlibStatic.cmake: -------------------------------------------------------------------------------- 1 | # FindZlibStatic.cmake 2 | 3 | set(ZLIB_USE_STATIC_LIBS ON) 4 | find_package(ZLIB 1.2.3 QUIET) 5 | 6 | if(NOT ZLIB_FOUND) 7 | message(STATUS "Downloading zlib using FetchContent") 8 | 9 | include(FetchContent) 10 | FetchContent_Declare( 11 | zlibGitRepo 12 | GIT_REPOSITORY "https://github.com/madler/zlib" 13 | GIT_TAG "develop" 14 | ) 15 | 16 | set(ZLIB_BUILD_SHARED OFF) 17 | set(ZLIB_BUILD_TESTING OFF) 18 | FetchContent_MakeAvailable(zlibGitRepo) 19 | 20 | if(TARGET zlibstatic) 21 | set(ZLIB_FOUND TRUE) 22 | set(ZLIB_LIBRARY zlibstatic) 23 | get_target_property(ZLIB_INCLUDE_DIRS zlibstatic INTERFACE_INCLUDE_DIRECTORIES) 24 | 25 | # The shared library was not built, and the zlibstatic target 26 | # unfortunately does not provide the version - hence this workaround 27 | get_target_property(_zlib_source_dir zlibstatic SOURCE_DIR) 28 | file(STRINGS "${_zlib_source_dir}/zlib.h" _zlib_version_line REGEX "#define ZLIB_VERSION") 29 | string(REGEX REPLACE ".*#define ZLIB_VERSION \"([0-9\.]+).*" "\\1" ZLIB_VERSION "${_zlib_version_line}") 30 | 31 | # Normally ZLIB::ZLIB is the shared library, 32 | # but this will force zstr to use the static library 33 | add_library(ZLIB::ZLIB ALIAS zlibstatic) 34 | else() 35 | message(WARNING "Failed to download zlib") 36 | return() 37 | endif() 38 | endif() 39 | 40 | if(TARGET zlibstatic) 41 | # Ensure any future calls to find_package(ZLIB) find the zlib 42 | # found by this module 43 | get_target_property(ZLIB_ROOT zlibstatic SOURCE_DIR) 44 | endif() 45 | 46 | include(FindPackageHandleStandardArgs) 47 | find_package_handle_standard_args(ZLIB 48 | REQUIRED_VARS ZLIB_LIBRARY ZLIB_INCLUDE_DIRS 49 | VERSION_VAR ZLIB_VERSION 50 | NAME_MISMATCHED 51 | ) 52 | -------------------------------------------------------------------------------- /cmake/modules/Findgcem.cmake: -------------------------------------------------------------------------------- 1 | # Findgcem.cmake 2 | 3 | find_package(gcem CONFIG QUIET) 4 | 5 | if(NOT gcem_FOUND) 6 | message(STATUS "Downloading gcem using FetchContent") 7 | 8 | include(FetchContent) 9 | FetchContent_Declare( 10 | gcemGitRepo 11 | GIT_REPOSITORY "https://github.com/kthohr/gcem" 12 | GIT_TAG "master" 13 | ) 14 | FetchContent_MakeAvailable(gcemGitRepo) 15 | 16 | if(TARGET gcem) 17 | set(gcem_FOUND TRUE) 18 | endif() 19 | endif() 20 | 21 | get_target_property(gcem_INCLUDE_DIR gcem INTERFACE_INCLUDE_DIRECTORIES) 22 | 23 | include(FindPackageHandleStandardArgs) 24 | find_package_handle_standard_args(gcem 25 | REQUIRED_VARS gcem_INCLUDE_DIR 26 | VERSION_VAR gcem_VERSION 27 | ) 28 | -------------------------------------------------------------------------------- /cmake/toolchains/wasm32-emscripten-vcpkg.cmake: -------------------------------------------------------------------------------- 1 | if(DEFINED ENV{VCPKG_ROOT}) 2 | set(VCPKG_ROOT "$ENV{VCPKG_ROOT}") 3 | elseif(DEFINED ENV{VCPKG_INSTALLATION_ROOT}) 4 | set(VCPKG_ROOT "$ENV{VCPKG_INSTALLATION_ROOT}") 5 | else() 6 | message(FATAL_ERROR "Please set the VCPKG_ROOT environment variable") 7 | endif() 8 | 9 | if(DEFINED ENV{EMSDK}) 10 | set(EMSDK "$ENV{EMSDK}") 11 | else() 12 | message(FATAL_ERROR "Please set the EMSDK environment variable to the Emscripten SDK path") 13 | endif() 14 | 15 | include(${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake) 16 | 17 | set(VCPKG_TARGET_TRIPLET wasm32-emscripten) 18 | include(${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) 19 | -------------------------------------------------------------------------------- /console/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(dmf2mod_console) 2 | 3 | add_executable(${PROJECT_NAME} console.cpp) 4 | target_link_libraries(${PROJECT_NAME} dmf2mod) 5 | 6 | set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME dmf2mod) 7 | set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") 8 | -------------------------------------------------------------------------------- /console/console.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * console.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * Cross-platform command-line frontend for the dmf2mod core. 6 | * 7 | * Usage: 8 | * dmf2mod output.[ext] input.[ext] [options] 9 | * dmf2mod [ext] input.[ext] [options] 10 | * dmf2mod [option] 11 | */ 12 | 13 | #include "dmf2mod.h" 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | using namespace d2m; 20 | 21 | namespace { 22 | 23 | // Used for returning input/output info when parsing command-line arguments 24 | struct InputOutput 25 | { 26 | std::string input_file; 27 | ModuleType input_type; 28 | std::string output_file; 29 | ModuleType output_type; 30 | }; 31 | 32 | enum class OperationType 33 | { 34 | kError, 35 | kInfo, 36 | kConversion 37 | }; 38 | 39 | auto ParseArgs(std::vector& args, InputOutput& io) -> OperationType; 40 | void PrintHelp(std::string_view executable, ModuleType module_type); 41 | 42 | } // namespace 43 | 44 | auto main(int argc, char** argv) -> int 45 | { 46 | auto args = Utils::GetArgsAsVector(argc, argv); 47 | 48 | InputOutput io; 49 | auto operation_type = ParseArgs(args, io); 50 | if (operation_type == OperationType::kError) { return 1; } 51 | 52 | // A help message was printed or some other action that doesn't require conversion 53 | if (operation_type == OperationType::kInfo) { return 0; } 54 | 55 | auto options = Factory::Create(io.output_type); 56 | if (!options) 57 | { 58 | std::cerr << "ERROR: Failed to create ConversionOptionsBase-derived object for the module type '" << Utils::GetFileExtension(io.output_file) 59 | << "'. The module may not be properly registered with dmf2mod.\n"; 60 | return 1; 61 | } 62 | 63 | if (!args.empty() && options->ParseArgs(args)) 64 | { 65 | // An error occurred while parsing the module-specific arguments 66 | return 1; 67 | } 68 | 69 | if (!args.empty()) 70 | { 71 | // All the arguments should have been consumed by this point but they aren't 72 | std::cerr << "ERROR: Unrecognized argument(s): "; 73 | for (unsigned i = 0; i < args.size(); i++) 74 | { 75 | std::cerr << args[i]; 76 | if (i + 1 != args.size()) { std::cerr << ", "; } 77 | } 78 | std::cerr << "\n"; 79 | return 1; 80 | } 81 | 82 | auto input = Factory::Create(io.input_type); 83 | if (!input) 84 | { 85 | std::cerr << "ERROR: Not enough memory.\n"; 86 | return 1; 87 | } 88 | 89 | ////////// IMPORT ////////// 90 | 91 | // Import the input file by inferring module type: 92 | input->Import(io.input_file); 93 | if (input->HandleResults()) { return 1; } 94 | 95 | ////////// CONVERT ////////// 96 | 97 | // Convert the input module to the output module type: 98 | auto output = input->Convert(io.output_type, options); 99 | if (!output) 100 | { 101 | std::cerr << "ERROR: Not enough memory or input and output types are the same.\n"; 102 | return 1; 103 | } 104 | 105 | if (output->HandleResults()) { return 1; } 106 | 107 | ////////// EXPORT ////////// 108 | 109 | // Export the converted module to disk: 110 | output->Export(io.output_file); 111 | if (output->HandleResults()) { return 1; } 112 | 113 | return 0; 114 | } 115 | 116 | namespace { 117 | 118 | auto ParseArgs(std::vector& args, InputOutput& io) -> OperationType 119 | { 120 | io.input_file.clear(); 121 | io.input_type = ModuleType::kNone; 122 | io.output_file.clear(); 123 | io.output_type = ModuleType::kNone; 124 | 125 | const auto arg_count = args.size(); 126 | 127 | if (arg_count == 1) 128 | { 129 | PrintHelp(args[0], ModuleType::kNone); 130 | return OperationType::kInfo; 131 | } 132 | else if (arg_count == 2) 133 | { 134 | if (args[1] == "--help") 135 | { 136 | PrintHelp(args[0], ModuleType::kNone); 137 | return OperationType::kInfo; 138 | } 139 | else if (args[1] == "-v" || args[1] == "--version") 140 | { 141 | std::cout << kVersion << "\n"; 142 | return OperationType::kInfo; 143 | } 144 | else 145 | { 146 | std::cerr << "ERROR: Could not parse command-line arguments.\n"; 147 | return OperationType::kError; 148 | } 149 | } 150 | else if (arg_count >= 3) // 3 is the minimum needed to perform a conversion 151 | { 152 | if (args[1] == "--help") 153 | { 154 | PrintHelp(args[0], Utils::GetTypeFromCommandName(args[2])); 155 | return OperationType::kInfo; 156 | } 157 | 158 | std::vector args_only_flags(args.begin() + 3, args.end()); 159 | 160 | if (GlobalOptions::Get().ParseArgs(args_only_flags, true)) 161 | { 162 | return OperationType::kError; 163 | } 164 | 165 | if (GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kVerbose).GetValue()) // If --verbose=true 166 | { 167 | const bool help_provided = GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kHelp).GetExplicitlyProvided(); 168 | if (help_provided) 169 | { 170 | std::cout << "Ignoring the \"--help\" command.\n"; 171 | } 172 | 173 | const bool version_provided = GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kVersion).GetExplicitlyProvided(); 174 | if (version_provided) 175 | { 176 | std::cout << "Ignoring the \"--version\" command.\n"; 177 | } 178 | } 179 | 180 | const bool force = GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kForce).GetValue(); 181 | 182 | // Get input file 183 | if (Utils::FileExists(args[2])) 184 | { 185 | io.input_type = Utils::GetTypeFromFilename(args[2]); 186 | if (io.input_type != ModuleType::kNone) 187 | { 188 | io.input_file = args[2]; 189 | } 190 | else 191 | { 192 | std::cerr << "ERROR: Input file type '" << Utils::GetFileExtension(args[2]) << "' is unsupported.\n"; 193 | return OperationType::kError; 194 | } 195 | } 196 | else 197 | { 198 | std::cerr << "ERROR: The input file '" << args[2] << "' could not be found.\n"; 199 | return OperationType::kError; 200 | } 201 | 202 | // Get output file 203 | if (Utils::GetFileExtension(args[1]).empty()) 204 | { 205 | io.output_type = Utils::GetTypeFromCommandName(args[1]); 206 | if (io.output_type != ModuleType::kNone) 207 | { 208 | const auto dot_pos = io.input_file.rfind('.'); 209 | if (dot_pos == 0 || dot_pos + 1 >= io.input_file.size()) 210 | { 211 | std::cerr << "ERROR: The input file is invalid.\n"; 212 | return OperationType::kError; 213 | } 214 | 215 | auto ext = Utils::GetExtensionFromType(io.output_type); 216 | if (ext.empty()) 217 | { 218 | std::cerr << "ERROR: The output type does not have a file extension defined.\n"; 219 | return OperationType::kError; 220 | } 221 | 222 | // Construct output filename from the input filename 223 | io.output_file = io.input_file.substr(0, dot_pos + 1) + std::string{ext}; 224 | } 225 | else 226 | { 227 | std::cerr << "ERROR: Output file type '" << args[1] << "' is unsupported.\n"; 228 | return OperationType::kError; 229 | } 230 | } 231 | else 232 | { 233 | io.output_file = args[1]; 234 | io.output_type = Utils::GetTypeFromFilename(args[1]); 235 | if (io.output_type == ModuleType::kNone) 236 | { 237 | std::cerr << "ERROR: '" << Utils::GetFileExtension(args[1]) << "' is not a valid module type.\n"; 238 | return OperationType::kError; 239 | } 240 | } 241 | 242 | if (Utils::FileExists(io.output_file) && !force) 243 | { 244 | std::cerr << "ERROR: The output file '" << io.output_file << "' already exists. Run with the '-f' flag to allow the file to be overwritten.\n"; 245 | return OperationType::kError; 246 | } 247 | 248 | if (io.input_type == io.output_type) 249 | { 250 | std::cout << "The output file is the same type as the input file. No conversion necessary.\n"; 251 | return OperationType::kError; 252 | } 253 | 254 | // TODO: Check if a conversion between the two types is possible 255 | 256 | // At this point, the input and output file arguments have been deemed valid 257 | 258 | // Remove executable, output file, and input file from the args list, since they've already been processed 259 | // What is left are module-specific command-line arguments 260 | args = args_only_flags; 261 | 262 | return OperationType::kConversion; 263 | } 264 | 265 | return OperationType::kError; 266 | } 267 | 268 | void PrintHelp(std::string_view executable, ModuleType module_type) 269 | { 270 | // If module-specific help was requested 271 | if (module_type != ModuleType::kNone) 272 | { 273 | ConversionOptions::PrintHelp(module_type); 274 | return; 275 | } 276 | 277 | // Else, print generic help 278 | 279 | std::cout.setf(std::ios_base::left); 280 | std::cout << "Usage: dmf2mod output.[ext] input.dmf [options]\n"; 281 | std::cout << std::setw(7) << "" << "dmf2mod" << " [ext] input.dmf [options]\n"; 282 | std::cout << std::setw(7) << "" << "dmf2mod" << " [option]\n"; 283 | 284 | std::cout << "Options:\n"; 285 | 286 | GlobalOptions::Get().GetDefinitions()->PrintHelp(); 287 | } 288 | 289 | } // namespace 290 | -------------------------------------------------------------------------------- /include/core/config_types.h: -------------------------------------------------------------------------------- 1 | /* 2 | * config_types.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines the main enum used by the factory 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace d2m { 11 | 12 | // Add all supported modules to this enum 13 | enum class ModuleType 14 | { 15 | kNone = 0, 16 | kDMF, 17 | kMOD, 18 | #ifndef NDEBUG 19 | kDebug, 20 | #endif 21 | }; 22 | 23 | } // namespace d2m 24 | -------------------------------------------------------------------------------- /include/core/conversion_options.h: -------------------------------------------------------------------------------- 1 | /* 2 | * conversion_options.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines an interface for conversion options. 6 | * All conversion options classes must inherit ConversionOptionsInterface. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "core/factory.h" 12 | #include "core/options.h" 13 | #include "utils/utils.h" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace d2m { 23 | 24 | // Forward declares 25 | class ConversionOptionsBase; 26 | template class ConversionOptionsInterface; 27 | 28 | 29 | // Specialized Info class for ConversionOptionsBase-derived classes 30 | template<> 31 | struct Info : public InfoBase 32 | { 33 | OptionDefinitionCollection option_definitions; 34 | }; 35 | 36 | 37 | // Base class for conversion options 38 | class ConversionOptionsBase : public OptionCollection, public EnableReflection, public std::enable_shared_from_this 39 | { 40 | protected: 41 | // See ConversionOptionsInterface constructor 42 | ConversionOptionsBase() = default; 43 | 44 | public: 45 | virtual ~ConversionOptionsBase() = default; 46 | 47 | /* 48 | * Cast ConversionOptionsPtr to std::shared_ptr where T is the derived ConversionOptions class 49 | */ 50 | template, bool> = true> 51 | auto Cast() const -> std::shared_ptr 52 | { 53 | return std::static_pointer_cast(shared_from_this()); 54 | } 55 | 56 | /* 57 | * Get the filename of the output file. Returns empty string if error occurred. 58 | */ 59 | auto GetOutputFilename() const -> std::string { return output_file_; } 60 | 61 | /* 62 | * Prints help message for this module's options 63 | */ 64 | virtual void PrintHelp() const = 0; 65 | 66 | /* 67 | * Prints help message for the options of the given module type 68 | */ 69 | static void PrintHelp(ModuleType module_type); 70 | 71 | protected: 72 | std::string output_file_; 73 | }; 74 | 75 | 76 | // All conversion options classes must inherit this using CRTP 77 | template 78 | class ConversionOptionsInterface : public EnableFactory 79 | { 80 | protected: 81 | ConversionOptionsInterface() 82 | { 83 | OptionCollection::SetDefinitions(&(this->GetInfo()->option_definitions)); 84 | } 85 | 86 | void PrintHelp() const override 87 | { 88 | ConversionOptionsBase::PrintHelp(this->GetType()); 89 | } 90 | 91 | public: 92 | ConversionOptionsInterface(const ConversionOptionsInterface&) = delete; 93 | ConversionOptionsInterface(ConversionOptionsInterface&&) noexcept = delete; 94 | virtual ~ConversionOptionsInterface() = default; 95 | }; 96 | 97 | } // namespace d2m 98 | -------------------------------------------------------------------------------- /include/core/data.h: -------------------------------------------------------------------------------- 1 | /* 2 | * data.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines a class template for storing and accessing module data (orders, patterns, rows, etc.) 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/note.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace d2m { 18 | 19 | using OrderIndex = std::uint16_t; 20 | using PatternIndex = std::uint16_t; 21 | using ChannelIndex = std::uint8_t; 22 | using RowIndex = std::uint16_t; 23 | 24 | enum class DataStorageType 25 | { 26 | kNone, 27 | kCOR, // Iteration order: Channels --> Orders --> (Pattern) Rows 28 | kORC, // Iteration order: Orders --> (Pattern) Rows --> Channels 29 | }; 30 | 31 | /* 32 | * Global data for a module. This is information such as the title and author. 33 | * Can be customized if a module type has more global information to be stored. 34 | */ 35 | 36 | template 37 | struct ModuleGlobalDataDefault 38 | { 39 | static constexpr DataStorageType storage_type = data_storage_type; 40 | std::string title; 41 | std::string author; 42 | }; 43 | 44 | template 45 | struct ModuleGlobalData : public ModuleGlobalDataDefault<> {}; 46 | 47 | /* 48 | * Different modules have significantly different per-channel row contents, so 49 | * providing one single generic implementation for use by every module doesn't 50 | * make much sense. Each module should provide their own Row implementation. 51 | */ 52 | 53 | struct RowDefault 54 | { 55 | NoteSlot note; 56 | }; 57 | 58 | template 59 | struct Row : public RowDefault {}; 60 | 61 | /* 62 | * Some module formats contain additional data for each channel or row. 63 | * Specializations for ChannelMetadata and PatternMetadata can be created 64 | * for any module format which requires it. 65 | */ 66 | 67 | struct ChannelMetadataDefault 68 | { 69 | // No defaults at this time 70 | }; 71 | 72 | template 73 | struct ChannelMetadata : public ChannelMetadataDefault {}; 74 | 75 | struct PatternMetadataDefault 76 | { 77 | // No defaults at this time 78 | }; 79 | 80 | template 81 | struct PatternMetadata : public PatternMetadataDefault {}; 82 | 83 | 84 | namespace detail { 85 | /* 86 | * The class templates below allow different module formats in dmf2mod to choose an underlying 87 | * storage data structure that works best for them while maintaining a common interface to 88 | * that data. 89 | * 90 | * The motivation for this is that different module formats store their pattern matrix and 91 | * pattern data differently, so this affects the kind of C++ data structures their module data 92 | * more naturally maps to. If a data structure that maps more naturally to the module's data is used, 93 | * better performance should be possible when iterating through the data while importing/exporting. 94 | * Additionally, some formats such as MOD do not have per-channel patterns, meaning that all channels 95 | * are essentially linked together as far as patterns are concerned. This necessitates a flexible 96 | * way of storing module data which can work for any module format. 97 | */ 98 | 99 | class ModuleDataStorageBase 100 | { 101 | public: 102 | virtual ~ModuleDataStorageBase() = default; 103 | auto GetNumChannels() const -> ChannelIndex { return num_channels_; } 104 | auto GetNumOrders() const -> OrderIndex { return num_orders_; } 105 | auto GetNumRows() const -> RowIndex { return num_rows_; } 106 | protected: 107 | virtual void CleanUpData() = 0; 108 | virtual void SetPatternMatrix(ChannelIndex channels, OrderIndex orders, RowIndex rows) = 0; 109 | virtual void SetNumPatterns() = 0; 110 | virtual void SetPatterns() = 0; 111 | ChannelIndex num_channels_ = 0; 112 | OrderIndex num_orders_ = 0; // Total orders (pattern matrix rows) 113 | RowIndex num_rows_ = 0; // Rows per pattern 114 | }; 115 | 116 | //! Primary template 117 | template 118 | class ModuleDataStorage; 119 | 120 | template 121 | class ModuleDataStorage : public ModuleDataStorageBase 122 | { 123 | public: 124 | using RowType = Row; 125 | using PatternType = RowType*; // [row] 126 | 127 | using PatternMatrixType = std::vector>; // [channel][order] 128 | using NumPatternsType = std::vector; // [channel] 129 | using PatternStorageType = std::vector; // [channel][pattern id] 130 | using PatternMetadataType = PatternMetadata; 131 | using PatternMetadataStorageType = std::vector>; // [channel][pattern id] 132 | 133 | auto GetPatternId(ChannelIndex channel, OrderIndex order) const -> PatternIndex { return pattern_matrix_[channel][order]; } 134 | void SetPatternId(ChannelIndex channel, OrderIndex order, PatternIndex pattern_id) { pattern_matrix_[channel][order] = pattern_id; } 135 | auto GetNumPatterns(ChannelIndex channel) const -> PatternIndex { return num_patterns_[channel]; } 136 | void SetNumPatterns(ChannelIndex channel, PatternIndex num_patterns) { num_patterns_[channel] = num_patterns; } 137 | auto GetPattern(ChannelIndex channel, OrderIndex order) const -> PatternType { return patterns_[channel][GetPatternId(channel, order)]; } 138 | void SetPattern(ChannelIndex channel, OrderIndex order, PatternType&& pattern) { patterns_[channel][GetPatternId(channel, order)] = std::move(pattern); } // TODO: Deep copy? 139 | auto GetPatternById(ChannelIndex channel, PatternIndex pattern_id) const -> PatternType { return patterns_[channel][pattern_id]; } 140 | void SetPatternById(ChannelIndex channel, PatternIndex pattern_id, PatternType&& pattern) { patterns_[channel][pattern_id] = std::move(pattern); } // TODO: Deep copy? 141 | auto GetRow(ChannelIndex channel, OrderIndex order, RowIndex row) const -> const RowType& { return GetPattern(channel, order)[row]; } 142 | void SetRow(ChannelIndex channel, OrderIndex order, RowIndex row, const RowType& row_value) { GetPattern(channel, order)[row] = row_value; } 143 | auto GetRowById(ChannelIndex channel, PatternIndex pattern_id, RowIndex row) const -> const RowType& { return GetPatternById(channel, pattern_id)[row]; } 144 | void SetRowById(ChannelIndex channel, PatternIndex pattern_id, RowIndex row, const RowType& row_value) { GetPatternById(channel, pattern_id)[row] = row_value; } 145 | auto GetPatternMetadata(ChannelIndex channel, PatternIndex pattern_id) const -> const PatternMetadataType& { return pattern_metadata_[channel][pattern_id]; } 146 | void SetPatternMetadata(ChannelIndex channel, PatternIndex pattern_id, const PatternMetadataType& pattern_metadata) { pattern_metadata_[channel][pattern_id] = pattern_metadata; } 147 | 148 | protected: 149 | ModuleDataStorage() = default; 150 | ~ModuleDataStorage() override { CleanUpData(); } 151 | 152 | void CleanUpData() override 153 | { 154 | if (!num_patterns_.empty()) 155 | { 156 | ChannelIndex channel = 0; 157 | for (const auto& num_patterns : num_patterns_) 158 | { 159 | for (PatternIndex pattern_id = 0; pattern_id < num_patterns; ++pattern_id) 160 | { 161 | delete[] patterns_[channel][pattern_id]; 162 | patterns_[channel][pattern_id] = nullptr; 163 | } 164 | delete[] patterns_[channel]; 165 | patterns_[channel] = nullptr; 166 | ++channel; 167 | } 168 | patterns_.clear(); 169 | } 170 | pattern_matrix_.clear(); 171 | num_patterns_.clear(); 172 | pattern_metadata_.clear(); 173 | num_channels_ = 0; 174 | num_orders_ = 0; 175 | num_rows_ = 0; 176 | } 177 | 178 | void SetPatternMatrix(ChannelIndex channels, OrderIndex orders, RowIndex rows) override 179 | { 180 | CleanUpData(); 181 | 182 | num_channels_ = channels; 183 | num_orders_ = orders; 184 | num_rows_ = rows; 185 | 186 | pattern_matrix_.resize(num_channels_); 187 | 188 | for (ChannelIndex channel = 0; channel < num_channels_; ++channel) 189 | { 190 | pattern_matrix_[channel].resize(num_orders_); 191 | } 192 | } 193 | 194 | void SetNumPatterns() override 195 | { 196 | num_patterns_.resize(num_channels_); 197 | 198 | for (ChannelIndex channel = 0; channel < num_channels_; ++channel) 199 | { 200 | num_patterns_[channel] = *std::max_element(pattern_matrix_[channel].begin(), pattern_matrix_[channel].end()) + 1; 201 | } 202 | } 203 | 204 | void SetPatterns() override 205 | { 206 | patterns_.resize(num_channels_); 207 | if constexpr (!std::is_empty_v) 208 | { 209 | // Only set it if it's going to be used 210 | pattern_metadata_.resize(num_channels_); 211 | } 212 | 213 | for (ChannelIndex channel = 0; channel < num_channels_; ++channel) 214 | { 215 | const PatternIndex num_patterns = num_patterns_[channel]; 216 | patterns_[channel] = new PatternType[num_patterns]; 217 | 218 | for (PatternIndex pattern_id = 0; pattern_id < num_patterns; ++pattern_id) 219 | { 220 | patterns_[channel][pattern_id] = new RowType[num_rows_](); 221 | if constexpr (!std::is_empty_v) 222 | { 223 | // Only set it if it's going to be used 224 | pattern_metadata_[channel].resize(num_patterns); 225 | } 226 | } 227 | } 228 | } 229 | 230 | PatternMatrixType pattern_matrix_{}; // Stores patterns IDs for each channel and order in the pattern matrix 231 | NumPatternsType num_patterns_{}; // Patterns per channel 232 | PatternStorageType patterns_{}; // [channel][pattern id] 233 | PatternMetadataStorageType pattern_metadata_{}; // [channel][pattern id] 234 | }; 235 | 236 | template 237 | class ModuleDataStorage : public ModuleDataStorageBase 238 | { 239 | public: 240 | using RowType = Row; 241 | using PatternType = RowType**; // [row][channel] 242 | 243 | using PatternMatrixType = std::vector; // [order] (No per-channel patterns) 244 | using NumPatternsType = PatternIndex; // (No per-channel patterns) 245 | using PatternStorageType = std::vector; // [order] 246 | using PatternMetadataType = PatternMetadata; 247 | using PatternMetadataStorageType = std::vector; // [pattern id] (No per-channel patterns) 248 | 249 | auto GetPatternId(OrderIndex order) const -> PatternIndex { return pattern_matrix_[order]; } 250 | void SetPatternId(OrderIndex order, PatternIndex pattern_id) { pattern_matrix_[order] = pattern_id; } 251 | auto GetNumPatterns() const -> PatternIndex { return num_patterns_; } 252 | void SetNumPatterns(PatternIndex num_patterns) { num_patterns_ = num_patterns; } 253 | auto GetPattern(OrderIndex order) const -> PatternType { return patterns_[GetPatternId(order)]; } 254 | void SetPattern(OrderIndex order, PatternType&& pattern) { patterns_[GetPatternId(order)] = std::move(pattern); } // TODO: Deep copy? 255 | auto GetPatternById(PatternIndex pattern_id) const -> PatternType { return patterns_[pattern_id]; } 256 | void SetPatternById(PatternIndex pattern_id, PatternType&& pattern) { patterns_[pattern_id] = std::move(pattern); } // TODO: Deep copy? 257 | auto GetRow(ChannelIndex channel, OrderIndex order, RowIndex row) const -> const RowType& { return GetPattern(order)[row][channel]; } 258 | void SetRow(ChannelIndex channel, OrderIndex order, RowIndex row, const RowType& row_value) { GetPattern(order)[row][channel] = row_value; } 259 | auto GetRowById(ChannelIndex channel, PatternIndex pattern_id, RowIndex row) const -> const RowType& { return GetPatternById(pattern_id)[row][channel]; } 260 | void SetRowById(ChannelIndex channel, PatternIndex pattern_id, RowIndex row, const RowType& row_value) { GetPatternById(pattern_id)[row][channel] = row_value; } 261 | auto GetPatternMetadata(PatternIndex pattern_id) const -> const PatternMetadataType& { return pattern_metadata_[pattern_id]; } 262 | void SetPatternMetadata(PatternIndex pattern_id, const PatternMetadataType& pattern_metadata) { pattern_metadata_[pattern_id] = pattern_metadata; } 263 | 264 | protected: 265 | ModuleDataStorage() = default; 266 | ~ModuleDataStorage() override { CleanUpData(); } 267 | 268 | void CleanUpData() override 269 | { 270 | for (PatternIndex pattern_id = 0; pattern_id < num_patterns_; ++pattern_id) 271 | { 272 | for (RowIndex row = 0; row < num_rows_; ++row) 273 | { 274 | delete[] patterns_[pattern_id][row]; 275 | patterns_[pattern_id][row] = nullptr; 276 | } 277 | delete[] patterns_[pattern_id]; 278 | patterns_[pattern_id] = nullptr; 279 | } 280 | patterns_.clear(); 281 | 282 | pattern_matrix_.clear(); 283 | num_patterns_ = 0; 284 | pattern_metadata_.clear(); 285 | num_channels_ = 0; 286 | num_orders_ = 0; 287 | num_rows_ = 0; 288 | } 289 | 290 | void SetPatternMatrix(ChannelIndex channels, OrderIndex orders, RowIndex rows) override 291 | { 292 | CleanUpData(); 293 | num_channels_ = channels; 294 | num_orders_ = orders; 295 | num_rows_ = rows; 296 | pattern_matrix_.resize(num_orders_); 297 | } 298 | 299 | void SetNumPatterns() override { num_patterns_ = *std::max_element(pattern_matrix_.begin(), pattern_matrix_.end()) + 1; } 300 | 301 | void SetPatterns() override 302 | { 303 | patterns_.resize(num_patterns_); 304 | if constexpr (!std::is_empty_v) 305 | { 306 | // Only set it if it's going to be used 307 | pattern_metadata_.resize(num_patterns_); 308 | } 309 | 310 | for (PatternIndex pattern_id = 0; pattern_id < num_patterns_; ++pattern_id) 311 | { 312 | patterns_[pattern_id] = new RowType*[num_rows_]; 313 | for (RowIndex row = 0; row < num_rows_; ++row) 314 | { 315 | patterns_[pattern_id][row] = new RowType[num_channels_](); 316 | } 317 | } 318 | } 319 | 320 | PatternMatrixType pattern_matrix_{}; // Stores patterns IDs for each order in the pattern matrix 321 | NumPatternsType num_patterns_{}; // Number of patterns 322 | PatternStorageType patterns_{}; // [pattern id] 323 | PatternMetadataStorageType pattern_metadata_{}; // [pattern id] 324 | }; 325 | 326 | /* 327 | * Define additional ModuleDataStorage specializations here as needed 328 | */ 329 | } // namespace detail 330 | 331 | /* 332 | * ModuleData stores and provides access to song data such as 333 | * orders, patterns, rows, and other information. 334 | */ 335 | 336 | template 337 | class ModuleData final : public detail::ModuleDataStorage::storage_type, ModuleClass> 338 | { 339 | public: 340 | using RowType = Row; 341 | using ChannelMetadataType = ChannelMetadata; 342 | using PatternMetadataType = PatternMetadata; 343 | using GlobalDataType = ModuleGlobalData; 344 | 345 | static constexpr DataStorageType storage_type = GlobalDataType::storage_type; 346 | static_assert(storage_type != DataStorageType::kNone, 347 | "Storage type must be defined for ModuleData through the ModuleGlobalData struct"); 348 | 349 | using Storage = typename detail::ModuleDataStorage; 350 | 351 | ModuleData() = default; 352 | ~ModuleData() override { CleanUp(); } 353 | 354 | // This is the 1st initialization method to call 355 | void AllocatePatternMatrix(ChannelIndex channels, OrderIndex orders, RowIndex rows) 356 | { 357 | Storage::SetPatternMatrix(channels, orders, rows); 358 | } 359 | 360 | // This is the 2nd initialization method to call 361 | void AllocateChannels() 362 | { 363 | // Call this after all the pattern IDs are set 364 | Storage::SetNumPatterns(); 365 | 366 | if constexpr (!std::is_empty_v) 367 | { 368 | // Only set it if it's going to be used 369 | channel_metadata_.resize(Storage::num_channels_); 370 | } 371 | } 372 | 373 | // This is the 3rd and final initialization method to call 374 | void AllocatePatterns() 375 | { 376 | Storage::SetPatterns(); 377 | } 378 | 379 | /////// DIRECT ACCESS GETTERS /////// 380 | 381 | auto PatternMatrixRef() const -> const typename Storage::PatternMatrixType& { return Storage::pattern_matrix_; } 382 | auto PatternMatrixRef() -> typename Storage::PatternMatrixType& { return Storage::pattern_matrix_; } 383 | auto NumPatternsRef() const -> const typename Storage::NumPatternsType& { return Storage::num_patterns_; } 384 | auto NumPatternsRef() -> typename Storage::NumPatternsType& { return Storage::num_patterns_; } 385 | auto PatternsRef() const -> const typename Storage::PatternStorageType& { return Storage::patterns_; } 386 | auto PatternsRef() -> typename Storage::PatternStorageType& { return Storage::patterns_; } 387 | auto PatternMetadataRef() const -> const typename Storage::PatternMetadataStorageType& { return Storage::pattern_metadata_; } 388 | auto PatternMetadataRef() -> typename Storage::PatternMetadataStorageType& { return Storage::pattern_metadata_; } 389 | auto ChannelMetadataRef() const -> const std::vector& { return channel_metadata_; } 390 | auto ChannelMetadataRef() -> std::vector& { return channel_metadata_; } 391 | auto GlobalData() const -> const GlobalDataType& { return global_data_; } 392 | auto GlobalData() -> GlobalDataType& { return global_data_; } 393 | 394 | /////// GETTERS / SETTERS /////// 395 | 396 | auto GetChannelMetadata(ChannelIndex channel) const -> const ChannelMetadataType& { return channel_metadata_[channel]; } 397 | void SetChannelMetadata(ChannelIndex channel, const ChannelMetadataType& channelMetadata) { channel_metadata_[channel] = channelMetadata; } 398 | 399 | static constexpr auto GetStorageType() -> DataStorageType { return storage_type; } 400 | 401 | /////// Other 402 | 403 | void CleanUp() 404 | { 405 | Storage::CleanUpData(); 406 | channel_metadata_.clear(); 407 | global_data_ = {}; 408 | } 409 | 410 | private: 411 | 412 | // Metadata (optional module-specific info) 413 | std::vector channel_metadata_; // [channel] 414 | 415 | // Global information about a particular module file 416 | GlobalDataType global_data_; 417 | }; 418 | 419 | } // namespace d2m 420 | -------------------------------------------------------------------------------- /include/core/effects.h: -------------------------------------------------------------------------------- 1 | /* 2 | * effects.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines effects which are common to many module file types 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace d2m { 13 | 14 | using EffectCode = std::int8_t; 15 | using EffectValue = std::int16_t; 16 | 17 | struct Effect 18 | { 19 | EffectCode code; 20 | EffectValue value; 21 | }; 22 | 23 | constexpr auto operator==(const Effect& lhs, const Effect& rhs) -> bool 24 | { 25 | return lhs.code == rhs.code && lhs.value == rhs.value; 26 | } 27 | 28 | /** 29 | * In Module data storage, this may be used, but in generated data, 30 | * only the effects which actually do something should be present 31 | */ 32 | inline constexpr EffectValue kEffectValueless = -1; 33 | 34 | // Defines common effects used by multiple module types 35 | namespace Effects 36 | { 37 | enum 38 | { 39 | // All common effects are less than 0 40 | kNoEffect = 0, 41 | kArp = -1, 42 | kPortUp = -2, 43 | kPortDown = -3, 44 | kPort2Note = -4, 45 | kVibrato = -5, 46 | kPort2NoteVolSlide = -6, 47 | kVibratoVolSlide = -7, 48 | kTremolo = -8, 49 | kPanning = -9, 50 | kSpeedA = -10, 51 | kVolSlide = -11, 52 | kPosJump = -12, 53 | kRetrigger = -13, 54 | kPatBreak = -14, 55 | kNoteCut = -15, 56 | kNoteDelay = -16, 57 | kTempo = -17, 58 | kSpeedB = -18, 59 | }; 60 | // Modules should implement effects specific to them using positive values starting with 1. 61 | } 62 | 63 | } // namespace d2m 64 | -------------------------------------------------------------------------------- /include/core/factory.h: -------------------------------------------------------------------------------- 1 | /* 2 | * factory.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Implementation of the abstract factory pattern. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/config_types.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace d2m { 25 | 26 | // Forward declares 27 | template class Factory; 28 | 29 | namespace detail { 30 | struct EnableFactoryBase {}; 31 | struct EnableReflectionBase {}; 32 | 33 | template constexpr bool factory_enabled_v = std::is_base_of_v; 34 | template constexpr bool reflection_enabled_v = std::is_base_of_v; 35 | } // namespace detail 36 | 37 | template 38 | class BuilderBase 39 | { 40 | public: 41 | // Only the factory can use BuilderBase 42 | friend class Factory; 43 | 44 | virtual ~BuilderBase() = default; 45 | 46 | protected: 47 | BuilderBase() = default; 48 | virtual auto Build() const -> std::shared_ptr = 0; 49 | }; 50 | 51 | /** 52 | * Builds an instance of a factory-enabled class. 53 | * 54 | * Derived must have a default constructor accessible by this class and also must have a public destructor. 55 | * Can specialize this, but it must inherit from BuilderBase and Factory must have access to its members. 56 | */ 57 | template 58 | class Builder : public BuilderBase 59 | { 60 | // Only the factory can use Builder 61 | friend class Factory; 62 | 63 | //! Default constructs an object of Derived wrapped in a shared_ptr 64 | auto Build() const -> std::shared_ptr override { return std::shared_ptr(new Derived{}); }; 65 | }; 66 | 67 | 68 | struct InfoBase 69 | { 70 | ModuleType type = ModuleType::kNone; 71 | }; 72 | 73 | /** 74 | * Static data for a factory-enabled class. 75 | * Can specialize this, but it must inherit from InfoBase. 76 | */ 77 | template 78 | struct Info : public InfoBase {}; 79 | 80 | 81 | template 82 | class Factory 83 | { 84 | private: 85 | ~Factory() { Clear(); } 86 | 87 | struct InitializeImpl 88 | { 89 | // Note: Factories will have to implement this. 90 | InitializeImpl(); 91 | }; 92 | 93 | /* 94 | * Initialize must be called at the start of every public Factory method to 95 | * ensure lazy initialization of the Factory whenever it is first used. 96 | */ 97 | static auto Initialize() -> bool 98 | { 99 | if (!initialized_) 100 | { 101 | Clear(); 102 | // This gets around static initialization ordering issues: 103 | [[maybe_unused]] auto init = std::make_unique(); 104 | initialized_ = true; 105 | } 106 | return true; 107 | } 108 | 109 | public: 110 | Factory() = delete; 111 | Factory(const Factory&) = delete; 112 | Factory(Factory&&) noexcept = delete; 113 | auto operator=(const Factory&) -> Factory& = delete; 114 | auto operator=(Factory&&) noexcept -> Factory& = delete; 115 | 116 | static auto Create(ModuleType module_type) -> std::shared_ptr 117 | { 118 | [[maybe_unused]] static bool init = Initialize(); 119 | if (builders_.find(module_type) != builders_.end()) { return builders_.at(module_type)->Build(); } 120 | assert(false && "Factory is not initialized for Base."); 121 | return nullptr; 122 | } 123 | 124 | static auto GetInfo(ModuleType module_type) -> const Info* 125 | { 126 | [[maybe_unused]] static bool init = Initialize(); 127 | const auto it = info_.find(module_type); 128 | if (it != info_.end()) { return it->second.get(); } 129 | assert(false && "Factory is not initialized for Base."); 130 | return nullptr; 131 | } 132 | 133 | template, bool> = true> 134 | static auto Create() -> std::shared_ptr 135 | { 136 | // Initialize() not needed here because GetEnumFromType calls it 137 | static ModuleType module_type = GetEnumFromType(); 138 | return std::static_pointer_cast(Create(module_type)); 139 | } 140 | 141 | template, bool> = true> 142 | static auto GetInfo() -> const Info* 143 | { 144 | // Initialize() not needed here because GetEnumFromType calls it 145 | static ModuleType module_type = GetEnumFromType(); 146 | return GetInfo(module_type); 147 | } 148 | 149 | static auto TypeInfo() -> const std::map>>& 150 | { 151 | [[maybe_unused]] static bool init = Initialize(); 152 | return info_; 153 | } 154 | 155 | static auto GetInitializedTypes() -> std::vector 156 | { 157 | [[maybe_unused]] static bool init = Initialize(); 158 | std::vector vec; 159 | for (const auto& map_pair : builders_) 160 | { 161 | vec.push_back(map_pair.first); 162 | } 163 | return vec; 164 | } 165 | 166 | template, bool> = true> 167 | static auto GetEnumFromType() -> ModuleType 168 | { 169 | [[maybe_unused]] static bool init = Initialize(); 170 | const auto& type = std::type_index(typeid(Type)); 171 | const auto it = type_to_enum_.find(type); 172 | if (it != type_to_enum_.end()) { return it->second; } 173 | assert(false && "Factory is not initialized for Type."); 174 | return ModuleType::kNone; 175 | } 176 | 177 | private: 178 | template 179 | static void Register() 180 | { 181 | static_assert(detail::factory_enabled_v, 182 | "Cannot register a class which does not inherit from EnableFactory"); 183 | static_assert(std::is_base_of_v>, "Info must derive from InfoBase"); 184 | static_assert(std::is_base_of_v, Builder>, 185 | "Builder must derive from BuilderBase"); 186 | 187 | builders_[module_type] = std::make_unique>(); 188 | auto temp = std::make_unique>(); 189 | temp->type = module_type; 190 | info_[module_type] = std::move(temp); 191 | type_to_enum_[std::type_index(typeid(Type))] = module_type; 192 | } 193 | 194 | template 195 | static void Register(Info&& info) 196 | { 197 | static_assert(detail::factory_enabled_v, 198 | "Cannot register a class which does not inherit from EnableFactory"); 199 | static_assert(std::is_base_of_v>, "Info must derive from InfoBase"); 200 | static_assert(std::is_base_of_v, Builder>, 201 | "Builder must derive from BuilderBase"); 202 | 203 | const ModuleType module_type = info.type; 204 | 205 | builders_[module_type] = std::make_unique>(); 206 | info_[module_type] = std::make_unique>(std::move(info)); 207 | type_to_enum_[std::type_index(typeid(Type))] = module_type; 208 | } 209 | 210 | template 211 | static void Register(Args&&... info_args) 212 | { 213 | Register(Info{InfoBase{module_type}, std::forward(info_args)...}); 214 | } 215 | 216 | static void Clear() 217 | { 218 | builders_.clear(); 219 | info_.clear(); 220 | initialized_ = false; 221 | } 222 | 223 | static std::map>> builders_; 224 | static std::map>> info_; 225 | static std::map type_to_enum_; 226 | static bool initialized_; 227 | }; 228 | 229 | 230 | // Initialize static members 231 | template std::map>> Factory::builders_{}; 232 | template std::map>> Factory::info_{}; 233 | template std::map Factory::type_to_enum_{}; 234 | template bool Factory::initialized_ = false; 235 | 236 | 237 | // If a base inherits this using CRTP, derived classes will have knowledge about themselves after Factory initialization 238 | template 239 | struct EnableReflection : public detail::EnableReflectionBase 240 | { 241 | virtual auto GetType() const -> ModuleType = 0; 242 | virtual auto GetInfo() const -> const Info* = 0; 243 | }; 244 | 245 | 246 | /** 247 | * If a base class inherits from EnableReflection, EnableReflection will declare pure virtual methods which must be implemented 248 | * in a derived class. One of the derived classes will be EnableFactory. EnableFactory must know whether to implement the pure 249 | * virtual methods or not, and this depends on whether the base class inherits from EnableReflection. In order to conditionally 250 | * implement these methods, EnableFactory uses std::conditional_t to inherit from either Base or ReflectionImpl. 251 | * ReflectionImpl implements the pure virtual methods. 252 | */ 253 | template 254 | struct ReflectionImpl : public Base 255 | { 256 | auto GetType() const -> ModuleType final 257 | { 258 | static const ModuleType module_type = Factory::template GetEnumFromType(); 259 | return module_type; 260 | } 261 | 262 | auto GetInfo() const -> const Info* final 263 | { 264 | return Factory::GetInfo(GetType()); 265 | } 266 | }; 267 | 268 | 269 | //! Inherit this class using CRTP to enable factory for any class 270 | template 271 | struct EnableFactory 272 | : public detail::EnableFactoryBase 273 | , public std::conditional_t, ReflectionImpl, Base> 274 | { 275 | static_assert(std::is_base_of_v>, "Info must inherit from InfoBase"); 276 | static_assert(std::is_base_of_v, Builder>, 277 | "Builder must inherit from BuilderBase"); 278 | }; 279 | 280 | } // namespace d2m 281 | -------------------------------------------------------------------------------- /include/core/generated_data.h: -------------------------------------------------------------------------------- 1 | /* 2 | * generated_data.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines GeneratedData and related class templates 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/data.h" 11 | #include "core/module_base.h" 12 | #include "core/note.h" 13 | #include "core/state.h" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace d2m { 22 | 23 | namespace detail { 24 | 25 | struct GenDataDefinitionTag {}; 26 | 27 | template 28 | struct WrappedGenData {}; 29 | 30 | template 31 | struct WrappedGenData> 32 | { 33 | // type is either an empty tuple or a tuple with each Ts wrapped in an optional 34 | using type = std::conditional_t, std::tuple...>>; 35 | }; 36 | 37 | template 38 | using WrappedGenDataType = typename WrappedGenData::type; 39 | 40 | } // namespace detail 41 | 42 | /////////////////////////////////////////////////////////// 43 | // COMMON GENERATED DATA TYPES 44 | /////////////////////////////////////////////////////////// 45 | 46 | using TotalOrdersGenData = OrderIndex; 47 | using NoteOffUsedGenData = bool; 48 | using ChannelNoteExtremesGenData = std::map>; 49 | template using SoundIndexNoteExtremesGenData = std::map::type, std::pair>; 50 | template using SoundIndexesUsedGenData = std::set::type>; 51 | template using StateGenData = ModuleState; 52 | 53 | /////////////////////////////////////////////////////////// 54 | // COMMON GENERATED DATA DEFINITION 55 | /////////////////////////////////////////////////////////// 56 | 57 | template 58 | struct GeneratedDataCommonDefinition : public detail::GenDataDefinitionTag 59 | { 60 | //! Number of variants in GenDataEnumCommon (remember to update this after changing the enum) 61 | static constexpr int kCommonCount = 6; 62 | static constexpr int kLowerBound = -kCommonCount; 63 | 64 | using ModuleClass = T; 65 | 66 | enum GenDataEnumCommon 67 | { 68 | //kDuplicateOrders =-7, 69 | kTotalOrders =-6, 70 | kNoteOffUsed =-5, 71 | kChannelNoteExtremes =-4, 72 | kSoundIndexNoteExtremes =-3, 73 | kSoundIndexesUsed =-2, 74 | kState =-1, 75 | }; 76 | 77 | // Lowest to highest 78 | using GenDataCommon = std::tuple< 79 | TotalOrdersGenData, // Gen data's total orders <= data's total orders 80 | NoteOffUsedGenData, 81 | ChannelNoteExtremesGenData, 82 | SoundIndexNoteExtremesGenData, 83 | SoundIndexesUsedGenData, 84 | StateGenData 85 | >; 86 | }; 87 | 88 | /////////////////////////////////////////////////////////// 89 | // GENERATED DATA STORAGE 90 | /////////////////////////////////////////////////////////// 91 | 92 | namespace detail { 93 | 94 | // Compile-time for loop helper 95 | template 96 | void ClearAllGenDataHelper(Storage* storage, std::integer_sequence) 97 | { 98 | (storage->template Clear(), ...); 99 | } 100 | 101 | // Calls GeneratedDataStorage::Clear() for every type of generated data 102 | template 103 | void ClearAllGenData(Storage* storage) 104 | { 105 | ClearAllGenDataHelper(storage, std::make_integer_sequence{}); 106 | } 107 | 108 | } // namespace detail 109 | 110 | template 111 | class GeneratedDataStorage : public CommonDef 112 | { 113 | public: 114 | //! Number of module-specific gen data types 115 | static constexpr int kUpperBound = sizeof...(Ts); 116 | 117 | using typename CommonDef::GenDataEnumCommon; 118 | using typename CommonDef::ModuleClass; 119 | 120 | // The GenDataEnum for any module-specific types should be defined 121 | // in the GeneratedData template specialization 122 | 123 | using GenDataModuleSpecific = std::tuple; 124 | 125 | // Single tuple of all data types stored by this gen data storage 126 | using GenData = detail::tuple_cat_t; 127 | 128 | // Single tuple of all wrapped data types stored by this gen data storage. They should all be optionals. 129 | using GenDataWrapped = detail::WrappedGenDataType; 130 | 131 | // Returns an immutable reference to generated data at index gen_data_index 132 | template 133 | constexpr auto Get() const -> const auto& 134 | { 135 | return std::get(data_); 136 | } 137 | 138 | // Returns a mutable reference to generated data at index gen_data_index 139 | template 140 | constexpr auto Get() -> auto& 141 | { 142 | return std::get(data_); 143 | } 144 | 145 | // For convenience 146 | 147 | constexpr auto GetState() const -> const std::optional>& 148 | { 149 | return Get(); 150 | } 151 | 152 | constexpr auto GetState() -> std::optional>& 153 | { 154 | return Get(); 155 | } 156 | 157 | constexpr auto GetNumOrders() const -> const std::optional& 158 | { 159 | return Get(); 160 | } 161 | 162 | /** 163 | * Destroys any generated data at index gen_data_index. 164 | * Call this after any change which would make the data invalid. 165 | */ 166 | template 167 | void Clear() 168 | { 169 | auto& data = std::get(data_); 170 | if (data.has_value()) 171 | { 172 | data.reset(); 173 | generated_.reset(); 174 | status_ = 0; 175 | } 176 | } 177 | 178 | //! Destroys all generated data 179 | void ClearAll() 180 | { 181 | detail::ClearAllGenData<-CommonDef::kCommonCount, kUpperBound>(this); 182 | generated_.reset(); 183 | status_ = 0; 184 | } 185 | 186 | auto IsValid() const -> bool { return generated_.has_value(); } 187 | auto GetGenerated() const -> std::optional { return generated_; } 188 | void SetGenerated(std::optional val) { generated_ = val; } 189 | auto GetStatus() const -> std::size_t { return status_; } 190 | void SetStatus(std::size_t val) { status_ = val; } 191 | 192 | protected: 193 | // Stores all generated data 194 | GenDataWrapped data_; 195 | 196 | // The value passed to GenerateDataImpl. Has a value if gen data is valid. 197 | std::optional generated_; 198 | 199 | // The value returned by GenerateDataImpl. Only valid if IsValid() == true. 200 | std::size_t status_ = 0; 201 | }; 202 | 203 | /////////////////////////////////////////////////////////// 204 | // GENERATED DATA PRIMARY TEMPLATE 205 | /////////////////////////////////////////////////////////// 206 | 207 | /** 208 | * The following is the generated data storage primary class template. 209 | * 210 | * It can be specialized to add additional supported generated data if desired. 211 | * Any specializations must inherit from GeneratedDataStorage and pass the correct 212 | * common definition struct plus the new module-specific types to the template parameter. 213 | * In addition, specializations must define GenDataEnumCommon and GenDataEnum. 214 | * All generated data types must have a "==" operator defined for them. 215 | */ 216 | template 217 | struct GeneratedData 218 | : public GeneratedDataStorage 219 | /* Module-specific types go here in any specializations */> 220 | { 221 | //using Parent = GeneratedDataStorage>; 222 | using typename GeneratedDataCommonDefinition::GenDataEnumCommon; 223 | enum GenDataEnum {}; 224 | }; 225 | 226 | } // namespace d2m 227 | -------------------------------------------------------------------------------- /include/core/global_options.h: -------------------------------------------------------------------------------- 1 | /* 2 | * global_options.h 3 | * Written by Dalton Messmer . 4 | * 5 | * TODO: All these options should be made into Console-specific options. 6 | * For verbose, an output stream should be given to Module classes to 7 | * use rather than always using std::cout/std::cerr. This would be 8 | * great for Web App builds because a stringstream could be provided 9 | * and stdout won't need to be redirected. 10 | */ 11 | 12 | #pragma once 13 | 14 | #include "core/options.h" 15 | 16 | namespace d2m { 17 | 18 | class GlobalOptions 19 | { 20 | public: 21 | enum class OptionEnum 22 | { 23 | kForce, 24 | kHelp, 25 | kVerbose, 26 | kVersion 27 | }; 28 | 29 | static void Set(const OptionCollection& global_options) { global_options_ = global_options; } 30 | static auto Get() -> OptionCollection& { return global_options_; } 31 | 32 | private: 33 | static OptionCollection global_options_; 34 | }; 35 | 36 | } // namespace d2m 37 | -------------------------------------------------------------------------------- /include/core/module.h: -------------------------------------------------------------------------------- 1 | /* 2 | * module.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines an interface for Modules. 6 | * All module classes must inherit ModuleInterface. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "core/conversion_options.h" 12 | #include "core/data.h" 13 | #include "core/factory.h" 14 | #include "core/generated_data.h" 15 | #include "core/global_options.h" 16 | #include "core/module_base.h" 17 | #include "core/note.h" 18 | #include "core/status.h" 19 | 20 | #include 21 | #include 22 | 23 | namespace d2m { 24 | 25 | // All module classes must derive from this using CRTP 26 | template 27 | class ModuleInterface : public EnableFactory 28 | { 29 | public: 30 | virtual ~ModuleInterface() = default; 31 | 32 | auto GetData() const -> const ModuleData& { return data_; } 33 | auto GetGlobalData() const -> const ModuleGlobalData& { return GetData().GlobalData(); } 34 | auto GetGeneratedData() const -> std::shared_ptr> { return generated_data_; } 35 | 36 | auto GetTitle() const -> std::string_view final { return GetGlobalData().title; } 37 | auto GetAuthor() const -> std::string_view final { return GetGlobalData().author; } 38 | 39 | auto GenerateData(std::size_t data_flags = 0) const -> std::size_t final 40 | { 41 | // If generated data has already been created using the same data_flags, just return that 42 | if (generated_data_->IsValid() && generated_data_->GetGenerated().value() == data_flags) 43 | { 44 | return generated_data_->GetStatus(); 45 | } 46 | 47 | // Else, need to generate data 48 | generated_data_->ClearAll(); 49 | const std::size_t status = GenerateDataImpl(data_flags); 50 | generated_data_->SetGenerated(data_flags); 51 | generated_data_->SetStatus(status); 52 | return status; 53 | } 54 | 55 | protected: 56 | ModuleInterface() = default; 57 | 58 | auto GetData() -> ModuleData& { return data_; } 59 | auto GetGlobalData() -> ModuleGlobalData& { return GetData().GlobalData(); } 60 | auto GetGeneratedDataMut() const -> std::shared_ptr> { return generated_data_; } 61 | 62 | // data_flags specifies what data was requested to be generated 63 | virtual auto GenerateDataImpl(std::size_t data_flags) const -> std::size_t = 0; 64 | 65 | private: 66 | // Song information for a particular module file 67 | ModuleData data_; 68 | 69 | // Information about a module file which must be calculated. 70 | // Cannot be stored directly because other Modules need to modify its contents without modifying the Module 71 | const std::shared_ptr> generated_data_ = std::make_shared>(); 72 | }; 73 | 74 | } // namespace d2m 75 | -------------------------------------------------------------------------------- /include/core/module_base.h: -------------------------------------------------------------------------------- 1 | /* 2 | * module_base.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines the base class for Modules. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/conversion_options.h" 11 | #include "core/factory.h" 12 | #include "core/status.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | namespace d2m { 19 | 20 | // Forward declares 21 | class ModuleBase; 22 | class ConversionOptionsBase; 23 | 24 | // Type aliases to make usage easier 25 | using Module = ModuleBase; 26 | using ModulePtr = std::shared_ptr; 27 | using ConversionOptions = ConversionOptionsBase; 28 | using ConversionOptionsPtr = std::shared_ptr; 29 | 30 | // Specialized Info class for Modules 31 | template<> 32 | struct Info : public InfoBase 33 | { 34 | std::string friendly_name; 35 | std::string command_name; 36 | std::string file_extension; 37 | }; 38 | 39 | // Base class for all module types (DMF, MOD, XM, etc.) 40 | class ModuleBase : public EnableReflection, public std::enable_shared_from_this 41 | { 42 | protected: 43 | ModuleBase() = default; 44 | 45 | // TODO: Use this 46 | enum class ExportState 47 | { 48 | kEmpty, kInvalid, kReady 49 | }; 50 | 51 | public: 52 | virtual ~ModuleBase() = default; 53 | 54 | /* 55 | * Cast ModulePtr to std::shared_ptr where T is the derived Module class 56 | */ 57 | template, bool> = true> 58 | auto Cast() const -> std::shared_ptr 59 | { 60 | return std::static_pointer_cast(shared_from_this()); 61 | } 62 | 63 | /* 64 | * Import the specified module file 65 | * Returns true upon failure 66 | */ 67 | auto Import(const std::string& filename) -> bool; 68 | 69 | /* 70 | * Export module to the specified file 71 | * Returns true upon failure 72 | */ 73 | auto Export(const std::string& filename) -> bool; 74 | 75 | /* 76 | * Converts the module to the specified type using the provided conversion options 77 | */ 78 | auto Convert(ModuleType type, const ConversionOptionsPtr& options) -> ModulePtr; 79 | 80 | /* 81 | * Generates the generated data using optional data flags 82 | */ 83 | virtual auto GenerateData(std::size_t data_flags = 0) const -> std::size_t = 0; 84 | 85 | /* 86 | * Gets the Status object for the last import/export/convert 87 | */ 88 | auto GetStatus() const -> const Status& { return status_; } 89 | 90 | /* 91 | * Convenience wrapper for GetStatus().HandleResults() 92 | */ 93 | auto HandleResults() const -> bool { return status_.HandleResults(); } 94 | 95 | /* 96 | * Get the title of the module 97 | */ 98 | virtual auto GetTitle() const -> std::string_view = 0; 99 | 100 | /* 101 | * Get the author of the module 102 | */ 103 | virtual auto GetAuthor() const -> std::string_view = 0; 104 | 105 | protected: 106 | // Import() and Export() and Convert() are wrappers for these methods, which must be implemented by a module class: 107 | 108 | virtual void ImportImpl(const std::string& filename) = 0; 109 | virtual void ExportImpl(const std::string& filename) = 0; 110 | virtual void ConvertImpl(const ModulePtr& input) = 0; 111 | 112 | auto GetOptions() const -> ConversionOptionsPtr { return options_; } 113 | 114 | Status status_; 115 | 116 | private: 117 | ConversionOptionsPtr options_; 118 | }; 119 | 120 | } // namespace d2m 121 | -------------------------------------------------------------------------------- /include/core/note.h: -------------------------------------------------------------------------------- 1 | /* 2 | * note.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines a data structure for storing notes + helper functions. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace d2m { 15 | 16 | enum class NotePitch : std::uint8_t 17 | { 18 | kC=0, 19 | kCS, 20 | kD, 21 | kDS, 22 | kE, 23 | kF, 24 | kFS, 25 | kG, 26 | kGS, 27 | kA, 28 | kAS, 29 | kB 30 | }; 31 | 32 | namespace NoteTypes 33 | { 34 | enum { kEmpty, kNote, kOff }; // The order is important 35 | 36 | struct Empty {}; 37 | constexpr auto operator==(const Empty&, const Empty&) -> bool { return true; }; 38 | 39 | struct alignas(1) Note 40 | { 41 | NotePitch pitch : 4; 42 | uint8_t octave : 4; 43 | 44 | constexpr Note() : pitch(NotePitch::kC), octave(0) {} 45 | constexpr Note(NotePitch pitch, std::uint8_t octave) : pitch(pitch), octave(octave) {} 46 | 47 | constexpr auto operator>(Note rhs) const -> bool 48 | { 49 | return (this->octave << 4) + static_cast(this->pitch) > (rhs.octave << 4) + static_cast(rhs.pitch); 50 | } 51 | 52 | constexpr auto operator>=(Note rhs) const -> bool 53 | { 54 | return (this->octave << 4) + static_cast(this->pitch) >= (rhs.octave << 4) + static_cast(rhs.pitch); 55 | } 56 | 57 | constexpr auto operator<(Note rhs) const -> bool 58 | { 59 | return (this->octave << 4) + static_cast(this->pitch) < (rhs.octave << 4) + static_cast(rhs.pitch); 60 | } 61 | 62 | constexpr auto operator<=(Note rhs) const -> bool 63 | { 64 | return (this->octave << 4) + static_cast(this->pitch) <= (rhs.octave << 4) + static_cast(rhs.pitch); 65 | } 66 | 67 | constexpr auto operator==(Note rhs) const -> bool 68 | { 69 | return this->octave == rhs.octave && this->pitch == rhs.pitch; 70 | } 71 | 72 | constexpr auto operator!=(Note rhs) const -> bool 73 | { 74 | return this->octave != rhs.octave || this->pitch != rhs.pitch; 75 | } 76 | }; 77 | 78 | struct Off {}; 79 | constexpr auto operator==(const Off&, const Off&) -> bool { return true; }; 80 | }; 81 | 82 | using NoteSlot = std::variant; 83 | using Note = NoteTypes::Note; // For convenience 84 | 85 | constexpr auto NoteIsEmpty(const NoteSlot& note) -> bool { return note.index() == NoteTypes::kEmpty; } 86 | constexpr auto NoteHasPitch(const NoteSlot& note) -> bool { return note.index() == NoteTypes::kNote; } 87 | constexpr auto NoteIsOff(const NoteSlot& note) -> bool { return note.index() == NoteTypes::kOff; } 88 | constexpr auto GetNote(const NoteSlot& note) -> const Note& 89 | { 90 | assert(NoteHasPitch(note) && "NoteSlot variant must be using the Note alternative"); 91 | return std::get(note); 92 | } 93 | 94 | constexpr auto GetNote(NoteSlot& note) -> Note& 95 | { 96 | assert(NoteHasPitch(note) && "NoteSlot variant must be using the Note alternative"); 97 | return std::get(note); 98 | } 99 | 100 | constexpr auto GetNoteRange(const Note& low, const Note& high) -> int 101 | { 102 | // Returns range in semitones. Assumes high >= low. 103 | // Range is inclusive on both ends. 104 | 105 | return (high.octave - low.octave) * 12 + (static_cast(high.pitch) - static_cast(low.pitch)) + 1; 106 | } 107 | 108 | } // namespace d2m 109 | -------------------------------------------------------------------------------- /include/core/options.h: -------------------------------------------------------------------------------- 1 | /* 2 | * options.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Declares Option, OptionCollection, OptionDefinition, and OptionDefinitionCollection, 6 | * which are used when working with command-line options. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace d2m { 22 | 23 | // Forward declares 24 | class OptionDefinition; 25 | class OptionDefinitionCollection; 26 | class Option; 27 | class OptionCollection; 28 | 29 | enum OptionType 30 | { 31 | kOption = 0, 32 | kCommand = 1 33 | }; 34 | 35 | // Stores a definition for a single command-line option TODO: Refactor using builder pattern 36 | class OptionDefinition 37 | { 38 | public: 39 | friend class OptionDefinitionCollection; 40 | 41 | // The values correspond to ValueType indices 42 | enum Type 43 | { 44 | kBool = 0, 45 | kInt = 1, 46 | kDouble = 2, 47 | kString = 3, 48 | }; 49 | 50 | using ValueType = std::variant; 51 | 52 | // Constructors 53 | 54 | OptionDefinition() : default_value_{false} {} 55 | 56 | // OptionDefinition without accepted values; The value can be anything allowed by the variant 57 | template{} || (std::is_enum_v && std::is_convertible_v, int>), bool> = true> 58 | OptionDefinition(OptionType type, T id, const std::string& name, char short_name, const ValueType& default_value, std::string description) 59 | : option_type_{type}, id_{static_cast(id)}, name_{name}, short_name_{short_name}, default_value_{default_value}, description_{std::move(description)} 60 | { 61 | for (char c : name) 62 | { 63 | if (!std::isalnum(c)) 64 | { 65 | assert(false && "In OptionDefinition constructor: name must only contain alphanumeric characters or be empty."); 66 | } 67 | } 68 | 69 | assert((short_name == '\0' || std::isalpha(short_name)) && "In OptionDefinition constructor: short_name must be an alphabetic character or '\\0'."); 70 | 71 | value_type_ = static_cast(default_value.index()); 72 | } 73 | 74 | // OptionDefinition with accepted values; Ensures that default_value and accepted_values are the same type and are a valid variant alternative 75 | template, bool> = true> 76 | OptionDefinition(OptionType type, T id, const std::string& name, char short_name, const U& default_value, const std::initializer_list& accepted_values, const std::string& description) 77 | : OptionDefinition(type, id, name, short_name, default_value, description) 78 | { 79 | bool found = false; 80 | int i = 0; 81 | for (const U& val : accepted_values) 82 | { 83 | const auto& inserted_val = accepted_values_.emplace(ValueType(val), i++).first->first; 84 | accepted_values_ordered_.push_back(ValueType(val)); 85 | 86 | // Check for spaces (used when printing help) 87 | if (inserted_val.index() == OptionDefinition::kString) 88 | { 89 | const auto& str = std::get(inserted_val); 90 | if (str.find(' ') != std::string::npos) { accepted_values_contain_spaces_ = true; } 91 | } 92 | 93 | if (inserted_val == default_value_) { found = true; } 94 | } 95 | 96 | // Avoid "unused variable" warning 97 | if (!found) { assert(false && "In OptionDefinition constructor: accepted_values must contain the default value."); } 98 | } 99 | 100 | // Allows the use of string literals, which are converted to std::string 101 | template || (std::is_enum_v && std::is_convertible_v, int>), bool> = true> 102 | OptionDefinition(OptionType type, T id, const std::string& name, char short_name, const char* default_value, const std::initializer_list& accepted_values, const std::string& description) 103 | : OptionDefinition{type, id, name, short_name, std::string{default_value}, accepted_values, description} 104 | { 105 | } 106 | 107 | // Allows custom accepted values text which is used when printing help for this option. accepted_values_ is empty. 108 | template, bool> = true> /* U must be a valid variant alternative */ 109 | OptionDefinition(OptionType type, T id, const std::string& name, char short_name, const U& default_value, const char* custom_accepted_values_text, const std::string& description) 110 | : OptionDefinition{type, id, name, short_name, default_value, description} 111 | { 112 | custom_accepted_values_text_ = custom_accepted_values_text; 113 | } 114 | 115 | // Getters and helpers 116 | 117 | auto GetOptionType() const -> OptionType { return option_type_; } 118 | auto GetId() const -> int { return id_; } 119 | auto GetValueType() const -> Type { return value_type_; } 120 | auto GetName() const -> std::string_view { return name_; }; 121 | auto GetShortName() const -> char { return short_name_; } 122 | auto GetDisplayName() const -> std::string; 123 | auto GetDefaultValue() const -> const ValueType& { return default_value_; } 124 | auto GetAcceptedValues() const -> const std::map& { return accepted_values_; } 125 | auto GetAcceptedValuesOrdered() const -> const std::vector& { return accepted_values_ordered_; } 126 | auto GetDescription() const -> std::string_view { return description_; } 127 | 128 | auto HasName() const -> bool { return !name_.empty(); } 129 | auto HasShortName() const -> bool { return short_name_ != '\0'; } 130 | auto UsesAcceptedValues() const -> bool { return accepted_values_.size() > 0; } 131 | 132 | // Returns whether the given value can be assigned to an option with this option definition 133 | auto IsValid(const ValueType& value) const -> bool; 134 | 135 | // Prints help info 136 | void PrintHelp() const; 137 | 138 | protected: 139 | 140 | OptionType option_type_ = OptionType::kOption; 141 | 142 | // Used for quickly accessing specific options in OptionDefinitionCollection collection 143 | int id_ = -1; 144 | 145 | Type value_type_ = Type::kBool; 146 | std::string name_; 147 | char short_name_ = '\0'; 148 | 149 | ValueType default_value_; 150 | 151 | std::map accepted_values_; 152 | std::vector accepted_values_ordered_; // Stored in the order they were provided 153 | bool accepted_values_contain_spaces_ = false; // Whether double quotes are needed when printing 154 | 155 | std::string description_; 156 | 157 | // Only string-typed options can use custom accepted values text. 158 | // To use this feature, accepted values must take the form of: {"="} 159 | std::string custom_accepted_values_text_; 160 | }; 161 | 162 | 163 | // A collection of OptionDefinition objects 164 | class OptionDefinitionCollection 165 | { 166 | public: 167 | static constexpr int kNotFound = -1; 168 | 169 | OptionDefinitionCollection() = default; 170 | OptionDefinitionCollection(const OptionDefinitionCollection& other); 171 | OptionDefinitionCollection(const std::initializer_list& options); 172 | 173 | auto Count() const -> std::size_t; 174 | 175 | // Access 176 | auto GetIdMap() const -> const std::map& { return id_options_map_; } 177 | 178 | // Find methods 179 | auto FindById(int id) const -> const OptionDefinition*; 180 | auto FindByName(const std::string& name) const -> const OptionDefinition*; 181 | auto FindByShortName(char short_name) const -> const OptionDefinition*; 182 | auto FindIdByName(const std::string& name) const -> int; 183 | auto FindIdByShortName(char short_name) const -> int; 184 | 185 | // Other 186 | void PrintHelp() const; 187 | 188 | private: 189 | std::map id_options_map_; 190 | std::unordered_map name_options_map_; 191 | std::unordered_map short_name_options_map_; 192 | }; 193 | 194 | 195 | // An OptionDefinition + option value 196 | class Option 197 | { 198 | public: 199 | friend class OptionCollection; 200 | using ValueType = OptionDefinition::ValueType; 201 | 202 | Option() = default; 203 | 204 | // Construct with definitions defined elsewhere 205 | Option(const OptionDefinitionCollection* definitions, int id); 206 | 207 | // Construct with value. The definitions are defined elsewhere 208 | Option(const OptionDefinitionCollection* definitions, int id, ValueType value); 209 | 210 | void SetValue(ValueType& value); 211 | void SetValue(ValueType&& value); 212 | 213 | void SetValueToDefault(); 214 | 215 | auto GetValue() const -> const ValueType& { return value_; } 216 | 217 | template 218 | auto GetValue() const -> const T& 219 | { 220 | // Will throw an exception if T is the wrong type 221 | return std::get(value_); 222 | } 223 | 224 | auto GetValueAsIndex() const -> int 225 | { 226 | assert(GetDefinition()->UsesAcceptedValues()); 227 | return value_index_; 228 | } 229 | 230 | auto GetExplicitlyProvided() const -> bool 231 | { 232 | return explicitly_provided_; 233 | } 234 | 235 | auto GetDefinition() const -> const OptionDefinition*; 236 | 237 | private: 238 | 239 | // Rather than making a copy of definition for each Option, it instead will point to definitions defined elsewhere + an id. 240 | // This will work well for both definitions from the Info struct and custom definitions used by frontends. 241 | // TODO: Use OptionDefinition instead? Use std::shared_ptr's aliasing constructor? 242 | const OptionDefinitionCollection* definitions_ = nullptr; 243 | int id_ = -1; 244 | 245 | ValueType value_; 246 | 247 | // If using accepted values, this stores the index of value_ within the accepted values list (enables better performance) 248 | int value_index_; 249 | 250 | // Whether the user explicitly provided the value for this option 251 | bool explicitly_provided_ = false; 252 | }; 253 | 254 | 255 | // A collection of Option objects 256 | class OptionCollection 257 | { 258 | public: 259 | using ValueType = OptionDefinition::ValueType; 260 | 261 | OptionCollection() = default; 262 | explicit OptionCollection(const OptionDefinitionCollection* definitions); 263 | 264 | // Access to definitions 265 | 266 | void SetDefinitions(const OptionDefinitionCollection* definitions); 267 | auto GetDefinitions() const -> const OptionDefinitionCollection* { return definitions_; } 268 | 269 | // Access to collection 270 | 271 | auto GetOptionsMap() const -> const std::map& { return options_map_; } 272 | 273 | // Get options based on id, name, or short name 274 | 275 | auto GetOption(int id) const -> const Option& { return options_map_.at(id); } 276 | auto GetOption(int id) -> Option& { return options_map_[id]; } 277 | 278 | template && std::is_convertible_v, int>, bool> = true> 279 | auto GetOption(T id) const -> const Option& 280 | { 281 | return GetOption(static_cast(id)); 282 | } 283 | 284 | template && std::is_convertible_v, int>, bool> = true> 285 | auto GetOption(T id) -> Option& 286 | { 287 | return GetOption(static_cast(id)); 288 | } 289 | 290 | auto GetOption(const std::string& name) const -> const Option&; 291 | auto GetOption(const std::string& name) -> Option&; 292 | auto GetOption(char short_name) const -> const Option&; 293 | auto GetOption(char short_name) -> Option&; 294 | 295 | // Other 296 | 297 | auto ParseArgs(std::vector& args, bool ignore_unknown_args = false) -> bool; 298 | void SetValuesToDefault(); 299 | 300 | private: 301 | const OptionDefinitionCollection* definitions_ = nullptr; 302 | 303 | std::map options_map_; 304 | }; 305 | 306 | 307 | // Provides option value conversion tools 308 | class ModuleOptionUtils 309 | { 310 | public: 311 | using ValueType = OptionDefinition::ValueType; 312 | 313 | // Convert ValueType to a string 314 | static auto ConvertToString(const ValueType& value) -> std::string; 315 | 316 | // Convert string + type to a ValueType 317 | static auto ConvertToValue(std::string_view value_str, OptionDefinition::Type type, ValueType& return_val) -> bool; 318 | }; 319 | 320 | } // namespace d2m 321 | -------------------------------------------------------------------------------- /include/core/status.h: -------------------------------------------------------------------------------- 1 | /* 2 | * status.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Declares Status and ModuleException, which are used 6 | * for handling errors and warnings. 7 | * 8 | * Defines NotImplementedException. 9 | */ 10 | 11 | #pragma once 12 | 13 | #include "core/factory.h" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace d2m { 25 | 26 | // Forward declares 27 | class ModuleException; 28 | class Status; 29 | class NotImplementedException; 30 | 31 | // Used whenever an error occurs during import/converting/exporting. Can derive from this class. 32 | class ModuleException : public std::exception 33 | { 34 | public: 35 | 36 | // Common error codes are defined here 37 | // Module-specific error codes can be implemented using positive values 38 | enum class ImportError 39 | { 40 | kSuccess = 0 41 | }; 42 | 43 | enum class ExportError 44 | { 45 | kSuccess = 0, 46 | kFileOpen = -1 47 | }; 48 | 49 | enum class ConvertError 50 | { 51 | kSuccess = 0, 52 | kUnsuccessful = -1, // Applied to the input module 53 | kInvalidArgument = -2, 54 | kUnsupportedInputType = -3 55 | }; 56 | 57 | // The type of error 58 | enum class Category 59 | { 60 | kNone, 61 | kImport, 62 | kExport, 63 | kConvert 64 | }; 65 | 66 | auto what() const noexcept -> const char* override 67 | { 68 | return error_message_.c_str(); 69 | } 70 | 71 | auto str() const -> std::string_view 72 | { 73 | return error_message_; 74 | } 75 | 76 | ~ModuleException() override = default; 77 | ModuleException(ModuleException&&) noexcept = default; 78 | auto operator=(ModuleException&&) -> ModuleException& = default; 79 | 80 | public: // Should this be protected once DMF gets its own DMFException class? 81 | 82 | ModuleException() = default; 83 | ModuleException(const ModuleException&) = default; 84 | auto operator=(ModuleException&) -> ModuleException& = default; 85 | 86 | // Construct using an enum for an error code 87 | template && std::is_convertible_v, int>, bool> = true> 88 | ModuleException(Category category, T error_code, std::string_view error_message = "") 89 | : ModuleException{category, static_cast(error_code), error_message} {} 90 | 91 | // Construct using an integer for an error code 92 | ModuleException(Category category, int error_code, std::string_view error_message = "") 93 | { 94 | error_code_ = error_code; 95 | 96 | std::string_view category_str; 97 | switch (category) 98 | { 99 | case Category::kNone: 100 | category_str = "Init: "; break; 101 | case Category::kImport: 102 | category_str = "Import: "; break; 103 | case Category::kExport: 104 | category_str = "Export: "; break; 105 | case Category::kConvert: 106 | category_str = "Convert: "; break; 107 | } 108 | 109 | if (error_code_ > 0) 110 | { 111 | error_message_ = "ERROR: "; 112 | error_message_ += category_str; 113 | error_message_ += error_message; 114 | } 115 | else 116 | { 117 | error_message_ = "ERROR: "; 118 | error_message_ += category_str; 119 | error_message_ += CreateCommonErrorMessage(category, error_code_, error_message); 120 | } 121 | } 122 | 123 | protected: 124 | int error_code_; 125 | std::string error_message_; 126 | 127 | private: 128 | auto CreateCommonErrorMessage(Category category, int error_code, std::string_view arg) -> std::string; 129 | }; 130 | 131 | 132 | //! Provides warning information after module importing/converting/exporting 133 | class Status 134 | { 135 | public: 136 | // The source of the error/warning 137 | using Category = ModuleException::Category; 138 | 139 | Status() { Clear(); } 140 | 141 | auto ErrorOccurred() const -> bool { return error_.get(); } 142 | auto WarningsIssued() const -> bool { return !warning_messages_.empty(); } 143 | 144 | void PrintError(bool use_std_err = true) const; 145 | void PrintWarnings(bool use_std_err = false) const; 146 | 147 | //! Prints error and warnings that occurred during the last action. Returns true if an error occurred. 148 | auto HandleResults() const -> bool; 149 | 150 | void Clear() 151 | { 152 | warning_messages_.clear(); 153 | error_.reset(); 154 | } 155 | 156 | void AddError(ModuleException&& error) 157 | { 158 | if (!error_) { error_ = std::make_unique(std::move(error)); } 159 | else { *error_ = std::move(error); } 160 | } 161 | 162 | void AddWarning(const std::string& warning_message) 163 | { 164 | warning_messages_.push_back("WARNING: " + warning_message); 165 | } 166 | 167 | void Reset(Category action_type) 168 | { 169 | Clear(); 170 | category_ = action_type; 171 | } 172 | 173 | private: 174 | std::unique_ptr error_; 175 | std::vector warning_messages_; 176 | Category category_ = Category::kNone; 177 | }; 178 | 179 | 180 | //! NotImplementedException because I took exception to the standard library not implementing it 181 | class NotImplementedException : public std::logic_error 182 | { 183 | public: 184 | NotImplementedException() : std::logic_error{"Function not yet implemented."} {} 185 | }; 186 | 187 | } // namespace d2m 188 | -------------------------------------------------------------------------------- /include/dmf2mod.h: -------------------------------------------------------------------------------- 1 | /* 2 | * dmf2mod.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Include this header for access to dmf2mod core functionality 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/factory.h" 11 | #include "modules/debug.h" 12 | #include "modules/dmf.h" 13 | #include "modules/mod.h" 14 | #include "utils/utils.h" 15 | #include "version.h" 16 | -------------------------------------------------------------------------------- /include/modules/debug.h: -------------------------------------------------------------------------------- 1 | /* 2 | * debug.h 3 | * Written by Dalton Messmer . 4 | * 5 | * A debug "module" for debug builds 6 | */ 7 | 8 | #pragma once 9 | 10 | #ifndef NDEBUG 11 | 12 | #include "core/module.h" 13 | 14 | #include 15 | 16 | namespace d2m { 17 | 18 | /////////////////////////////////////////////////////////// 19 | // Setup template specializations used by DMF 20 | /////////////////////////////////////////////////////////// 21 | 22 | class Debug; 23 | 24 | template<> 25 | struct ModuleGlobalData : public ModuleGlobalDataDefault {}; 26 | 27 | /////////////////////////////////////////////////////////// 28 | // Debug primary classes 29 | /////////////////////////////////////////////////////////// 30 | 31 | class DebugConversionOptions : public ConversionOptionsInterface 32 | { 33 | public: 34 | // Factory requires destructor to be public 35 | ~DebugConversionOptions() override = default; 36 | 37 | enum class OptionEnum 38 | { 39 | kDump, 40 | kAppend, 41 | kGenDataFlags 42 | }; 43 | 44 | auto Dump() const -> bool { return GetOption(OptionEnum::kDump).GetValue(); } 45 | auto Append() const -> bool { return GetOption(OptionEnum::kAppend).GetValue(); } 46 | auto GenDataFlags() const -> std::size_t { return static_cast(GetOption(OptionEnum::kGenDataFlags).GetValue()); } 47 | 48 | private: 49 | // Only allow the Factory to construct this class 50 | friend class Builder; 51 | 52 | DebugConversionOptions() = default; 53 | }; 54 | 55 | class Debug final : public ModuleInterface 56 | { 57 | public: 58 | enum ImportError 59 | { 60 | kSuccess = 0, 61 | kUnspecifiedError 62 | }; 63 | 64 | // Factory requires destructor to be public 65 | ~Debug() override = default; 66 | 67 | private: 68 | // Only allow the Factory to construct this class 69 | friend class Builder; 70 | 71 | Debug() = default; 72 | 73 | void ImportImpl(const std::string& filename) override; 74 | void ExportImpl(const std::string& filename) override; 75 | void ConvertImpl(const ModulePtr& input) override; 76 | auto GenerateDataImpl(std::size_t data_flags) const -> std::size_t override { return 1; } 77 | 78 | std::string dump_; 79 | }; 80 | 81 | } // namespace d2m 82 | 83 | #endif // !NDEBUG 84 | -------------------------------------------------------------------------------- /include/modules/dmf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * dmf.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Declares all classes used for Deflemask's DMF files. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/module.h" 11 | #include "utils/stream_reader.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace d2m { 18 | 19 | /////////////////////////////////////////////////////////// 20 | // Setup template specializations used by DMF 21 | /////////////////////////////////////////////////////////// 22 | 23 | class DMF; 24 | 25 | namespace dmf { 26 | 27 | struct System 28 | { 29 | enum class Type 30 | { 31 | kError = 0, 32 | kYMU759, 33 | kGenesis, 34 | kGenesis_CH3, 35 | kSMS, 36 | kGameBoy, 37 | kPCEngine, 38 | kNES, 39 | kC64_SID_8580, 40 | kC64_SID_6581, 41 | kArcade, 42 | kNeoGeo, 43 | kNeoGeo_CH2, 44 | kSMS_OPLL, 45 | kNES_VRC7, 46 | kNES_FDS 47 | }; 48 | 49 | Type type; 50 | std::uint8_t id; 51 | std::string name; 52 | std::uint8_t channels; 53 | }; 54 | 55 | } // namespace dmf 56 | 57 | template<> 58 | struct ModuleGlobalData : public ModuleGlobalDataDefault 59 | { 60 | std::uint8_t dmf_format_version; 61 | dmf::System system; 62 | 63 | // Visual info 64 | std::uint8_t highlight_a_patterns; 65 | std::uint8_t highlight_b_patterns; 66 | 67 | // Module info 68 | std::uint8_t frames_mode; 69 | std::optional custom_hz_value; 70 | std::uint16_t global_tick; 71 | }; 72 | 73 | template<> 74 | struct Row 75 | { 76 | NoteSlot note; 77 | std::int16_t volume; 78 | std::array effect; // Deflemask allows four effects columns per channel regardless of the system 79 | std::int16_t instrument; 80 | }; 81 | 82 | template<> 83 | struct ChannelMetadata 84 | { 85 | std::uint8_t effect_columns_count; 86 | }; 87 | 88 | template<> 89 | struct PatternMetadata 90 | { 91 | std::string name; 92 | }; 93 | 94 | template<> 95 | struct SoundIndex 96 | { 97 | enum Types { kNone, kSquare, kWave, kNoise /*Other options here*/ }; 98 | 99 | using None = std::monostate; 100 | struct Square { std::uint8_t id; operator std::uint8_t() const { return id; } }; 101 | struct Wave { std::uint8_t id; operator std::uint8_t() const { return id; } }; 102 | struct Noise { std::uint8_t id; operator std::uint8_t() const { return id; } }; // Placeholder 103 | 104 | using type = std::variant; 105 | }; 106 | 107 | constexpr auto operator==(const SoundIndex::Square& lhs, const SoundIndex::Square& rhs) -> bool { return lhs.id == rhs.id; } 108 | constexpr auto operator==(const SoundIndex::Wave& lhs, const SoundIndex::Wave& rhs) -> bool { return lhs.id == rhs.id; } 109 | constexpr auto operator==(const SoundIndex::Noise& lhs, const SoundIndex::Noise& rhs) -> bool { return lhs.id == rhs.id; } 110 | 111 | /////////////////////////////////////////////////////////// 112 | // dmf namespace 113 | /////////////////////////////////////////////////////////// 114 | 115 | namespace dmf { 116 | 117 | //inline constexpr int kVolumeMax = 15; /* ??? */ 118 | inline constexpr int kGameBoyVolumeMax = 15; 119 | 120 | // Custom dmf2mod internal effect codes (see effects.h) 121 | namespace Effects 122 | { 123 | enum 124 | { 125 | kArpTickSpeed = 1, 126 | kNoteSlideUp, 127 | kNoteSlideDown, 128 | kSetVibratoMode, 129 | kSetFineVibratoDepth, 130 | kSetFinetune, 131 | kSetSamplesBank, 132 | kSyncSignal, 133 | kSetGlobalFinetune, 134 | kGameBoySetWave, 135 | kGameBoySetNoisePolyCounterMode, 136 | kGameBoySetDutyCycle, 137 | kGameBoySetSweepTimeShift, 138 | kGameBoySetSweepDir 139 | }; 140 | } 141 | 142 | struct ModuleInfo 143 | { 144 | std::uint8_t time_base, tick_time1, tick_time2; 145 | }; 146 | 147 | struct FMOps 148 | { 149 | // TODO: Use unions depending on DMF version? 150 | std::uint8_t am; 151 | std::uint8_t ar; // Attack 152 | std::uint8_t dr; // Decay? 153 | std::uint8_t mult; 154 | std::uint8_t rr; // Release 155 | std::uint8_t sl; // Sustain 156 | std::uint8_t tl; 157 | 158 | std::uint8_t dt2; 159 | std::uint8_t rs; 160 | std::uint8_t dt; 161 | std::uint8_t d2r; 162 | 163 | union 164 | { 165 | std::uint8_t ssg_mode; 166 | std::uint8_t egs; // EG-S in SMS OPLL / NES VRC7. 0 if OFF; 8 if ON. 167 | }; 168 | 169 | std::uint8_t dam, dvb, egt, ksl, sus, vib, ws, ksr; // Exclusive to DMF version 18 (0x12) and older 170 | }; 171 | 172 | struct Instrument 173 | { 174 | enum InstrumentMode 175 | { 176 | kInvalidMode = 0, 177 | kStandardMode, 178 | kFMMode 179 | }; 180 | 181 | std::string name; 182 | InstrumentMode mode; // TODO: Use union depending on mode? Would save space 183 | 184 | union 185 | { 186 | // Standard Instruments 187 | struct 188 | { 189 | std::uint8_t vol_env_size, arp_env_size, duty_noise_env_size, wavetable_env_size; 190 | std::int32_t* vol_env_value; 191 | std::int32_t* arp_env_value; 192 | std::int32_t* duty_noise_env_value; 193 | std::int32_t* wavetable_env_value; 194 | std::int8_t vol_env_loop_pos, arp_env_loop_pos, duty_noise_env_loop_pos, wavetable_env_loop_pos; 195 | std::uint8_t arp_macro_mode; 196 | 197 | // Commodore 64 exclusive 198 | std::uint8_t c64_tri_wave_en, c64_saw_wave_en, c64_pulse_wave_en, c64_noise_wave_en, 199 | c64_attack, c64_decay, c64_sustain, c64_release, c64_pulse_width, c64_ring_mod_en, 200 | c64_sync_mod_en, c64_to_filter, c64_vol_macro_to_filter_cutoff_en, c64_use_filter_values_from_inst; 201 | std::uint8_t c64_filter_resonance, c64_filter_cutoff, c64_filter_high_pass, c64_filter_low_pass, c64_filter_ch2_off; 202 | 203 | // Game Boy exclusive 204 | std::uint8_t gb_env_vol, gb_env_dir, gb_env_len, gb_sound_len; 205 | } std; 206 | 207 | // FM Instruments 208 | struct 209 | { 210 | std::uint8_t num_operators; 211 | 212 | union 213 | { 214 | std::uint8_t alg; 215 | std::uint8_t sus; // SMS OPLL / NES VRC7 exclusive 216 | }; 217 | 218 | std::uint8_t fb; 219 | std::uint8_t opll_preset; // SMS OPLL / NES VRC7 exclusive 220 | 221 | union 222 | { 223 | struct { std::uint8_t lfo, lfo2; }; 224 | struct { std::uint8_t dc, dm; }; // SMS OPLL / NES VRC7 exclusive 225 | }; 226 | 227 | std::array ops; 228 | } fm; 229 | }; 230 | }; 231 | 232 | struct PCMSample 233 | { 234 | std::uint32_t size; 235 | std::string name; 236 | std::uint8_t rate, pitch, amp, bits; 237 | std::uint32_t cut_start, cut_end; 238 | std::uint16_t* data; 239 | }; 240 | 241 | // Deflemask Game Boy channels 242 | namespace GameBoyChannel 243 | { 244 | enum 245 | { 246 | kSquare1 = 0, kSquare2 = 1, kWave = 2, kNoise = 3 247 | }; 248 | } 249 | 250 | } // namespace dmf 251 | 252 | /////////////////////////////////////////////////////////// 253 | // DMF primary classes 254 | /////////////////////////////////////////////////////////// 255 | 256 | class DMFConversionOptions : public ConversionOptionsInterface 257 | { 258 | public: 259 | // Factory requires destructor to be public 260 | ~DMFConversionOptions() override = default; 261 | 262 | private: 263 | // Only allow the Factory to construct this class 264 | friend class Builder; 265 | 266 | DMFConversionOptions() = default; 267 | }; 268 | 269 | class DMF final : public ModuleInterface 270 | { 271 | public: 272 | enum ImportError 273 | { 274 | kSuccess = 0, 275 | kUnspecifiedError 276 | }; 277 | 278 | enum class ImportWarning {}; 279 | enum class ExportError {}; 280 | enum class ExportWarning {}; 281 | enum class ConvertError {}; 282 | enum class ConvertWarning {}; 283 | 284 | using SystemType = dmf::System::Type; 285 | 286 | // Factory requires destructor to be public 287 | ~DMF() override; 288 | 289 | // Returns the initial BPM of the module 290 | void GetBPM(unsigned& numerator, unsigned& denominator) const; 291 | auto GetBPM() const -> double; 292 | 293 | auto GetSystem() const -> const dmf::System& { return GetGlobalData().system; } 294 | static auto SystemInfo(SystemType system_type) -> const dmf::System&; 295 | 296 | // TODO: Create a module-independent storage system for wavetables, PCM samples, instruments, etc. 297 | auto GetTotalWavetables() const -> std::uint8_t { return total_wavetables_; } 298 | auto GetWavetableValues() const -> std::uint32_t** { return wavetable_values_; } 299 | auto GetWavetableValue(unsigned wavetable, unsigned index) const -> std::uint32_t { return wavetable_values_[wavetable][index]; } 300 | 301 | private: 302 | // Only allow the Factory to construct this class 303 | friend class Builder; 304 | 305 | DMF() = default; 306 | void CleanUp(); 307 | 308 | void ImportImpl(const std::string& filename) override; 309 | void ExportImpl(const std::string& filename) override; 310 | void ConvertImpl(const ModulePtr& input) override; 311 | auto GenerateDataImpl(std::size_t data_flags) const -> std::size_t override; 312 | 313 | // Import helper class 314 | class Importer; 315 | 316 | dmf::ModuleInfo module_info_; // TODO: Eventually remove 317 | std::uint8_t total_instruments_ = 0; 318 | dmf::Instrument* instruments_ = nullptr; 319 | std::uint8_t total_wavetables_ = 0; 320 | std::uint32_t* wavetable_sizes_ = nullptr; 321 | std::uint32_t** wavetable_values_ = nullptr; 322 | std::uint8_t total_pcm_samples_ = 0; 323 | dmf::PCMSample* pcm_samples_ = nullptr; 324 | }; 325 | 326 | } // namespace d2m 327 | -------------------------------------------------------------------------------- /include/modules/mod.h: -------------------------------------------------------------------------------- 1 | /* 2 | * mod.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Declares all classes used for ProTracker's MOD files. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/module.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace d2m { 18 | 19 | /////////////////////////////////////////////////////////// 20 | // Setup template specializations used by MOD 21 | /////////////////////////////////////////////////////////// 22 | 23 | class MOD; 24 | 25 | template<> 26 | struct ModuleGlobalData : public ModuleGlobalDataDefault 27 | { 28 | // In the future, we'll be able to detect when a MOD module 29 | // was created with dmf2mod, which will help when converting 30 | // from MOD to another module type. 31 | bool made_with_dmf2mod; 32 | }; 33 | 34 | template<> 35 | struct Row 36 | { 37 | SoundIndexType sample; 38 | NoteSlot note; 39 | Effect effect; 40 | }; 41 | 42 | /////////////////////////////////////////////////////////// 43 | // mod namespace 44 | /////////////////////////////////////////////////////////// 45 | 46 | namespace mod { 47 | 48 | // Custom dmf2mod internal effect codes (see effects.h) 49 | namespace Effects 50 | { 51 | enum 52 | { 53 | kSetSampleOffset = 1, 54 | kSetVolume, 55 | kSetFilter, 56 | kFineSlideUp, 57 | kFineSlideDown, 58 | kSetGlissando, 59 | kSetVibratoWaveform, 60 | kSetFinetune, 61 | kLoopPattern, 62 | kSetTremoloWaveform, 63 | kFineVolSlideUp, 64 | kFineVolSlideDown, 65 | kDelayPattern, 66 | kInvertLoop 67 | }; 68 | } 69 | 70 | // Stores a MOD sample 71 | struct Sample 72 | { 73 | std::string name; // 22 characters 74 | SoundIndexType id; 75 | unsigned length; 76 | int finetune; 77 | unsigned volume; 78 | unsigned repeat_offset; 79 | unsigned repeat_length; 80 | 81 | std::vector data; 82 | }; 83 | 84 | } // namespace mod 85 | 86 | /////////////////////////////////////////////////////////// 87 | // MOD primary classes 88 | /////////////////////////////////////////////////////////// 89 | 90 | class MODException : public ModuleException 91 | { 92 | public: 93 | template 94 | MODException(Category category, T error_code, std::string_view args = "") 95 | : ModuleException(category, static_cast(error_code), CreateErrorMessage(category, static_cast(error_code), args)) {} 96 | 97 | private: 98 | // Creates module-specific error message from an error code and string argument 99 | static auto CreateErrorMessage(Category category, int error_code, std::string_view arg) -> std::string; 100 | }; 101 | 102 | class MODConversionOptions final : public ConversionOptionsInterface 103 | { 104 | public: 105 | // Factory requires destructor to be public 106 | ~MODConversionOptions() override = default; 107 | 108 | enum class OptionEnum 109 | { 110 | kArpeggio, kPortamento, kPort2Note, kVibrato, kTempoType 111 | }; 112 | 113 | enum class TempoType 114 | { 115 | kAccuracy, kEffectCompatibility 116 | }; 117 | 118 | auto AllowArpeggio() const -> bool { return GetOption(OptionEnum::kArpeggio).GetValue(); } 119 | auto AllowPortamento() const -> bool { return GetOption(OptionEnum::kPortamento).GetValue(); } 120 | auto AllowPort2Note() const -> bool { return GetOption(OptionEnum::kPort2Note).GetValue(); } 121 | auto AllowVibrato() const -> bool { return GetOption(OptionEnum::kVibrato).GetValue(); } 122 | auto GetTempoType() const -> TempoType { return TempoType{GetOption(OptionEnum::kTempoType).GetValueAsIndex()}; } 123 | 124 | auto AllowEffects() const -> bool { return AllowArpeggio() || AllowPortamento() || AllowPort2Note() || AllowVibrato(); } 125 | 126 | private: 127 | // Only allow the Factory to construct this class 128 | friend class Builder; 129 | 130 | MODConversionOptions() = default; 131 | }; 132 | 133 | class MOD final : public ModuleInterface 134 | { 135 | public: 136 | // Factory requires destructor to be public 137 | ~MOD() override = default; 138 | 139 | enum class ImportError { kSuccess = 0 }; 140 | enum class ImportWarning {}; 141 | 142 | enum class ExportError { kSuccess = 0 }; 143 | enum class ExportWarning {}; 144 | 145 | enum class ConvertError 146 | { 147 | kSuccess = 0, 148 | kNotGameBoy, 149 | kTooManyPatternMatrixRows, 150 | kOver64RowPattern, 151 | kWrongChannelCount 152 | }; 153 | 154 | enum class ConvertWarning 155 | { 156 | kNone = 0, 157 | kPitchHigh, 158 | kTempoLow, 159 | kTempoHigh, 160 | kTempoLowCompat, 161 | kTempoHighCompat, 162 | kTempoAccuracy, 163 | kEffectIgnored, 164 | kWaveDownsample, 165 | kMultipleEffects, 166 | kLoopbackInaccuracy 167 | }; 168 | 169 | static constexpr unsigned kVolumeMax = 64u; // Yes, there are 65 different values for the volume 170 | 171 | private: 172 | // Only allow the Factory to construct this class 173 | friend class Builder; 174 | 175 | MOD() = default; 176 | 177 | // Module requirements: 178 | void ImportImpl(const std::string& filename) override; 179 | void ExportImpl(const std::string& filename) override; 180 | void ConvertImpl(const ModulePtr& input) override; 181 | auto GenerateDataImpl(std::size_t data_flags) const -> std::size_t override { return 1; } 182 | 183 | // DMF -> MOD conversion 184 | class DMFConverter; 185 | 186 | // Export helpers: 187 | void ExportModuleName(std::ofstream& fout) const; 188 | void ExportSampleInfo(std::ofstream& fout) const; 189 | void ExportModuleInfo(std::ofstream& fout) const; 190 | void ExportPatterns(std::ofstream& fout) const; 191 | void ExportSampleData(std::ofstream& fout) const; 192 | 193 | // MOD file info: 194 | std::int8_t total_mod_samples_; 195 | std::map, mod::Sample> samples_; 196 | }; 197 | 198 | } // namespace d2m 199 | -------------------------------------------------------------------------------- /include/utils/hash.h: -------------------------------------------------------------------------------- 1 | /* 2 | * hash.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Custom hash functions 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | struct PairHash 15 | { 16 | template 17 | auto operator()(const std::pair& pair) const noexcept -> std::size_t 18 | { 19 | return std::hash{}(pair.first) ^ std::hash{}(pair.second); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /include/utils/stream_reader.h: -------------------------------------------------------------------------------- 1 | /* 2 | * stream_reader.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines a header-only wrapper for std::istream and derived classes 6 | * which provides convenient methods for reading strings and integers 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace d2m { 18 | 19 | namespace detail { 20 | template 21 | struct LoopUnroller 22 | { 23 | template 24 | inline void operator()(Operation& op) const noexcept { op(); LoopUnroller{}(op); } 25 | }; 26 | 27 | template<> 28 | struct LoopUnroller<0> 29 | { 30 | template 31 | inline void operator()(Operation& op) const noexcept {} 32 | }; 33 | 34 | // Adapted from: https://peter.bloomfield.online/using-cpp-templates-for-size-based-type-selection/ 35 | template 36 | using UIntSelector = 37 | std::conditional_t 42 | > 43 | >; 44 | } // namespace detail 45 | 46 | enum class Endianness { kUnspecified, kLittle, kBig }; 47 | 48 | /* 49 | * Wrapper for std::istream and derived classes which provides 50 | * convenient methods for reading strings and integers 51 | */ 52 | template, IStream>, bool> = true> 54 | class StreamReader 55 | { 56 | private: 57 | IStream stream_; 58 | 59 | template 60 | struct LittleEndianReadOperator 61 | { 62 | static constexpr std::uint8_t kShiftAmount = (num_bytes - 1) * 8; 63 | static_assert(num_bytes > 0); 64 | 65 | void operator()() 66 | { 67 | value >>= 8; 68 | value |= static_cast(stream_.get()) << kShiftAmount; 69 | } 70 | IStream& stream_; 71 | T value{}; 72 | }; 73 | 74 | template 75 | struct BigEndianReadOperator 76 | { 77 | void operator()() 78 | { 79 | value <<= 8; 80 | value |= static_cast(stream_.get()); 81 | } 82 | IStream& stream_; 83 | T value{}; 84 | }; 85 | 86 | template 87 | auto ReadIntLittleEndian() -> T 88 | { 89 | auto oper = LittleEndianReadOperator{stream()}; 90 | detail::LoopUnroller{}(oper); 91 | 92 | if constexpr (is_signed && num_bytes < sizeof(T)) 93 | { 94 | struct SignExtender { T value : num_bytes * 8; }; 95 | return SignExtender{oper.value}.value; 96 | } 97 | else 98 | { 99 | return oper.value; 100 | } 101 | } 102 | 103 | template 104 | auto ReadIntBigEndian() -> T 105 | { 106 | auto oper = BigEndianReadOperator{stream()}; 107 | detail::LoopUnroller{}(oper); 108 | 109 | if constexpr (is_signed && num_bytes < sizeof(T)) 110 | { 111 | struct SignExtender { T value : num_bytes * 8; }; 112 | return SignExtender{oper.value}.value; 113 | } 114 | else 115 | { 116 | return oper.value; 117 | } 118 | } 119 | 120 | public: 121 | StreamReader() = default; 122 | 123 | // StreamReader constructs and owns the istream object 124 | template 125 | StreamReader(Args&&... args) : stream_{std::forward(args)...} {} 126 | 127 | StreamReader(const StreamReader&) = delete; 128 | StreamReader(StreamReader&&) noexcept = delete; 129 | auto operator=(const StreamReader&) -> StreamReader& = delete; 130 | auto operator=(StreamReader&&) noexcept -> StreamReader& = delete; 131 | 132 | auto stream() const -> const IStream& { return stream_; } 133 | auto stream() -> IStream& { return stream_; } 134 | 135 | auto ReadStr(unsigned length) -> std::string 136 | { 137 | std::string temp_str; 138 | temp_str.assign(length, '\0'); 139 | stream_.read(&temp_str[0], length); 140 | return temp_str; 141 | } 142 | 143 | auto ReadPStr() -> std::string 144 | { 145 | // P-Strings (Pascal strings) are prefixed with a 1 byte length 146 | std::uint8_t string_length = stream_.get(); 147 | return ReadStr(string_length); 148 | } 149 | 150 | auto ReadBytes(unsigned length) -> std::vector 151 | { 152 | std::vector temp_bytes; 153 | temp_bytes.assign(length, '\0'); 154 | stream_.read(&temp_bytes[0], length); 155 | return temp_bytes; 156 | } 157 | 158 | template 159 | auto ReadInt() 160 | { 161 | using UIntType = std::conditional_t<(num_bytes > 1), detail::UIntSelector, std::uint8_t>; 162 | using ReturnType = std::conditional_t, UIntType>; 163 | 164 | static_assert(num_bytes <= 8 && num_bytes >= 1, "Accepted range for num_bytes: 1 <= num_bytes <= 8"); 165 | if constexpr (num_bytes > 1) 166 | { 167 | static_assert(endian != Endianness::kUnspecified, "Set the endianness when creating StreamReader or set it in this method's template parameters"); 168 | if constexpr (endian == Endianness::kLittle) 169 | { 170 | return static_cast(ReadIntLittleEndian()); 171 | } 172 | else 173 | { 174 | return static_cast(ReadIntBigEndian()); 175 | } 176 | } 177 | else 178 | { 179 | // For single-byte reads, the size of the return value is guaranteed 180 | // to be 1 byte and setting the signed parameter is unnecessary 181 | return static_cast(stream_.get()); 182 | } 183 | } 184 | }; 185 | 186 | } // namespace d2m 187 | -------------------------------------------------------------------------------- /include/utils/utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * utils.h 3 | * Written by Dalton Messmer . 4 | * 5 | * Declares various utility methods used by dmf2mod. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "core/config_types.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace d2m { 18 | 19 | // Class containing miscellaneous helpful static methods 20 | class Utils 21 | { 22 | public: 23 | // File utils 24 | static auto GetBaseNameFromFilename(std::string_view filename) -> std::string; 25 | static auto ReplaceFileExtension(std::string_view filename, std::string_view new_file_extension) -> std::string; 26 | static auto GetFileExtension(std::string_view filename) -> std::string; 27 | static auto FileExists(std::string_view filename) -> bool; 28 | 29 | // File utils which require Factory initialization 30 | static auto GetTypeFromFilename(std::string_view filename) -> ModuleType; 31 | static auto GetTypeFromFileExtension(std::string_view extension) -> ModuleType; 32 | static auto GetTypeFromCommandName(std::string_view command_name) -> ModuleType; 33 | static auto GetExtensionFromType(ModuleType module_type) -> std::string_view; 34 | 35 | // Command-line arguments and options utils 36 | static auto GetArgsAsVector(int argc, char** argv) -> std::vector; 37 | 38 | // String utils (borrowed from Stack Overflow) 39 | static void StringTrimLeft(std::string& str) 40 | { 41 | // Trim string from start (in place) 42 | str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) { 43 | return !std::isspace(ch); 44 | })); 45 | } 46 | 47 | static void StringTrimRight(std::string& str) 48 | { 49 | // Trim string from end (in place) 50 | str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) { 51 | return !std::isspace(ch); 52 | }).base(), str.end()); 53 | } 54 | 55 | static void StringTrimBothEnds(std::string& str) 56 | { 57 | // Trim string from both ends (in place) 58 | StringTrimLeft(str); 59 | StringTrimRight(str); 60 | } 61 | }; 62 | 63 | } // namespace d2m 64 | -------------------------------------------------------------------------------- /include/version.h: -------------------------------------------------------------------------------- 1 | /* 2 | * version.h 3 | * Written by Dalton Messmer . 4 | * 5 | * dmf2mod version info 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace d2m { 13 | 14 | inline constexpr int kVersionMajor = 0; 15 | inline constexpr int kVersionMinor = 2; 16 | inline constexpr int kVersionPatch = 0; 17 | inline constexpr std::string_view kVersion = "0.2.0-alpha"; 18 | 19 | } // namespace d2m 20 | -------------------------------------------------------------------------------- /include/version.h.in: -------------------------------------------------------------------------------- 1 | /* 2 | * version.h 3 | * Written by Dalton Messmer . 4 | * 5 | * dmf2mod version info 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace d2m { 13 | 14 | inline constexpr int kVersionMajor = @PROJECT_VERSION_MAJOR@; 15 | inline constexpr int kVersionMinor = @PROJECT_VERSION_MINOR@; 16 | inline constexpr int kVersionPatch = @PROJECT_VERSION_PATCH@; 17 | inline constexpr std::string_view kVersion = "@PROJECT_VERSION@"; 18 | 19 | } // namespace d2m 20 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SRC "${CMAKE_CURRENT_SOURCE_DIR}") 2 | 3 | set(CORE_SOURCES 4 | ${SRC}/dmf2mod.cpp 5 | ${SRC}/core/conversion_options.cpp 6 | ${SRC}/core/factory.cpp 7 | ${SRC}/core/global_options.cpp 8 | ${SRC}/core/module.cpp 9 | ${SRC}/core/options.cpp 10 | ${SRC}/core/status.cpp 11 | ) 12 | 13 | set(MODULE_SOURCES 14 | ${SRC}/modules/dmf.cpp 15 | ${SRC}/modules/mod.cpp 16 | ${SRC}/modules/debug.cpp 17 | ) 18 | 19 | set(UTILS_SOURCES 20 | ${SRC}/utils/utils.cpp 21 | ) 22 | 23 | set(DMF2MOD_SOURCES 24 | ${CORE_SOURCES} 25 | ${MODULE_SOURCES} 26 | ${UTILS_SOURCES} 27 | PARENT_SCOPE 28 | ) 29 | -------------------------------------------------------------------------------- /src/core/conversion_options.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * conversion_options.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * See conversion_options.h 6 | */ 7 | 8 | #include "core/conversion_options.h" 9 | 10 | #include "core/module_base.h" 11 | 12 | #include 13 | 14 | namespace d2m { 15 | 16 | void ConversionOptionsBase::PrintHelp(ModuleType module_type) 17 | { 18 | const auto& definitions = Factory::GetInfo(module_type)->option_definitions; 19 | 20 | std::string name = Factory::GetInfo(module_type)->command_name; 21 | std::string ext = Factory::GetInfo(module_type)->command_name; 22 | if (name.empty() || ext.empty()) { return; } 23 | 24 | for (auto& c : name) { c = toupper(c); } 25 | for (auto& c : ext) { c = toupper(c); } 26 | 27 | if (definitions.Count() == 0) 28 | { 29 | std::cout << ext << " files have no conversion options.\n"; 30 | return; 31 | } 32 | 33 | std::cout << name << " Options:\n"; 34 | 35 | definitions.PrintHelp(); 36 | } 37 | 38 | } // namespace d2m 39 | -------------------------------------------------------------------------------- /src/core/factory.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * factory.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * Implements InitializeImpl for each factory. 6 | */ 7 | 8 | #include "core/factory.h" 9 | 10 | #include "dmf2mod.h" 11 | 12 | namespace d2m { 13 | 14 | template<> 15 | Factory::InitializeImpl::InitializeImpl() 16 | { 17 | using MODOptionEnum = MODConversionOptions::OptionEnum; 18 | auto mod_options = OptionDefinitionCollection{ 19 | /* Type / Option id / Full name / Short / Default / Possib. vals / Description */ 20 | {kOption, MODOptionEnum::kArpeggio, "arp", '\0', false, "Allow arpeggio effects"}, 21 | {kOption, MODOptionEnum::kPortamento, "port", '\0', false, "Allow portamento up/down effects"}, 22 | {kOption, MODOptionEnum::kPort2Note, "port2note", '\0', false, "Allow portamento to note effects"}, 23 | {kOption, MODOptionEnum::kVibrato, "vib", '\0', false, "Allow vibrato effects"}, 24 | {kOption, MODOptionEnum::kTempoType, "tempo", '\0', "accuracy", {"accuracy", "compat"}, "Prioritize tempo accuracy or compatibility with effects"}, 25 | }; 26 | 27 | Register(); 28 | Register(std::move(mod_options)); 29 | 30 | #ifndef NDEBUG 31 | using DebugOptionEnum = DebugConversionOptions::OptionEnum; 32 | auto debug_options = OptionDefinitionCollection{ 33 | /* Type / Option id / Full name / Short / Default / Possib. vals / Description */ 34 | {kCommand, DebugOptionEnum::kDump, "dump", 'd', false, "Dump generated data"}, 35 | {kOption, DebugOptionEnum::kAppend, "append", 'a', true, "Append results to log file or overwrite"}, 36 | {kOption, DebugOptionEnum::kGenDataFlags, "gen", 'g', 0, "Flags to use when generating data"} 37 | }; 38 | Register(std::move(debug_options)); 39 | #endif 40 | }; 41 | 42 | template<> 43 | Factory::InitializeImpl::InitializeImpl() 44 | { 45 | Register("Deflemask", "dmf", "dmf"); 46 | Register("ProTracker", "mod", "mod"); 47 | 48 | #ifndef NDEBUG 49 | Register("Debug", "debug", "log"); 50 | #endif 51 | }; 52 | 53 | } // namespace d2m 54 | -------------------------------------------------------------------------------- /src/core/global_options.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * global_options.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * See global_options.h 6 | */ 7 | 8 | #include "core/global_options.h" 9 | 10 | namespace d2m { 11 | 12 | using OptionEnum = GlobalOptions::OptionEnum; 13 | 14 | static const OptionDefinitionCollection kOptionDefinitions = { 15 | {kOption, OptionEnum::kForce, "force", 'f', false, "Overwrite output file."}, 16 | {kCommand, OptionEnum::kHelp, "help", '\0', "", "[module type]", "Display this help message. Provide module type (i.e. mod) for module-specific options."}, 17 | {kOption, OptionEnum::kVerbose, "verbose", '\0', false, "Print debug info to console in addition to errors and/or warnings."}, 18 | {kCommand, OptionEnum::kVersion, "version", 'v', false, "Display the dmf2mod version."} 19 | }; 20 | 21 | OptionCollection GlobalOptions::global_options_{&kOptionDefinitions}; 22 | 23 | } // namespace d2m 24 | -------------------------------------------------------------------------------- /src/core/module.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * module.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * See module.h 6 | */ 7 | 8 | #include "core/module.h" 9 | 10 | #include "utils/utils.h" 11 | 12 | #include 13 | 14 | namespace d2m { 15 | 16 | auto ModuleBase::Import(const std::string& filename) -> bool 17 | { 18 | status_.Reset(Status::Category::kImport); 19 | try 20 | { 21 | ImportImpl(filename); 22 | return false; 23 | } 24 | catch (ModuleException& e) 25 | { 26 | status_.AddError(std::move(e)); 27 | } 28 | 29 | return true; 30 | } 31 | 32 | auto ModuleBase::Export(const std::string& filename) -> bool 33 | { 34 | status_.Reset(Status::Category::kExport); 35 | try 36 | { 37 | ExportImpl(filename); 38 | return false; 39 | } 40 | catch (ModuleException& e) 41 | { 42 | status_.AddError(std::move(e)); 43 | } 44 | 45 | return true; 46 | } 47 | 48 | auto ModuleBase::Convert(ModuleType type, const ConversionOptionsPtr& options) -> ModulePtr 49 | { 50 | ModuleBase* input = this; // For clarity 51 | input->status_.Reset(Status::Category::kConvert); 52 | 53 | // Don't convert if the types are the same 54 | if (type == input->GetType()) { return nullptr; } 55 | 56 | // Create new module object 57 | ModulePtr output = Factory::Create(type); 58 | if (!output) { return nullptr; } 59 | 60 | output->status_.Reset(Status::Category::kConvert); 61 | output->options_ = options; 62 | 63 | try 64 | { 65 | // Perform the conversion 66 | assert(shared_from_this() != nullptr); 67 | output->ConvertImpl(shared_from_this()); 68 | } 69 | catch (ModuleException& e) 70 | { 71 | output->status_.AddError(std::move(e)); 72 | input->status_.AddError(ModuleException(ModuleException::Category::kConvert, ModuleException::ConvertError::kUnsuccessful)); 73 | } 74 | 75 | return output; 76 | } 77 | 78 | } // namespace d2m 79 | -------------------------------------------------------------------------------- /src/core/options.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * options.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines Option, OptionCollection, OptionDefinition, and OptionDefinitionCollection, 6 | * which are used when working with command-line options. 7 | */ 8 | 9 | #include "core/options.h" 10 | 11 | #include "utils/utils.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace d2m { 20 | 21 | // OptionDefinition 22 | 23 | auto OptionDefinition::GetDisplayName() const -> std::string 24 | { 25 | if (!name_.empty()) { return "--" + name_; } 26 | return '-' + std::string(1, short_name_); 27 | } 28 | 29 | auto OptionDefinition::IsValid(const ValueType& value) const -> bool 30 | { 31 | if (value.index() != GetValueType()) { return false; } 32 | if (!UsesAcceptedValues()) { return true; } 33 | return accepted_values_.count(value) > 0; 34 | } 35 | 36 | void OptionDefinition::PrintHelp() const 37 | { 38 | std::cout.setf(std::ios_base::left); 39 | 40 | std::string str1 = " "; 41 | if (HasShortName()) 42 | { 43 | str1 += '-' + std::string(1, GetShortName()); 44 | if (HasName()) { str1 += ", "; } 45 | } 46 | if (HasName()) 47 | { 48 | str1 += "--"; 49 | str1 += GetName(); 50 | } 51 | 52 | const bool use_double_quotes = accepted_values_contain_spaces_; 53 | const char preferred_separator = GetOptionType() == kCommand ? ' ' : '='; 54 | 55 | const OptionDefinition::Type option_type = GetValueType(); 56 | if (UsesAcceptedValues() && option_type != OptionDefinition::kBool) 57 | { 58 | str1 += preferred_separator; 59 | str1 += '['; 60 | 61 | unsigned i = 0; 62 | const auto total = GetAcceptedValuesOrdered().size(); 63 | for (const auto& val : GetAcceptedValuesOrdered()) 64 | { 65 | switch (option_type) 66 | { 67 | case OptionDefinition::kInt: 68 | str1 += std::to_string(std::get(val)); break; 69 | case OptionDefinition::kDouble: 70 | str1 += std::to_string(std::get(val)); break; 71 | case OptionDefinition::kString: 72 | if (use_double_quotes) { str1 += '"' + std::get(val) + '"'; } 73 | else { str1 += std::get(val); } 74 | break; 75 | default: 76 | break; 77 | } 78 | 79 | if (i + 1 != total) { str1 += ", "; } 80 | ++i; 81 | } 82 | 83 | str1 += ']'; 84 | } 85 | else if (!custom_accepted_values_text_.empty()) // If it uses custom accepted values text 86 | { 87 | str1 += preferred_separator; 88 | str1 += custom_accepted_values_text_; 89 | } 90 | else 91 | { 92 | switch (option_type) 93 | { 94 | case OptionDefinition::kInt: 95 | case OptionDefinition::kDouble: 96 | str1 += preferred_separator; 97 | str1 += ""; break; 98 | case OptionDefinition::kString: 99 | str1 += preferred_separator; 100 | str1 += "\"\""; break; 101 | default: 102 | break; 103 | } 104 | } 105 | 106 | std::string str2 = std::string{GetDescription()} + ' '; 107 | switch (option_type) 108 | { 109 | case OptionDefinition::kBool: 110 | { 111 | const bool default_value = std::get(GetDefaultValue()); 112 | if (default_value) 113 | { 114 | // Only print the default value if it is true 115 | str2 += "(Default: true)"; 116 | } 117 | break; 118 | } 119 | case OptionDefinition::kInt: 120 | { 121 | str2 += "(Default: "; 122 | str2 += std::to_string(std::get(GetDefaultValue())); 123 | str2 += ')'; 124 | break; 125 | } 126 | case OptionDefinition::kDouble: 127 | { 128 | str2 += "(Default: "; 129 | str2 += std::to_string(std::get(GetDefaultValue())); 130 | str2 += ')'; 131 | break; 132 | } 133 | case OptionDefinition::kString: 134 | { 135 | const std::string_view default_value = std::get(GetDefaultValue()); 136 | if (!default_value.empty()) 137 | { 138 | str2 += "(Default: "; 139 | if (use_double_quotes) 140 | { 141 | str2 += '"'; 142 | str2 += default_value; 143 | str2 += '"'; 144 | } 145 | else { str2 += default_value; } 146 | str2 += ')'; 147 | } 148 | break; 149 | } 150 | default: 151 | break; 152 | } 153 | 154 | std::cout << std::setw(30) << str1 << str2 << '\n'; 155 | } 156 | 157 | // OptionDefinitionCollection 158 | 159 | OptionDefinitionCollection::OptionDefinitionCollection(const OptionDefinitionCollection& other) 160 | { 161 | id_options_map_ = other.id_options_map_; 162 | for (auto& map_pair : id_options_map_) 163 | { 164 | OptionDefinition* module_option = &map_pair.second; 165 | 166 | const auto name = std::string{module_option->GetName()}; 167 | name_options_map_[name] = module_option; 168 | 169 | const char short_name = module_option->GetShortName(); 170 | short_name_options_map_[short_name] = module_option; 171 | } 172 | } 173 | 174 | OptionDefinitionCollection::OptionDefinitionCollection(const std::initializer_list& options) 175 | { 176 | // Initialize collection + mappings 177 | id_options_map_.clear(); 178 | name_options_map_.clear(); 179 | short_name_options_map_.clear(); 180 | for (auto& option : options) 181 | { 182 | // Id mapping 183 | const int id = option.GetId(); 184 | assert(id_options_map_.count(id) == 0 && "OptionDefinitionCollection(...): Duplicate option id found."); 185 | id_options_map_[id] = option; // Uses copy constructor 186 | 187 | // Name mapping 188 | if (option.HasName()) 189 | { 190 | const auto name = std::string{option.GetName()}; 191 | assert(name_options_map_.count(name) == 0 && "OptionDefinitionCollection(...): Duplicate option name found."); 192 | name_options_map_[name] = &id_options_map_[id]; 193 | } 194 | 195 | // Short name mapping 196 | if (option.HasShortName()) 197 | { 198 | const char short_name = option.GetShortName(); 199 | assert(short_name_options_map_.count(short_name) == 0 && "OptionDefinitionCollection(...): Duplicate option short name found."); 200 | short_name_options_map_[short_name] = &id_options_map_[id]; 201 | } 202 | } 203 | } 204 | 205 | auto OptionDefinitionCollection::Count() const -> std::size_t 206 | { 207 | return id_options_map_.size(); 208 | } 209 | 210 | auto OptionDefinitionCollection::FindById(int id) const -> const OptionDefinition* 211 | { 212 | const auto it = id_options_map_.find(id); 213 | return it != id_options_map_.end() ? &it->second : nullptr; 214 | } 215 | 216 | auto OptionDefinitionCollection::FindByName(const std::string& name) const -> const OptionDefinition* 217 | { 218 | const auto it = name_options_map_.find(name); 219 | return it != name_options_map_.end() ? it->second : nullptr; 220 | } 221 | 222 | auto OptionDefinitionCollection::FindByShortName(char short_name) const -> const OptionDefinition* 223 | { 224 | const auto it = short_name_options_map_.find(short_name); 225 | return it != short_name_options_map_.end() ? it->second : nullptr; 226 | } 227 | 228 | auto OptionDefinitionCollection::FindIdByName(const std::string& name) const -> int 229 | { 230 | const OptionDefinition* ptr = FindByName(name); 231 | if (!ptr) { return kNotFound; } 232 | return ptr->GetId(); 233 | } 234 | 235 | auto OptionDefinitionCollection::FindIdByShortName(char short_name) const -> int 236 | { 237 | const OptionDefinition* ptr = FindByShortName(short_name); 238 | if (!ptr) { return kNotFound; } 239 | return ptr->GetId(); 240 | } 241 | 242 | void OptionDefinitionCollection::PrintHelp() const 243 | { 244 | for (const auto& map_pair : id_options_map_) 245 | { 246 | const OptionDefinition& definition = map_pair.second; 247 | definition.PrintHelp(); 248 | } 249 | } 250 | 251 | // Option 252 | 253 | Option::Option(const OptionDefinitionCollection* definitions, int id) 254 | : definitions_{definitions} 255 | , id_{id} 256 | { 257 | assert(definitions_ && "Option definition cannot be null."); 258 | SetValueToDefault(); 259 | } 260 | 261 | Option::Option(const OptionDefinitionCollection* definitions, int id, ValueType value) 262 | : definitions_{definitions} 263 | , id_{id} 264 | { 265 | assert(definitions_ && "Option definition cannot be null."); 266 | SetValue(value); 267 | } 268 | 269 | void Option::SetValue(ValueType& value) 270 | { 271 | assert(GetDefinition()->IsValid(value) && "The value is not a valid type."); 272 | value_ = value; 273 | if (GetDefinition()->UsesAcceptedValues()) 274 | { 275 | const auto& accepted_values = GetDefinition()->GetAcceptedValues(); 276 | const int index = accepted_values.at(value_); 277 | value_index_ = index; 278 | } 279 | } 280 | 281 | void Option::SetValue(ValueType&& value) 282 | { 283 | assert(GetDefinition()->IsValid(value) && "The value is not a valid type."); 284 | value_ = std::move(value); 285 | if (GetDefinition()->UsesAcceptedValues()) 286 | { 287 | const auto& accepted_values = GetDefinition()->GetAcceptedValues(); 288 | const int index = accepted_values.at(value_); 289 | value_index_ = index; 290 | } 291 | } 292 | 293 | void Option::SetValueToDefault() 294 | { 295 | const OptionDefinition* definition = GetDefinition(); 296 | value_ = definition->GetDefaultValue(); 297 | if (GetDefinition()->UsesAcceptedValues()) 298 | { 299 | const auto& accepted_values = GetDefinition()->GetAcceptedValues(); 300 | const int index = accepted_values.at(value_); 301 | value_index_ = index; 302 | } 303 | } 304 | 305 | auto Option::GetDefinition() const -> const OptionDefinition* 306 | { 307 | assert(definitions_ && "Option definitions were null."); 308 | const OptionDefinition* definition = definitions_->FindById(id_); 309 | assert(definition && "Option definition was not found."); 310 | return definition; 311 | } 312 | 313 | // OptionCollection 314 | 315 | OptionCollection::OptionCollection(const OptionDefinitionCollection* definitions) 316 | { 317 | SetDefinitions(definitions); 318 | } 319 | 320 | void OptionCollection::SetDefinitions(const OptionDefinitionCollection* definitions) 321 | { 322 | definitions_ = definitions; 323 | 324 | // Create options and set them to their default value 325 | options_map_.clear(); 326 | if (definitions) // If no option definitions were given, this will be null 327 | { 328 | for (const auto& map_pair : definitions->GetIdMap()) 329 | { 330 | const int id = map_pair.first; 331 | options_map_.try_emplace(id, definitions, id); 332 | } 333 | } 334 | else 335 | { 336 | // definitions_ must always point to an OptionDefinitionCollection, even if it is empty. TODO: Not always? 337 | definitions_ = nullptr; 338 | } 339 | } 340 | 341 | auto OptionCollection::GetOption(const std::string& name) const -> const Option& 342 | { 343 | const int id = definitions_->FindIdByName(name); 344 | assert(id != OptionDefinitionCollection::kNotFound && "Option with the given name wasn't found in the collection."); 345 | return GetOption(id); 346 | } 347 | 348 | auto OptionCollection::GetOption(const std::string& name) -> Option& 349 | { 350 | const int id = definitions_->FindIdByName(name); 351 | assert(id != OptionDefinitionCollection::kNotFound && "Option with the given name wasn't found in the collection."); 352 | return GetOption(id); 353 | } 354 | 355 | auto OptionCollection::GetOption(char short_name) const -> const Option& 356 | { 357 | const int id = definitions_->FindIdByShortName(short_name); 358 | assert(id != OptionDefinitionCollection::kNotFound && "Option with the given short name wasn't found in the collection."); 359 | return GetOption(id); 360 | } 361 | 362 | auto OptionCollection::GetOption(char short_name) -> Option& 363 | { 364 | const int id = definitions_->FindIdByShortName(short_name); 365 | assert(id != OptionDefinitionCollection::kNotFound && "Option with the given short name wasn't found in the collection."); 366 | return GetOption(id); 367 | } 368 | 369 | void OptionCollection::SetValuesToDefault() 370 | { 371 | for (auto& map_pair : options_map_) 372 | { 373 | auto& option = map_pair.second; 374 | option.SetValueToDefault(); 375 | } 376 | } 377 | 378 | auto OptionCollection::ParseArgs(std::vector& args, bool ignore_unknown_args) -> bool 379 | { 380 | /* 381 | * Examples of command-line arguments that can be parsed: 382 | * --foo 383 | * --foo="bar 123" 384 | * --foo bar 385 | * --foo -123.0 386 | * -f 387 | * -f=bar 388 | * -f=true 389 | * -f 3 390 | * 391 | * Command-line options passed in with with double-quotes around 392 | * them do not have double-quotes here. 393 | * Arguments are checked against option definitions for a match 394 | * and to determine the type (bool, int, double, or string). 395 | * Valid arguments that do not match any option definition are 396 | * ignored and left in the args vector, while matching arguments 397 | * are "consumed" and removed from it. 398 | */ 399 | 400 | std::unordered_set options_parsed; 401 | 402 | // Sets the value of an option given a value string 403 | auto SetValue = [this, &options_parsed](std::string_view value_str, const OptionDefinition* option_def) -> bool 404 | { 405 | auto& option = options_map_[option_def->GetId()]; 406 | 407 | OptionDefinition::ValueType value_temp; 408 | if (ModuleOptionUtils::ConvertToValue(value_str, option_def->GetValueType(), value_temp)) 409 | { 410 | return true; // Error occurred 411 | } 412 | 413 | if (!option_def->IsValid(value_temp)) 414 | { 415 | const std::string_view option_type_str = option_def->GetOptionType() == kOption ? "option" : "command"; 416 | std::cerr << "ERROR: The value \"" << value_str << "\" is not valid for the " << option_type_str << " \"" << option_def->GetDisplayName() << "\".\n"; 417 | return true; // The value is not valid for this option definition 418 | } 419 | 420 | option.SetValue(std::move(value_temp)); 421 | option.explicitly_provided_ = true; 422 | options_parsed.insert(option_def->GetId()); 423 | return false; 424 | }; 425 | 426 | const OptionDefinition* handling_option = nullptr; 427 | 428 | /* 429 | * If the previous argument is syntactically correct yet unrecognized (maybe it's a module option and 430 | * we are reading global options now), the current argument in the following loop may be its value if 431 | * the arguments were passed like: "--unrecognized value". Or it may be another option - recognized or not. 432 | */ 433 | bool arg_might_be_value = false; 434 | 435 | // Main loop 436 | for (int i = 0; i < static_cast(args.size()); ++i) 437 | { 438 | auto& arg = args[i]; 439 | Utils::StringTrimBothEnds(arg); 440 | if (arg.empty()) 441 | { 442 | args.erase(args.begin() + i); 443 | --i; // Adjust for item just erased 444 | continue; 445 | } 446 | 447 | const OptionDefinition* def = handling_option; 448 | auto equals_pos = std::string::npos; 449 | 450 | const bool this_arg_is_value = handling_option != nullptr; 451 | if (this_arg_is_value) 452 | { 453 | handling_option = nullptr; 454 | 455 | // Set the value 456 | if (SetValue(arg.c_str(), def)) { return true; } // Error occurred 457 | 458 | // Erase both the flag and value since they have been consumed 459 | args.erase(args.begin() + i - 1, args.begin() + i + 1); 460 | i -= 2; // Adjust for items just erased 461 | continue; 462 | } 463 | 464 | if (arg.size() <= 1 || arg[0] != '-') 465 | { 466 | if (arg_might_be_value) 467 | { 468 | // Hopefully this is just a value from the preceding unrecognized argument 469 | arg_might_be_value = false; 470 | continue; 471 | } 472 | 473 | // Error: Invalid argument 474 | std::cerr << "ERROR: Invalid option: \"" << arg << "\"\n"; 475 | return true; 476 | } 477 | 478 | const bool using_short_name = arg[1] != '-'; 479 | if (using_short_name) // -f format argument (short name) 480 | { 481 | if (!isalpha(arg[1])) 482 | { 483 | if (arg_might_be_value) 484 | { 485 | // Hopefully this is just a value from the preceding unrecognized argument 486 | arg_might_be_value = false; 487 | continue; 488 | } 489 | 490 | // Error: Short names must be alphabetic 491 | std::cerr << "ERROR: Invalid flag \"" << arg << "\": Flags must be comprised of only alphabetic characters.\n"; 492 | return true; 493 | } 494 | 495 | equals_pos = arg.find_first_of('='); 496 | const bool using_equals = equals_pos != std::string::npos; 497 | if (using_equals) // Using the form: "-f=" 498 | { 499 | if (equals_pos != 2) 500 | { 501 | if (arg_might_be_value) 502 | { 503 | // Hopefully this is just a value from the preceding unrecognized argument 504 | arg_might_be_value = false; 505 | continue; 506 | } 507 | 508 | // Error: Short flags with an '=' must be of the form: "-f=" 509 | std::cerr << "ERROR: Invalid flag \"" << arg << "\": Unable to parse.\n"; 510 | return true; 511 | } 512 | 513 | // At this point, argument is deemed syntactically valid 514 | def = definitions_->FindByShortName(arg[1]); 515 | } 516 | else // Using the form: "-f", "-f ", or "-abcdef" 517 | { 518 | const bool using_several_short_args = arg.size() > 2; 519 | if (!using_several_short_args) // Using the form: "-f" or "-f " 520 | { 521 | if (!isalpha(arg[1])) 522 | { 523 | if (arg_might_be_value) 524 | { 525 | // Hopefully this is just a value from the preceding unrecognized argument 526 | arg_might_be_value = false; 527 | continue; 528 | } 529 | 530 | // Error: Short flags with an '=' must be of the form: "-f=" 531 | std::cerr << "ERROR: Invalid flag \"" << arg << "\": Unable to parse.\n"; 532 | return true; 533 | } 534 | 535 | // At this point, argument is deemed syntactically valid 536 | def = definitions_->FindByShortName(arg[1]); 537 | } 538 | else // Using the form: "-abcdef" - this form cannot have a value after it 539 | { 540 | for (unsigned j = 1; j < arg.size(); ++j) 541 | { 542 | const char c = arg[j]; 543 | if (!isalpha(c)) 544 | { 545 | if (arg_might_be_value) 546 | { 547 | // Hopefully this is just a value from the preceding unrecognized argument 548 | arg_might_be_value = false; 549 | break; // Break out of inner loop, then hit the continue 550 | } 551 | 552 | // Error: Short names must be alphabetic 553 | std::cerr << "ERROR: Invalid flag '" << arg[j] << "': Flags must be comprised of only alphabetic characters.\n"; 554 | return true; 555 | } 556 | 557 | const OptionDefinition* temp_def = definitions_->FindByShortName(c); 558 | 559 | // Skip unrecognized options 560 | if (!temp_def) { continue; } 561 | 562 | if (temp_def->GetValueType() != OptionDefinition::kBool) 563 | { 564 | // Error: When multiple short flags are strung together, all of them must be boolean-typed options 565 | return true; 566 | } 567 | 568 | // Set the value 569 | SetValue("1", temp_def); 570 | 571 | arg.erase(j--, 1); // Remove this flag from argument, since it has been consumed 572 | } 573 | 574 | arg_might_be_value = false; // Impossible for next arg to be a value 575 | 576 | if (arg == "-") 577 | { 578 | // Erase argument since it has been consumed 579 | args.erase(args.begin() + i); 580 | i--; // Adjust for item just erased 581 | } 582 | 583 | // SetValue() was called here and does not need to be called below. 584 | // Or this was a value from an unrecognized argument, which we are skipping. 585 | continue; 586 | } 587 | } 588 | } 589 | else // --foo format argument 590 | { 591 | equals_pos = arg.find_first_of('='); 592 | const auto name = arg.substr(2, equals_pos - 2); // From start to '=' or end of string - whichever comes first 593 | def = definitions_->FindByName(name); 594 | } 595 | 596 | if (!def) 597 | { 598 | // Syntactically valid argument, yet unknown 599 | arg_might_be_value = true; // Next arg may be this unrecognized option's value 600 | continue; // Skip 601 | } 602 | 603 | // At this point, the current argument is both syntactically valid and a recognized option. 604 | 605 | /* 606 | * This is a recognized option, not the preceding unrecognised option's value. 607 | * Therefore, the next argument cannot be an unrecognized argument's value: 608 | */ 609 | arg_might_be_value = false; 610 | 611 | if (options_parsed.count(def->GetId()) > 0) 612 | { 613 | // ERROR: Setting the same option twice 614 | const std::string_view option_type_str = def->GetOptionType() == kOption ? "option" : "command"; 615 | if (using_short_name) 616 | { 617 | std::cerr << "ERROR: The " << option_type_str << " \"-" << def->GetShortName() << "\" is used more than once.\n"; 618 | } 619 | else 620 | { 621 | std::cerr << "ERROR: The " << option_type_str << " \"--" << def->GetName() << "\" is used more than once.\n"; 622 | } 623 | 624 | return true; 625 | } 626 | 627 | std::string value_str; 628 | if (equals_pos != std::string::npos) // Value is after the '=' 629 | { 630 | value_str = arg.substr(equals_pos + 1); 631 | 632 | // Set the value 633 | if (SetValue(value_str.c_str(), def)) { return true; } // Error occurred 634 | 635 | // Erase argument since it has been consumed 636 | args.erase(args.begin() + i); 637 | --i; // Adjust for item just erased 638 | } 639 | else // No '=' present 640 | { 641 | if (def->GetValueType() != OptionDefinition::kBool) 642 | { 643 | // The next argument will be the value 644 | handling_option = def; 645 | } 646 | else 647 | { 648 | // The presence of a bool argument means its value is true. Unless '=' is present, no value is expected to follow. 649 | // So unlike other types, "--foobar false" would not be valid, but "--foobar=false" is. 650 | 651 | // Set the value 652 | SetValue("1", def); 653 | 654 | // Erase argument since it has been consumed 655 | args.erase(args.begin() + i); 656 | --i; // Adjust for item just erased 657 | } 658 | } 659 | } 660 | 661 | if (handling_option) 662 | { 663 | const std::string_view option_type_str = handling_option->GetOptionType() == kOption ? "option" : "command"; 664 | std::cerr << "ERROR: The " << option_type_str << " \"" << handling_option->GetDisplayName() << "\" did not provide a value.\n"; 665 | return true; 666 | } 667 | 668 | if (!ignore_unknown_args && !args.empty()) 669 | { 670 | std::string error_str = "ERROR: Unknown option(s): "; 671 | for (unsigned i = 0; i < args.size(); ++i) 672 | { 673 | error_str += args[i]; 674 | if (i != args.size() - 1) { error_str += ", "; } 675 | } 676 | std::cerr << error_str << "\n"; 677 | return true; 678 | } 679 | 680 | return false; 681 | } 682 | 683 | // ModuleOptionUtils 684 | 685 | auto ModuleOptionUtils::ConvertToString(const ValueType& value) -> std::string 686 | { 687 | const auto type = static_cast(value.index()); 688 | switch (type) 689 | { 690 | case OptionDefinition::kBool: 691 | return std::get(value) ? "true" : "false"; 692 | case OptionDefinition::kInt: 693 | return std::to_string(std::get(value)); 694 | case OptionDefinition::kDouble: 695 | return std::to_string(std::get(value)); 696 | case OptionDefinition::kString: 697 | return std::get(value); 698 | default: 699 | return "ERROR"; 700 | } 701 | } 702 | 703 | auto ModuleOptionUtils::ConvertToValue(std::string_view value_str, OptionDefinition::Type type, ValueType& return_val) -> bool 704 | { 705 | switch (type) 706 | { 707 | case OptionDefinition::kBool: 708 | { 709 | auto value_str_lower = std::string{value_str}; 710 | unsigned i = 0; 711 | while (i < value_str_lower.size()) 712 | { 713 | value_str_lower[i] = tolower(value_str_lower[i]); 714 | ++i; 715 | } 716 | 717 | if (value_str_lower == "0" || value_str_lower == "false") { return_val = false; } 718 | else if (value_str_lower == "1" || value_str_lower == "true") { return_val = true; } 719 | else 720 | { 721 | // Error: Invalid value for boolean-typed option 722 | std::cerr << "ERROR: Invalid value \"" << value_str << "\" for boolean-typed option.\n"; 723 | return true; 724 | } 725 | } break; 726 | case OptionDefinition::kInt: 727 | { 728 | int val = 0; 729 | std::from_chars(value_str.data(), value_str.data() + value_str.size(), val); 730 | return_val = val; 731 | } break; 732 | case OptionDefinition::kDouble: 733 | { 734 | // TODO: Use std::from_chars when library support is available 735 | return_val = std::stof(std::string{value_str}); 736 | } break; 737 | case OptionDefinition::kString: 738 | { 739 | return_val = std::string{value_str}; 740 | } break; 741 | } 742 | 743 | return false; 744 | } 745 | 746 | } // namespace d2m 747 | -------------------------------------------------------------------------------- /src/core/status.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * status.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines Status and ModuleException, which are used 6 | * for handling errors and warnings. 7 | */ 8 | 9 | #include "core/status.h" 10 | 11 | #include 12 | 13 | namespace d2m { 14 | 15 | void Status::PrintError(bool use_std_err) const 16 | { 17 | if (!ErrorOccurred()) { return; } 18 | 19 | if (use_std_err) { std::cerr << error_->what() << "\n"; } 20 | else { std::cout << error_->what() << "\n"; } 21 | } 22 | 23 | void Status::PrintWarnings(bool use_std_err) const 24 | { 25 | if (!warning_messages_.empty()) 26 | { 27 | for (const auto& message : warning_messages_) 28 | { 29 | if (use_std_err) { std::cerr << message << "\n"; } 30 | else { std::cout << message << "\n"; } 31 | } 32 | } 33 | } 34 | 35 | auto Status::HandleResults() const -> bool 36 | { 37 | PrintError(); 38 | 39 | /* 40 | std::string action_str; 41 | switch (category_) 42 | { 43 | case Category::kNone: 44 | action_str = "init"; break; 45 | case Category::kImport: 46 | action_str = "import"; break; 47 | case Category::kExport: 48 | action_str = "export"; break; 49 | case Category::kConvert: 50 | action_str = "conversion"; break; 51 | } 52 | */ 53 | 54 | if (WarningsIssued()) 55 | { 56 | if (ErrorOccurred()) { std::cerr << "\n"; } 57 | 58 | //const std::string plural = warning_messages_.size() > 1 ? "s" : ""; 59 | //std::cout << "Warning" << plural << " issued during " << action_str << ":\n"; 60 | 61 | PrintWarnings(); 62 | } 63 | return ErrorOccurred(); 64 | } 65 | 66 | auto ModuleException::CreateCommonErrorMessage(Category category, int error_code, std::string_view arg) -> std::string 67 | { 68 | switch (category) 69 | { 70 | case Category::kNone: break; 71 | case Category::kImport: 72 | switch (static_cast(error_code)) 73 | { 74 | case ImportError::kSuccess: return "No error."; 75 | default: break; 76 | } 77 | break; 78 | case Category::kExport: 79 | switch (static_cast(error_code)) 80 | { 81 | case ExportError::kSuccess: return "No error."; 82 | case ExportError::kFileOpen: return "Failed to open file for writing."; 83 | default: break; 84 | } 85 | break; 86 | case Category::kConvert: 87 | switch (static_cast(error_code)) 88 | { 89 | case ConvertError::kSuccess: return "No error."; 90 | case ConvertError::kUnsuccessful: // This is the only convert error applied to the input module. 91 | return "Module conversion was unsuccessful. See the output module's status for more information."; 92 | case ConvertError::kInvalidArgument: return "Invalid argument."; 93 | case ConvertError::kUnsupportedInputType: 94 | return "Input type '" + std::string{arg} + "' is unsupported for this module."; 95 | default: break; 96 | } 97 | break; 98 | } 99 | return ""; 100 | } 101 | 102 | } // namespace d2m 103 | -------------------------------------------------------------------------------- /src/dmf2mod.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * dmf2mod.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * See dmf2mod.h 6 | */ 7 | 8 | #include "dmf2mod.h" 9 | -------------------------------------------------------------------------------- /src/modules/debug.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * debug.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * A debug "module" for debug builds 6 | */ 7 | 8 | #include "modules/debug.h" 9 | 10 | #ifndef NDEBUG 11 | 12 | #include "dmf2mod.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | namespace d2m { 19 | 20 | void Debug::ImportImpl(const std::string& filename) 21 | { 22 | // Not implemented 23 | throw NotImplementedException{}; 24 | } 25 | 26 | void Debug::ConvertImpl(const ModulePtr& input) 27 | { 28 | dump_.clear(); 29 | if (!input) 30 | { 31 | throw MODException(ModuleException::Category::kConvert, ModuleException::ConvertError::kInvalidArgument); 32 | } 33 | 34 | const auto options = GetOptions()->Cast(); 35 | const auto append = options->Append(); 36 | const auto flags = options->GenDataFlags(); 37 | 38 | const bool verbose = GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kVerbose).GetValue(); 39 | if (verbose) 40 | { 41 | std::cout << "~~~DEBUG~~~\n"; 42 | std::cout << "Append results to log file: " << append << "\n"; 43 | std::cout << "Generated data flags: " << flags << "\n\n"; 44 | } 45 | 46 | [[maybe_unused]] auto result = input->GenerateData(flags); 47 | dump_ += "GenerateData result: " + std::to_string(result) + "\n"; 48 | 49 | static_assert(ChannelState::kCommonCount == 12); 50 | static_assert(ChannelState::kOneShotCommonCount == 3); 51 | 52 | /* 53 | switch (input->GetType()) 54 | { 55 | case ModuleType::kDMF: 56 | { 57 | using Common = ChannelState::ChannelOneShotCommonDefinition; 58 | auto derived = input->Cast(); 59 | auto gen_data = derived->GetGeneratedData(); 60 | 61 | const auto& note_delay = gen_data->Get(); 62 | if (note_delay) 63 | { 64 | for (auto& [index, extremes] : note_delay.value()) 65 | { 66 | if (std::is_same_v::Square, decltype(index)>) 67 | { 68 | dump_ += ""; 69 | } 70 | } 71 | } 72 | 73 | break; 74 | } 75 | case ModuleType::kMOD: 76 | { 77 | auto derived = input->Cast(); 78 | auto gen_data = derived->GetGeneratedData(); 79 | 80 | break; 81 | } 82 | default: 83 | break; 84 | } 85 | */ 86 | } 87 | 88 | void Debug::ExportImpl(const std::string& filename) 89 | { 90 | const auto options = GetOptions()->Cast(); 91 | auto mode = std::ios::out; 92 | if (options->Append()) { mode |= std::ios::app; } 93 | 94 | std::ofstream out_file{filename, mode}; 95 | if (!out_file.is_open()) 96 | { 97 | throw MODException(ModuleException::Category::kExport, ModuleException::ExportError::kFileOpen); 98 | } 99 | 100 | // ... 101 | 102 | out_file.close(); 103 | 104 | const bool verbose = GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kVerbose).GetValue(); 105 | if (verbose) { std::cout << "Wrote log to disk.\n\n"; } 106 | } 107 | 108 | } // namespace d2m 109 | 110 | #endif // !NDEBUG 111 | -------------------------------------------------------------------------------- /src/utils/utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * utils.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * Defines various utility methods used by dmf2mod. 6 | */ 7 | 8 | #include "utils/utils.h" 9 | 10 | #include "core/factory.h" 11 | #include "core/module.h" 12 | 13 | #include 14 | 15 | namespace d2m { 16 | 17 | // File utils 18 | 19 | auto Utils::GetBaseNameFromFilename(std::string_view filename) -> std::string 20 | { 21 | return std::filesystem::path{filename}.stem().string(); 22 | } 23 | 24 | auto Utils::ReplaceFileExtension(std::string_view filename, std::string_view new_file_extension) -> std::string 25 | { 26 | // new_file_extension may or may not contain a dot 27 | return std::filesystem::path{filename}.replace_extension(new_file_extension).string(); 28 | } 29 | 30 | auto Utils::GetFileExtension(std::string_view filename) -> std::string 31 | { 32 | const auto ext = std::filesystem::path{filename}.extension().string(); 33 | if (!ext.empty()) { return ext.substr(1); } // Remove "." 34 | return std::string{}; 35 | } 36 | 37 | auto Utils::FileExists(std::string_view filename) -> bool 38 | { 39 | std::error_code ec; 40 | return std::filesystem::is_regular_file(filename, ec); 41 | } 42 | 43 | // File utils which require Factory initialization 44 | 45 | auto Utils::GetTypeFromFilename(std::string_view filename) -> ModuleType 46 | { 47 | const auto ext = Utils::GetFileExtension(filename); 48 | return GetTypeFromFileExtension(ext); 49 | } 50 | 51 | auto Utils::GetTypeFromFileExtension(std::string_view extension) -> ModuleType 52 | { 53 | if (extension.empty()) { return ModuleType::kNone; } 54 | 55 | for (const auto& [type, info] : Factory::TypeInfo()) 56 | { 57 | if (static_cast*>(info.get())->file_extension == extension) 58 | { 59 | return type; 60 | } 61 | } 62 | 63 | return ModuleType::kNone; 64 | } 65 | 66 | auto Utils::GetTypeFromCommandName(std::string_view command_name) -> ModuleType 67 | { 68 | if (command_name.empty()) { return ModuleType::kNone; } 69 | 70 | for (const auto& [type, info] : Factory::TypeInfo()) 71 | { 72 | if (static_cast*>(info.get())->command_name == command_name) 73 | { 74 | return type; 75 | } 76 | } 77 | 78 | return ModuleType::kNone; 79 | } 80 | 81 | auto Utils::GetExtensionFromType(ModuleType moduleType) -> std::string_view 82 | { 83 | return static_cast*>(Factory::GetInfo(moduleType))->file_extension; 84 | } 85 | 86 | // Command-line arguments and options utils 87 | 88 | auto Utils::GetArgsAsVector(int argc, char** argv) -> std::vector 89 | { 90 | auto args = std::vector(argc, ""); 91 | for (int i = 0; i < argc; i++) 92 | { 93 | args[i] = argv[i]; 94 | } 95 | return args; 96 | } 97 | 98 | } // namespace d2m 99 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", 3 | "name": "dmf2mod", 4 | "dependencies": [ 5 | { 6 | "name": "gcem", 7 | "default-features": false 8 | }, 9 | { 10 | "name": "zstr", 11 | "default-features": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /webapp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Setting up for web app") 2 | 3 | project(dmf2mod_webapp) 4 | 5 | # Source files 6 | set(WEBAPP_SOURCES 7 | ${DMF2MOD_ROOT}/webapp/webapp.cpp 8 | ) 9 | 10 | # Warnings 11 | set(WARNING_FLAGS "-Wall -Wundefined-func-template -Wno-unknown-pragmas -Wno-\#warnings -Werror -Wno-error=unused-variable -Wno-deprecated-non-prototype") 12 | 13 | # WebAssembly flags 14 | set(WASM_COMMON_FLAGS "-fexceptions -s DISABLE_EXCEPTION_CATCHING=0") 15 | set(WASM_COMPILE_FLAGS "-s INLINING_LIMIT=1") 16 | set(WASM_LINKER_FLAGS "-s USE_ZLIB=0 -s ASSERTIONS=1 -s MODULARIZE=0 -s AGGRESSIVE_VARIABLE_ELIMINATION=1 -s NO_EXIT_RUNTIME=1") 17 | set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s FORCE_FILESYSTEM=1 -s EXPORTED_RUNTIME_METHODS=\"['FS']\"") 18 | 19 | set(WASM_FLAGS "${WASM_COMMON_FLAGS} ${WASM_COMPILE_FLAGS}") 20 | set(WASM_LFLAGS "--bind -lidbfs.js ${WASM_LINKER_FLAGS} ${WASM_COMMON_FLAGS}") 21 | 22 | # Determine whether to build WASM or asm.js version 23 | option(USE_WASM "Use WASM (asm.js is used otherwise)" ON) 24 | if (USE_WASM) 25 | message(STATUS "Using WebAssembly") 26 | set(USE_WASM_FLAG "-s WASM=1") 27 | else() 28 | message(STATUS "Using asm.js") 29 | set(USE_WASM_FLAG "-s WASM=0") 30 | endif() 31 | 32 | # Set compiler flags 33 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_FLAGS} ${WARNING_FLAGS}") 34 | message(STATUS "em++ compile flags: ${CMAKE_CXX_FLAGS}") 35 | 36 | # Only CMAKE_CXX_FLAGS and the LINK_FLAGS property have been working for me. Don't bother with EMCC_CFLAGS or CMAKE_EXE_LINKER_FLAGS. 37 | 38 | add_executable(${PROJECT_NAME} ${DMF2MOD_SOURCES} ${WEBAPP_SOURCES}) 39 | 40 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) 41 | set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME dmf2mod) 42 | set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/html) 43 | 44 | # Set linker flags 45 | set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${WASM_LFLAGS} ${USE_WASM_FLAG} ${WARNING_FLAGS} --pre-js ${DMF2MOD_ROOT}/webapp/pre.js") 46 | message(STATUS "em++ linker flags: ${WASM_LFLAGS} ${USE_WASM_FLAG} ${WARNING_FLAGS} --pre-js ${DMF2MOD_ROOT}/webapp/pre.js") 47 | 48 | target_link_libraries(${PROJECT_NAME} PRIVATE gcem zstr::zstr) 49 | target_include_directories(${PROJECT_NAME} PUBLIC ${DMF2MOD_ROOT}/include) 50 | 51 | add_custom_command( 52 | TARGET ${PROJECT_NAME} 53 | POST_BUILD 54 | COMMAND ${CMAKE_COMMAND} -E copy_directory 55 | "${DMF2MOD_ROOT}/webapp/ui" 56 | "${CMAKE_BINARY_DIR}/html" 57 | ) 58 | -------------------------------------------------------------------------------- /webapp/pre.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pre.js 3 | * Written by Dalton Messmer . 4 | * 5 | * Input to emscripten compiler. 6 | */ 7 | 8 | /*global Module, FS, errorMessage, warningMessage, statusMessageIsError*/ 9 | 10 | // Set up IDBFS file system 11 | // From: https://badlydrawnrod.github.io/posts/2020/06/07/emscripten-indexeddb/ 12 | Module["preRun"] = function () { 13 | FS.mkdir("/working"); 14 | FS.mount(IDBFS, {}, "/working"); 15 | } 16 | 17 | Module["print"] = function (e) { 18 | //std::cout redirects to here 19 | console.log(e); 20 | } 21 | 22 | Module["printErr"] = function (e) { 23 | //std::cerr redirects to here 24 | //console.log(e); 25 | if (statusMessageIsError) 26 | errorMessage += e + "\n"; 27 | else 28 | warningMessage += e + "\n"; 29 | } 30 | -------------------------------------------------------------------------------- /webapp/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dmf2mod 5 | 6 | 7 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |

dmf2mod

41 |

Deflemask module converter

42 |
Loading web app...
43 |
44 |
45 |
46 | 58 |
59 |
60 |
61 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /webapp/ui/webapp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * webapp.js 3 | * Written by Dalton Messmer . 4 | * 5 | * Event handlers and helper functions used by the dmf2mod web application 6 | */ 7 | 8 | /*global Module, FS*/ 9 | const app = document.getElementById("dmf2mod_app"); 10 | const appFieldset = document.getElementById("dmf2mod_app_fieldset"); 11 | const statusArea = document.getElementById("status_area"); 12 | 13 | let appInitialised = false; 14 | let errorMessage = ""; 15 | let warningMessage = ""; 16 | let statusMessageIsError = true; 17 | 18 | Module["onRuntimeInitialized"] = function () { 19 | console.log("Loaded dmf2mod"); 20 | } 21 | 22 | Module["postRun"] = function () { 23 | console.log("Initialized dmf2mod"); 24 | loadOptions(); 25 | app.style.display = "block"; 26 | document.getElementById("loading").style.display = "none"; 27 | appInitialised = true; 28 | } 29 | 30 | function setStatusMessage() { 31 | statusArea.innerHTML = ""; 32 | if (errorMessage.length > 0) { 33 | errorMessage = errorMessage.replace("\n", "
"); 34 | statusArea.innerHTML += "
" + errorMessage + "
"; 35 | errorMessage = ""; 36 | } 37 | if (warningMessage.length > 0) { 38 | warningMessage = warningMessage.replace("\n", "
"); 39 | statusArea.innerHTML += "
" + warningMessage + "
"; 40 | warningMessage = ""; 41 | } 42 | } 43 | 44 | function disableControls(disable) { 45 | // TODO: This makes everything appear disabled/enabled, but clicking the Convert button 46 | // multiple times before the convertFile function returns will still make it convert the 47 | // file multiple times. It's as if the button is never disabled. I've tried for hours to 48 | // fix it and cannot do so. 49 | appFieldset.disabled = disable; 50 | } 51 | 52 | function loadOptions() { 53 | let optionsElement = document.getElementById("options"); 54 | optionsElement.innerHTML = ""; 55 | 56 | const availMods = Module.getAvailableModules(); 57 | if (availMods.size() === 0) { 58 | optionsElement.innerHTML += "
ERROR: No supported modules"; 59 | return; 60 | } 61 | 62 | // Add available modules to convert to 63 | for (let i = 0; i < availMods.size(); i++) { 64 | const mtype = availMods.get(i); 65 | const m = Module.getExtensionFromType(mtype); 66 | 67 | // Add radio button 68 | let typesHTML = "" + m + ""; 73 | 74 | optionsElement.innerHTML += typesHTML; 75 | } 76 | 77 | // Add module-specific command-line options 78 | for (let i = 0; i < availMods.size(); i++) { 79 | const mtype = availMods.get(i); 80 | const m = Module.getExtensionFromType(mtype); 81 | let optionsHTML = "
"; 84 | } else { 85 | optionsHTML += " none;'>"; 86 | } 87 | 88 | const optionDefs = Module.getOptionDefinitions(mtype); 89 | if (optionDefs.size() === 0) { 90 | optionsElement.innerHTML += optionsHTML + "
(No options)
"; 91 | continue; 92 | } 93 | 94 | optionsHTML += "

"; 95 | 96 | for (let j = 0; j < optionDefs.size(); j++) { 97 | const o = optionDefs.get(j); 98 | const nameHTML = "" + o.displayName + " "; 99 | 100 | if (o.acceptedValues.size() === 0) { 101 | switch (o.valueType) { 102 | case Module.OptionValueType.BOOL: 103 | const defaultIsChecked = o.defaultValue === "true" ? true : false; 104 | optionsHTML += nameHTML + getSliderHTML(m, o.name, defaultIsChecked); 105 | break; 106 | case Module.OptionValueType.INT: 107 | optionsHTML += nameHTML + getNumberHTML(m, o.name, true, o.defaultValue); 108 | break; 109 | case Module.OptionValueType.DOUBLE: 110 | optionsHTML += nameHTML + getNumberHTML(m, o.name, false, o.defaultValue); 111 | break; 112 | case Module.OptionValueType.STRING: 113 | optionsHTML += nameHTML + getTextboxHTML(m, o.name, o.defaultValue); 114 | break; 115 | } 116 | } else { 117 | optionsHTML += nameHTML + getDropdownHTML(m, o.name, o.acceptedValues, o.defaultValue); 118 | } 119 | optionsHTML += "
"; 120 | } 121 | optionsElement.innerHTML += optionsHTML + ""; 122 | } 123 | } 124 | 125 | function getSliderHTML(id, optionName, checked) { 126 | let html = ""; 140 | return html; 141 | } 142 | 143 | function getTextboxHTML(id, optionName, defaultValue) { 144 | return ""; 145 | } 146 | 147 | function getDropdownHTML(id, optionName, dropdownOptions, defaultValue) { 148 | let html = ""; 158 | return html; 159 | } 160 | 161 | function onOutputTypeChange(elem) { 162 | // Hide command-line options for all modules 163 | let elements = document.getElementsByClassName("module_options"); 164 | for (const element of elements) { 165 | element.style.display = "none"; 166 | } 167 | 168 | // Show command-line options for the current module 169 | const optionsId = "div_" + elem.value; 170 | document.getElementById(optionsId).style.display = "block"; 171 | } 172 | 173 | function getOutputType() { 174 | return document.querySelector("input[name='output_type']:checked").value; 175 | } 176 | 177 | function getOptions() { 178 | const currentOutputModule = getOutputType(); 179 | const optionsForCurrentModule = document.getElementsByClassName(currentOutputModule + "_option"); 180 | 181 | let vo = new Module.VectorOption(); 182 | 183 | const len = optionsForCurrentModule.length; 184 | for (let i = 0; i < len; i++) { 185 | const option = optionsForCurrentModule[i]; 186 | 187 | if (option.nodeName.toLowerCase() == "input" && option.type == "checkbox") { 188 | vo.push_back([option.name, option.checked ? "true" : "false"]); 189 | } else { 190 | vo.push_back([option.name, option.value]); 191 | } 192 | } 193 | return vo; 194 | } 195 | 196 | async function importFileLocal(externalFile, internalFilename) { 197 | if (!appInitialised) { 198 | return true; 199 | } 200 | 201 | // Convert blob to Uint8Array (more abstract: ArrayBufferView) 202 | let data = new Uint8Array(await externalFile.arrayBuffer()); 203 | 204 | // Store the file 205 | let stream = FS.open(internalFilename, "w+"); 206 | FS.write(stream, data, 0, data.length, 0); 207 | FS.close(stream); 208 | 209 | return false; 210 | } 211 | 212 | // From: https://stackoverflow.com/questions/63959571/how-do-i-pass-a-file-blob-from-javascript-to-emscripten-webassembly-c 213 | async function importFileOnline(url, internalFilename) { 214 | if (!appInitialised) { 215 | return true; 216 | } 217 | 218 | // Download a file 219 | let blob; 220 | try { 221 | blob = await fetch(url).then(response => { 222 | if (!response.ok) { 223 | return true; 224 | } 225 | return response.blob(); 226 | }); 227 | } catch (error) { 228 | console.log("importFileOnline: " + error); 229 | return true; 230 | } 231 | 232 | if (!blob) { 233 | console.log("importFileOnline: Bad response."); 234 | return true; 235 | } 236 | 237 | // Convert blob to Uint8Array (more abstract: ArrayBufferView) 238 | let data = new Uint8Array(await blob.arrayBuffer()); 239 | 240 | // Store the file 241 | let stream = FS.open(internalFilename, "w+"); 242 | FS.write(stream, data, 0, data.length, 0); 243 | FS.close(stream); 244 | 245 | return false; 246 | } 247 | 248 | async function convertFile() { 249 | disableControls(true); 250 | 251 | errorMessage = ""; 252 | warningMessage = ""; 253 | setStatusMessage(); 254 | 255 | // Check if input file was provided 256 | const inputFileElem = document.getElementById("input_file"); 257 | if (inputFileElem.value.length === 0) { 258 | disableControls(false); 259 | return true; 260 | } 261 | 262 | // Get input file and input filename to be used internally 263 | const externalFile = inputFileElem.files.item(0); 264 | const internalFilenameInput = externalFile.name; 265 | 266 | // Change file extension to get internal filename for output from dmf2mod 267 | const outputType = getOutputType(); 268 | const options = getOptions(); 269 | let internalFilenameOutput = internalFilenameInput.replace(/\.[^/.]+$/, "") + "." + outputType; 270 | 271 | if (internalFilenameInput == internalFilenameOutput) { 272 | // No conversion needed: Converting to same type 273 | warningMessage = "Same module type; No conversion needed"; 274 | setStatusMessage(); 275 | disableControls(false); 276 | return true; 277 | } 278 | 279 | let resp = await importFileLocal(externalFile, internalFilenameInput); 280 | if (resp) { 281 | disableControls(false); 282 | return true; 283 | } 284 | 285 | resp = Module.moduleImport(internalFilenameInput); 286 | setStatusMessage(); 287 | if (resp) { 288 | disableControls(false); 289 | return true; 290 | } 291 | 292 | let stream = FS.open(internalFilenameOutput, "w+"); 293 | FS.close(stream); 294 | 295 | const result = Module.moduleConvert(internalFilenameOutput, options); 296 | setStatusMessage(); 297 | if (result) { 298 | disableControls(false); 299 | return true; 300 | } 301 | 302 | const byteArray = FS.readFile(internalFilenameOutput); 303 | const blob = new Blob([byteArray]); 304 | 305 | let a = document.createElement("a"); 306 | a.download = internalFilenameOutput; 307 | a.href = URL.createObjectURL(blob); 308 | a.click(); 309 | 310 | disableControls(false); 311 | } 312 | 313 | app.addEventListener("submit", function () { 314 | convertFile(); 315 | }, false); 316 | -------------------------------------------------------------------------------- /webapp/webapp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * webapp.cpp 3 | * Written by Dalton Messmer . 4 | * 5 | * A dmf2mod wrapper for WebAssembly. 6 | */ 7 | 8 | #include "dmf2mod.h" 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | using namespace d2m; 16 | 17 | // NOTE: When using std::cout, make sure you end it with a newline to flush output. Otherwise nothing will appear. 18 | 19 | static ModulePtr kModule; 20 | static std::string kInputFilename; 21 | 22 | struct OptionDefinitionWrapper 23 | { 24 | using Type = OptionDefinition::Type; 25 | int id; 26 | OptionType option_type; 27 | Type value_type; 28 | std::string name; 29 | std::string display_name; 30 | std::string default_value; 31 | std::vector accepted_values; 32 | std::string description; 33 | }; 34 | 35 | struct OptionWrapper 36 | { 37 | std::string name; 38 | std::string value; 39 | }; 40 | 41 | static auto WrapOptionDefinition(const OptionDefinition& definition) -> OptionDefinitionWrapper; 42 | static auto UnwrapOptions(ConversionOptionsPtr& options, const std::vector& options_wrapped) -> bool; 43 | static void SetStatusType(bool is_error); 44 | 45 | auto main() -> int 46 | { 47 | // Initialize global options (for web app, user won't provide them) 48 | GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kForce).SetValue(true); 49 | GlobalOptions::Get().GetOption(GlobalOptions::OptionEnum::kVerbose).SetValue(false); 50 | 51 | return 0; 52 | } 53 | 54 | ////////////////////////// 55 | // Exported functions // 56 | ////////////////////////// 57 | 58 | /* 59 | * Returns a vector of ints representing the module type that are supported. 60 | * Int is used instead of ModuleType to avoid the need to redefine the ModuleType 61 | * enum in the Emscripten binding. 62 | */ 63 | auto GetAvailableModulesWrapper() -> std::vector 64 | { 65 | std::vector int_vec; 66 | const std::vector modules = Factory::GetInitializedTypes(); 67 | std::transform(modules.cbegin(), modules.cend(), std::back_inserter(int_vec), [](ModuleType m){ return static_cast(m); }); 68 | return int_vec; 69 | } 70 | 71 | /* 72 | * Returns the module file extension when given a module type 73 | */ 74 | auto GetExtensionFromTypeWrapper(int module_type) -> std::string 75 | { 76 | return std::string{Utils::GetExtensionFromType(static_cast(module_type))}; 77 | } 78 | 79 | /* 80 | * Returns a vector of option definitions for the given module type 81 | */ 82 | auto GetOptionDefinitionsWrapper(int module_type) -> std::vector 83 | { 84 | auto options = Factory::GetInfo(static_cast(module_type))->option_definitions; 85 | std::vector ret; 86 | 87 | if (options.Count() == 0) { return ret; } 88 | for (const auto& map_pair : options.GetIdMap()) 89 | { 90 | ret.push_back(WrapOptionDefinition(map_pair.second)); 91 | } 92 | 93 | return ret; 94 | } 95 | 96 | /* 97 | * Imports and stores module from specified filename 98 | * Returns true upon failure 99 | */ 100 | auto ModuleImport(std::string filename) -> bool 101 | { 102 | SetStatusType(true); 103 | 104 | const ModuleType input_type = Utils::GetTypeFromFilename(filename); 105 | if (input_type == ModuleType::kNone) 106 | { 107 | std::cerr << "The input file is not recognized as a supported module type.\n\n"; 108 | return true; 109 | } 110 | 111 | kInputFilename = filename; 112 | 113 | kModule = Factory::Create(input_type); 114 | if (!kModule) 115 | { 116 | std::cerr << "Error during import:\n"; 117 | std::cerr << "ERROR: Not enough memory.\n"; 118 | return true; 119 | } 120 | 121 | kModule->Import(filename); 122 | 123 | if (kModule->GetStatus().ErrorOccurred()) 124 | { 125 | std::cerr << "Errors during import:\n"; 126 | kModule->GetStatus().PrintError(); 127 | return true; 128 | } 129 | 130 | if (kModule->GetStatus().WarningsIssued()) 131 | { 132 | SetStatusType(false); 133 | std::cerr << "Warnings during import:\n"; 134 | kModule->GetStatus().PrintWarnings(true); 135 | } 136 | 137 | return false; 138 | } 139 | 140 | /* 141 | * Converts the previously imported module to a module of the given file extension. 142 | * Returns true if an error occurred, or false if successful. 143 | */ 144 | auto ModuleConvert(std::string output_filename, const std::vector& options_wrapped) -> bool 145 | { 146 | if (!kModule) { return true; } // Need to import the module first 147 | if (output_filename.empty()) { return true; } // Invalid argument 148 | if (output_filename == kInputFilename) { return true; } // Same type; No conversion necessary 149 | 150 | SetStatusType(true); 151 | const auto module_type = Utils::GetTypeFromFilename(output_filename); 152 | if (module_type == ModuleType::kNone) 153 | { 154 | std::cerr << "The output file is not recognized as a supported module type.\n\n"; 155 | return true; 156 | } 157 | 158 | // Create conversion options object 159 | ConversionOptionsPtr options = Factory::Create(module_type); 160 | if (!options) 161 | { 162 | std::cerr << "Error occurred when creating ConversionOptions object. Likely a registration issue.\n\n"; 163 | return true; // Registration issue 164 | } 165 | 166 | // Set options 167 | if (UnwrapOptions(options, options_wrapped)) { return true; } // Error unwrapping options 168 | 169 | ModulePtr output = kModule->Convert(module_type, options); 170 | if (!output) { return true; } 171 | 172 | if (output->GetStatus().ErrorOccurred()) 173 | { 174 | SetStatusType(true); 175 | std::cerr << "Error during conversion:\n"; 176 | output->GetStatus().PrintError(); 177 | return true; 178 | } 179 | 180 | if (output->GetStatus().WarningsIssued()) 181 | { 182 | SetStatusType(false); 183 | std::cerr << "Warning(s) during conversion:\n"; 184 | output->GetStatus().PrintWarnings(true); 185 | } 186 | 187 | SetStatusType(true); 188 | 189 | if (output->Export(output_filename)) 190 | { 191 | std::cerr << "Error during export:\n"; 192 | output->GetStatus().PrintError(); 193 | return true; 194 | } 195 | 196 | return false; 197 | } 198 | 199 | 200 | //////////////////////// 201 | // Helper functions // 202 | //////////////////////// 203 | 204 | static auto WrapOptionDefinition(const OptionDefinition& definition) -> OptionDefinitionWrapper 205 | { 206 | OptionDefinitionWrapper ret; 207 | ret.id = definition.GetId(); 208 | ret.option_type = definition.GetOptionType(); 209 | ret.value_type = definition.GetValueType(); 210 | ret.name = definition.HasName() ? definition.GetName() : std::string(1, definition.GetShortName()); 211 | ret.display_name = definition.GetDisplayName(); 212 | ret.default_value = ModuleOptionUtils::ConvertToString(definition.GetDefaultValue()); 213 | ret.description = definition.GetDescription(); 214 | 215 | ret.accepted_values.clear(); 216 | for (const auto& val : definition.GetAcceptedValuesOrdered()) 217 | { 218 | ret.accepted_values.push_back(ModuleOptionUtils::ConvertToString(val)); 219 | } 220 | 221 | return ret; 222 | } 223 | 224 | static auto UnwrapOptions(ConversionOptionsPtr& options, const std::vector& options_wrapped) -> bool 225 | { 226 | for (const auto& option_wrapped : options_wrapped) 227 | { 228 | auto& option = options->GetOption(option_wrapped.name); 229 | 230 | OptionDefinition::ValueType val; 231 | if (ModuleOptionUtils::ConvertToValue(option_wrapped.value, option.GetDefinition()->GetValueType(), val)) 232 | { 233 | return true; // Error converting string to ValueType 234 | } 235 | 236 | option.SetValue(std::move(val)); 237 | } 238 | return false; 239 | } 240 | 241 | static void SetStatusType(bool is_error) 242 | { 243 | EM_ASM({ 244 | statusMessageIsError = $0; 245 | }, is_error); 246 | } 247 | 248 | 249 | /////////////////////////// 250 | // Emscripten Bindings // 251 | /////////////////////////// 252 | 253 | EMSCRIPTEN_BINDINGS(dmf2mod) { 254 | // Register functions 255 | emscripten::function("getAvailableModules", &GetAvailableModulesWrapper); 256 | emscripten::function("getExtensionFromType", &GetExtensionFromTypeWrapper); 257 | emscripten::function("getOptionDefinitions", &GetOptionDefinitionsWrapper); 258 | emscripten::function("moduleImport", &ModuleImport); 259 | emscripten::function("moduleConvert", &ModuleConvert); 260 | 261 | // Register vectors 262 | emscripten::register_vector("VectorInt"); 263 | emscripten::register_vector("VectorString"); 264 | emscripten::register_vector("VectorOptionDefinition"); 265 | emscripten::register_vector("VectorOption"); 266 | 267 | // Register enums 268 | emscripten::enum_("OptionType") 269 | .value("OPTION", kOption) 270 | .value("COMMAND", kCommand); 271 | 272 | emscripten::enum_("OptionValueType") 273 | .value("BOOL", OptionDefinitionWrapper::Type::kBool) 274 | .value("INT", OptionDefinitionWrapper::Type::kInt) 275 | .value("DOUBLE", OptionDefinitionWrapper::Type::kDouble) 276 | .value("STRING", OptionDefinitionWrapper::Type::kString); 277 | 278 | // Register structs 279 | emscripten::value_object("OptionDefinition") 280 | .field("id", &OptionDefinitionWrapper::id) 281 | .field("optionType", &OptionDefinitionWrapper::option_type) 282 | .field("valueType", &OptionDefinitionWrapper::value_type) 283 | .field("name", &OptionDefinitionWrapper::name) 284 | .field("displayName", &OptionDefinitionWrapper::display_name) 285 | .field("defaultValue", &OptionDefinitionWrapper::default_value) 286 | .field("acceptedValues", &OptionDefinitionWrapper::accepted_values) 287 | .field("description", &OptionDefinitionWrapper::description); 288 | 289 | emscripten::value_array("Option") 290 | .element(&OptionWrapper::name) 291 | .element(&OptionWrapper::value); 292 | } 293 | --------------------------------------------------------------------------------