diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e060924f..6d356bedb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,10 @@ jobs: - { name: 'Windows MSVC x86_64', os: windows-latest, compiler: msvc, arch: x86_64 } - { name: 'Windows MinGW x86', os: ubuntu-20.04, compiler: mingw, arch: x86 } - { name: 'Windows MinGW x86_64', os: ubuntu-20.04, compiler: mingw, arch: x86_64 } - - { name: 'macOS', os: macos-latest } - - { name: 'Ubuntu', os: ubuntu-18.04 } + - { name: 'macOS x86_64', os: macos-latest, arch: x86_64 } + - { name: 'macOS ARM', os: macos-latest, arch: arm64 } + - { name: 'Linux x86_64', os: ubuntu-18.04, arch: x86_64 } + - { name: 'Linux ARM', os: ubuntu-18.04, arch: armhf } fail-fast: false name: ${{ matrix.config.name }} @@ -76,10 +78,10 @@ jobs: package_name="${package_name}-${{ matrix.config.arch }}" package_ext="" # Directory, uploading will automatically zip it elif [ '${{ runner.os }}' == 'macOS' ]; then - package_name="${package_name}-macOS" + package_name="${package_name}-macOS-${{ matrix.config.arch }}" package_ext=".dmg" else - package_name="${package_name}-Linux" + package_name="${package_name}-Linux-${{ matrix.config.arch }}" package_ext=".AppImage" fi @@ -116,8 +118,8 @@ jobs: mingw-w64 \ mingw-w64-tools - - name: Install Dependencies [Ubuntu] - if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + - name: Install Dependencies [Linux x86_64] + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'x86_64' }} run: | sudo apt update sudo apt install \ @@ -131,8 +133,31 @@ jobs: wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" chmod +x appimagetool-x86_64.AppImage + - name: Install Dependencies [Linux armhf] + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'armhf' }} + run: | + sudo sed -ri "s/^deb /deb [arch=amd64] /" /etc/apt/sources.list + echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ bionic main universe" | sudo tee -a /etc/apt/sources.list + echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main universe" | sudo tee -a /etc/apt/sources.list + sudo dpkg --add-architecture armhf + sudo apt update + sudo apt install \ + crossbuild-essential-armhf \ + appstream + sudo apt install \ + libsdl2-dev:armhf \ + libfmt-dev:armhf \ + librtmidi-dev:armhf \ + libsndfile1-dev:armhf \ + zlib1g-dev:armhf \ + libjack-jackd2-dev:armhf + wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-armhf" + chmod +x appimagetool-x86_64.AppImage + ls /usr/arm-linux-gnueabihf/lib + - name: Configure (System Libraries) - if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'x86_64' }} run: | export USE_WAE=ON export CMAKE_EXTRA_ARGS=() @@ -163,7 +188,7 @@ jobs: "${CMAKE_EXTRA_ARGS[@]}" - name: Build (System Libraries) - if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'x86_64' }} run: | cmake \ --build ${PWD}/build \ @@ -171,14 +196,14 @@ jobs: --parallel ${{ steps.build-cores.outputs.amount }} - name: Install (System Libraries) - if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'x86_64' }} run: | cmake \ --install ${PWD}/build \ --config ${{ env.BUILD_TYPE }} - name: Cleanup (System Libraries) - if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'x86_64' }} run: | rm -rf build/ target/ @@ -201,7 +226,13 @@ jobs: elif [ '${{ matrix.config.compiler }}' == 'mingw' ]; then CMAKE_EXTRA_ARGS+=('-DCMAKE_TOOLCHAIN_FILE=scripts/Cross-MinGW-${{ steps.windows-identify.outputs.mingw-target }}.cmake') elif [ '${{ runner.os }}' == 'macOS' ]; then - CMAKE_EXTRA_ARGS+=('-DCMAKE_OSX_DEPLOYMENT_TARGET="10.9"') + if [ '${{ matrix.config.arch }}' == 'arm64' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_OSX_DEPLOYMENT_TARGET="11.0"' '-DCMAKE_OSX_ARCHITECTURES=arm64') + else + CMAKE_EXTRA_ARGS+=('-DCMAKE_OSX_DEPLOYMENT_TARGET="10.9"') + fi + elif [ '${{ runner.os }}' == 'Linux' ] && [ '${{ matrix.config.arch }}' == 'armhf' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_TOOLCHAIN_FILE=scripts/Cross-Linux-armhf.cmake') fi cmake \ @@ -255,7 +286,7 @@ jobs: mv Furnace-*-Darwin.dmg ../${{ steps.package-identify.outputs.filename }} popd - - name: Package [Ubuntu] + - name: Package [Linux] if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} run: | #if [ '${{ env.BUILD_TYPE }}' == 'Release' ]; then @@ -273,7 +304,11 @@ jobs: cp -v ../../res/AppRun ./ popd - ../appimagetool-x86_64.AppImage furnace.AppDir + if [ '${{ matrix.config.arch }}' == 'armhf' ]; then + ../appimagetool-x86_64.AppImage --runtime-file=../runtime-armhf furnace.AppDir + else + ../appimagetool-x86_64.AppImage furnace.AppDir + fi mv Furnace-*.AppImage ../${{ steps.package-identify.outputs.filename }} popd diff --git a/CMakeLists.txt b/CMakeLists.txt index c808c9a7b..c24a09525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,8 @@ set(USE_SDL2_DEFAULT ON) set(USE_SNDFILE_DEFAULT ON) set(SYSTEM_SDL2_DEFAULT OFF) +include(CheckIncludeFile) + if (ANDROID) set(USE_RTMIDI_DEFAULT OFF) set(USE_BACKWARD_DEFAULT OFF) @@ -31,7 +33,16 @@ if (ANDROID) endif() else() set(USE_RTMIDI_DEFAULT ON) - set(USE_BACKWARD_DEFAULT ON) + if (WIN32 OR APPLE) + set(USE_BACKWARD_DEFAULT ON) + else() + CHECK_INCLUDE_FILE(execinfo.h EXECINFO_FOUND) + if (EXECINFO_FOUND) + set(USE_BACKWARD_DEFAULT ON) + else() + set(USE_BACKWARD_DEFAULT OFF) + endif() + endif() endif() find_package(PkgConfig) @@ -55,6 +66,8 @@ option(SYSTEM_RTMIDI "Use a system-installed version of RtMidi instead of the ve option(SYSTEM_ZLIB "Use a system-installed version of zlib instead of the vendored one" OFF) option(SYSTEM_SDL2 "Use a system-installed version of SDL2 instead of the vendored one" ${SYSTEM_SDL2_DEFAULT}) option(WARNINGS_ARE_ERRORS "Whether warnings in furnace's C++ code should be treated as errors" OFF) +option(WITH_DEMOS "Install demo songs" ON) +option(WITH_INSTRUMENTS "Install instruments" ON) set(DEPENDENCIES_INCLUDE_DIRS "") @@ -221,6 +234,11 @@ if (USE_SDL2) set(SDL_SHARED OFF CACHE BOOL "Force no dynamically-linked SDL" FORCE) set(SDL_STATIC ON CACHE BOOL "Force statically-linked SDL" FORCE) endif() + # https://github.com/libsdl-org/SDL/issues/5535 + # disable PipeWire support due to an unfixable bug: + # Looks like their headers have a C90 violation... I imagine they're probably on C99 so not the craziest bug in the world. Definitely file this at the PipeWire repository as well so they know this is out there. + set(SDL_PIPEWIRE OFF CACHE BOOL "Use Pipewire audio" FORCE) + # https://github.com/libsdl-org/SDL/issues/1481 # On 2014-06-22 17:15:50 +0000, Sam Lantinga wrote: # If you link SDL statically, you also need to define HAVE_LIBC so it builds with the C runtime that your application uses. @@ -450,6 +468,7 @@ src/engine/platform/scc.cpp src/engine/platform/ymz280b.cpp src/engine/platform/namcowsg.cpp src/engine/platform/rf5c68.cpp +src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp ) @@ -463,6 +482,10 @@ if (WIN32) list(APPEND ENGINE_SOURCES res/furnace.rc) endif() +set(CLI_SOURCES +src/cli/cli.cpp +) + set(GUI_SOURCES extern/imgui_patched/imgui.cpp extern/imgui_patched/imgui_draw.cpp @@ -510,6 +533,7 @@ src/gui/midiMap.cpp src/gui/newSong.cpp src/gui/orders.cpp src/gui/osc.cpp +src/gui/patManager.cpp src/gui/pattern.cpp src/gui/piano.cpp src/gui/presets.cpp @@ -545,14 +569,17 @@ endif() if (NOT WIN32 AND NOT APPLE) list(APPEND GUI_SOURCES src/gui/icon.c) - include(CheckIncludeFile) - CHECK_INCLUDE_FILE(sys/io.h SYS_IO_FOUND) CHECK_INCLUDE_FILE(linux/input.h LINUX_INPUT_FOUND) CHECK_INCLUDE_FILE(linux/kd.h LINUX_KD_FOUND) if (SYS_IO_FOUND) - list(APPEND DEPENDENCIES_DEFINES HAVE_SYS_IO) - message(STATUS "PC speaker output: outb()") + try_compile(HAVE_INOUTB ${CMAKE_BINARY_DIR}/check SOURCES ${CMAKE_SOURCE_DIR}/src/check/check_sysIO.c) + if (HAVE_INOUTB) + list(APPEND DEPENDENCIES_DEFINES HAVE_SYS_IO) + message(STATUS "PC speaker output: outb()") + else() + message(STATUS "sys/io.h found but inb()/outb() not present") + endif() endif() if (LINUX_INPUT_FOUND) list(APPEND DEPENDENCIES_DEFINES HAVE_LINUX_INPUT) @@ -564,13 +591,24 @@ if (NOT WIN32 AND NOT APPLE) endif() endif() -set(USED_SOURCES ${ENGINE_SOURCES} ${AUDIO_SOURCES} src/main.cpp) +if (NOT WIN32) + try_compile(HAVE_DIRENT_TYPE ${CMAKE_BINARY_DIR}/check SOURCES ${CMAKE_SOURCE_DIR}/src/check/check_dirent_type.c) + if (HAVE_DIRENT_TYPE) + list(APPEND DEPENDENCIES_DEFINES HAVE_DIRENT_TYPE) + endif() +endif() + +set(USED_SOURCES ${ENGINE_SOURCES} ${AUDIO_SOURCES} ${CLI_SOURCES} src/main.cpp) if (USE_BACKWARD) list(APPEND USED_SOURCES src/backtrace.cpp) if (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") list(APPEND DEPENDENCIES_LIBRARIES dbghelp psapi) endif() + find_library(EXECINFO_IS_LIBRARY execinfo) + if (EXECINFO_IS_LIBRARY) + list(APPEND DEPENDENCIES_LIBRARIES execinfo) + endif() message(STATUS "Using backward-cpp") else() message(STATUS "Not using backward-cpp") @@ -669,24 +707,29 @@ if (PKG_CONFIG_FOUND AND (SYSTEM_FMT OR SYSTEM_LIBSNDFILE OR SYSTEM_ZLIB OR SYST endif() if (NOT ANDROID OR TERMUX) - install(TARGETS furnace RUNTIME DESTINATION bin) - if (NOT WIN32 AND NOT APPLE) include(GNUInstallDirs) + install(TARGETS furnace RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES res/furnace.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES res/furnace.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(DIRECTORY papers DESTINATION ${CMAKE_INSTALL_DOCDIR}) install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/furnace) - install(DIRECTORY demos DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace) - install(DIRECTORY instruments DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace) + if (WITH_DEMOS) + install(DIRECTORY demos DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace) + endif() + if (WITH_INSTRUMENTS) + install(DIRECTORY instruments DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace) + endif() foreach(num 16 32 64 128 256 512) set(res ${num}x${num}) install(FILES res/icon.iconset/icon_${res}.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${res}/apps) install(FILES res/icon.iconset/icon_${res}@2x.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${res}@2/apps) endforeach() install(FILES res/logo.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/1024x1024/apps) + else() + install(TARGETS furnace RUNTIME DESTINATION bin) endif() - + set(CPACK_PACKAGE_NAME "Furnace") set(CPACK_PACKAGE_VENDOR "tildearrow") set(CPACK_PACKAGE_DESCRIPTION "free and open-source chiptune tracker") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97efdf8cd..51903d8b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,11 @@ the coding style is described here: - don't use `auto` unless needed. - use `String` for `std::string` (this is typedef'd in ta-utils.h). - prefer using operator for String (std::string) comparisons (a==""). +- if you have to work with C strings, only use safe C string operations: + - snprintf + - strncpy + - strncat + - any other operation which specifies a limit some files (particularly the ones in `src/engine/platform/sound` and `extern/`) don't follow this style. diff --git a/README.md b/README.md index b75db0d05..d2308dd75 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a - SID (6581/8580) used in Commodore 64 - Mikey used in Atari Lynx - ZX Spectrum beeper (SFX-like engine) + - Commodore PET - TIA used in Atari 2600 - Game Boy - modern/fantasy: @@ -202,6 +203,8 @@ Available options: | `SYSTEM_ZLIB` | `OFF` | Use a system-installed version of zlib instead of the vendored one | | `SYSTEM_SDL2` | `OFF` | Use a system-installed version of SDL2 instead of the vendored one | | `WARNINGS_ARE_ERRORS` | `OFF` (but consider enabling this & reporting any errors that arise from it!) | Whether warnings in furnace's C++ code should be treated as errors | +| `WITH_DEMOS` | `ON` | Install demo songs on `make install` | +| `WITH_INSTRUMENTS` | `ON` | Install demo instruments on `make install` | ## console usage diff --git a/TODO.md b/TODO.md index 5032ea32e..13f31300e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,5 @@ # to-do for 0.6pre1.5-0.6pre2 -- rewrite the system name detection function anyway - - this involves the addition of a new "system" field in the song (which solves the problem) - - songs made in older versions will go through old system name detection for compatibility -- Game Boy envelope macro/sequence - volume commands should work on Game Boy - ability to customize `OFF`, `===` and `REL` - stereo separation control for AY diff --git a/demos/Bullet_Hell.fur b/demos/Bullet_Hell.fur new file mode 100644 index 000000000..48c50f674 Binary files /dev/null and b/demos/Bullet_Hell.fur differ diff --git a/demos/Egyptian_Rule.fur b/demos/Egyptian_Rule.fur new file mode 100644 index 000000000..1715906ec Binary files /dev/null and b/demos/Egyptian_Rule.fur differ diff --git a/demos/Melody_of_Certain_Feelings.fur b/demos/Melody_of_Certain_Feelings.fur deleted file mode 100644 index 6ccf4e13c..000000000 Binary files a/demos/Melody_of_Certain_Feelings.fur and /dev/null differ diff --git a/demos/MetalSlug_BaseCamp_SMS_TIA.fur b/demos/MetalSlug_BaseCamp_SMS_TIA.fur new file mode 100644 index 000000000..dd2fa6af3 Binary files /dev/null and b/demos/MetalSlug_BaseCamp_SMS_TIA.fur differ diff --git a/demos/Phoenix_cover.fur b/demos/Phoenix_cover.fur new file mode 100644 index 000000000..1a031443a Binary files /dev/null and b/demos/Phoenix_cover.fur differ diff --git a/demos/UNATCOPCM.fur b/demos/UNATCOPCM.fur deleted file mode 100644 index 0a5560782..000000000 Binary files a/demos/UNATCOPCM.fur and /dev/null differ diff --git a/demos/ecolove.fur b/demos/ecolove.fur deleted file mode 100644 index 744b0c9f6..000000000 Binary files a/demos/ecolove.fur and /dev/null differ diff --git a/demos/home_wfl_opl3.fur b/demos/home_wfl_opl3.fur new file mode 100644 index 000000000..af2e952f6 Binary files /dev/null and b/demos/home_wfl_opl3.fur differ diff --git a/demos/neon_night_riders_TFMX.fur b/demos/neon_night_riders_TFMX.fur deleted file mode 100644 index a15f44892..000000000 Binary files a/demos/neon_night_riders_TFMX.fur and /dev/null differ diff --git a/demos/wolf3d.fur b/demos/wolf3d.fur deleted file mode 100644 index a99731ff1..000000000 Binary files a/demos/wolf3d.fur and /dev/null differ diff --git a/extern/backward/backward.hpp b/extern/backward/backward.hpp index 5edda65ad..04032a4dc 100644 --- a/extern/backward/backward.hpp +++ b/extern/backward/backward.hpp @@ -221,6 +221,14 @@ #include #include #include +// https://github.com/tildearrow/furnace/issues/588 +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#include +#undef _GNU_SOURCE +#else +#include +#endif #if BACKWARD_HAS_BFD == 1 // NOTE: defining PACKAGE{,_VERSION} is required before including @@ -233,13 +241,6 @@ #define PACKAGE_VERSION #endif #include -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#include -#undef _GNU_SOURCE -#else -#include -#endif #endif #if BACKWARD_HAS_DW == 1 @@ -254,13 +255,6 @@ #include #include #include -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#include -#undef _GNU_SOURCE -#else -#include -#endif #endif #if (BACKWARD_HAS_BACKTRACE == 1) || (BACKWARD_HAS_BACKTRACE_SYMBOL == 1) diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index 776ad3738..63ae3b879 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -58,13 +58,13 @@ SOFTWARE. #ifndef PATH_MAX #define PATH_MAX 260 #endif // PATH_MAX -#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined (__EMSCRIPTEN__) +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined (__EMSCRIPTEN__) || defined(__HAIKU__) #define UNIX #define stricmp strcasecmp #include // this option need c++17 #ifndef USE_STD_FILESYSTEM - #include + #include #endif // USE_STD_FILESYSTEM #define PATH_SEP '/' #endif // defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__) @@ -1547,28 +1547,53 @@ namespace IGFD for (i = 0; i < n; i++) { struct dirent* ent = files[i]; - + std::string where = path + std::string("/") + std::string(ent->d_name); char fileType = 0; - switch (ent->d_type) +#ifdef HAVE_DIRENT_TYPE + if (ent->d_type != DT_UNKNOWN) { - case DT_REG: - fileType = 'f'; break; - case DT_DIR: - fileType = 'd'; break; - case DT_LNK: - std::string where = path+std::string("/")+std::string(ent->d_name); - DIR* dirTest = opendir(where.c_str()); - if (dirTest==NULL) { - if (errno==ENOTDIR) { - fileType = 'f'; - } else { - fileType = 'l'; - } - } else { - fileType = 'd'; - closedir(dirTest); - } - break; + switch (ent->d_type) + { + case DT_REG: + fileType = 'f'; break; + case DT_DIR: + fileType = 'd'; break; + case DT_LNK: + DIR* dirTest = opendir(where.c_str()); + if (dirTest == NULL) + { + if (errno == ENOTDIR) + { + fileType = 'f'; + } + else + { + fileType = 'l'; + } + } + else + { + fileType = 'd'; + closedir(dirTest); + } + break; + } + } + else +#endif // HAVE_DIRENT_TYPE + { + struct stat filestat; + if (stat(where.c_str(), &filestat) == 0) + { + if (S_ISDIR(filestat.st_mode)) + { + fileType = 'd'; + } + else + { + fileType = 'f'; + } + } } auto fileNameExt = ent->d_name; diff --git a/extern/nfd-modified/src/nfd_win.cpp b/extern/nfd-modified/src/nfd_win.cpp index 9273bb1e7..b4fa5a5fa 100644 --- a/extern/nfd-modified/src/nfd_win.cpp +++ b/extern/nfd-modified/src/nfd_win.cpp @@ -368,7 +368,7 @@ static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *path static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath ) { - if ( !defaultPath || strlen(defaultPath) == 0 ) + if ( !defaultPath || strlen(defaultPath) == 0 || strcmp(defaultPath,"\\")==0 ) return NFD_OKAY; wchar_t *defaultPathW = {0}; diff --git a/instruments/FM/bass/acoustic bass.dmp b/instruments/FM/bass/Acoustic Bass 2.dmp similarity index 100% rename from instruments/FM/bass/acoustic bass.dmp rename to instruments/FM/bass/Acoustic Bass 2.dmp diff --git a/instruments/FM/effect/GEN_Wind.fui b/instruments/FM/effect/GEN_Wind.fui new file mode 100644 index 000000000..181d90d7f Binary files /dev/null and b/instruments/FM/effect/GEN_Wind.fui differ diff --git a/instruments/FM/guitar/Acoustic Nylon Guitar.dmp b/instruments/FM/guitar/Acoustic Nylon Guitar.dmp index b79addcf1..35fac263c 100644 Binary files a/instruments/FM/guitar/Acoustic Nylon Guitar.dmp and b/instruments/FM/guitar/Acoustic Nylon Guitar.dmp differ diff --git a/instruments/FM/guitar/Acoustic Steel Guitar.dmp b/instruments/FM/guitar/Acoustic Steel Guitar.dmp index 64e77b9c8..295a29d6e 100644 Binary files a/instruments/FM/guitar/Acoustic Steel Guitar.dmp and b/instruments/FM/guitar/Acoustic Steel Guitar.dmp differ diff --git a/instruments/FM/guitar/Electric Guitar Harmonics.dmp b/instruments/FM/guitar/Electric Guitar Harmonics.dmp index 07f210328..14a14b118 100644 Binary files a/instruments/FM/guitar/Electric Guitar Harmonics.dmp and b/instruments/FM/guitar/Electric Guitar Harmonics.dmp differ diff --git a/instruments/OPL/Low Overdriven Guitar (Sine Carrier).fui b/instruments/OPL/Low Overdriven Guitar (Sine Carrier).fui new file mode 100644 index 000000000..9fae9177b Binary files /dev/null and b/instruments/OPL/Low Overdriven Guitar (Sine Carrier).fui differ diff --git a/instruments/OPL/Low Overdriven Guitar (Square Carrier).fui b/instruments/OPL/Low Overdriven Guitar (Square Carrier).fui new file mode 100644 index 000000000..dfb5eba1e Binary files /dev/null and b/instruments/OPL/Low Overdriven Guitar (Square Carrier).fui differ diff --git a/papers/doc/4-instrument/n163.md b/papers/doc/4-instrument/n163.md index 0bd914453..ca4dd2e7c 100644 --- a/papers/doc/4-instrument/n163.md +++ b/papers/doc/4-instrument/n163.md @@ -2,7 +2,7 @@ Namco 163 instrument editor consists of two tabs: one controlling various parameters for waveform initialize and macro tab containing 10 macros. -## N163 +## Namco 163 - [Initial Waveform] - Determines the initial waveform for playing. - [Initial Waveform position in RAM] - Determines the initial waveform position will be load to RAM. - [Initial Waveform length in RAM] - Determines the initial waveform length will be load to RAM. diff --git a/papers/doc/6-sample/README.md b/papers/doc/6-sample/README.md index 0c038fdb9..59613da27 100644 --- a/papers/doc/6-sample/README.md +++ b/papers/doc/6-sample/README.md @@ -4,29 +4,54 @@ In the context of Furnace, a sound sample (usually just referred to as a sample) In Furnace, these samples can be generated by importing a .wav (think of it as an higher quality MP3) file. -## supported systems +## supported chips -As of Furnace 0.6, the following sound chips have sample support: - - NES/Ricoh 2A03 (with DPCM support and only on channel 5) - - Sega Genesis/YM2612 (channel 6 only; but only if there exists a `1701` effect that gets played on or before a trigger for a sample, or if you are using an instrument with Sample type) - - PC Engine/TurboGrafx 16/Huc6280 (same conditions as above) - - Amiga/Paula (on all channels) - - Arcade/SEGA PCM (same as above) - - Neo Geo/Neo Geo CD (on the last 7 channels (6 if you are using Neo Geo CD) only and can be resampled the same way as above) - - Seta/Allumer X1-010 (same as YM2612) - - Atari Lynx - - MSM6258 and MSM6295 - - YMU759/MA-2 (last channel only) - - QSound - - ZX Spectrum 48k - - RF5C68 - - WonderSwan - - Tildearrow Sound Unit - - VERA (last channel only) - - Y8590 (last channel only) - - And a few more that I've forgotten to mention. +as of Furnace 0.6, the following sound chips have sample support: -Furnace also has a feature where you can make an Amiga formarted instrument on the YM2612 and Huc6280 to resample a sample you have in the module. +- NES/Ricoh 2A03 (with DPCM support and only on channel 5) +- Sega Genesis/YM2612 (channel 6 only) +- PC Engine/TurboGrafx-16/HuC6280 +- Amiga/Paula +- SegaPCM +- Neo Geo/Neo Geo CD/YM2610 (ADPCM channels only) +- Seta/Allumer X1-010 +- Atari Lynx +- MSM6258 and MSM6295 +- YMU759/MA-2 (last channel only) +- QSound +- ZX Spectrum 48k (1-bit) +- RF5C68 +- WonderSwan +- tildearrow Sound Unit +- VERA (last channel only) +- Y8950 (last channel only) +- a few more that I've forgotten to mention + +## compatible sample mode + +effect `17xx` enables/disables compatible sample mode whether supported (e.g. on Sega Genesis or PC Engine). + +in this mode, samples are mapped to notes in an octave from C to B, allowing you to use up to 12 samples. +if you need to use more samples, you may change the sample bank using effect `EBxx`. + +use of this mode is discouraged in favor of Sample type instruments. + +## notes + +due to limitations in some of those sound chips, some restrictions exist: + +- Amiga: sample lengths and loop will be set to an even number, and your sample can't be longer than 131070. +- NES: if on DPCM mode, only a limited selection of frequencies is available, and loop position isn't supported (only entire sample). +- SegaPCM: your sample can't be longer than 65535, and the maximum frequency is 31.25KHz. +- QSound: your sample can't be longer than 65535, and the loop length shall not be greater than 32767. +- Neo Geo (ADPCM-A): no looping supported. your samples will play at ~18.5KHz. +- Neo Geo (ADPCM-B): no loop position supported (only entire sample), and the maximum frequency is ~55KHz. +- YM2608: the maximum frequency is ~55KHz. +- MSM6258/MSM6295: no arbitrary frequency. +- ZX Spectrum Beeper: your sample can't be longer than 2048, and it always plays at ~55KHz. +- Seta/Allumer X1-010: frequency resolution is terrible in the lower end. your sample can't be longer than 131072. + +furthermore, many of these chips have a limited amount of sample memory. check memory usage in window > statistics. # the sample editor @@ -34,11 +59,12 @@ You can actually tweak your samples in Furnace's sample editor, which can be acc In there, you can modify certain data pertaining to your sample, such as the: - volume of the sample in percentage, where 100% is the current level of the sample (note that you can distort it if you put it too high) - - the sample rate, from 0Hz (no sample movement) to 65535Hz (65.5kHz). + - the sample rate. - what frequencies to filter, along with filter level/sweep and resonance options (much like the C64) - and many more. The changes you make will be applied as soon as you've committed them to your sample, but they can be undoed and redoed, just like text. # tips -If you have a sample you wanna use that is about 44100 or anything over 32000Hz, downsample the sample to 32000Hz so that the pitch of the sample in Furnace stays like the original audio file. You can do this in Audacity by going to the bottom left of the screen (If you see "Project Rate (Hz)" you are there), change the project rate to 32000Hz and save the file to wav in Audacity using "File -> Export -> Export as WAV". + +if you have a sample you wanna use that is about 44100 or anything over 32000Hz, downsample the sample to 32000Hz so that the pitch of the sample in Furnace stays like the original audio file, diff --git a/papers/doc/7-systems/README.md b/papers/doc/7-systems/README.md index 6d428f3eb..c4dc6d493 100644 --- a/papers/doc/7-systems/README.md +++ b/papers/doc/7-systems/README.md @@ -27,6 +27,7 @@ this is a list of systems that Furnace supports, including each system's effects - [WonderSwan](wonderswan.md) - [Bubble System WSG](bubblesystem.md) - [Namco 163](n163.md) +- [Namco WSG](namco.md) - [Yamaha OPL](opl.md) - [PC Speaker](pcspkr.md) - [Commodore PET](pet.md) diff --git a/papers/doc/7-systems/n163.md b/papers/doc/7-systems/n163.md index 6057fa3c9..3c2f389f9 100644 --- a/papers/doc/7-systems/n163.md +++ b/papers/doc/7-systems/n163.md @@ -1,4 +1,4 @@ -# Namco C163 +# Namco 163 (also called N163, Namco C163, Namco 106 (sic), Namco 160 or Namco 129) This is one of Namco's NES mappers, with up to 8 wavetable channels. It has also 128 byte of internal RAM, and both channel register and wavetables are stored here. Wavetables are variable size and freely allocable anywhere in RAM, it means it can use part of or continuously pre-loaded waveform and its sequences in RAM. But waveform RAM area becomes smaller as more channels are activated; as channel registers consumes 8 bytes for each channel. You must avoid conflict with channel register area and waveform for avoid broken channel playback. diff --git a/papers/doc/7-systems/opz.md b/papers/doc/7-systems/opz.md index 61e931394..c7952a55b 100644 --- a/papers/doc/7-systems/opz.md +++ b/papers/doc/7-systems/opz.md @@ -1,5 +1,7 @@ # Yamaha OPZ (YM2414) +**disclaimer: despite the name, this has nothing to do with teenage engineering's OP-Z synth!** + this is the YM2151's little-known successor, used in the Yamaha TX81Z and a few other Yamaha synthesizers. oh, and the Korg Z3 too. it adds these features on top of the YM2151: diff --git a/papers/doc/7-systems/soundunit.md b/papers/doc/7-systems/soundunit.md index d9d0abd70..89e346558 100644 --- a/papers/doc/7-systems/soundunit.md +++ b/papers/doc/7-systems/soundunit.md @@ -25,7 +25,7 @@ This is a fantasy sound chip, used in the specs2 fantasy computer designed by ti - `17xx`: set volume sweep period low byte - `18xx`: set volume sweep period high byte - `19xx`: set cutoff sweep period low byte -- `1Axx`: set cutoff sweep period low byte +- `1Axx`: set cutoff sweep period high byte - `1Bxx`: set frequency sweep boundary - `1Cxx`: set volume sweep boundary - `1Dxx`: set cutoff sweep boundary diff --git a/papers/doc/7-systems/x1-010.md b/papers/doc/7-systems/x1-010.md index 411afb3e7..80b5b41a3 100644 --- a/papers/doc/7-systems/x1-010.md +++ b/papers/doc/7-systems/x1-010.md @@ -8,9 +8,9 @@ Allumer rebadged it for their own arcade hardware. It has 16 channels, which can all be switched between PCM sample or wavetable playback mode. Wavetable playback needs to paired with envelope, similar to AY PSG, but shapes are stored in RAM and as such are user-definable. -In furnace, this chip can be configured for original arcade mono output or stereo output - it simulates early 'incorrect' emulation on some mono hardware, but it is also based on the assumption that each channel is connected to each output. +In Furnace, this chip can be configured for original arcade mono output or stereo output - it simulates early 'incorrect' emulation on some mono hardware, but it is also based on the assumption that each channel is connected to each output. -# waveform types +# Waveform types This chip supports 2 types of waveforms, needs to be paired to external 8 KB RAM to access these features: @@ -44,4 +44,4 @@ In furnace, you can enable the envelope shape split mode. When it is set, its wa - `y` is the denominator. - if `x` or `y` are 0 this will disable auto-envelope mode. -* PCM frequency: 255 step, fomula: `step * (Chip clock / 8192)`; 1.95KHz to 498KHz if Chip clock is 16MHz. +* PCM frequency: 255 step, formula: `step * (Chip clock / 8192)`; 1.95KHz to 498KHz if Chip clock is 16MHz. diff --git a/papers/doc/7-systems/zxbeep.md b/papers/doc/7-systems/zxbeep.md index 1c6cd74f3..9e25284bf 100644 --- a/papers/doc/7-systems/zxbeep.md +++ b/papers/doc/7-systems/zxbeep.md @@ -2,9 +2,12 @@ Rather than having a dedicated sound synthesizer, early ZX Spectrum models had one piezo beeper, controlled by Z80 CPU and ULA chip. It's capabilities should be on par with an IBM PC speaker... right? -Not really - very soon talented programmers found out ways to output much more than one square wave channel. A lot of ZX beeper routines do exist, as of 0.6pre1 Furnace supports only one - Follin-like engine with 6 channels of narrow pulse wave and click drums. +Not really - very soon talented programmers found out ways to output much more than one square wave channel. A lot of ZX beeper routines do exist, but as of 0.6pre1 Furnace supports only one - Follin-like engine with 6 channels of narrow pulse wave and click drums. # effects -- `12xx`: set pulse width -- `17xx`: trigger overlay drums. +- `12xx`: set pulse width. +- `17xx`: trigger overlay drum. + - `xx` is the sample number. + - overlay drums are 1-bit and always play at 55930Hz (NTSC) or 55420Hz (PAL). + - the maximum length is 2048! \ No newline at end of file diff --git a/papers/export-tech.md b/papers/export-tech.md index 3bf715469..dbfadece3 100644 --- a/papers/export-tech.md +++ b/papers/export-tech.md @@ -4,28 +4,23 @@ TODO -## pattern data +## macro data -read sequentially. +read length, loop and then release (1 byte). +if it is a 2-byte macro, read a dummy byte. -first byte determines what to read next: +then read data. + +## binary command stream + +read channel, command and values. + +if channel is 80 or higher, then it is a special command: ``` -NVI..EEE - -N: note -V: volume -I: instrument - -EEE: effect count (0-7) +fb xx xx xx xx: set tick rate +fc xx xx: wait xxxx ticks +fd xx: wait xx ticks +fe: wait one tick +ff: stop ``` - -if you read 0, end of pattern. -otherwise read in following order: - -1. note -2. volume -3. instrument -4. effect and effect value - -then read number of rows until next value, minus 1. diff --git a/papers/format.md b/papers/format.md index 3e3583463..1a8cd7219 100644 --- a/papers/format.md +++ b/papers/format.md @@ -32,6 +32,11 @@ these fields are 0 in format versions prior to 100 (0.6pre1). the format versions are: +- 106: Furnace dev106 +- 105: Furnace dev105 +- 104: Furnace dev104 +- 103: Furnace dev103 +- 102: Furnace 0.6pre1 (dev102) - 101: Furnace 0.6pre1 (dev101) - 100: Furnace 0.6pre1 - 99: Furnace dev99 @@ -238,6 +243,11 @@ size | description | - 0xbe: YM2612 extra features - 7 channels | - 0xbf: T6W28 - 4 channels | - 0xc0: PCM DAC - 1 channel + | - 0xc1: YM2612 CSM - 10 channels + | - 0xc2: Neo Geo CSM (YM2610) - 18 channels + | - 0xc3: OPN CSM - 10 channels + | - 0xc4: PC-98 CSM - 20 channels + | - 0xc5: YM2610B CSM - 20 channels | - 0xde: YM2610B extended - 19 channels | - 0xe0: QSound - 19 channels | - 0xfd: Dummy System - 8 channels @@ -330,6 +340,13 @@ size | description 1 | number of additional subsongs 3 | reserved 4?? | pointers to subsong data + --- | **additional metadata** (>=103) + STR | system name + STR | album/category/game name + STR | song name (Japanese) + STR | song author (Japanese) + STR | system name (Japanese) + STR | album/category/game name (Japanese) ``` # subsong @@ -796,6 +813,43 @@ size | description 1 | vib depth 1 | am depth 23 | reserved + --- | **Sound Unit data** (>=104) + 1 | use sample + 1 | switch roles of phase reset timer and frequency + --- | **Game Boy envelope sequence** (>=105) + 1 | length + ??? | hardware sequence data + | size is length*3: + | 1 byte: command + | - 0: set envelope + | - 1: set sweep + | - 2: wait + | - 3: wait for release + | - 4: loop + | - 5: loop until release + | 2 bytes: data + | - for set envelope: + | - 1 byte: parameter + | - bit 4-7: volume + | - bit 3: direction + | - bit 0-2: length + | - 1 byte: sound length + | - for set sweep: + | - 1 byte: parameter + | - bit 4-6: length + | - bit 3: direction + | - bit 0-2: shift + | - 1 byte: nothing + | - for wait: + | - 1 byte: length (in ticks) + | - 1 byte: nothing + | - for wait for release: + | - 2 bytes: nothing + | - for loop/loop until release: + | - 2 bytes: position + --- | **Game Boy extra flags** (>=106) + 1 | use software envelope + 1 | always init hard env on new note ``` # wavetable @@ -812,7 +866,47 @@ size | description 4?? | wavetable data ``` -# sample +# sample (>=102) + +this is the new sample storage format used in Furnace dev102 and higher. + +``` +size | description +-----|------------------------------------ + 4 | "SMP2" block ID + 4 | size of this block + STR | sample name + 4 | length + 4 | compatibility rate + 4 | C-4 rate + 1 | depth + | - 0: ZX Spectrum overlay drum (1-bit) + | - 1: 1-bit NES DPCM (1-bit) + | - 3: YMZ ADPCM + | - 4: QSound ADPCM + | - 5: ADPCM-A + | - 6: ADPCM-B + | - 8: 8-bit PCM + | - 9: BRR (SNES) + | - 10: VOX + | - 16: 16-bit PCM + 3 | reserved + 4 | loop start + | - -1 means no loop + 4 | loop end + | - -1 means no loop + 16 | sample presence bitfields + | - for future use. + | - indicates whether the sample should be present in the memory of a system. + | - read 4 32-bit numbers (for 4 memory banks per system, e.g. YM2610 + | does ADPCM-A and ADPCM-B on separate memory banks). + ??? | sample data + | - size is length +``` + +# old sample (<102) + +this format is present when saving using previous Furnace versions. ``` size | description @@ -821,7 +915,7 @@ size | description 4 | size of this block STR | sample name 4 | length - 4 | rate + 4 | compatibility rate 2 | volume (<58) or reserved 2 | pitch (<58) or reserved 1 | depth diff --git a/scripts/Cross-Linux-armhf.cmake b/scripts/Cross-Linux-armhf.cmake new file mode 100644 index 000000000..3e490ae12 --- /dev/null +++ b/scripts/Cross-Linux-armhf.cmake @@ -0,0 +1,15 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(TARGET_PREFIX arm-linux-gnueabihf) + +set(CMAKE_C_COMPILER ${TARGET_PREFIX}-gcc) +set(CMAKE_CXX_COMPILER ${TARGET_PREFIX}-g++) +set(PKG_CONFIG_EXECUTABLE ${TARGET_PREFIX}-pkg-config) + +set(CMAKE_FIND_ROOT_PATH /usr/${TARGET_PREFIX} /usr/lib/${TARGET_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) diff --git a/src/asm/6502/macroInt.s b/src/asm/6502/macroInt.s new file mode 100644 index 000000000..c115a807e --- /dev/null +++ b/src/asm/6502/macroInt.s @@ -0,0 +1,32 @@ +macroState=$50 ; pointer to state +macroAddr=$52 ; pointer to address + +; macro state takes 4 bytes +; macroPos bits: +; 7: had +; 6: will + +; x: macro +macroIntRun: + lda macroAddr,x + ora macroAddr+1,x + beq :+ + + ; do macro +: rts + +; set the macro address, then call +; x: macro +macroIntInit: + lda #0 + sta macroState,x + sta macroPos,x + txa + rol + tax + lda macroAddr,x + ora macroAddr+1,x + beq :+ + lda #$40 + sta macroState,x +: rts diff --git a/src/audio/jack.cpp b/src/audio/jack.cpp index 6e7b7bf1f..f9502cf4f 100644 --- a/src/audio/jack.cpp +++ b/src/audio/jack.cpp @@ -52,17 +52,30 @@ void TAAudioJACK::onBufferSize(jack_nframes_t bufsize) { } void TAAudioJACK::onProcess(jack_nframes_t nframes) { - if (audioProcCallback!=NULL) { - if (midiIn!=NULL) midiIn->gather(); - audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize); - } for (int i=0; idesc.bufsize) { + delete[] inBufs[i]; + inBufs[i]=new float[nframes]; + } + memcpy(iInBufs[i],inBufs[i],nframes*sizeof(float)); + } + for (int i=0; idesc.bufsize) { + delete[] outBufs[i]; + outBufs[i]=new float[nframes]; + } + } + if (audioProcCallback!=NULL) { + if (midiIn!=NULL) midiIn->gather(); + audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,nframes); } for (int i=0; i msg; if (port==NULL) return false; - while (true) { - TAMidiMessage m; - double t=port->getMessage(&msg); - if (msg.empty()) break; + try { + while (true) { + TAMidiMessage m; + double t=port->getMessage(&msg); + if (msg.empty()) break; - // parse message - m.time=t; - m.type=msg[0]; - if (m.type!=TA_MIDI_SYSEX && msg.size()>1) { - memcpy(m.data,msg.data()+1,MIN(msg.size()-1,7)); - } else if (m.type==TA_MIDI_SYSEX) { - m.sysExData.reset(new unsigned char[msg.size()]); - m.sysExLen=msg.size(); - logD("got a SysEx of length %ld!",msg.size()); - memcpy(m.sysExData.get(),msg.data(),msg.size()); + // parse message + m.time=t; + m.type=msg[0]; + if (m.type!=TA_MIDI_SYSEX && msg.size()>1) { + memcpy(m.data,msg.data()+1,MIN(msg.size()-1,7)); + } else if (m.type==TA_MIDI_SYSEX) { + m.sysExData.reset(new unsigned char[msg.size()]); + m.sysExLen=msg.size(); + logD("got a SysEx of length %ld!",msg.size()); + memcpy(m.sysExData.get(),msg.data(),msg.size()); + } + queue.push(m); } - queue.push(m); + } catch (RtMidiError& e) { + logE("MIDI input error! %s",e.what()); + closeDevice(); + return false; } return true; } @@ -180,7 +186,12 @@ bool TAMidiOutRtMidi::send(const TAMidiMessage& what) { return false; } len=what.sysExLen; - port->sendMessage(what.sysExData.get(),len); + try { + port->sendMessage(what.sysExData.get(),len); + } catch (RtMidiError& e) { + logE("MIDI output error! %s",e.what()); + return false; + } return true; break; case TA_MIDI_MTC_FRAME: @@ -194,7 +205,12 @@ bool TAMidiOutRtMidi::send(const TAMidiMessage& what) { len=1; break; } - port->sendMessage((const unsigned char*)&what.type,len); + try { + port->sendMessage((const unsigned char*)&what.type,len); + } catch (RtMidiError& e) { + logE("MIDI output error! %s",e.what()); + return false; + } return true; } diff --git a/src/check/check_dirent_type.c b/src/check/check_dirent_type.c new file mode 100644 index 000000000..e65a0d6be --- /dev/null +++ b/src/check/check_dirent_type.c @@ -0,0 +1,7 @@ +#include + +int main(int, char**) { + struct dirent deTest = { }; + unsigned char deType = deTest.d_type; + return 0; +} diff --git a/src/check/check_sysIO.c b/src/check/check_sysIO.c new file mode 100644 index 000000000..653721e6d --- /dev/null +++ b/src/check/check_sysIO.c @@ -0,0 +1,6 @@ +#include + +int main(int, char**) { + inb(0x61); + outb(0x00,0x61); +} diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp new file mode 100644 index 000000000..87bf76dda --- /dev/null +++ b/src/cli/cli.cpp @@ -0,0 +1,148 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "cli.h" +#include "../ta-log.h" + +bool cliQuit=false; + +#ifndef _WIN32 +static void handleTerm(int) { + cliQuit=true; +} +#endif + +void FurnaceCLI::bindEngine(DivEngine* eng) { + e=eng; +} + +bool FurnaceCLI::loop() { + bool escape=false; + bool escapeSecondStage=false; + while (!cliQuit) { +#ifdef _WIN32 + int c; + c=fgetc(stdin); + if (c==EOF) break; +#else + unsigned char c; + if (read(STDIN_FILENO,&c,1)<=0) continue; +#endif + if (escape) { + if (escapeSecondStage) { + switch (c) { + case 'C': // right + e->setOrder(e->getOrder()+1); + escape=false; + escapeSecondStage=false; + break; + case 'D': // left + e->setOrder(e->getOrder()-1); + escape=false; + escapeSecondStage=false; + break; + default: + escape=false; + escapeSecondStage=false; + break; + } + } else { + switch (c) { + case '[': case 'O': + escapeSecondStage=true; + break; + default: + escape=false; + break; + } + } + } else { + switch (c) { + case 0x1b: // + escape=true; + break; + case 'h': // left + e->setOrder(e->getOrder()-1); + break; + case 'l': // right + e->setOrder(e->getOrder()+1); + break; + case ' ': + if (e->isHalted()) { + e->resume(); + } else { + e->halt(); + } + break; + } + } + } + printf("\n"); + return true; +} + +bool FurnaceCLI::finish() { +#ifdef _WIN32 +#else + if (tcsetattr(0,TCSAFLUSH,&termpropold)!=0) { + logE("could not set console attributes!"); + logE("you may have to run `reset` on your terminal."); + return false; + } +#endif + return true; +} + +// blatantly copied from tildearrow/tfmxplay +bool FurnaceCLI::init() { +#ifdef _WIN32 + winin=GetStdHandle(STD_INPUT_HANDLE); + winout=GetStdHandle(STD_OUTPUT_HANDLE); + int termprop=0; + int termpropi=0; + GetConsoleMode(winout,(LPDWORD)&termprop); + GetConsoleMode(winin,(LPDWORD)&termpropi); + termprop|=ENABLE_VIRTUAL_TERMINAL_PROCESSING; + termpropi&=~ENABLE_LINE_INPUT; + SetConsoleMode(winout,termprop); + SetConsoleMode(winin,termpropi); +#else + sigemptyset(&intsa.sa_mask); + intsa.sa_flags=0; + intsa.sa_handler=handleTerm; + sigaction(SIGINT,&intsa,NULL); + + if (tcgetattr(0,&termprop)!=0) { + logE("could not get console attributes!"); + return false; + } + memcpy(&termpropold,&termprop,sizeof(struct termios)); + termprop.c_lflag&=~ECHO; + termprop.c_lflag&=~ICANON; + if (tcsetattr(0,TCSAFLUSH,&termprop)!=0) { + logE("could not set console attributes!"); + return false; + } +#endif + return true; +} + +FurnaceCLI::FurnaceCLI(): + e(NULL) { +} diff --git a/src/cli/cli.h b/src/cli/cli.h new file mode 100644 index 000000000..55ad36b38 --- /dev/null +++ b/src/cli/cli.h @@ -0,0 +1,56 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _FUR_CLI_H +#define _FUR_CLI_H + + +#include "../engine/engine.h" + +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +class FurnaceCLI { + DivEngine* e; + +#ifdef _WIN32 + HANDLE winin; + HANDLE winout; +#else + struct sigaction intsa; + struct termios termprop; + struct termios termpropold; +#endif + + public: + void bindEngine(DivEngine* eng); + bool loop(); + bool finish(); + bool init(); + FurnaceCLI(); +}; + +#endif diff --git a/src/engine/config.cpp b/src/engine/config.cpp index 92866a9f9..f404c0a4f 100644 --- a/src/engine/config.cpp +++ b/src/engine/config.cpp @@ -23,11 +23,84 @@ #include #ifdef _WIN32 +#include "winStuff.h" #define CONFIG_FILE "\\furnace.cfg" #else +#ifdef __HAIKU__ +#include +#include +#endif +#include +#include +#include #define CONFIG_FILE "/furnace.cfg" #endif +void DivEngine::initConfDir() { +#ifdef _WIN32 + // maybe move this function in here instead? + configPath=getWinConfigPath(); +#elif defined (IS_MOBILE) + configPath=SDL_GetPrefPath(); +#else +#ifdef __HAIKU__ + char userSettingsDir[PATH_MAX]; + status_t findUserDir = find_directory(B_USER_SETTINGS_DIRECTORY,0,true,userSettingsDir,PATH_MAX); + if (findUserDir==B_OK) { + configPath=userSettingsDir; + } else { + logW("unable to find/create user settings directory (%s)!",strerror(findUserDir)); + configPath="."; + return; + } +#else + // TODO this should check XDG_CONFIG_HOME first + char* home=getenv("HOME"); + if (home==NULL) { + int uid=getuid(); + struct passwd* entry=getpwuid(uid); + if (entry==NULL) { + logW("unable to determine home directory (%s)!",strerror(errno)); + configPath="."; + return; + } else { + configPath=entry->pw_dir; + } + } else { + configPath=home; + } +#ifdef __APPLE__ + configPath+="/Library/Application Support"; +#else + // FIXME this doesn't honour XDG_CONFIG_HOME *at all* + configPath+="/.config"; +#endif // __APPLE__ +#endif // __HAIKU__ +#ifdef __APPLE__ + configPath+="/Furnace"; +#else + configPath+="/furnace"; +#endif // __APPLE__ + struct stat st; + std::string pathSep="/"; + configPath+=pathSep; + size_t sepPos=configPath.find(pathSep,1); + while (sepPos!=std::string::npos) { + std::string subpath=configPath.substr(0,sepPos++); + if (stat(subpath.c_str(),&st)!=0) { + logI("creating config path element %s ...",subpath.c_str()); + if (mkdir(subpath.c_str(),0755)!=0) { + logW("could not create config path element %s! (%s)",subpath.c_str(),strerror(errno)); + configPath="."; + return; + } + } + sepPos=configPath.find(pathSep,sepPos); + } + configPath.resize(configPath.length()-pathSep.length()); +#endif // _WIN32 +} + bool DivEngine::saveConf() { configFile=configPath+String(CONFIG_FILE); FILE* f=ps_fopen(configFile.c_str(),"wb"); diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 03bc23ca6..fb9be5800 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -55,6 +55,18 @@ enum DivDispatchCmds { DIV_CMD_PRE_PORTA, // (inPorta, isPortaOrSlide) DIV_CMD_PRE_NOTE, // used in C64 (note) + // these will be used in ROM export. + // do NOT implement! + DIV_CMD_HINT_VIBRATO, // (speed, depth) + DIV_CMD_HINT_VIBRATO_RANGE, // (range) + DIV_CMD_HINT_VIBRATO_SHAPE, // (shape) + DIV_CMD_HINT_PITCH, // (pitch) + DIV_CMD_HINT_ARPEGGIO, // (note1, note2) + DIV_CMD_HINT_VOLUME, // (vol) + DIV_CMD_HINT_VOL_SLIDE, // (amount, oneTick) + DIV_CMD_HINT_PORTA, // (target, speed) + DIV_CMD_HINT_LEGATO, // (note) + DIV_CMD_SAMPLE_MODE, // (enabled) DIV_CMD_SAMPLE_FREQ, // (frequency) DIV_CMD_SAMPLE_BANK, // (bank) @@ -399,6 +411,12 @@ class DivDispatch { */ virtual bool getDCOffRequired(); + /** + * check whether PRE_NOTE command is desired. + * @return truth. + */ + virtual bool getWantPreNote(); + /** * get a description of a dispatch-specific effect. * @param effect the effect. diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 227f60679..8d8db5c5a 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -54,6 +54,7 @@ #include "platform/su.h" #include "platform/swan.h" #include "platform/lynx.h" +#include "platform/zxbeeper.h" #include "platform/bubsyswsg.h" #include "platform/n163.h" #include "platform/pet.h" @@ -64,9 +65,9 @@ #include "platform/scc.h" #include "platform/ymz280b.h" #include "platform/rf5c68.h" +#include "platform/pcmdac.h" #include "platform/dummy.h" #include "../ta-log.h" -#include "platform/zxbeeper.h" #include "song.h" void DivDispatchContainer::setRates(double gotRate) { @@ -395,6 +396,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do dispatch=new DivPlatformNamcoWSG; ((DivPlatformNamcoWSG*)dispatch)->setDeviceType(30); break; + case DIV_SYSTEM_PCM_DAC: + dispatch=new DivPlatformPCMDAC; + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index e68208ea8..68c492219 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include "dispatch.h" #define _USE_MATH_DEFINES #include "engine.h" #include "instrument.h" @@ -27,15 +28,11 @@ #include "../audio/sdlAudio.h" #endif #include -#ifndef _WIN32 -#include -#include -#include -#endif #ifdef HAVE_JACK #include "../audio/jack.h" #endif #include +#include #ifdef HAVE_SNDFILE #include "sfWrapper.h" #endif @@ -181,6 +178,308 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { } } +#define EXPORT_BUFSIZE 2048 + +double DivEngine::benchmarkPlayback() { + float* outBuf[2]; + outBuf[0]=new float[EXPORT_BUFSIZE]; + outBuf[1]=new float[EXPORT_BUFSIZE]; + + curOrder=0; + prevOrder=0; + remainingLoops=1; + playSub(false); + + std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); + + // benchmark + while (playing) { + nextBuf(NULL,outBuf,0,2,EXPORT_BUFSIZE); + } + + std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); + + delete[] outBuf[0]; + delete[] outBuf[1]; + + double t=(double)(std::chrono::duration_cast(timeEnd-timeStart).count())/1000000.0; + printf("[RESULT] %fs\n",t); + return t; +} + +double DivEngine::benchmarkSeek() { + double t[20]; + curOrder=curSubSong->ordersLen-1; + prevOrder=curSubSong->ordersLen-1; + + // benchmark + for (int i=0; i<20; i++) { + std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); + playSub(false); + std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); + t[i]=(double)(std::chrono::duration_cast(timeEnd-timeStart).count())/1000000.0; + printf("[#%d] %fs\n",i+1,t[i]); + } + + double tMin=DBL_MAX; + double tMax=0.0; + double tAvg=0.0; + for (int i=0; i<20; i++) { + if (t[i]tMax) tMax=t[i]; + tAvg+=t[i]; + } + tAvg/=20.0; + + printf("[RESULT] min %fs max %fs average %fs\n",tMin,tMax,tAvg); + return tAvg; +} + +#define WRITE_TICK \ + if (!wroteTick) { \ + wroteTick=true; \ + if (binary) { \ + if (tick-lastTick>255) { \ + w->writeC(0xfc); \ + w->writeS(tick-lastTick); \ + } else if (tick-lastTick>1) { \ + w->writeC(0xfd); \ + w->writeC(tick-lastTick); \ + } else { \ + w->writeC(0xfe); \ + } \ + } else { \ + w->writeText(fmt::sprintf(">> TICK %d\n",tick)); \ + } \ + lastTick=tick; \ + } + +void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { + w->writeC(c.cmd); + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + case DIV_CMD_HINT_LEGATO: + if (c.value==DIV_NOTE_NULL) { + w->writeC(0xff); + } else { + w->writeC(c.value+60); + } + break; + case DIV_CMD_NOTE_OFF: + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + break; + case DIV_CMD_INSTRUMENT: + case DIV_CMD_HINT_VIBRATO_RANGE: + case DIV_CMD_HINT_VIBRATO_SHAPE: + case DIV_CMD_HINT_PITCH: + case DIV_CMD_HINT_VOLUME: + case DIV_CMD_SAMPLE_MODE: + case DIV_CMD_SAMPLE_FREQ: + case DIV_CMD_SAMPLE_BANK: + case DIV_CMD_SAMPLE_POS: + case DIV_CMD_SAMPLE_DIR: + case DIV_CMD_FM_HARD_RESET: + case DIV_CMD_FM_LFO: + case DIV_CMD_FM_LFO_WAVE: + case DIV_CMD_FM_FB: + case DIV_CMD_FM_EXTCH: + case DIV_CMD_FM_AM_DEPTH: + case DIV_CMD_FM_PM_DEPTH: + case DIV_CMD_STD_NOISE_FREQ: + case DIV_CMD_STD_NOISE_MODE: + case DIV_CMD_WAVE: + case DIV_CMD_GB_SWEEP_TIME: + case DIV_CMD_GB_SWEEP_DIR: + case DIV_CMD_PCE_LFO_MODE: + case DIV_CMD_PCE_LFO_SPEED: + case DIV_CMD_NES_DMC: + case DIV_CMD_C64_CUTOFF: + case DIV_CMD_C64_RESONANCE: + case DIV_CMD_C64_FILTER_MODE: + case DIV_CMD_C64_RESET_TIME: + case DIV_CMD_C64_RESET_MASK: + case DIV_CMD_C64_FILTER_RESET: + case DIV_CMD_C64_DUTY_RESET: + case DIV_CMD_C64_EXTENDED: + case DIV_CMD_AY_ENVELOPE_SET: + case DIV_CMD_AY_ENVELOPE_LOW: + case DIV_CMD_AY_ENVELOPE_HIGH: + case DIV_CMD_AY_ENVELOPE_SLIDE: + case DIV_CMD_AY_NOISE_MASK_AND: + case DIV_CMD_AY_NOISE_MASK_OR: + case DIV_CMD_AY_AUTO_ENVELOPE: + w->writeC(c.value); + break; + case DIV_CMD_PANNING: + case DIV_CMD_HINT_VIBRATO: + case DIV_CMD_HINT_ARPEGGIO: + case DIV_CMD_HINT_PORTA: + case DIV_CMD_FM_TL: + case DIV_CMD_FM_AM: + case DIV_CMD_FM_AR: + case DIV_CMD_FM_DR: + case DIV_CMD_FM_SL: + case DIV_CMD_FM_D2R: + case DIV_CMD_FM_RR: + case DIV_CMD_FM_DT: + case DIV_CMD_FM_DT2: + case DIV_CMD_FM_RS: + case DIV_CMD_FM_KSR: + case DIV_CMD_FM_VIB: + case DIV_CMD_FM_SUS: + case DIV_CMD_FM_WS: + case DIV_CMD_FM_SSG: + case DIV_CMD_FM_REV: + case DIV_CMD_FM_EG_SHIFT: + case DIV_CMD_FM_MULT: + case DIV_CMD_FM_FINE: + case DIV_CMD_AY_IO_WRITE: + case DIV_CMD_AY_AUTO_PWM: + w->writeC(c.value); + w->writeC(c.value2); + break; + case DIV_CMD_PRE_PORTA: + w->writeC((c.value?0x80:0)|(c.value2?0x40:0)); + break; + case DIV_CMD_HINT_VOL_SLIDE: + case DIV_CMD_C64_FINE_DUTY: + case DIV_CMD_C64_FINE_CUTOFF: + w->writeS(c.value); + break; + case DIV_CMD_FM_FIXFREQ: + w->writeS((c.value<<12)|(c.value2&0x7ff)); + break; + case DIV_CMD_NES_SWEEP: + w->writeC((c.value?8:0)|(c.value2&0x77)); + break; + default: + logW("unimplemented command %s!",cmdName[c.cmd]); + break; + } +} + +SafeWriter* DivEngine::saveCommand(bool binary) { + stop(); + repeatPattern=false; + setOrder(0); + BUSY_BEGIN_SOFT; + // determine loop point + int loopOrder=0; + int loopRow=0; + int loopEnd=0; + walkSong(loopOrder,loopRow,loopEnd); + logI("loop point: %d %d",loopOrder,loopRow); + + SafeWriter* w=new SafeWriter; + w->init(); + + // write header + if (binary) { + w->write("FCS",4); + } else { + w->writeText("# Furnace Command Stream\n\n"); + + w->writeText("[Information]\n"); + w->writeText(fmt::sprintf("name: %s\n",song.name)); + w->writeText(fmt::sprintf("author: %s\n",song.author)); + w->writeText(fmt::sprintf("category: %s\n",song.category)); + w->writeText(fmt::sprintf("system: %s\n",song.systemName)); + + w->writeText("\n"); + + w->writeText("[SubSongInformation]\n"); + w->writeText(fmt::sprintf("name: %s\n",curSubSong->name)); + w->writeText(fmt::sprintf("tickRate: %f\n",curSubSong->hz)); + + w->writeText("\n"); + + w->writeText("[SysDefinition]\n"); + // TODO + + w->writeText("\n"); + } + + // play the song ourselves + bool done=false; + playSub(false); + + if (!binary) { + w->writeText("[Stream]\n"); + } + int tick=0; + bool oldCmdStreamEnabled=cmdStreamEnabled; + cmdStreamEnabled=true; + double curDivider=divider; + int lastTick=0; + while (!done) { + if (nextTick(false,true) || !playing) { + done=true; + } + // get command stream + bool wroteTick=false; + if (curDivider!=divider) { + curDivider=divider; + WRITE_TICK; + if (binary) { + w->writeC(0xfb); + w->writeI((int)(curDivider*65536)); + } else { + w->writeText(fmt::sprintf(">> SET_RATE %f\n",curDivider)); + } + } + for (DivCommand& i: cmdStream) { + switch (i.cmd) { + // strip away hinted/useless commands + case DIV_ALWAYS_SET_VOLUME: + break; + case DIV_CMD_GET_VOLUME: + break; + case DIV_CMD_VOLUME: + break; + case DIV_CMD_NOTE_PORTA: + break; + case DIV_CMD_LEGATO: + break; + case DIV_CMD_PITCH: + break; + case DIV_CMD_PRE_NOTE: + break; + default: + WRITE_TICK; + if (binary) { + w->writeC(i.chan); + writePackedCommandValues(w,i); + } else { + w->writeText(fmt::sprintf(" %d: %s %d %d\n",i.chan,cmdName[i.cmd],i.value,i.value2)); + } + break; + } + } + cmdStream.clear(); + tick++; + } + cmdStreamEnabled=oldCmdStreamEnabled; + + if (binary) { + w->writeC(0xff); + } else { + if (!playing) { + w->writeText(">> END\n"); + } else { + w->writeText(">> LOOP 0\n"); + } + } + + remainingLoops=-1; + playing=false; + freelance=false; + extValuePresent=false; + BUSY_END; + + return w; +} + void _runExportThread(DivEngine* caller) { caller->runExportThread(); } @@ -189,8 +488,6 @@ bool DivEngine::isExporting() { return exporting; } -#define EXPORT_BUFSIZE 2048 - #ifdef HAVE_SNDFILE void DivEngine::runExportThread() { size_t fadeOutSamples=got.rate*exportFadeOut; @@ -786,7 +1083,7 @@ void DivEngine::initSongWithDesc(const int* description) { } } -void DivEngine::createNew(const int* description) { +void DivEngine::createNew(const int* description, String sysName) { quitDispatch(); BUSY_BEGIN; saveLock.lock(); @@ -796,6 +1093,11 @@ void DivEngine::createNew(const int* description) { if (description!=NULL) { initSongWithDesc(description); } + if (sysName=="") { + song.systemName=getSongSystemLegacyName(song,!getConfInt("noMultiSystem",0)); + } else { + song.systemName=sysName; + } recalcChans(); saveLock.unlock(); BUSY_END; @@ -1879,11 +2181,13 @@ void DivEngine::delInstrument(int index) { song.ins.erase(song.ins.begin()+index); song.insLen=song.ins.size(); for (int i=0; ipatLen; k++) { - if (curPat[i].data[j]->data[k][2]>index) { - curPat[i].data[j]->data[k][2]--; + for (size_t j=0; jpat[i].data[k]==NULL) continue; + for (int l=0; lpatLen; l++) { + if (song.subsong[j]->pat[i].data[k]->data[l][2]>index) { + song.subsong[j]->pat[i].data[k]->data[l][2]--; + } } } } @@ -1894,7 +2198,10 @@ void DivEngine::delInstrument(int index) { } int DivEngine::addWave() { - if (song.wave.size()>=256) return -1; + if (song.wave.size()>=256) { + lastError="too many wavetables!"; + return -1; + } BUSY_BEGIN; saveLock.lock(); DivWavetable* wave=new DivWavetable; @@ -1906,50 +2213,62 @@ int DivEngine::addWave() { return waveCount; } -bool DivEngine::addWaveFromFile(const char* path, bool addRaw) { +int DivEngine::addWavePtr(DivWavetable* which) { if (song.wave.size()>=256) { lastError="too many wavetables!"; - return false; + delete which; + return -1; } + BUSY_BEGIN; + saveLock.lock(); + int waveCount=(int)song.wave.size(); + song.wave.push_back(which); + song.waveLen=waveCount+1; + saveLock.unlock(); + BUSY_END; + return song.waveLen; +} + +DivWavetable* DivEngine::waveFromFile(const char* path, bool addRaw) { FILE* f=ps_fopen(path,"rb"); if (f==NULL) { lastError=fmt::sprintf("%s",strerror(errno)); - return false; + return NULL; } unsigned char* buf; ssize_t len; if (fseek(f,0,SEEK_END)!=0) { fclose(f); lastError=fmt::sprintf("could not seek to end: %s",strerror(errno)); - return false; + return NULL; } len=ftell(f); if (len<0) { fclose(f); lastError=fmt::sprintf("could not determine file size: %s",strerror(errno)); - return false; + return NULL; } if (len==(SIZE_MAX>>1)) { fclose(f); lastError="file size is invalid!"; - return false; + return NULL; } if (len==0) { fclose(f); lastError="file is empty"; - return false; + return NULL; } if (fseek(f,0,SEEK_SET)!=0) { fclose(f); lastError=fmt::sprintf("could not seek to beginning: %s",strerror(errno)); - return false; + return NULL; } buf=new unsigned char[len]; if (fread(buf,1,len,f)!=(size_t)len) { logW("did not read entire wavetable file buffer!"); delete[] buf; lastError=fmt::sprintf("could not read entire file: %s",strerror(errno)); - return false; + return NULL; } fclose(f); @@ -1977,7 +2296,7 @@ bool DivEngine::addWaveFromFile(const char* path, bool addRaw) { lastError="invalid wavetable header/data!"; delete wave; delete[] buf; - return false; + return NULL; } } else { try { @@ -2018,7 +2337,7 @@ bool DivEngine::addWaveFromFile(const char* path, bool addRaw) { } else { delete wave; delete[] buf; - return false; + return NULL; } } } catch (EndOfFileException& e) { @@ -2036,7 +2355,7 @@ bool DivEngine::addWaveFromFile(const char* path, bool addRaw) { } else { delete wave; delete[] buf; - return false; + return NULL; } } } @@ -2044,17 +2363,10 @@ bool DivEngine::addWaveFromFile(const char* path, bool addRaw) { delete wave; delete[] buf; lastError="premature end of file"; - return false; + return NULL; } - BUSY_BEGIN; - saveLock.lock(); - int waveCount=(int)song.wave.size(); - song.wave.push_back(wave); - song.waveLen=waveCount+1; - saveLock.unlock(); - BUSY_END; - return true; + return wave; } void DivEngine::delWave(int index) { @@ -2070,7 +2382,10 @@ void DivEngine::delWave(int index) { } int DivEngine::addSample() { - if (song.sample.size()>=256) return -1; + if (song.sample.size()>=256) { + lastError="too many samples!"; + return -1; + } BUSY_BEGIN; saveLock.lock(); DivSample* sample=new DivSample; @@ -2086,11 +2401,28 @@ int DivEngine::addSample() { return sampleCount; } -int DivEngine::addSampleFromFile(const char* path) { +int DivEngine::addSamplePtr(DivSample* which) { if (song.sample.size()>=256) { lastError="too many samples!"; + delete which; return -1; } + int sampleCount=(int)song.sample.size(); + BUSY_BEGIN; + saveLock.lock(); + song.sample.push_back(which); + song.sampleLen=sampleCount+1; + saveLock.unlock(); + renderSamples(); + BUSY_END; + return sampleCount; +} + +DivSample* DivEngine::sampleFromFile(const char* path) { + if (song.sample.size()>=256) { + lastError="too many samples!"; + return NULL; + } BUSY_BEGIN; warnings=""; @@ -2123,7 +2455,6 @@ int DivEngine::addSampleFromFile(const char* path) { if (extS==".dmc") { // read as .dmc size_t len=0; DivSample* sample=new DivSample; - int sampleCount=(int)song.sample.size(); sample->name=stripPath; FILE* f=ps_fopen(path,"rb"); @@ -2131,7 +2462,7 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not open file! (%s)",strerror(errno)); delete sample; - return -1; + return NULL; } if (fseek(f,0,SEEK_END)<0) { @@ -2139,7 +2470,7 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not get file length! (%s)",strerror(errno)); delete sample; - return -1; + return NULL; } len=ftell(f); @@ -2149,7 +2480,7 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError="file is empty!"; delete sample; - return -1; + return NULL; } if (len==(SIZE_MAX>>1)) { @@ -2157,7 +2488,7 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError="file is invalid!"; delete sample; - return -1; + return NULL; } if (fseek(f,0,SEEK_SET)<0) { @@ -2165,12 +2496,12 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not seek to beginning of file! (%s)",strerror(errno)); delete sample; - return -1; + return NULL; } sample->rate=33144; sample->centerRate=33144; - sample->depth=1; + sample->depth=DIV_SAMPLE_DEPTH_1BIT_DPCM; sample->init(len*8); if (fread(sample->dataDPCM,1,len,f)==0) { @@ -2178,22 +2509,16 @@ int DivEngine::addSampleFromFile(const char* path) { BUSY_END; lastError=fmt::sprintf("could not read file! (%s)",strerror(errno)); delete sample; - return -1; + return NULL; } - - saveLock.lock(); - song.sample.push_back(sample); - song.sampleLen=sampleCount+1; - saveLock.unlock(); - renderSamples(); BUSY_END; - return sampleCount; + return sample; } } #ifndef HAVE_SNDFILE lastError="Furnace was not compiled with libsndfile!"; - return -1; + return NULL; #else SF_INFO si; SFWrapper sfWrap; @@ -2205,15 +2530,15 @@ int DivEngine::addSampleFromFile(const char* path) { if (err==SF_ERR_SYSTEM) { lastError=fmt::sprintf("could not open file! (%s %s)",sf_error_number(err),strerror(errno)); } else { - lastError=fmt::sprintf("could not open file! (%s)",sf_error_number(err)); + lastError=fmt::sprintf("could not open file! (%s)\nif this is raw sample data, you may import it by right-clicking the Load Sample icon and selecting \"import raw\".",sf_error_number(err)); } - return -1; + return NULL; } if (si.frames>16777215) { lastError="this sample is too big! max sample size is 16777215."; sfWrap.doClose(); BUSY_END; - return -1; + return NULL; } void* buf=NULL; sf_count_t sampleLen=sizeof(short); @@ -2245,9 +2570,9 @@ int DivEngine::addSampleFromFile(const char* path) { int index=0; if ((si.format&SF_FORMAT_SUBMASK)==SF_FORMAT_PCM_U8) { - sample->depth=8; + sample->depth=DIV_SAMPLE_DEPTH_8BIT; } else { - sample->depth=16; + sample->depth=DIV_SAMPLE_DEPTH_16BIT; } sample->init(si.frames); if ((si.format&SF_FORMAT_SUBMASK)==SF_FORMAT_PCM_U8) { @@ -2302,6 +2627,7 @@ int DivEngine::addSampleFromFile(const char* path) { if(inst.loop_count && inst.loops[0].mode == SF_LOOP_FORWARD) { sample->loopStart=inst.loops[0].start; + sample->loopEnd=inst.loops[0].end; if(inst.loops[0].end < (unsigned int)sampleCount) sampleCount=inst.loops[0].end; } @@ -2310,16 +2636,181 @@ int DivEngine::addSampleFromFile(const char* path) { if (sample->centerRate<4000) sample->centerRate=4000; if (sample->centerRate>64000) sample->centerRate=64000; sfWrap.doClose(); - saveLock.lock(); - song.sample.push_back(sample); - song.sampleLen=sampleCount+1; - saveLock.unlock(); - renderSamples(); BUSY_END; - return sampleCount; + return sample; #endif } +DivSample* DivEngine::sampleFromFileRaw(const char* path, DivSampleDepth depth, int channels, bool bigEndian, bool unsign) { + if (song.sample.size()>=256) { + lastError="too many samples!"; + return NULL; + } + if (channels<1) { + lastError="invalid channel count"; + return NULL; + } + if (depth!=DIV_SAMPLE_DEPTH_8BIT && depth!=DIV_SAMPLE_DEPTH_16BIT) { + if (channels!=1) { + lastError="channel count has to be 1 for non-8/16-bit format"; + return NULL; + } + } + BUSY_BEGIN; + warnings=""; + + const char* pathRedux=strrchr(path,DIR_SEPARATOR); + if (pathRedux==NULL) { + pathRedux=path; + } else { + pathRedux++; + } + String stripPath; + const char* pathReduxEnd=strrchr(pathRedux,'.'); + if (pathReduxEnd==NULL) { + stripPath=pathRedux; + } else { + for (const char* i=pathRedux; i!=pathReduxEnd && (*i); i++) { + stripPath+=*i; + } + } + + size_t len=0; + size_t lenDivided=0; + DivSample* sample=new DivSample; + sample->name=stripPath; + + FILE* f=ps_fopen(path,"rb"); + if (f==NULL) { + BUSY_END; + lastError=fmt::sprintf("could not open file! (%s)",strerror(errno)); + delete sample; + return NULL; + } + + if (fseek(f,0,SEEK_END)<0) { + fclose(f); + BUSY_END; + lastError=fmt::sprintf("could not get file length! (%s)",strerror(errno)); + delete sample; + return NULL; + } + + len=ftell(f); + + if (len==0) { + fclose(f); + BUSY_END; + lastError="file is empty!"; + delete sample; + return NULL; + } + + if (len==(SIZE_MAX>>1)) { + fclose(f); + BUSY_END; + lastError="file is invalid!"; + delete sample; + return NULL; + } + + if (fseek(f,0,SEEK_SET)<0) { + fclose(f); + BUSY_END; + lastError=fmt::sprintf("could not seek to beginning of file! (%s)",strerror(errno)); + delete sample; + return NULL; + } + + lenDivided=len/channels; + + unsigned int samples=lenDivided; + switch (depth) { + case DIV_SAMPLE_DEPTH_1BIT: + case DIV_SAMPLE_DEPTH_1BIT_DPCM: + samples=lenDivided*8; + break; + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: + case DIV_SAMPLE_DEPTH_QSOUND_ADPCM: + case DIV_SAMPLE_DEPTH_ADPCM_A: + case DIV_SAMPLE_DEPTH_ADPCM_B: + case DIV_SAMPLE_DEPTH_VOX: + samples=lenDivided*2; + break; + case DIV_SAMPLE_DEPTH_8BIT: + samples=lenDivided; + break; + case DIV_SAMPLE_DEPTH_BRR: + samples=16*((lenDivided+8)/9); + break; + case DIV_SAMPLE_DEPTH_16BIT: + samples=(lenDivided+1)/2; + break; + default: + break; + } + + if (samples>16777215) { + fclose(f); + BUSY_END; + lastError="this sample is too big! max sample size is 16777215."; + delete sample; + return NULL; + } + + sample->rate=32000; + sample->centerRate=32000; + sample->depth=depth; + sample->init(samples); + + unsigned char* buf=new unsigned char[len]; + if (fread(buf,1,len,f)==0) { + fclose(f); + BUSY_END; + lastError=fmt::sprintf("could not read file! (%s)",strerror(errno)); + delete[] buf; + delete sample; + return NULL; + } + + fclose(f); + + // import sample + size_t pos=0; + if (depth==DIV_SAMPLE_DEPTH_16BIT) { + for (unsigned int i=0; i=len) break; + if (bigEndian) { + accum+=(short)(((short)((buf[pos]<<8)|buf[pos+1]))^(unsign?0x8000:0)); + } else { + accum+=(short)(((short)(buf[pos]|(buf[pos+1]<<8)))^(unsign?0x8000:0)); + } + pos+=2; + } + accum/=channels; + sample->data16[i]=accum; + } + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { + for (unsigned int i=0; i=len) break; + accum+=(signed char)(buf[pos++]^(unsign?0x80:0)); + } + accum/=channels; + sample->data8[i]=accum; + } + } else { + memcpy(sample->getCurBuf(),buf,len); + } + delete[] buf; + + BUSY_END; + return sample; +} + void DivEngine::delSample(int index) { BUSY_BEGIN; sPreview.sample=-1; @@ -2497,13 +2988,15 @@ void DivEngine::moveOrderDown() { void DivEngine::exchangeIns(int one, int two) { for (int i=0; ipatLen; k++) { - if (curPat[i].data[j]->data[k][2]==one) { - curPat[i].data[j]->data[k][2]=two; - } else if (curPat[i].data[j]->data[k][2]==two) { - curPat[i].data[j]->data[k][2]=one; + for (size_t j=0; jpat[i].data[k]==NULL) continue; + for (int l=0; lpatLen; l++) { + if (song.subsong[j]->pat[i].data[k]->data[l][2]==one) { + song.subsong[j]->pat[i].data[k]->data[l][2]=two; + } else if (song.subsong[j]->pat[i].data[k]->data[l][2]==two) { + song.subsong[j]->pat[i].data[k]->data[l][2]=one; + } } } } @@ -2927,36 +3420,6 @@ void DivEngine::quitDispatch() { BUSY_END; } -#define CHECK_CONFIG_DIR_MAC() \ - configPath+="/Library/Application Support/Furnace"; \ - if (stat(configPath.c_str(),&st)<0) { \ - logI("creating config dir..."); \ - if (mkdir(configPath.c_str(),0755)<0) { \ - logW("could not make config dir! (%s)",strerror(errno)); \ - configPath="."; \ - } \ - } - -#define CHECK_CONFIG_DIR() \ - configPath+="/.config"; \ - if (stat(configPath.c_str(),&st)<0) { \ - logI("creating user config dir..."); \ - if (mkdir(configPath.c_str(),0755)<0) { \ - logW("could not make user config dir! (%s)",strerror(errno)); \ - configPath="."; \ - } \ - } \ - if (configPath!=".") { \ - configPath+="/furnace"; \ - if (stat(configPath.c_str(),&st)<0) { \ - logI("creating config dir..."); \ - if (mkdir(configPath.c_str(),0755)<0) { \ - logW("could not make config dir! (%s)",strerror(errno)); \ - configPath="."; \ - } \ - } \ - } - bool DivEngine::initAudioBackend() { // load values if (audioEngine==DIV_AUDIO_NULL) { @@ -2969,6 +3432,7 @@ bool DivEngine::initAudioBackend() { lowQuality=getConfInt("audioQuality",0); forceMono=getConfInt("forceMono",0); + clampSamples=getConfInt("clampSamples",0); lowLatency=getConfInt("lowLatency",0); metroVol=(float)(getConfInt("metroVol",100))/100.0f; if (metroVol<0.0f) metroVol=0.0f; @@ -3065,6 +3529,7 @@ bool DivEngine::initAudioBackend() { bool DivEngine::deinitAudioBackend() { if (output!=NULL) { + output->quit(); if (output->midiIn) { if (output->midiIn->isDeviceOpen()) { logI("closing MIDI input."); @@ -3078,53 +3543,19 @@ bool DivEngine::deinitAudioBackend() { } } output->quitMidi(); - output->quit(); delete output; output=NULL; - audioEngine=DIV_AUDIO_NULL; + //audioEngine=DIV_AUDIO_NULL; } return true; } -#ifdef _WIN32 -#include "winStuff.h" -#endif - bool DivEngine::init() { // register systems if (!systemsRegistered) registerSystems(); - + // init config -#ifdef _WIN32 - configPath=getWinConfigPath(); -#elif defined(IS_MOBILE) - configPath=SDL_GetPrefPath("tildearrow","furnace"); -#else - struct stat st; - char* home=getenv("HOME"); - if (home==NULL) { - int uid=getuid(); - struct passwd* entry=getpwuid(uid); - if (entry==NULL) { - logW("unable to determine config directory! (%s)",strerror(errno)); - configPath="."; - } else { - configPath=entry->pw_dir; -#ifdef __APPLE__ - CHECK_CONFIG_DIR_MAC(); -#else - CHECK_CONFIG_DIR(); -#endif - } - } else { - configPath=home; -#ifdef __APPLE__ - CHECK_CONFIG_DIR_MAC(); -#else - CHECK_CONFIG_DIR(); -#endif - } -#endif + initConfDir(); logD("config path: %s",configPath.c_str()); loadConf(); @@ -3140,6 +3571,12 @@ bool DivEngine::init() { preset.push_back(0); initSongWithDesc(preset.data()); } + String sysName=getConfString("initialSysName",""); + if (sysName=="") { + song.systemName=getSongSystemLegacyName(song,!getConfInt("noMultiSystem",0)); + } else { + song.systemName=sysName; + } hasLoadedSomething=true; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 23290d3be..5d1ca216e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -45,11 +45,15 @@ #define BUSY_BEGIN_SOFT softLocked=true; isBusy.lock(); #define BUSY_END isBusy.unlock(); softLocked=false; -#define DIV_VERSION "0.6pre1 (dev101)" -#define DIV_ENGINE_VERSION 101 +#define DIV_VERSION "dev106" +#define DIV_ENGINE_VERSION 106 // for imports #define DIV_VERSION_MOD 0xff01 +#define DIV_VERSION_FC 0xff02 + +// "Namco C163" +#define DIV_C163_DEFAULT_NAME "Namco 163" enum DivStatusView { DIV_STATUS_NOTHING=0, @@ -60,7 +64,7 @@ enum DivStatusView { enum DivAudioEngines { DIV_AUDIO_JACK=0, DIV_AUDIO_SDL=1, - + DIV_AUDIO_NULL=126, DIV_AUDIO_DUMMY=127 }; @@ -160,7 +164,7 @@ struct DivNoteEvent { struct DivDispatchContainer { DivDispatch* dispatch; blip_buffer_t* bb[2]; - size_t bbInLen; + size_t bbInLen, runtotal, runLeft, runPos, lastAvail; int temp[2], prevSample[2]; short* bbIn[2]; short* bbOut[2]; @@ -178,6 +182,10 @@ struct DivDispatchContainer { dispatch(NULL), bb{NULL,NULL}, bbInLen(0), + runtotal(0), + runLeft(0), + runPos(0), + lastAvail(0), temp{0,0}, prevSample{0,0}, bbIn{NULL,NULL}, @@ -276,6 +284,8 @@ enum DivChanTypes { DIV_CH_OP=5 }; +extern const char* cmdName[]; + class DivEngine { DivDispatchContainer disCont[32]; TAAudio* output; @@ -297,6 +307,7 @@ class DivEngine { bool stopExport; bool halted; bool forceMono; + bool clampSamples; bool cmdStreamEnabled; bool softLocked; bool firstTick; @@ -355,6 +366,7 @@ class DivEngine { short vibTable[64]; int reversePitchTable[4096]; int pitchTable[4096]; + char c163NameCS[1024]; int midiBaseChan; bool midiPoly; size_t midiAgeCounter; @@ -396,6 +408,7 @@ class DivEngine { bool loadFur(unsigned char* file, size_t len); bool loadMod(unsigned char* file, size_t len); bool loadFTM(unsigned char* file, size_t len); + bool loadFC(unsigned char* file, size_t len); void loadDMP(SafeReader& reader, std::vector& ret, String& stripPath); void loadTFI(SafeReader& reader, std::vector& ret, String& stripPath); @@ -453,7 +466,7 @@ class DivEngine { String encodeSysDesc(std::vector& desc); std::vector decodeSysDesc(String desc); // start fresh - void createNew(const int* description); + void createNew(const int* description, String sysName); // load a file. bool load(unsigned char* f, size_t length); // save as .dmf. @@ -465,9 +478,11 @@ class DivEngine { // specify system to build ROM for. SafeWriter* buildROM(int sys); // dump to VGM. - SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171); + SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171, bool patternHints=false); // dump to ZSM. SafeWriter* saveZSM(unsigned int zsmrate=60, bool loop=true); + // dump command stream. + SafeWriter* saveCommand(bool binary=false); // export to an audio file bool saveAudio(const char* path, int loops, DivAudioExportModes mode, double fadeOutTime=0.0); // wait for audio export to finish @@ -479,9 +494,16 @@ class DivEngine { // notify wavetable change void notifyWaveChange(int wave); + // benchmark (returns time in seconds) + double benchmarkPlayback(); + double benchmarkSeek(); + // returns the minimum VGM version which may carry the specified system, or 0 if none. int minVGMVersion(DivSystem which); + // determine and setup config dir + void initConfDir(); + // save config bool saveConf(); @@ -573,14 +595,14 @@ class DivEngine { DivInstrumentType getPreferInsSecondType(int ch); // get song system name - String getSongSystemName(bool isMultiSystemAcceptable=true); + String getSongSystemLegacyName(DivSong& ds, bool isMultiSystemAcceptable=true); // get sys name const char* getSystemName(DivSystem sys); // get japanese system name const char* getSystemNameJ(DivSystem sys); - + // convert sample rate format int fileToDivRate(int frate); int divToFileRate(int drate); @@ -657,7 +679,7 @@ class DivEngine { // is playing bool isPlaying(); - + // is running bool isRunning(); @@ -686,8 +708,11 @@ class DivEngine { // add wavetable int addWave(); - // add wavetable from file - bool addWaveFromFile(const char* path, bool loadRaw=true); + // add wavetable from pointer + int addWavePtr(DivWavetable* which); + + // get wavetable from file + DivWavetable* waveFromFile(const char* path, bool loadRaw=true); // delete wavetable void delWave(int index); @@ -695,8 +720,14 @@ class DivEngine { // add sample int addSample(); - // add sample from file - int addSampleFromFile(const char* path); + // add sample from pointer + int addSamplePtr(DivSample* which); + + // get sample from file + DivSample* sampleFromFile(const char* path); + + // get raw sample + DivSample* sampleFromFileRaw(const char* path, DivSampleDepth depth, int channels, bool bigEndian, bool unsign); // delete sample void delSample(int index); @@ -735,7 +766,7 @@ class DivEngine { void autoNoteOn(int chan, int ins, int note, int vol=-1); void autoNoteOff(int chan, int note, int vol=-1); void autoNoteOffAll(); - + // set whether autoNoteIn is mono or poly void setAutoNotePoly(bool poly); @@ -756,7 +787,7 @@ class DivEngine { // get dispatch channel state void* getDispatchChanState(int chan); - + // get register pool unsigned char* getRegisterPool(int sys, int& size, int& depth); @@ -792,7 +823,7 @@ class DivEngine { // set the console mode. void setConsoleMode(bool enable); - + // get metronome bool getMetronome(); @@ -853,7 +884,7 @@ class DivEngine { // remove system bool removeSystem(int index, bool preserveOrder=true); - + // write to register on system void poke(int sys, unsigned int addr, unsigned short val); @@ -865,7 +896,7 @@ class DivEngine { // get warnings String getWarnings(); - + // switch master bool switchMaster(); diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index ddc5edf6b..589ca6a58 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -28,6 +28,8 @@ #define DIV_DMF_MAGIC ".DelekDefleMask." #define DIV_FUR_MAGIC "-Furnace module-" #define DIV_FTM_MAGIC "FamiTracker Module" +#define DIV_FC13_MAGIC "SMOD" +#define DIV_FC14_MAGIC "FC14" struct InflateBlock { unsigned char* buf; @@ -588,7 +590,9 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { logD("GB data: vol %d dir %d len %d sl %d",ins->gb.envVol,ins->gb.envDir,ins->gb.envLen,ins->gb.soundLen); } else if (ds.system[0]==DIV_SYSTEM_GB) { - // try to convert macro to envelope + // set software envelope flag + ins->gb.softEnv=true; + // try to convert macro to envelope in case the user decides to switch to them if (ins->std.volMacro.len>0) { ins->gb.envVol=ins->std.volMacro.val[0]; if (ins->std.volMacro.val[0]std.volMacro.val[1]) { @@ -598,7 +602,6 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ins->gb.soundLen=ins->std.volMacro.len*2; } } - addWarning("Game Boy volume macros converted to envelopes. may not be perfect!"); } } @@ -799,17 +802,17 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { sample->rate=ymuSampleRate*400; } if (ds.version>0x15) { - sample->depth=reader.readC(); - if (sample->depth!=8 && sample->depth!=16) { + sample->depth=(DivSampleDepth)reader.readC(); + if (sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT) { logW("%d: sample depth is wrong! (%d)",i,sample->depth); - sample->depth=16; + sample->depth=DIV_SAMPLE_DEPTH_16BIT; } } else { if (ds.version>0x08) { - sample->depth=16; + sample->depth=DIV_SAMPLE_DEPTH_16BIT; } else { // it appears samples were stored as ADPCM back then - sample->depth=3; + sample->depth=DIV_SAMPLE_DEPTH_YMZ_ADPCM; } } if (length>0) { @@ -838,7 +841,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { if (k>=sample->samples) { break; } - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { float next=(float)(data[(unsigned int)j]-0x80)*mult; sample->data8[k++]=fmin(fmax(next,-128),127); } else { @@ -907,6 +910,8 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.system[1]=DIV_SYSTEM_FDS; } + ds.systemName=getSongSystemLegacyName(ds,!getConfInt("noMultiSystem",0)); + if (active) quitDispatch(); BUSY_BEGIN_SOFT; saveLock.lock(); @@ -1482,7 +1487,22 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { for (int i=0; i=103) { + ds.systemName=reader.readString(); + ds.category=reader.readString(); + ds.nameJ=reader.readString(); + ds.authorJ=reader.readString(); + ds.systemNameJ=reader.readString(); + ds.categoryJ=reader.readString(); + } else { + ds.systemName=getSongSystemLegacyName(ds,!getConfInt("noMultiSystem",0)); + } + + // read subsongs + if (ds.version>=95) { for (int i=0; iname=reader.readString(); sample->samples=reader.readI(); + if (!isNewSample) { + sample->loopEnd=sample->samples; + } sample->rate=reader.readI(); - if (ds.version<58) { - vol=reader.readS(); - pitch=reader.readS(); - } else { - reader.readI(); - } - sample->depth=reader.readC(); - // reserved - reader.readC(); + if (isNewSample) { + sample->centerRate=reader.readI(); + sample->depth=(DivSampleDepth)reader.readC(); - // while version 32 stored this value, it was unused. - if (ds.version>=38) { - sample->centerRate=(unsigned short) reader.readS(); - } else { - reader.readS(); - } + // reserved + reader.readC(); + reader.readC(); + reader.readC(); - if (ds.version>=19) { sample->loopStart=reader.readI(); + sample->loopEnd=reader.readI(); + + for (int i=0; i<4; i++) { + reader.readI(); + } } else { - reader.readI(); + if (ds.version<58) { + vol=reader.readS(); + pitch=reader.readS(); + } else { + reader.readI(); + } + sample->depth=(DivSampleDepth)reader.readC(); + + // reserved + reader.readC(); + + // while version 32 stored this value, it was unused. + if (ds.version>=38) { + sample->centerRate=(unsigned short)reader.readS(); + } else { + reader.readS(); + } + + if (ds.version>=19) { + sample->loopStart=reader.readI(); + } else { + reader.readI(); + } } if (ds.version>=58) { // modern sample @@ -1670,9 +1713,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } // render data - if (sample->depth!=8 && sample->depth!=16) { + if (sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT) { logW("%d: sample depth is wrong! (%d)",i,sample->depth); - sample->depth=16; + sample->depth=DIV_SAMPLE_DEPTH_16BIT; } sample->samples=(double)sample->samples/samplePitches[pitch]; sample->init(sample->samples); @@ -1683,7 +1726,7 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (k>=sample->samples) { break; } - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { float next=(float)(data[(unsigned int)j]-0x80)*mult; sample->data8[k++]=fmin(fmax(next,-128),127); } else { @@ -1842,26 +1885,33 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { } if (memcmp(magic,"M.K.",4)==0 || memcmp(magic,"M!K!",4)==0 || memcmp(magic,"M&K!",4)==0) { logD("detected a ProTracker module"); + ds.systemName="Amiga"; chCount=4; } else if (memcmp(magic,"CD81",4)==0 || memcmp(magic,"OKTA",4)==0 || memcmp(magic,"OCTA",4)==0) { logD("detected an Oktalyzer/Octalyzer/OctaMED module"); + ds.systemName="Amiga (8-channel)"; chCount=8; } else if (memcmp(magic+1,"CHN",3)==0 && magic[0]>='1' && magic[0]<='9') { logD("detected a FastTracker module"); + ds.systemName="PC"; chCount=magic[0]-'0'; } else if (memcmp(magic,"FLT",3)==0 && magic[3]>='1' && magic[3]<='9') { logD("detected a Fairlight module"); + ds.systemName="Amiga"; chCount=magic[3]-'0'; } else if (memcmp(magic,"TDZ",3)==0 && magic[3]>='1' && magic[3]<='9') { logD("detected a TakeTracker module"); + ds.systemName="PC"; chCount=magic[3]-'0'; } else if ((memcmp(magic+2,"CH",2)==0 || memcmp(magic+2,"CN",2)==0) && (magic[0]>='1' && magic[0]<='9' && magic[1]>='0' && magic[1]<='9')) { logD("detected a Fast/TakeTracker module"); + ds.systemName="PC"; chCount=((magic[0]-'0')*10)+(magic[1]-'0'); } else { insCount=15; logD("possibly a Soundtracker module"); + ds.systemName="Amiga"; chCount=4; } @@ -1877,7 +1927,7 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { logD("reading samples... (%d)",insCount); for (int i=0; idepth=8; + sample->depth=DIV_SAMPLE_DEPTH_8BIT; sample->name=reader.readString(22); logD("%d: %s",i+1,sample->name); int slen=((unsigned short)reader.readS_BE())*2; @@ -1897,8 +1947,8 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { loopLen=0; } if (loopLen>=2) { - if (loopEndloopStart=loopStart; + sample->loopEnd=loopEnd; } sample->init(slen); ds.sample.push_back(sample); @@ -2212,6 +2262,691 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { return success; } +unsigned char fcXORTriangle[32]={ + 0xc0, 0xc0, 0xd0, 0xd8, 0xe0, 0xe8, 0xf0, 0xf8, 0x00, 0xf8, 0xf0, 0xe8, 0xe0, 0xd8, 0xd0, 0xc8, + 0xc0, 0xb8, 0xb0, 0xa8, 0xa0, 0x98, 0x90, 0x88, 0x80, 0x88, 0x90, 0x98, 0xa0, 0xa8, 0xb0, 0xb8 +}; + +unsigned char fcCustom1[32]={ + 0x45, 0x45, 0x79, 0x7d, 0x7a, 0x77, 0x70, 0x66, 0x61, 0x58, 0x53, 0x4d, 0x2c, 0x20, 0x18, 0x12, + 0x04, 0xdb, 0xd3, 0xcd, 0xc6, 0xbc, 0xb5, 0xae, 0xa8, 0xa3, 0x9d, 0x99, 0x93, 0x8e, 0x8b, 0x8a +}; + +unsigned char fcCustom2[32]={ + 0x45, 0x45, 0x79, 0x7d, 0x7a, 0x77, 0x70, 0x66, 0x5b, 0x4b, 0x43, 0x37, 0x2c, 0x20, 0x18, 0x12, + 0x04, 0xf8, 0xe8, 0xdb, 0xcf, 0xc6, 0xbe, 0xb0, 0xa8, 0xa4, 0x9e, 0x9a, 0x95, 0x94, 0x8d, 0x83 +}; + +unsigned char fcTinyTriangle[16]={ + 0x00, 0x00, 0x40, 0x60, 0x7f, 0x60, 0x40, 0x20, 0x00, 0xe0, 0xc0, 0xa0, 0x80, 0xa0, 0xc0, 0xe0 +}; + +void generateFCPresetWave(int index, DivWavetable* wave) { + wave->max=255; + wave->len=32; + + switch (index) { + case 0x00: case 0x01: case 0x02: case 0x03: + case 0x04: case 0x05: case 0x06: case 0x07: + case 0x08: case 0x09: case 0x0a: case 0x0b: + case 0x0c: case 0x0d: case 0x0e: case 0x0f: + // XOR triangle + for (int i=0; i<32; i++) { + wave->data[i]=(unsigned char)((fcXORTriangle[i]^0x80)^(((index+15)data[i]=(index>i)?0x01:0xff; + } + break; + case 0x20: case 0x21: case 0x22: case 0x23: + case 0x24: case 0x25: case 0x26: case 0x27: + // tiny pulse + for (int i=0; i<32; i++) { + wave->data[i]=((index-0x18)>(i&15))?0x01:0xff; + } + break; + case 0x28: + case 0x2e: + // saw + for (int i=0; i<32; i++) { + wave->data[i]=i<<3; + } + break; + case 0x29: + case 0x2f: + // tiny saw + for (int i=0; i<32; i++) { + wave->data[i]=(i<<4)&0xff; + } + break; + case 0x2a: + // custom 1 + for (int i=0; i<32; i++) { + wave->data[i]=fcCustom1[i]^0x80; + } + break; + case 0x2b: + // custom 2 + for (int i=0; i<32; i++) { + wave->data[i]=fcCustom2[i]^0x80; + } + break; + case 0x2c: case 0x2d: + // tiny triangle + for (int i=0; i<32; i++) { + wave->data[i]=fcTinyTriangle[i&15]^0x80; + } + break; + default: + for (int i=0; i<32; i++) { + wave->data[i]=i; + } + break; + } +} + +bool DivEngine::loadFC(unsigned char* file, size_t len) { + struct InvalidHeaderException {}; + bool success=false; + char magic[4]={0,0,0,0}; + SafeReader reader=SafeReader(file,len); + warnings=""; + bool isFC14=false; + unsigned int patPtr, freqMacroPtr, volMacroPtr, samplePtr, wavePtr; + unsigned int seqLen, patLen, freqMacroLen, volMacroLen, sampleLen; + + unsigned char waveLen[80]; + //unsigned char waveLoopLen[40]; + + struct FCSequence { + unsigned char pat[4]; + signed char transpose[4]; + signed char offsetIns[4]; + unsigned char speed; + }; + std::vector seq; + struct FCPattern { + unsigned char note[32]; + unsigned char val[32]; + }; + std::vector pat; + struct FCMacro { + unsigned char val[64]; + }; + std::vector freqMacros; + std::vector volMacros; + + struct FCSample { + unsigned short loopLen, len, loopStart; + } sample[10]; + + try { + DivSong ds; + ds.tuning=436.0; + ds.version=DIV_VERSION_FC; + //ds.linearPitch=0; + //ds.pitchMacroIsLinear=false; + //ds.noSlidesOnFirstTick=true; + //ds.rowResetsArpPos=true; + ds.pitchSlideSpeed=8; + ds.ignoreJumpAtEnd=false; + + // load here + if (!reader.seek(0,SEEK_SET)) { + logE("premature end of file!"); + lastError="incomplete file"; + delete[] file; + return false; + } + reader.read(magic,4); + + if (memcmp(magic,DIV_FC13_MAGIC,4)==0) { + isFC14=false; + } else if (memcmp(magic,DIV_FC14_MAGIC,4)==0) { + isFC14=true; + } else { + logW("the magic isn't complete"); + throw EndOfFileException(&reader,reader.tell()); + } + + ds.systemLen=1; + ds.system[0]=DIV_SYSTEM_AMIGA; + ds.systemVol[0]=64; + ds.systemPan[0]=0; + ds.systemFlags[0]=1|(80<<8); // PAL + ds.systemName="Amiga"; + + seqLen=reader.readI_BE(); + if (seqLen%13) { + logW("sequence length is not multiple of 13 (%d)",seqLen); + //throw EndOfFileException(&reader,reader.tell()); + } + patPtr=reader.readI_BE(); + patLen=reader.readI_BE(); + if (patLen%64) { + logW("pattern length is not multiple of 64 (%d)",patLen); + throw EndOfFileException(&reader,reader.tell()); + } + freqMacroPtr=reader.readI_BE(); + freqMacroLen=reader.readI_BE(); + if (freqMacroLen%64) { + logW("freq sequence length is not multiple of 64 (%d)",freqMacroLen); + //throw EndOfFileException(&reader,reader.tell()); + } + volMacroPtr=reader.readI_BE(); + volMacroLen=reader.readI_BE(); + if (volMacroLen%64) { + logW("vol sequence length is not multiple of 64 (%d)",volMacroLen); + //throw EndOfFileException(&reader,reader.tell()); + } + samplePtr=reader.readI_BE(); + if (isFC14) { + wavePtr=reader.readI_BE(); // wave len + sampleLen=0; + } else { + sampleLen=reader.readI_BE(); + wavePtr=0; + } + + logD("patPtr: %x",patPtr); + logD("patLen: %d",patLen); + logD("freqMacroPtr: %x",freqMacroPtr); + logD("freqMacroLen: %d",freqMacroLen); + logD("volMacroPtr: %x",volMacroPtr); + logD("volMacroLen: %d",volMacroLen); + logD("samplePtr: %x",samplePtr); + if (isFC14) { + logD("wavePtr: %x",wavePtr); + } else { + logD("sampleLen: %d",sampleLen); + } + + // sample info + logD("samples: (%x)",reader.tell()); + for (int i=0; i<10; i++) { + sample[i].len=reader.readS_BE(); + sample[i].loopStart=reader.readS_BE(); + sample[i].loopLen=reader.readS_BE(); + + logD("- %d: %d (%d, %d)",i,sample[i].len,sample[i].loopStart,sample[i].loopLen); + } + + // wavetable lengths + if (isFC14) { + logD("wavetables:"); + for (int i=0; i<80; i++) { + waveLen[i]=(unsigned char)reader.readC(); + + logD("- %d: %.4x",i,waveLen[i]); + } + } + + // sequences + seqLen/=13; + logD("reading sequences... (%d)",seqLen); + for (unsigned int i=0; idepth=DIV_SAMPLE_DEPTH_8BIT; + if (sample[i].len>0) { + s->init(sample[i].len*2); + } + s->name=fmt::sprintf("Sample %d",i+1); + if (sample[i].loopLen>1) { + s->loopStart=sample[i].loopStart; + s->loopEnd=sample[i].loopStart+(sample[i].loopLen*2); + } + reader.read(s->data8,sample[i].len*2); + ds.sample.push_back(s); + } + ds.sampleLen=(int)ds.sample.size(); + + // wavetables + if (isFC14) { + if (!reader.seek(wavePtr,SEEK_SET)) { + logE("premature end of file!"); + lastError="incomplete file"; + delete[] file; + return false; + } + logD("reading wavetables..."); + for (int i=0; i<80; i++) { + DivWavetable* w=new DivWavetable; + w->min=0; + w->max=255; + w->len=MIN(256,waveLen[i]*2); + + for (int i=0; i<256; i++) { + w->data[i]=128; + } + + if (waveLen[i]>0) { + signed char* waveArray=new signed char[waveLen[i]*2]; + reader.read(waveArray,waveLen[i]*2); + int howMany=waveLen[i]*2; + if (howMany>256) howMany=256; + for (int i=0; idata[i]=waveArray[i]+128; + } + delete[] waveArray; + } else { + logV("empty wave %d",i); + generateFCPresetWave(i,w); + } + + ds.wave.push_back(w); + } + } else { + // generate preset waves + for (int i=0; i<48; i++) { + DivWavetable* w=new DivWavetable; + generateFCPresetWave(i,w); + ds.wave.push_back(w); + } + } + ds.waveLen=(int)ds.wave.size(); + + // convert + ds.subsong[0]->ordersLen=seqLen; + ds.subsong[0]->patLen=32; + ds.subsong[0]->hz=50; + ds.subsong[0]->pal=true; + ds.subsong[0]->customTempo=true; + ds.subsong[0]->pat[3].effectCols=3; + ds.subsong[0]->speed1=3; + ds.subsong[0]->speed2=3; + + int lastIns[4]; + int lastNote[4]; + signed char lastTranspose[4]; + bool isSliding[4]; + + memset(lastIns,-1,4*sizeof(int)); + memset(lastNote,-1,4*sizeof(int)); + memset(lastTranspose,0,4); + memset(isSliding,0,4*sizeof(bool)); + + for (unsigned int i=0; iorders.ord[j][i]=i; + DivPattern* p=ds.subsong[0]->pat[j].getPattern(i,true); + if (j==3 && seq[i].speed) { + p->data[0][6]=0x09; + p->data[0][7]=seq[i].speed; + p->data[0][8]=0x0f; + p->data[0][9]=seq[i].speed; + } + + bool ignoreNext=false; + + for (int k=0; k<32; k++) { + FCPattern& fp=pat[seq[i].pat[j]]; + if (fp.note[k]>0 && fp.note[k]<0x49) { + lastNote[j]=fp.note[k]; + short note=(fp.note[k]+seq[i].transpose[j])%12; + short octave=2+((fp.note[k]+seq[i].transpose[j])/12); + if (fp.note[k]>=0x3d) octave-=6; + if (note==0) { + note=12; + octave--; + } + octave&=0xff; + p->data[k][0]=note; + p->data[k][1]=octave; + if (isSliding[j]) { + isSliding[j]=false; + p->data[k][4]=2; + p->data[k][5]=0; + } + } else if (fp.note[k]==0x49) { + if (k>0) { + p->data[k-1][4]=0x0d; + p->data[k-1][5]=0; + } + } else if (k==0 && lastTranspose[j]!=seq[i].transpose[j]) { + p->data[0][2]=lastIns[j]; + p->data[0][4]=0x03; + p->data[0][5]=0xff; + lastTranspose[j]=seq[i].transpose[j]; + + short note=(lastNote[j]+seq[i].transpose[j])%12; + short octave=2+((lastNote[j]+seq[i].transpose[j])/12); + if (lastNote[j]>=0x3d) octave-=6; + if (note==0) { + note=12; + octave--; + } + octave&=0xff; + p->data[k][0]=note; + p->data[k][1]=octave; + } + if (fp.val[k]) { + if (ignoreNext) { + ignoreNext=false; + } else { + if (fp.val[k]==0xf0) { + p->data[k][0]=100; + p->data[k][1]=0; + p->data[k][2]=-1; + } else if (fp.val[k]&0xe0) { + if (fp.val[k]&0x40) { + p->data[k][4]=2; + p->data[k][5]=0; + isSliding[j]=false; + } else if (fp.val[k]&0x80) { + isSliding[j]=true; + if (k<31) { + if (fp.val[k+1]&0x20) { + p->data[k][4]=2; + p->data[k][5]=fp.val[k+1]&0x1f; + } else { + p->data[k][4]=1; + p->data[k][5]=fp.val[k+1]&0x1f; + } + ignoreNext=true; + } else { + p->data[k][4]=2; + p->data[k][5]=0; + } + } + } else { + p->data[k][2]=(fp.val[k]+seq[i].offsetIns[j])&0x3f; + lastIns[j]=p->data[k][2]; + } + } + } else if (fp.note[k]>0 && fp.note[k]<0x49) { + p->data[k][2]=seq[i].offsetIns[j]; + lastIns[j]=p->data[k][2]; + } + } + } + } + + // convert instruments + for (unsigned int i=0; itype=DIV_INS_AMIGA; + ins->name=fmt::sprintf("Instrument %d",i); + ins->amiga.useWave=true; + unsigned char seqSpeed=m.val[0]; + unsigned char freqMacro=m.val[1]; + unsigned char vibSpeed=m.val[2]; + unsigned char vibDepth=m.val[3]; + unsigned char vibDelay=m.val[4]; + + unsigned char lastVal=m.val[5]; + + signed char loopMap[64]; + memset(loopMap,-1,64); + + signed char loopMapFreq[64]; + memset(loopMapFreq,-1,64); + + signed char loopMapWave[64]; + memset(loopMapWave,-1,64); + + // volume sequence + ins->std.volMacro.len=0; + for (int j=5; j<64; j++) { + loopMap[j]=ins->std.volMacro.len; + if (m.val[j]==0xe1) { // end + break; + } else if (m.val[j]==0xe0) { // loop + if (++j>=64) break; + ins->std.volMacro.loop=loopMap[m.val[j]&63]; + break; + } else if (m.val[j]==0xe8) { // sustain + if (++j>=64) break; + unsigned char susTime=m.val[j]; + // TODO: <= or std.volMacro.val[ins->std.volMacro.len]=lastVal; + if (++ins->std.volMacro.len>=128) break; + } + if (ins->std.volMacro.len>=128) break; + } else if (m.val[j]==0xe9 || m.val[j]==0xea) { // volume slide + if (++j>=64) break; + signed char slideStep=m.val[j]; + if (++j>=64) break; + unsigned char slideTime=m.val[j]; + // TODO: <= or 0) { + lastVal+=slideStep; + if (lastVal>63) lastVal=63; + } else { + if (-slideStep>lastVal) { + lastVal=0; + } else { + lastVal-=slideStep; + } + } + ins->std.volMacro.val[ins->std.volMacro.len]=lastVal; + if (++ins->std.volMacro.len>=128) break; + } + } else { + // TODO: replace with upcoming macro speed + for (int k=0; kstd.volMacro.val[ins->std.volMacro.len]=m.val[j]; + lastVal=m.val[j]; + if (++ins->std.volMacro.len>=128) break; + } + if (ins->std.volMacro.len>=128) break; + } + } + + // frequency sequence + lastVal=0; + ins->amiga.initSample=-1; + if (freqMacrostd.arpMacro.len; + loopMapWave[j]=ins->std.waveMacro.len; + if (fm.val[j]==0xe1) { + if (ins->std.arpMacro.mode) { + ins->std.arpMacro.loop=(signed int)ins->std.arpMacro.len-1; + } + break; + } else if (fm.val[j]==0xe2 || fm.val[j]==0xe4) { + if (++j>=64) break; + unsigned char wave=fm.val[j]; + if (wave<10) { // sample + if (ins->amiga.initSample==-1) { + ins->amiga.initSample=wave; + ins->amiga.useWave=false; + } + } else { // waveform + ins->std.waveMacro.val[ins->std.waveMacro.len]=wave-10; + ins->std.waveMacro.open=true; + lastVal=wave; + //if (++ins->std.arpMacro.len>=128) break; + } + } else if (fm.val[j]==0xe0) { + if (++j>=64) break; + ins->std.arpMacro.loop=loopMapFreq[fm.val[j]&63]; + ins->std.waveMacro.loop=loopMapWave[fm.val[j]&63]; + break; + } else if (fm.val[j]==0xe3) { + logV("unhandled vibrato!"); + } else if (fm.val[j]==0xe8) { + logV("unhandled sustain!"); + } else if (fm.val[j]==0xe7) { + if (++j>=64) break; + fm=freqMacros[MIN(fm.val[j],freqMacros.size()-1)]; + j=0; + } else if (fm.val[j]==0xe9) { + logV("unhandled pack!"); + } else if (fm.val[j]==0xea) { + logV("unhandled pitch!"); + } else { + if (fm.val[j]>0x80) { + ins->std.arpMacro.val[ins->std.arpMacro.len]=fm.val[j]-0x80+24; + ins->std.arpMacro.mode=1; // TODO: variable fixed/relative mode + } else { + ins->std.arpMacro.val[ins->std.arpMacro.len]=fm.val[j]; + } + if (lastVal>=10) { + ins->std.waveMacro.val[ins->std.waveMacro.len]=lastVal-10; + } + ins->std.arpMacro.open=true; + if (++ins->std.arpMacro.len>=128) break; + if (++ins->std.waveMacro.len>=128) break; + } + } + } + + // waveform width + if (lastVal>=10 && (unsigned int)(lastVal-10)amiga.waveLen=ds.wave[lastVal-10]->len-1; + } + + // vibrato + for (int j=0; j<=vibDelay; j++) { + ins->std.pitchMacro.val[ins->std.pitchMacro.len]=0; + if (++ins->std.pitchMacro.len>=128) break; + } + int vibPos=0; + ins->std.pitchMacro.loop=ins->std.pitchMacro.len; + do { + vibPos+=vibSpeed; + if (vibPos>vibDepth) vibPos=vibDepth; + ins->std.pitchMacro.val[ins->std.pitchMacro.len]=vibPos*32; + if (++ins->std.pitchMacro.len>=128) break; + } while (vibPosstd.pitchMacro.val[ins->std.pitchMacro.len]=vibPos*32; + if (++ins->std.pitchMacro.len>=128) break; + } while (vibPos>-vibDepth); + do { + vibPos+=vibSpeed; + if (vibPos>0) vibPos=0; + ins->std.pitchMacro.val[ins->std.pitchMacro.len]=vibPos*32; + if (++ins->std.pitchMacro.len>=128) break; + } while (vibPos<0); + + ds.ins.push_back(ins); + } + ds.insLen=(int)ds.ins.size(); + + // optimize + ds.subsong[0]->optimizePatterns(); + ds.subsong[0]->rearrangePatterns(); + + if (active) quitDispatch(); + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + changeSong(0); + recalcChans(); + saveLock.unlock(); + BUSY_END; + if (active) { + initDispatch(); + BUSY_BEGIN; + renderSamples(); + reset(); + BUSY_END; + } + success=true; + } catch (EndOfFileException& e) { + //logE("premature end of file!"); + lastError="incomplete file"; + } catch (InvalidHeaderException& e) { + //logE("invalid header!"); + lastError="invalid header!"; + } + return success; +} + #define CHECK_BLOCK_VERSION(x) \ if (blockVersion>x) { \ logE("incompatible block version %d for %s!",blockVersion,blockName); \ @@ -2445,8 +3180,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { const int dpcmNotes=(blockVersion>=2)?96:72; for (int j=0; jamiga.noteMap[j]=(short)((unsigned char)reader.readC())-1; - ins->amiga.noteFreq[j]=(unsigned char)reader.readC(); + ins->amiga.noteMap[j].map=(short)((unsigned char)reader.readC())-1; + ins->amiga.noteMap[j].freq=(unsigned char)reader.readC(); if (blockVersion>=6) { reader.readC(); // DMC value } @@ -2684,6 +3419,8 @@ bool DivEngine::load(unsigned char* f, size_t slen) { return loadFTM(file,len); } else if (memcmp(file,DIV_FUR_MAGIC,16)==0) { return loadFur(file,len); + } else if (memcmp(file,DIV_FC13_MAGIC,4)==0 || memcmp(file,DIV_FC14_MAGIC,4)==0) { + return loadFC(file,len); } // step 3: try loading as .mod @@ -2778,15 +3515,27 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { // high short is channel // low short is pattern number std::vector patsToWrite; - bool alreadyAdded[256]; - for (int i=0; iordersLen; k++) { - if (alreadyAdded[subs->orders.ord[i][k]]) continue; - patsToWrite.push_back(PatToWrite(j,i,subs->orders.ord[i][k])); - alreadyAdded[subs->orders.ord[i][k]]=true; + if (getConfInt("saveUnusedPatterns",0)==1) { + for (int i=0; ipat[i].data[k]==NULL) continue; + patsToWrite.push_back(PatToWrite(j,i,k)); + } + } + } + } else { + bool alreadyAdded[256]; + for (int i=0; iordersLen; k++) { + if (alreadyAdded[subs->orders.ord[i][k]]) continue; + patsToWrite.push_back(PatToWrite(j,i,subs->orders.ord[i][k])); + alreadyAdded[subs->orders.ord[i][k]]=true; + } } } } @@ -2953,6 +3702,14 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeI(0); } + // additional metadata + w->writeString(song.systemName,false); + w->writeString(song.category,false); + w->writeString(song.nameJ,false); + w->writeString(song.authorJ,false); + w->writeString(song.systemNameJ,false); + w->writeString(song.categoryJ,false); + blockEndSeek=w->tell(); w->seek(blockStartSeek,SEEK_SET); w->writeI(blockEndSeek-blockStartSeek-4); @@ -3031,18 +3788,24 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { for (int i=0; itell()); - w->write("SMPL",4); + w->write("SMP2",4); blockStartSeek=w->tell(); w->writeI(0); w->writeString(sample->name,false); w->writeI(sample->samples); w->writeI(sample->rate); - w->writeI(0); // reserved (for now) + w->writeI(sample->centerRate); w->writeC(sample->depth); + w->writeC(0); // reserved + w->writeC(0); w->writeC(0); - w->writeS(sample->centerRate); w->writeI(sample->loopStart); + w->writeI(sample->loopEnd); + + for (int i=0; i<4; i++) { + w->writeI(0xffffffff); + } w->write(sample->getCurBuf(),sample->getCurBufLen()); diff --git a/src/engine/fileOpsIns.cpp b/src/engine/fileOpsIns.cpp index b7ca22c1e..62c3a1d8a 100644 --- a/src/engine/fileOpsIns.cpp +++ b/src/engine/fileOpsIns.cpp @@ -21,6 +21,7 @@ #include "../ta-log.h" #include "../fileutils.h" #include +#include enum DivInsFormats { DIV_INSFORMAT_DMP, @@ -732,6 +733,8 @@ void DivEngine::loadOPLI(SafeReader& reader, std::vector& ret, S ins = new DivInstrument; ins->type = DIV_INS_OPL; ins->name = fmt::sprintf("%s (2)", insName); + ins->fm.alg = (feedConnect2nd & 0x1); + ins->fm.fb = ((feedConnect2nd >> 1) & 0xF); for (int i : {1,0}) { readOpliOp(reader, ins->fm.op[i]); } @@ -1262,7 +1265,7 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St patchNameRead = lfoRead = characteristicRead = m1Read = c1Read = m2Read = c2Read = false; newPatch = NULL; }; - auto readIntStrWithinRange = [](String&& input, int limitLow, int limitHigh) -> int { + auto readIntStrWithinRange = [](String&& input, int limitLow = INT_MIN, int limitHigh = INT_MAX) -> int { int x = std::stoi(input.c_str()); if (x > limitHigh || x < limitLow) { throw std::invalid_argument(fmt::sprintf("%s is out of bounds of range [%d..%d]", input, limitLow, limitHigh)); @@ -1280,7 +1283,7 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St op.mult = readIntStrWithinRange(reader.readStringToken(), 0, 15); op.dt = fmDtRegisterToFurnace(readIntStrWithinRange(reader.readStringToken(), 0, 7)); op.dt2 = readIntStrWithinRange(reader.readStringToken(), 0, 3); - op.am = readIntStrWithinRange(reader.readStringToken(), 0, 1); + op.am = readIntStrWithinRange(reader.readStringToken(), 0) > 0 ? 1 : 0; }; auto seekGroupValStart = [](SafeReader& reader, int pos) { // Seek to position then move to next ':' character @@ -1497,6 +1500,8 @@ void DivEngine::loadWOPL(SafeReader& reader, std::vector& ret, S ins = new DivInstrument; ins->type = DIV_INS_OPL; ins->name = fmt::sprintf("%s (2)", insName); + ins->fm.alg = (feedConnect2nd & 0x1); + ins->fm.fb = ((feedConnect2nd >> 1) & 0xF); for (int i : {1,0}) { patchSum += readWoplOp(reader, ins->fm.op[i]); } diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 196764d0f..2836e6f95 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -387,8 +387,12 @@ void DivInstrument::putInsData(SafeWriter* w) { // sample map w->writeC(amiga.useNoteMap); if (amiga.useNoteMap) { - w->write(amiga.noteFreq,120*sizeof(unsigned int)); - w->write(amiga.noteMap,120*sizeof(short)); + for (int note=0; note<120; note++) { + w->writeI(amiga.noteMap[note].freq); + } + for (int note=0; note<120; note++) { + w->writeS(amiga.noteMap[note].map); + } } // N163 @@ -524,6 +528,21 @@ void DivInstrument::putInsData(SafeWriter* w) { w->writeC(0); } + // Sound Unit + w->writeC(su.useSample); + w->writeC(su.switchRoles); + + // GB hardware sequence + w->writeC(gb.hwSeqLen); + for (int i=0; iwriteC(gb.hwSeq[i].cmd); + w->writeS(gb.hwSeq[i].data); + } + + // GB additional flags + w->writeC(gb.softEnv); + w->writeC(gb.alwaysInit); + blockEndSeek=w->tell(); w->seek(blockStartSeek,SEEK_SET); w->writeI(blockEndSeek-blockStartSeek-4); @@ -932,8 +951,12 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { if (version>=67) { amiga.useNoteMap=reader.readC(); if (amiga.useNoteMap) { - reader.read(amiga.noteFreq,120*sizeof(unsigned int)); - reader.read(amiga.noteMap,120*sizeof(short)); + for (int note=0; note<120; note++) { + amiga.noteMap[note].freq=reader.readI(); + } + for (int note=0; note<120; note++) { + amiga.noteMap[note].map=reader.readS(); + } } } @@ -1067,6 +1090,27 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { for (int k=0; k<23; k++) reader.readC(); } + // Sound Unit + if (version>=104) { + su.useSample=reader.readC(); + su.switchRoles=reader.readC(); + } + + // GB hardware sequence + if (version>=105) { + gb.hwSeqLen=reader.readC(); + for (int i=0; i=106) { + gb.softEnv=reader.readC(); + gb.alwaysInit=reader.readC(); + } + return DIV_DATA_SUCCESS; } @@ -1106,3 +1150,148 @@ bool DivInstrument::save(const char* path) { w->finish(); return true; } + +bool DivInstrument::saveDMP(const char* path) { + SafeWriter* w=new SafeWriter(); + w->init(); + + // write version + w->writeC(11); + + // guess the system + switch (type) { + case DIV_INS_FM: + // we can't tell between Genesis, Neo Geo and Arcade ins type yet + w->writeC(0x02); + w->writeC(1); + break; + case DIV_INS_STD: + // we can't tell between SMS and NES ins type yet + w->writeC(0x03); + w->writeC(0); + break; + case DIV_INS_GB: + w->writeC(0x04); + w->writeC(0); + break; + case DIV_INS_C64: + w->writeC(0x07); + w->writeC(0); + break; + case DIV_INS_PCE: + w->writeC(0x06); + w->writeC(0); + break; + case DIV_INS_OPLL: + // ??? + w->writeC(0x13); + w->writeC(1); + break; + case DIV_INS_OPZ: + // data will be lost + w->writeC(0x08); + w->writeC(1); + break; + case DIV_INS_FDS: + // ??? + w->writeC(0x06); + w->writeC(0); + break; + default: + // not supported by .dmp + w->finish(); + return false; + } + + if (type==DIV_INS_FM || type==DIV_INS_OPLL || type==DIV_INS_OPZ) { + w->writeC(fm.fms); + w->writeC(fm.fb); + w->writeC(fm.alg); + w->writeC(fm.ams); + + // TODO: OPLL params + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=fm.op[i]; + w->writeC(op.mult); + w->writeC(op.tl); + w->writeC(op.ar); + w->writeC(op.dr); + w->writeC(op.sl); + w->writeC(op.rr); + w->writeC(op.am); + w->writeC(op.rs); + w->writeC(op.dt|(op.dt2<<4)); + w->writeC(op.d2r); + w->writeC(op.ssgEnv); + } + } else { + if (type!=DIV_INS_GB) { + w->writeC(std.volMacro.len); + for (int i=0; iwriteI(std.volMacro.val[i]); + } + if (std.volMacro.len>0) w->writeC(std.volMacro.loop); + } + + w->writeC(std.arpMacro.len); + for (int i=0; iwriteI(std.arpMacro.val[i]+12); + } + if (std.arpMacro.len>0) w->writeC(std.arpMacro.loop); + w->writeC(std.arpMacro.mode); + + w->writeC(std.dutyMacro.len); + for (int i=0; iwriteI(std.dutyMacro.val[i]+12); + } + if (std.dutyMacro.len>0) w->writeC(std.dutyMacro.loop); + + w->writeC(std.waveMacro.len); + for (int i=0; iwriteI(std.waveMacro.val[i]+12); + } + if (std.waveMacro.len>0) w->writeC(std.waveMacro.loop); + + if (type==DIV_INS_C64) { + w->writeC(c64.triOn); + w->writeC(c64.sawOn); + w->writeC(c64.pulseOn); + w->writeC(c64.noiseOn); + w->writeC(c64.a); + w->writeC(c64.d); + w->writeC(c64.s); + w->writeC(c64.r); + w->writeC((c64.duty*100)/4095); + w->writeC(c64.ringMod); + w->writeC(c64.oscSync); + w->writeC(c64.toFilter); + w->writeC(c64.volIsCutoff); + w->writeC(c64.initFilter); + w->writeC(c64.res); + w->writeC((c64.cut*100)/2047); + w->writeC(c64.hp); + w->writeC(c64.lp); + w->writeC(c64.bp); + w->writeC(c64.ch3off); + } + if (type==DIV_INS_GB) { + w->writeC(gb.envVol); + w->writeC(gb.envDir); + w->writeC(gb.envLen); + w->writeC(gb.soundLen); + } + } + + FILE* outFile=ps_fopen(path,"wb"); + if (outFile==NULL) { + logE("could not save instrument: %s!",strerror(errno)); + w->finish(); + return false; + } + if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) { + logW("did not write entire instrument!"); + } + fclose(outFile); + w->finish(); + return true; +} diff --git a/src/engine/instrument.h b/src/engine/instrument.h index c4af6f042..9f49a627a 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -261,12 +261,32 @@ struct DivInstrumentSTD { }; struct DivInstrumentGB { - unsigned char envVol, envDir, envLen, soundLen; + unsigned char envVol, envDir, envLen, soundLen, hwSeqLen; + bool softEnv, alwaysInit; + enum HWSeqCommands: unsigned char { + DIV_GB_HWCMD_ENVELOPE=0, + DIV_GB_HWCMD_SWEEP, + DIV_GB_HWCMD_WAIT, + DIV_GB_HWCMD_WAIT_REL, + DIV_GB_HWCMD_LOOP, + DIV_GB_HWCMD_LOOP_REL, + + DIV_GB_HWCMD_MAX + }; + struct HWSeqCommand { + unsigned char cmd; + unsigned short data; + } hwSeq[256]; DivInstrumentGB(): envVol(15), envDir(0), envLen(2), - soundLen(64) {} + soundLen(64), + hwSeqLen(0), + softEnv(false), + alwaysInit(false) { + memset(hwSeq,0,256*sizeof(int)); + } }; struct DivInstrumentC64 { @@ -306,12 +326,18 @@ struct DivInstrumentC64 { }; struct DivInstrumentAmiga { + struct SampleMap { + int freq; + short map; + SampleMap(int f=0, short m=-1): + freq(f), + map(m) {} + }; short initSample; bool useNoteMap; bool useWave; unsigned char waveLen; - int noteFreq[120]; - short noteMap[120]; + SampleMap noteMap[120]; /** * get the sample at specified note. @@ -321,7 +347,7 @@ struct DivInstrumentAmiga { if (useNoteMap) { if (note<0) note=0; if (note>119) note=119; - return noteMap[note]; + return noteMap[note].map; } return initSample; } @@ -334,7 +360,7 @@ struct DivInstrumentAmiga { if (useNoteMap) { if (note<0) note=0; if (note>119) note=119; - return noteFreq[note]; + return noteMap[note].freq; } return -1; } @@ -344,8 +370,9 @@ struct DivInstrumentAmiga { useNoteMap(false), useWave(false), waveLen(31) { - memset(noteMap,-1,120*sizeof(short)); - memset(noteFreq,0,120*sizeof(int)); + for (SampleMap& elem: noteMap) { + elem=SampleMap(); + } } }; @@ -430,6 +457,14 @@ struct DivInstrumentWaveSynth { param4(0) {} }; +struct DivInstrumentSoundUnit { + bool useSample; + bool switchRoles; + DivInstrumentSoundUnit(): + useSample(false), + switchRoles(false) {} +}; + struct DivInstrument { String name; bool mode; @@ -443,6 +478,7 @@ struct DivInstrument { DivInstrumentFDS fds; DivInstrumentMultiPCM multipcm; DivInstrumentWaveSynth ws; + DivInstrumentSoundUnit su; /** * save the instrument to a SafeWriter. @@ -464,6 +500,13 @@ struct DivInstrument { * @return whether it was successful. */ bool save(const char* path); + + /** + * save this instrument to a file in .dmp format. + * @param path file path. + * @return whether it was successful. + */ + bool saveDMP(const char* path); DivInstrument(): name(""), type(DIV_INS_FM) { diff --git a/src/engine/pattern.cpp b/src/engine/pattern.cpp index 0a561376b..77255e084 100644 --- a/src/engine/pattern.cpp +++ b/src/engine/pattern.cpp @@ -18,6 +18,7 @@ */ #include "engine.h" +#include "../ta-log.h" static DivPattern emptyPat; @@ -40,6 +41,44 @@ DivPattern* DivChannelData::getPattern(int index, bool create) { return data[index]; } +std::vector> DivChannelData::optimize() { + std::vector> ret; + for (int i=0; i<256; i++) { + if (data[i]!=NULL) { + // compare + for (int j=0; j<256; j++) { + if (j==i) continue; + if (data[j]==NULL) continue; + if (memcmp(data[i]->data,data[j]->data,256*32*sizeof(short))==0) { + delete data[j]; + data[j]=NULL; + logV("%d == %d",i,j); + ret.push_back(std::pair(j,i)); + } + } + } + } + return ret; +} + +std::vector> DivChannelData::rearrange() { + std::vector> ret; + for (int i=0; i<256; i++) { + if (data[i]==NULL) { + for (int j=i; j<256; j++) { + if (data[j]!=NULL) { + data[i]=data[j]; + data[j]=NULL; + logV("%d -> %d",j,i); + ret.push_back(std::pair(j,i)); + if (++i>=256) break; + } + } + } + } + return ret; +} + void DivChannelData::wipePatterns() { for (int i=0; i<256; i++) { if (data[i]!=NULL) { @@ -54,81 +93,6 @@ void DivPattern::copyOn(DivPattern* dest) { memcpy(dest->data,data,sizeof(data)); } -SafeReader* DivPattern::compile(int len, int fxRows) { - SafeWriter w; - w.init(); - short lastNote, lastOctave, lastInstr, lastVolume, lastEffect[8], lastEffectVal[8]; - unsigned char rows=0; - - lastNote=0; - lastOctave=0; - lastInstr=-1; - lastVolume=-1; - memset(lastEffect,-1,8*sizeof(short)); - memset(lastEffectVal,-1,8*sizeof(short)); - - for (int i=0; i struct DivPattern { String name; @@ -28,14 +29,6 @@ struct DivPattern { * @param dest the destination pattern. */ void copyOn(DivPattern* dest); - - /** - * don't use yet! - * @param len the pattern length - * @param fxRows number of effect ...columns - * @return a SafeReader. - */ - SafeReader* compile(int len=256, int fxRows=1); DivPattern(); }; @@ -59,6 +52,20 @@ struct DivChannelData { */ DivPattern* getPattern(int index, bool create); + /** + * optimize pattern data. + * not thread-safe! use a mutex! + * @return a list of From -> To pairs + */ + std::vector> optimize(); + + /** + * re-arrange NULLs. + * not thread-safe! use a mutex! + * @return a list of From -> To pairs + */ + std::vector> rearrange(); + /** * destroy all patterns on this DivChannelData. */ diff --git a/src/engine/platform/abstract.cpp b/src/engine/platform/abstract.cpp index 54366e1ba..5b732e7e9 100644 --- a/src/engine/platform/abstract.cpp +++ b/src/engine/platform/abstract.cpp @@ -90,6 +90,10 @@ bool DivDispatch::getDCOffRequired() { return false; } +bool DivDispatch::getWantPreNote() { + return false; +} + const char* DivDispatch::getEffectName(unsigned char effect) { return NULL; } diff --git a/src/engine/platform/amiga.cpp b/src/engine/platform/amiga.cpp index 65b44136f..1b0075307 100644 --- a/src/engine/platform/amiga.cpp +++ b/src/engine/platform/amiga.cpp @@ -114,12 +114,10 @@ void DivPlatformAmiga::acquire(short* bufL, short* bufR, size_t start, size_t le if (chan[i].audPossamples) { writeAudDat(s->data8[chan[i].audPos++]); } - if (chan[i].audPos>=s->samples || chan[i].audPos>=131071) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[i].audPos=s->loopStart; - } else { - chan[i].sample=-1; - } + if (s->isLoopable() && chan[i].audPos>=MIN(131071,s->getEndPosition())) { + chan[i].audPos=s->loopStart; + } else if (chan[i].audPos>=MIN(131071,s->samples)) { + chan[i].sample=-1; } } else { chan[i].sample=-1; diff --git a/src/engine/platform/c64.cpp b/src/engine/platform/c64.cpp index 640df54e6..9049ffec4 100644 --- a/src/engine/platform/c64.cpp +++ b/src/engine/platform/c64.cpp @@ -513,6 +513,10 @@ bool DivPlatformC64::getDCOffRequired() { return true; } +bool DivPlatformC64::getWantPreNote() { + return true; +} + void DivPlatformC64::reset() { for (int i=0; i<3; i++) { chan[i]=DivPlatformC64::Channel(); diff --git a/src/engine/platform/c64.h b/src/engine/platform/c64.h index d9dc08040..ae1034a82 100644 --- a/src/engine/platform/c64.h +++ b/src/engine/platform/c64.h @@ -97,6 +97,7 @@ class DivPlatformC64: public DivDispatch { void setFlags(unsigned int flags); void notifyInsChange(int ins); bool getDCOffRequired(); + bool getWantPreNote(); DivMacroInt* getChanMacroInt(int ch); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); diff --git a/src/engine/platform/gb.cpp b/src/engine/platform/gb.cpp index 2b7dcf6df..c29329b95 100644 --- a/src/engine/platform/gb.cpp +++ b/src/engine/platform/gb.cpp @@ -21,8 +21,8 @@ #include "../engine.h" #include -#define rWrite(a,v) if (!skipRegisterWrites) {GB_apu_write(gb,a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } -#define immWrite(a,v) {GB_apu_write(gb,a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) {writes.emplace(a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } #define CHIP_DIVIDER 16 @@ -84,6 +84,12 @@ const char* DivPlatformGB::getEffectName(unsigned char effect) { void DivPlatformGB::acquire(short* bufL, short* bufR, size_t start, size_t len) { for (size_t i=start; iapu_output.final_sample.left; bufR[i]=gb->apu_output.final_sample.right; @@ -97,10 +103,11 @@ void DivPlatformGB::acquire(short* bufL, short* bufR, size_t start, size_t len) void DivPlatformGB::updateWave() { rWrite(0x1a,0); for (int i=0; i<16; i++) { - int nibble1=15-ws.output[i<<1]; - int nibble2=15-ws.output[1+(i<<1)]; + int nibble1=15-ws.output[((i<<1)+antiClickWavePos-1)&31]; + int nibble2=15-ws.output[((1+(i<<1))+antiClickWavePos-1)&31]; rWrite(0x30+i,(nibble1<<4)|nibble2); } + antiClickWavePos&=31; } static unsigned char chanMuteMask[4]={ @@ -151,8 +158,32 @@ static unsigned char noiseTable[256]={ }; void DivPlatformGB::tick(bool sysTick) { + if (antiClickEnabled && sysTick && chan[2].freq>0) { + antiClickPeriodCount+=((chipClock>>1)/MAX(parent->getCurHz(),1.0f)); + antiClickWavePos+=antiClickPeriodCount/chan[2].freq; + antiClickPeriodCount%=chan[2].freq; + } + for (int i=0; i<4; i++) { chan[i].std.next(); + if (chan[i].softEnv) { + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LINEAR(chan[i].vol&15,MIN(15,chan[i].std.vol.val),15); + if (chan[i].outVol<0) chan[i].outVol=0; + + if (i==2) { + rWrite(16+i*5+2,gbVolMap[chan[i].outVol]); + chan[i].soundLen=64; + } else { + chan[i].envLen=0; + chan[i].envDir=1; + chan[i].envVol=chan[i].outVol; + chan[i].soundLen=64; + + if (!chan[i].keyOn) chan[i].killIt=true; + } + } + } if (chan[i].std.arp.had) { if (i==3) { // noise if (chan[i].std.arp.mode) { @@ -180,10 +211,9 @@ void DivPlatformGB::tick(bool sysTick) { } if (chan[i].std.duty.had) { chan[i].duty=chan[i].std.duty.val; - DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_GB); if (i!=2) { - rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); - } else { + rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(chan[i].soundLen&63))); + } else if (!chan[i].softEnv) { if (parent->song.waveDutyIsVol) { rWrite(16+i*5+2,gbVolMap[(chan[i].std.duty.val&3)<<2]); } @@ -213,6 +243,10 @@ void DivPlatformGB::tick(bool sysTick) { if (chan[i].std.phaseReset.had) { if (chan[i].std.phaseReset.val==1) { chan[i].keyOn=true; + if (i==2) { + antiClickWavePos=0; + antiClickPeriodCount=0; + } } } if (i==2) { @@ -223,14 +257,64 @@ void DivPlatformGB::tick(bool sysTick) { } } } + // run hardware sequence + if (chan[i].active) { + if (--chan[i].hwSeqDelay<=0) { + chan[i].hwSeqDelay=0; + DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_GB); + int hwSeqCount=0; + while (chan[i].hwSeqPosgb.hwSeqLen && hwSeqCount<4) { + bool leave=false; + unsigned short data=ins->gb.hwSeq[chan[i].hwSeqPos].data; + switch (ins->gb.hwSeq[chan[i].hwSeqPos].cmd) { + case DivInstrumentGB::DIV_GB_HWCMD_ENVELOPE: + if (!chan[i].softEnv) { + chan[i].envLen=data&7; + chan[i].envDir=(data&8)?1:0; + chan[i].envVol=(data>>4)&15; + chan[i].soundLen=data>>8; + chan[i].keyOn=true; + } + break; + case DivInstrumentGB::DIV_GB_HWCMD_SWEEP: + chan[i].sweep=data; + chan[i].sweepChanged=true; + break; + case DivInstrumentGB::DIV_GB_HWCMD_WAIT: + chan[i].hwSeqDelay=data+1; + leave=true; + break; + case DivInstrumentGB::DIV_GB_HWCMD_WAIT_REL: + if (!chan[i].released) { + chan[i].hwSeqPos--; + leave=true; + } + break; + case DivInstrumentGB::DIV_GB_HWCMD_LOOP: + chan[i].hwSeqPos=data-1; + break; + case DivInstrumentGB::DIV_GB_HWCMD_LOOP_REL: + if (!chan[i].released) { + chan[i].hwSeqPos=data-1; + } + break; + } + + chan[i].hwSeqPos++; + if (leave) break; + hwSeqCount++; + } + } + } + if (chan[i].sweepChanged) { chan[i].sweepChanged=false; if (i==0) { rWrite(16+i*5,chan[i].sweep); } } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_GB); if (i==3) { // noise int ntPos=chan[i].baseFreq; if (ntPos<0) ntPos=0; @@ -244,10 +328,11 @@ void DivPlatformGB::tick(bool sysTick) { if (chan[i].keyOn) { if (i==2) { // wave rWrite(16+i*5,0x80); - rWrite(16+i*5+2,gbVolMap[chan[i].vol]); + rWrite(16+i*5+2,gbVolMap[chan[i].outVol]); } else { - rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); - rWrite(16+i*5+2,((chan[i].vol<<4))|(ins->gb.envLen&7)|((ins->gb.envDir&1)<<3)); + rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(chan[i].soundLen&63))); + rWrite(16+i*5+2,((chan[i].envVol<<4))|(chan[i].envLen&7)|((chan[i].envDir&1)<<3)); + chan[i].lastKill=chan[i].envVol; } } if (chan[i].keyOff) { @@ -259,15 +344,35 @@ void DivPlatformGB::tick(bool sysTick) { } if (i==3) { // noise rWrite(16+i*5+3,(chan[i].freq&0xff)|(chan[i].duty?8:0)); - rWrite(16+i*5+4,((chan[i].keyOn||chan[i].keyOff)?0x80:0x00)|((ins->gb.soundLen<64)<<6)); + rWrite(16+i*5+4,((chan[i].keyOn||chan[i].keyOff)?0x80:0x00)|((chan[i].soundLen<64)<<6)); } else { rWrite(16+i*5+3,(2048-chan[i].freq)&0xff); - rWrite(16+i*5+4,(((2048-chan[i].freq)>>8)&7)|((chan[i].keyOn||chan[i].keyOff)?0x80:0x00)|((ins->gb.soundLen<63)<<6)); + rWrite(16+i*5+4,(((2048-chan[i].freq)>>8)&7)|((chan[i].keyOn||chan[i].keyOff)?0x80:0x00)|((chan[i].soundLen<63)<<6)); } if (chan[i].keyOn) chan[i].keyOn=false; if (chan[i].keyOff) chan[i].keyOff=false; chan[i].freqChanged=false; } + if (chan[i].killIt) { + if (i!=2) { + //rWrite(16+i*5+2,8); + int killDelta=chan[i].lastKill-chan[i].outVol+1; + if (killDelta<0) killDelta+=16; + chan[i].lastKill=chan[i].outVol; + + if (killDelta!=1) { + rWrite(16+i*5+2,((chan[i].envVol<<4))|8); + for (int j=0; jgb.softEnv; chan[c.chan].macroInit(ins); if (c.chan==2) { if (chan[c.chan].wave<0) { @@ -299,17 +408,35 @@ int DivPlatformGB::dispatch(DivCommand c) { } ws.init(ins,32,15,chan[c.chan].insChanged); } + if ((chan[c.chan].insChanged || ins->gb.alwaysInit) && !chan[c.chan].softEnv) { + if (!chan[c.chan].soManyHacksToMakeItDefleCompatible && c.chan!=2) { + chan[c.chan].envVol=ins->gb.envVol; + } + chan[c.chan].envLen=ins->gb.envLen; + chan[c.chan].envDir=ins->gb.envDir; + chan[c.chan].soundLen=ins->gb.soundLen; + if (!chan[c.chan].soManyHacksToMakeItDefleCompatible && c.chan!=2) { + chan[c.chan].vol=chan[c.chan].envVol; + chan[c.chan].outVol=chan[c.chan].envVol; + } + } + if (c.chan==2 && chan[c.chan].softEnv) { + chan[c.chan].soundLen=64; + } chan[c.chan].insChanged=false; break; } case DIV_CMD_NOTE_OFF: chan[c.chan].active=false; chan[c.chan].keyOff=true; + chan[c.chan].hwSeqPos=0; + chan[c.chan].hwSeqDelay=0; chan[c.chan].macroInit(NULL); break; case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: chan[c.chan].std.release(); + chan[c.chan].released=true; break; case DIV_CMD_INSTRUMENT: if (chan[c.chan].ins!=c.value || c.value2==1) { @@ -317,17 +444,33 @@ int DivPlatformGB::dispatch(DivCommand c) { chan[c.chan].insChanged=true; if (c.chan!=2) { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_GB); - chan[c.chan].vol=ins->gb.envVol; - if (parent->song.gbInsAffectsEnvelope) { - rWrite(16+c.chan*5+2,((chan[c.chan].vol<<4))|(ins->gb.envLen&7)|((ins->gb.envDir&1)<<3)); + if (!ins->gb.softEnv) { + chan[c.chan].envVol=ins->gb.envVol; + chan[c.chan].envLen=ins->gb.envLen; + chan[c.chan].envDir=ins->gb.envDir; + chan[c.chan].soundLen=ins->gb.soundLen; + chan[c.chan].vol=chan[c.chan].envVol; + chan[c.chan].outVol=chan[c.chan].vol; + if (parent->song.gbInsAffectsEnvelope) { + rWrite(16+c.chan*5+2,((chan[c.chan].vol<<4))|(chan[c.chan].envLen&7)|((chan[c.chan].envDir&1)<<3)); + } } } } break; case DIV_CMD_VOLUME: chan[c.chan].vol=c.value; + chan[c.chan].outVol=c.value; if (c.chan==2) { - rWrite(16+c.chan*5+2,gbVolMap[chan[c.chan].vol]); + rWrite(16+c.chan*5+2,gbVolMap[chan[c.chan].outVol]); + } + if (!chan[c.chan].softEnv) { + chan[c.chan].envVol=chan[c.chan].vol; + chan[c.chan].soManyHacksToMakeItDefleCompatible=true; + } else if (c.chan!=2) { + chan[c.chan].envVol=chan[c.chan].vol; + if (!chan[c.chan].keyOn) chan[c.chan].killIt=true; + chan[c.chan].freqChanged=true; } break; case DIV_CMD_GET_VOLUME: @@ -462,7 +605,7 @@ void DivPlatformGB::reset() { } memset(gb,0,sizeof(GB_gameboy_t)); memset(regPool,0,128); - gb->model=GB_MODEL_DMG_B; + gb->model=model; GB_apu_init(gb); GB_set_sample_rate(gb,rate); // enable all channels @@ -471,12 +614,23 @@ void DivPlatformGB::reset() { lastPan=0xff; immWrite(0x25,procMute()); immWrite(0x24,0x77); + + antiClickPeriodCount=0; + antiClickWavePos=0; +} + +int DivPlatformGB::getPortaFloor(int ch) { + return 24; } bool DivPlatformGB::isStereo() { return true; } +bool DivPlatformGB::getDCOffRequired() { + return (model==GB_MODEL_AGB); +} + void DivPlatformGB::notifyInsChange(int ins) { for (int i=0; i<4; i++) { if (chan[i].ins==ins) { @@ -489,7 +643,7 @@ void DivPlatformGB::notifyWaveChange(int wave) { if (chan[2].wave==wave) { ws.changeWave1(wave); updateWave(); - if (!chan[2].keyOff) chan[2].keyOn=true; + if (!chan[2].keyOff && chan[2].active) chan[2].keyOn=true; } } @@ -507,6 +661,24 @@ void DivPlatformGB::poke(std::vector& wlist) { for (DivRegWrite& i: wlist) immWrite(i.addr,i.val); } +void DivPlatformGB::setFlags(unsigned int flags) { + antiClickEnabled=!(flags&8); + switch (flags&3) { + case 0: + model=GB_MODEL_DMG_B; + break; + case 1: + model=GB_MODEL_CGB_C; + break; + case 2: + model=GB_MODEL_CGB_E; + break; + case 3: + model=GB_MODEL_AGB; + break; + } +} + int DivPlatformGB::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { chipClock=4194304; rate=chipClock/16; @@ -518,7 +690,9 @@ int DivPlatformGB::init(DivEngine* p, int channels, int sugRate, unsigned int fl parent=p; dumpWrites=false; skipRegisterWrites=false; + model=GB_MODEL_DMG_B; gb=new GB_gameboy_t; + setFlags(flags); reset(); return 4; } diff --git a/src/engine/platform/gb.h b/src/engine/platform/gb.h index fe2f6e51b..00dcca036 100644 --- a/src/engine/platform/gb.h +++ b/src/engine/platform/gb.h @@ -24,13 +24,18 @@ #include "../macroInt.h" #include "../waveSynth.h" #include "sound/gb/gb.h" +#include class DivPlatformGB: public DivDispatch { struct Channel { int freq, baseFreq, pitch, pitch2, note, ins; unsigned char duty, sweep; - bool active, insChanged, freqChanged, sweepChanged, keyOn, keyOff, inPorta; - signed char vol, outVol, wave; + bool active, insChanged, freqChanged, sweepChanged, keyOn, keyOff, inPorta, released, softEnv, killIt; + bool soManyHacksToMakeItDefleCompatible; + signed char vol, outVol, wave, lastKill; + unsigned char envVol, envDir, envLen, soundLen; + unsigned short hwSeqPos; + short hwSeqDelay; DivMacroInt std; void macroInit(DivInstrument* which) { std.init(which); @@ -52,17 +57,38 @@ class DivPlatformGB: public DivDispatch { keyOn(false), keyOff(false), inPorta(false), + released(false), + softEnv(false), + killIt(false), + soManyHacksToMakeItDefleCompatible(false), vol(15), outVol(15), - wave(-1) {} + wave(-1), + lastKill(0), + envVol(0), + envDir(0), + envLen(0), + soundLen(0), + hwSeqPos(0), + hwSeqDelay(0) {} }; Channel chan[4]; DivDispatchOscBuffer* oscBuf[4]; bool isMuted[4]; + bool antiClickEnabled; unsigned char lastPan; DivWaveSynth ws; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {} + }; + std::queue writes; + + int antiClickPeriodCount, antiClickWavePos; GB_gameboy_t* gb; + GB_model_t model; unsigned char regPool[128]; unsigned char procMute(); @@ -80,7 +106,9 @@ class DivPlatformGB: public DivDispatch { void forceIns(); void tick(bool sysTick=true); void muteChannel(int ch, bool mute); + int getPortaFloor(int ch); bool isStereo(); + bool getDCOffRequired(); void notifyInsChange(int ins); void notifyWaveChange(int wave); void notifyInsDeletion(void* ins); @@ -88,6 +116,7 @@ class DivPlatformGB: public DivDispatch { void poke(std::vector& wlist); const char** getRegisterSheet(); const char* getEffectName(unsigned char effect); + void setFlags(unsigned int flags); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); ~DivPlatformGB(); diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index afd59b336..b6dbb8d12 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -153,14 +153,13 @@ void DivPlatformGenesis::processDAC() { if (chan[i].dacPeriod>=(chipClock/576)) { if (s->samples>0) { while (chan[i].dacPeriod>=(chipClock/576)) { - if (++chan[i].dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples && !chan[i].dacDirection) { - chan[i].dacPos=s->loopStart; - } else { - chan[i].dacSample=-1; - chan[i].dacPeriod=0; - break; - } + ++chan[i].dacPos; + if (!chan[i].dacDirection && (s->isLoopable() && chan[i].dacPos>=s->getEndPosition())) { + chan[i].dacPos=s->loopStart; + } else if (chan[i].dacPos>=s->samples) { + chan[i].dacSample=-1; + chan[i].dacPeriod=0; + break; } chan[i].dacPeriod-=(chipClock/576); } @@ -200,14 +199,13 @@ void DivPlatformGenesis::processDAC() { chan[5].dacReady=false; } } - if (++chan[5].dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples && !chan[5].dacDirection) { - chan[5].dacPos=s->loopStart; - } else { - chan[5].dacSample=-1; - if (parent->song.brokenDACMode) { - rWrite(0x2b,0); - } + chan[5].dacPos++; + if (!chan[5].dacDirection && (s->isLoopable() && chan[5].dacPos>=s->getEndPosition())) { + chan[5].dacPos=s->loopStart; + } else if (chan[5].dacPos>=s->samples) { + chan[5].dacSample=-1; + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); } } while (chan[5].dacPeriod>=rate) chan[5].dacPeriod-=rate; diff --git a/src/engine/platform/genesisext.cpp b/src/engine/platform/genesisext.cpp index 1c44bcfb9..1f320dd34 100644 --- a/src/engine/platform/genesisext.cpp +++ b/src/engine/platform/genesisext.cpp @@ -24,6 +24,8 @@ #define CHIP_FREQBASE fmFreqBase #define CHIP_DIVIDER fmDivBase +#define IS_REALLY_MUTED(x) (isMuted[x] && (x<5 || !softPCM || (isMuted[5] && isMuted[6]))) + int DivPlatformGenesisExt::dispatch(DivCommand c) { if (c.chan<2) { return DivPlatformGenesis::dispatch(c); @@ -418,6 +420,16 @@ void DivPlatformGenesisExt::tick(bool sysTick) { } } if (writeSomething) { + if (chan[7].active) { // CSM + writeMask^=0xf0; + } + /*printf( + "Mask: %c %c %c %c\n", + (writeMask&0x10)?'1':'-', + (writeMask&0x20)?'2':'-', + (writeMask&0x40)?'3':'-', + (writeMask&0x80)?'4':'-' + );*/ immWrite(0x28,writeMask); } } @@ -478,6 +490,13 @@ void DivPlatformGenesisExt::tick(bool sysTick) { if (chan[7].active) { // CSM writeMask^=0xf0; } + /*printf( + "Mask: %c %c %c %c\n", + (writeMask&0x10)?'1':'-', + (writeMask&0x20)?'2':'-', + (writeMask&0x40)?'3':'-', + (writeMask&0x80)?'4':'-' + );*/ immWrite(0x28,writeMask); } @@ -525,7 +544,7 @@ void DivPlatformGenesisExt::forceIns() { rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); } rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); - rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + rWrite(chanOffs[i]+ADDR_LRAF,(IS_REALLY_MUTED(i)?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); if (chan[i].active) { chan[i].keyOn=true; chan[i].freqChanged=true; diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 3f5e92c03..7c349e5a5 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -158,12 +158,10 @@ void DivPlatformLynx::acquire(short* bufL, short* bufR, size_t start, size_t len WRITE_OUTPUT(i,(s->data8[chan[i].samplePos++]*chan[i].outVol)>>7); } - if (chan[i].samplePos>=(int)s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[i].samplePos=s->loopStart; - } else { - chan[i].sample=-1; - } + if (s->isLoopable() && chan[i].samplePos>=(int)s->getEndPosition()) { + chan[i].samplePos=s->loopStart; + } else if (chan[i].samplePos>=(int)s->samples) { + chan[i].sample=-1; } } } diff --git a/src/engine/platform/mmc5.cpp b/src/engine/platform/mmc5.cpp index dc9e5abad..2154e855f 100644 --- a/src/engine/platform/mmc5.cpp +++ b/src/engine/platform/mmc5.cpp @@ -62,12 +62,11 @@ void DivPlatformMMC5::acquire(short* bufL, short* bufR, size_t start, size_t len if (!isMuted[2]) { rWrite(0x5011,((unsigned char)s->data8[dacPos]+0x80)); } - if (++dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - dacPos=s->loopStart; - } else { - dacSample=-1; - } + dacPos++; + if (s->isLoopable() && dacPos>=s->getEndPosition()) { + dacPos=s->loopStart; + } else if (dacPos>=s->samples) { + dacSample=-1; } dacPeriod-=rate; } else { @@ -172,7 +171,7 @@ void DivPlatformMMC5::tick(bool sysTick) { // PCM if (chan[2].freqChanged) { - chan[2].freq=parent->calcFreq(chan[2].baseFreq,chan[2].pitch,false); + chan[2].freq=parent->calcFreq(chan[2].baseFreq,chan[2].pitch,false,0,chan[2].pitch2,1,1); if (chan[2].furnaceDac) { double off=1.0; if (dacSample>=0 && dacSamplesong.sampleLen) { @@ -202,7 +201,7 @@ int DivPlatformMMC5::dispatch(DivCommand c) { } dacPos=0; dacPeriod=0; - chan[c.chan].baseFreq=parent->song.tuning*pow(2.0f,((float)(c.value+3)/12.0f)); + chan[c.chan].baseFreq=parent->calcBaseFreq(1,1,c.value,false); if (c.value!=DIV_NOTE_NULL) { chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; @@ -283,7 +282,7 @@ int DivPlatformMMC5::dispatch(DivCommand c) { chan[c.chan].freqChanged=true; break; case DIV_CMD_NOTE_PORTA: { - int destFreq=NOTE_PERIODIC(c.value2); + int destFreq=(c.chan==2)?(parent->calcBaseFreq(1,1,c.value2,false)):(NOTE_PERIODIC(c.value2)); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -316,7 +315,11 @@ int DivPlatformMMC5::dispatch(DivCommand c) { } break; case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); + if (c.chan==2) { + chan[c.chan].baseFreq=parent->calcBaseFreq(1,1,c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0)),false); + } else { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); + } chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; break; diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index d801fe664..bd1be94a8 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -108,12 +108,11 @@ void DivPlatformNES::doWrite(unsigned short addr, unsigned char data) { rWrite(0x4011,next); \ } \ } \ - if (++dacPos>=s->samples) { \ - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { \ - dacPos=s->loopStart; \ - } else { \ - dacSample=-1; \ - } \ + dacPos++; \ + if (s->isLoopable() && dacPos>=s->getEndPosition()) { \ + dacPos=s->loopStart; \ + } else if (dacPos>=s->samples) { \ + dacSample=-1; \ } \ dacPeriod-=rate; \ } else { \ diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index 823e764bc..8b76db213 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -293,7 +293,7 @@ void DivPlatformOPL::acquire_nuked(short* bufL, short* bufR, size_t start, size_ if (!isMuted[adpcmChan]) { os[0]-=aOut.data[0]>>3; os[1]-=aOut.data[0]>>3; - oscBuf[adpcmChan]->data[oscBuf[adpcmChan]->needle++]+=aOut.data[0]; + oscBuf[adpcmChan]->data[oscBuf[adpcmChan]->needle++]=aOut.data[0]; } else { oscBuf[adpcmChan]->data[oscBuf[adpcmChan]->needle++]=0; } @@ -771,7 +771,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { int end=s->offB+s->lengthB-1; immWrite(11,(end>>2)&0xff); immWrite(12,(end>>10)&0xff); - immWrite(7,(s->loopStart>=0)?0xb0:0xa0); // start/repeat + immWrite(7,(s->isLoopable())?0xb0:0xa0); // start/repeat if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); @@ -807,7 +807,7 @@ int DivPlatformOPL::dispatch(DivCommand c) { int end=s->offB+s->lengthB-1; immWrite(11,(end>>2)&0xff); immWrite(12,(end>>10)&0xff); - immWrite(7,(s->loopStart>=0)?0xb0:0xa0); // start/repeat + immWrite(7,(s->isLoopable())?0xb0:0xa0); // start/repeat int freq=(65536.0*(double)s->rate)/(double)chipRateBase; immWrite(16,freq&0xff); immWrite(17,(freq>>8)&0xff); @@ -872,6 +872,13 @@ int DivPlatformOPL::dispatch(DivCommand c) { int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; chan[c.chan].fourOp=(ops==4); if (chan[c.chan].fourOp) { + /* + if (chan[c.chan+1].active) { + chan[c.chan+1].keyOff=true; + chan[c.chan+1].keyOn=false; + chan[c.chan+1].active=false; + }*/ + chan[c.chan+1].insChanged=true; chan[c.chan+1].macroInit(NULL); } update4OpMask=true; diff --git a/src/engine/platform/pce.cpp b/src/engine/platform/pce.cpp index 9e1302a0f..3fc7a05bd 100644 --- a/src/engine/platform/pce.cpp +++ b/src/engine/platform/pce.cpp @@ -90,12 +90,10 @@ void DivPlatformPCE::acquire(short* bufL, short* bufR, size_t start, size_t len) chWrite(i,0x04,0xdf); chWrite(i,0x06,(((unsigned char)s->data8[chan[i].dacPos]+0x80)>>3)); chan[i].dacPos++; - if (chan[i].dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[i].dacPos=s->loopStart; - } else { - chan[i].dacSample=-1; - } + if (s->isLoopable() && chan[i].dacPos>=s->getEndPosition()) { + chan[i].dacPos=s->loopStart; + } else if (chan[i].dacPos>=s->samples) { + chan[i].dacSample=-1; } chan[i].dacPeriod-=rate; } @@ -117,7 +115,7 @@ void DivPlatformPCE::acquire(short* bufL, short* bufR, size_t start, size_t len) pce->ResetTS(0); for (int i=0; i<6; i++) { - oscBuf[i]->data[oscBuf[i]->needle++]=(pce->channel[i].blip_prev_samp[0]+pce->channel[i].blip_prev_samp[1])<<1; + oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP((pce->channel[i].blip_prev_samp[0]+pce->channel[i].blip_prev_samp[1])<<1,-32768,32767); } tempL[0]=(tempL[0]>>1)+(tempL[0]>>2); @@ -135,14 +133,22 @@ void DivPlatformPCE::acquire(short* bufL, short* bufR, size_t start, size_t len) } void DivPlatformPCE::updateWave(int ch) { + if (chan[ch].pcm) { + chan[ch].deferredWaveUpdate=true; + return; + } chWrite(ch,0x04,0x5f); chWrite(ch,0x04,0x1f); for (int i=0; i<32; i++) { - chWrite(ch,0x06,chan[ch].ws.output[i]); + chWrite(ch,0x06,chan[ch].ws.output[(i+chan[ch].antiClickWavePos)&31]); } + chan[ch].antiClickWavePos&=31; if (chan[ch].active) { chWrite(ch,0x04,0x80|chan[ch].outVol); } + if (chan[ch].deferredWaveUpdate) { + chan[ch].deferredWaveUpdate=false; + } } // TODO: in octave 6 the noise table changes to a tonal one @@ -152,6 +158,13 @@ static unsigned char noiseFreq[12]={ void DivPlatformPCE::tick(bool sysTick) { for (int i=0; i<6; i++) { + // anti-click + if (antiClickEnabled && sysTick && chan[i].freq>0) { + chan[i].antiClickPeriodCount+=(chipClock/MAX(parent->getCurHz(),1.0f)); + chan[i].antiClickWavePos+=chan[i].antiClickPeriodCount/chan[i].freq; + chan[i].antiClickPeriodCount%=chan[i].freq; + } + chan[i].std.next(); if (chan[i].std.vol.had) { chan[i].outVol=VOL_SCALE_LOG(chan[i].vol&31,MIN(31,chan[i].std.vol.val),31); @@ -220,8 +233,12 @@ void DivPlatformPCE::tick(bool sysTick) { } chan[i].freqChanged=true; } + if (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1) { + chan[i].antiClickWavePos=0; + chan[i].antiClickPeriodCount=0; + } if (chan[i].active) { - if (chan[i].ws.tick() || (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1)) { + if (chan[i].ws.tick() || (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1) || chan[i].deferredWaveUpdate) { updateWave(i); } } @@ -557,10 +574,18 @@ void DivPlatformPCE::setFlags(unsigned int flags) { } else { chipClock=COLOR_NTSC; } + // flags&4 will be chip revision + antiClickEnabled=!(flags&8); rate=chipClock/12; for (int i=0; i<6; i++) { oscBuf[i]->rate=rate; } + + if (pce!=NULL) { + delete pce; + pce=NULL; + } + pce=new PCE_PSG(tempL,tempR,(flags&4)?PCE_PSG::REVISION_HUC6280A:PCE_PSG::REVISION_HUC6280); } void DivPlatformPCE::poke(unsigned int addr, unsigned short val) { @@ -579,8 +604,8 @@ int DivPlatformPCE::init(DivEngine* p, int channels, int sugRate, unsigned int f isMuted[i]=false; oscBuf[i]=new DivDispatchOscBuffer; } + pce=NULL; setFlags(flags); - pce=new PCE_PSG(tempL,tempR,PCE_PSG::REVISION_HUC6280A); reset(); return 6; } @@ -589,7 +614,10 @@ void DivPlatformPCE::quit() { for (int i=0; i<6; i++) { delete oscBuf[i]; } - delete pce; + if (pce!=NULL) { + delete pce; + pce=NULL; + } } DivPlatformPCE::~DivPlatformPCE() { diff --git a/src/engine/platform/pce.h b/src/engine/platform/pce.h index 870b5218a..17e191d44 100644 --- a/src/engine/platform/pce.h +++ b/src/engine/platform/pce.h @@ -28,12 +28,12 @@ class DivPlatformPCE: public DivDispatch { struct Channel { - int freq, baseFreq, pitch, pitch2, note; + int freq, baseFreq, pitch, pitch2, note, antiClickPeriodCount, antiClickWavePos; int dacPeriod, dacRate; unsigned int dacPos; int dacSample, ins; unsigned char pan; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, furnaceDac; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, furnaceDac, deferredWaveUpdate; signed char vol, outVol, wave; DivMacroInt std; DivWaveSynth ws; @@ -47,6 +47,8 @@ class DivPlatformPCE: public DivDispatch { pitch(0), pitch2(0), note(0), + antiClickPeriodCount(0), + antiClickWavePos(0), dacPeriod(0), dacRate(0), dacPos(0), @@ -62,6 +64,7 @@ class DivPlatformPCE: public DivDispatch { noise(false), pcm(false), furnaceDac(false), + deferredWaveUpdate(false), vol(31), outVol(31), wave(-1) {} @@ -69,6 +72,7 @@ class DivPlatformPCE: public DivDispatch { Channel chan[6]; DivDispatchOscBuffer* oscBuf[6]; bool isMuted[6]; + bool antiClickEnabled; struct QueuedWrite { unsigned char addr; unsigned char val; diff --git a/src/engine/platform/pcmdac.cpp b/src/engine/platform/pcmdac.cpp new file mode 100644 index 000000000..213cb85a3 --- /dev/null +++ b/src/engine/platform/pcmdac.cpp @@ -0,0 +1,368 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#define _USE_MATH_DEFINES +#include "pcmdac.h" +#include "../engine.h" +#include + +// to ease the driver, freqency register is a 8.16 counter relative to output sample rate +#define CHIP_FREQBASE 65536 + +void DivPlatformPCMDAC::acquire(short* bufL, short* bufR, size_t start, size_t len) { + const int depthScale=(15-outDepth); + int output=0; + for (size_t h=start; hdata[oscBuf->needle++]=0; + continue; + } + if (chan.useWave || (chan.sample>=0 && chan.samplesong.sampleLen)) { + chan.audPos+=chan.freq>>16; + chan.audSub+=(chan.freq&0xffff); + if (chan.audSub>=0x10000) { + chan.audSub-=0x10000; + chan.audPos+=1; + } + if (chan.useWave) { + if (chan.audPos>=(unsigned int)(chan.audLen<<1)) { + chan.audPos=0; + } + output=(chan.ws.output[chan.audPos]^0x80)<<8; + } else { + DivSample* s=parent->getSample(chan.sample); + if (s->samples>0) { + if (s->isLoopable() && chan.audPos>=s->getEndPosition()) { + chan.audPos=s->loopStart; + } else if (chan.audPos>=s->samples) { + chan.sample=-1; + } + if (chan.audPossamples) { + output=s->data16[chan.audPos]; + } + } else { + chan.sample=-1; + } + } + } + output=output*chan.vol*chan.envVol/16384; + oscBuf->data[oscBuf->needle++]=output; + if (outStereo) { + bufL[h]=((output*chan.panL)>>(depthScale+8))<>(depthScale+8))<>depthScale)<getIns(chan.ins,DIV_INS_AMIGA); + double off=1.0; + if (!chan.useWave && chan.sample>=0 && chan.samplesong.sampleLen) { + DivSample* s=parent->getSample(chan.sample); + off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; + } + chan.freq=off*parent->calcFreq(chan.baseFreq,chan.pitch,false,2,chan.pitch2,chipClock,CHIP_FREQBASE); + if (chan.freq>16777215) chan.freq=16777215; + if (chan.keyOn) { + if (!chan.std.vol.had) { + chan.envVol=64; + } + chan.keyOn=false; + } + if (chan.keyOff) { + chan.keyOff=false; + } + chan.freqChanged=false; + } +} + +int DivPlatformPCMDAC::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan.ins,DIV_INS_AMIGA); + if (ins->amiga.useWave) { + chan.useWave=true; + chan.audLen=(ins->amiga.waveLen+1)>>1; + if (chan.insChanged) { + if (chan.wave<0) { + chan.wave=0; + chan.ws.setWidth(chan.audLen<<1); + chan.ws.changeWave1(chan.wave); + } + } + } else { + chan.sample=ins->amiga.getSample(c.value); + chan.useWave=false; + } + if (c.value!=DIV_NOTE_NULL) { + chan.baseFreq=round(NOTE_FREQUENCY(c.value)); + } + if (chan.useWave || chan.sample<0 || chan.sample>=parent->song.sampleLen) { + chan.sample=-1; + } + if (chan.setPos) { + chan.setPos=false; + } else { + chan.audPos=0; + } + chan.audSub=0; + if (c.value!=DIV_NOTE_NULL) { + chan.freqChanged=true; + chan.note=c.value; + } + chan.active=true; + chan.keyOn=true; + chan.macroInit(ins); + if (!parent->song.brokenOutVol && !chan.std.vol.will) { + chan.envVol=64; + } + if (chan.useWave) { + chan.ws.init(ins,chan.audLen<<1,255,chan.insChanged); + } + chan.insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + chan.sample=-1; + chan.active=false; + chan.keyOff=true; + chan.macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan.std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan.ins!=c.value || c.value2==1) { + chan.ins=c.value; + chan.insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan.vol!=c.value) { + chan.vol=c.value; + if (!chan.std.vol.has) { + chan.envVol=64; + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan.vol; + break; + case DIV_CMD_PANNING: + chan.panL=c.value; + chan.panR=c.value2; + break; + case DIV_CMD_PITCH: + chan.pitch=c.value; + chan.freqChanged=true; + break; + case DIV_CMD_WAVE: + if (!chan.useWave) break; + chan.wave=c.value; + chan.keyOn=true; + chan.ws.changeWave1(chan.wave); + break; + case DIV_CMD_NOTE_PORTA: { + DivInstrument* ins=parent->getIns(chan.ins,DIV_INS_AMIGA); + chan.sample=ins->amiga.getSample(c.value2); + int destFreq=round(NOTE_FREQUENCY(c.value2)); + bool return2=false; + if (destFreq>chan.baseFreq) { + chan.baseFreq+=c.value; + if (chan.baseFreq>=destFreq) { + chan.baseFreq=destFreq; + return2=true; + } + } else { + chan.baseFreq-=c.value; + if (chan.baseFreq<=destFreq) { + chan.baseFreq=destFreq; + return2=true; + } + } + chan.freqChanged=true; + if (return2) { + chan.inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: { + chan.baseFreq=round(NOTE_FREQUENCY(c.value+((chan.std.arp.will && !chan.std.arp.mode)?(chan.std.arp.val):(0)))); + chan.freqChanged=true; + chan.note=c.value; + break; + } + case DIV_CMD_PRE_PORTA: + if (chan.active && c.value2) { + if (parent->song.resetMacroOnPorta) chan.macroInit(parent->getIns(chan.ins,DIV_INS_AMIGA)); + } + chan.inPorta=c.value; + break; + case DIV_CMD_SAMPLE_POS: + if (chan.useWave) break; + chan.audPos=c.value; + chan.setPos=true; + break; + case DIV_CMD_GET_VOLMAX: + return 255; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformPCMDAC::muteChannel(int ch, bool mute) { + isMuted=mute; +} + +void DivPlatformPCMDAC::forceIns() { + chan.insChanged=true; + chan.freqChanged=true; + chan.audPos=0; + chan.sample=-1; +} + +void* DivPlatformPCMDAC::getChanState(int ch) { + return &chan; +} + +DivDispatchOscBuffer* DivPlatformPCMDAC::getOscBuffer(int ch) { + return oscBuf; +} + +void DivPlatformPCMDAC::reset() { + chan=DivPlatformPCMDAC::Channel(); + chan.std.setEngine(parent); + chan.ws.setEngine(parent); + chan.ws.init(NULL,32,255); +} + +bool DivPlatformPCMDAC::isStereo() { + return true; +} + +DivMacroInt* DivPlatformPCMDAC::getChanMacroInt(int ch) { + return &chan.std; +} + +void DivPlatformPCMDAC::notifyInsChange(int ins) { + if (chan.ins==ins) { + chan.insChanged=true; + } +} + +void DivPlatformPCMDAC::notifyWaveChange(int wave) { + if (chan.useWave && chan.wave==wave) { + chan.ws.changeWave1(wave); + } +} + +void DivPlatformPCMDAC::notifyInsDeletion(void* ins) { + chan.std.notifyInsDeletion((DivInstrument*)ins); +} + +void DivPlatformPCMDAC::setFlags(unsigned int flags) { + // default to 44100Hz 16-bit stereo + if (!flags) flags=0x1f0000|44099; + rate=(flags&0xffff)+1; + // rate can't be too low or the resampler will break + if (rate<1000) rate=1000; + chipClock=rate; + outDepth=(flags>>16)&0xf; + outStereo=(flags>>20)&1; +} + +int DivPlatformPCMDAC::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + oscBuf=new DivDispatchOscBuffer; + isMuted=false; + setFlags(flags); + reset(); + return 1; +} + +void DivPlatformPCMDAC::quit() { + delete oscBuf; +} diff --git a/src/engine/platform/pcmdac.h b/src/engine/platform/pcmdac.h new file mode 100644 index 000000000..60d2a6fc0 --- /dev/null +++ b/src/engine/platform/pcmdac.h @@ -0,0 +1,99 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _PCM_DAC_H +#define _PCM_DAC_H + +#include "../dispatch.h" +#include +#include "../macroInt.h" +#include "../waveSynth.h" + +class DivPlatformPCMDAC: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, pitch2; + unsigned int audLoc; + unsigned short audLen; + unsigned int audPos; + int audSub; + int sample, wave, ins; + int note; + int panL, panR; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos; + int vol, envVol; + DivMacroInt std; + DivWaveSynth ws; + void macroInit(DivInstrument* which) { + std.init(which); + pitch2=0; + } + Channel(): + freq(0), + baseFreq(0), + pitch(0), + pitch2(0), + audLoc(0), + audLen(0), + audPos(0), + audSub(0), + sample(-1), + wave(-1), + ins(-1), + note(0), + panL(255), + panR(255), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + useWave(false), + setPos(false), + vol(255), + envVol(64) {} + }; + Channel chan; + DivDispatchOscBuffer* oscBuf; + bool isMuted; + int outDepth; + bool outStereo; + + friend void putDispatchChan(void*,int,int); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivDispatchOscBuffer* getOscBuffer(int chan); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + bool isStereo(); + DivMacroInt* getChanMacroInt(int ch); + void setFlags(unsigned int flags); + void notifyInsChange(int ins); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); +}; + +#endif diff --git a/src/engine/platform/pet.cpp b/src/engine/platform/pet.cpp index 493436271..dc524e45f 100644 --- a/src/engine/platform/pet.cpp +++ b/src/engine/platform/pet.cpp @@ -133,8 +133,14 @@ void DivPlatformPET::tick(bool sysTick) { } } if (chan.std.pitch.had) { - chan.freqChanged=true; + if (chan.std.pitch.mode) { + chan.pitch2+=chan.std.pitch.val; + CLAMP_VAR(chan.pitch2,-32768,32767); + } else { + chan.pitch2=chan.std.pitch.val; } + chan.freqChanged=true; + } if (chan.freqChanged || chan.keyOn || chan.keyOff) { chan.freq=parent->calcFreq(chan.baseFreq,chan.pitch,true,0,chan.pitch2,chipClock,CHIP_DIVIDER)-2; if (chan.freq>65535) chan.freq=65535; diff --git a/src/engine/platform/qsound.cpp b/src/engine/platform/qsound.cpp index 6eb14be7f..ca6a794cd 100644 --- a/src/engine/platform/qsound.cpp +++ b/src/engine/platform/qsound.cpp @@ -301,7 +301,7 @@ void DivPlatformQSound::tick(bool sysTick) { qsound_bank = 0x8000 | (s->offQSound >> 16); qsound_addr = s->offQSound & 0xffff; - int length = s->samples; + int length = s->getEndPosition(); if (length > 65536 - 16) { length = 65536 - 16; } @@ -358,14 +358,14 @@ void DivPlatformQSound::tick(bool sysTick) { } } chan[i].freq=off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,440.0,4096.0); - if (chan[i].freq>0xffff) chan[i].freq=0xffff; + if (chan[i].freq>0xefff) chan[i].freq=0xefff; if (chan[i].keyOn) { rWrite(q1_reg_map[Q1V_BANK][i], qsound_bank); rWrite(q1_reg_map[Q1V_END][i], qsound_end); rWrite(q1_reg_map[Q1V_LOOP][i], qsound_loop); rWrite(q1_reg_map[Q1V_START][i], qsound_addr); rWrite(q1_reg_map[Q1V_PHASE][i], 0x8000); - //logV("ch %d bank=%04x, addr=%04x, end=%04x, loop=%04x!",i,qsound_bank,qsound_addr,qsound_end,qsound_loop); + logV("ch %d bank=%04x, addr=%04x, end=%04x, loop=%04x!",i,qsound_bank,qsound_addr,qsound_end,qsound_loop); // Write sample address. Enable volume if (!chan[i].std.vol.had) { rWrite(q1_reg_map[Q1V_VOL][i], chan[i].vol << 4); diff --git a/src/engine/platform/rf5c68.cpp b/src/engine/platform/rf5c68.cpp index 176b6e7c7..e4a39d44e 100644 --- a/src/engine/platform/rf5c68.cpp +++ b/src/engine/platform/rf5c68.cpp @@ -142,7 +142,7 @@ void DivPlatformRF5C68::tick(bool sysTick) { if (chan[i].audPos>0) { start=start+MIN(chan[i].audPos,s->length8); } - if (s->loopStart>=0) { + if (s->isLoopable()) { loop=start+s->loopStart; } start=MIN(start,getSampleMemCapacity()-31); @@ -393,7 +393,7 @@ void DivPlatformRF5C68::renderSamples() { size_t memPos=0; for (int i=0; isong.sampleLen; i++) { DivSample* s=parent->song.sample[i]; - int length=s->length8; + int length=s->getEndPosition(DIV_SAMPLE_DEPTH_8BIT); int actualLength=MIN((int)(getSampleMemCapacity()-memPos)-31,length); if (actualLength>0) { s->offRF5C68=memPos; diff --git a/src/engine/platform/segapcm.cpp b/src/engine/platform/segapcm.cpp index 71d45298a..d66fcce0b 100644 --- a/src/engine/platform/segapcm.cpp +++ b/src/engine/platform/segapcm.cpp @@ -56,12 +56,10 @@ void DivPlatformSegaPCM::acquire(short* bufL, short* bufR, size_t start, size_t pcmR+=(s->data8[chan[i].pcm.pos>>8]*chan[i].chVolR); } chan[i].pcm.pos+=chan[i].pcm.freq; - if (chan[i].pcm.pos>=(s->samples<<8)) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[i].pcm.pos=s->loopStart<<8; - } else { - chan[i].pcm.sample=-1; - } + if (s->isLoopable() && chan[i].pcm.pos>=(s->getEndPosition()<<8)) { + chan[i].pcm.pos=s->loopStart<<8; + } else if (chan[i].pcm.pos>=(s->samples<<8)) { + chan[i].pcm.sample=-1; } } else { oscBuf[i]->data[oscBuf[i]->needle++]=0; @@ -202,7 +200,7 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { chan[c.chan].macroInit(ins); if (dumpWrites) { // Sega PCM writes DivSample* s=parent->getSample(chan[c.chan].pcm.sample); - int actualLength=(int)s->length8; + int actualLength=(int)(s->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)); if (actualLength>0xfeff) actualLength=0xfeff; addWrite(0x10086+(c.chan<<3),3+((s->offSegaPCM>>16)<<3)); addWrite(0x10084+(c.chan<<3),(s->offSegaPCM)&0xff); @@ -235,7 +233,7 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { chan[c.chan].furnacePCM=false; if (dumpWrites) { // Sega PCM writes DivSample* s=parent->getSample(chan[c.chan].pcm.sample); - int actualLength=(int)s->length8; + int actualLength=(int)(s->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)); if (actualLength>65536) actualLength=65536; addWrite(0x10086+(c.chan<<3),3+((s->offSegaPCM>>16)<<3)); addWrite(0x10084+(c.chan<<3),(s->offSegaPCM)&0xff); diff --git a/src/engine/platform/sound/gb/apu.c b/src/engine/platform/sound/gb/apu.c index 4c473997f..8836ddd29 100644 --- a/src/engine/platform/sound/gb/apu.c +++ b/src/engine/platform/sound/gb/apu.c @@ -1180,11 +1180,11 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) if ((value & 0x80)) { /* DMG bug: wave RAM gets corrupted if the channel is retriggerred 1 cycle before the APU reads from it. */ - /*if (!CGB && + if (!CGB && gb->apu.is_active[GB_WAVE] && gb->apu.wave_channel.sample_countdown == 0 && gb->apu.wave_channel.enable) { - unsigned offset = ((gb->apu.wave_channel.current_sample_index + 1) >> 1) & 0xF;*/ + unsigned offset = ((gb->apu.wave_channel.current_sample_index + 1) >> 1) & 0xF; /* This glitch varies between models and even specific instances: DMG-B: Most of them behave as emulated. A few behave differently. @@ -1193,7 +1193,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) Additionally, I believe DMGs, including those we behave differently than emulated, are all deterministic. */ - /*if (offset < 4) { + if (offset < 4) { gb->io_registers[GB_IO_WAV_START] = gb->io_registers[GB_IO_WAV_START + offset]; gb->apu.wave_channel.wave_form[0] = gb->apu.wave_channel.wave_form[offset / 2]; gb->apu.wave_channel.wave_form[1] = gb->apu.wave_channel.wave_form[offset / 2 + 1]; @@ -1206,7 +1206,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.wave_channel.wave_form + (offset & ~3) * 2, 8); } - }*/ + } if (!gb->apu.is_active[GB_WAVE]) { gb->apu.is_active[GB_WAVE] = true; update_sample(gb, GB_WAVE, diff --git a/src/engine/platform/sound/gb/gb.h b/src/engine/platform/sound/gb/gb.h index ac817395f..ca3650852 100644 --- a/src/engine/platform/sound/gb/gb.h +++ b/src/engine/platform/sound/gb/gb.h @@ -16,7 +16,7 @@ extern "C" { #define GB_STRUCT_VERSION 13 -#define CGB 0 +#define CGB (gb->model&GB_MODEL_CGB_FAMILY) #define GB_MODEL_FAMILY_MASK 0xF00 #define GB_MODEL_DMG_FAMILY 0x000 diff --git a/src/engine/platform/sound/k005289/k005289.cpp b/src/engine/platform/sound/k005289/k005289.cpp index 8b21245a1..c9bdf83ae 100644 --- a/src/engine/platform/sound/k005289/k005289.cpp +++ b/src/engine/platform/sound/k005289/k005289.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900 diff --git a/src/engine/platform/sound/k005289/k005289.hpp b/src/engine/platform/sound/k005289/k005289.hpp index d042e1d14..575a98b87 100644 --- a/src/engine/platform/sound/k005289/k005289.hpp +++ b/src/engine/platform/sound/k005289/k005289.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900 diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index 270e21bdf..791336c1b 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -26,37 +26,11 @@ #include #include -namespace Lynx -{ +#if defined( _MSC_VER ) -namespace -{ +#include -static constexpr int64_t CNT_MAX = std::numeric_limits::max() & ~15; - -#if defined ( __cpp_lib_bitops ) - -#define popcnt(X) std::popcount(X) - -#elif defined( _MSC_VER ) - -# include - -uint32_t popcnt( uint32_t x ) -{ - return __popcnt( x ); -} - -#elif defined( __GNUC__ ) - -uint32_t popcnt( uint32_t x ) -{ - return __builtin_popcount( x ); -} - -#else - -uint32_t popcnt( uint32_t x ) +static uint32_t popcnt_generic( uint32_t x ) { int v = 0; while ( x != 0 ) @@ -67,8 +41,61 @@ uint32_t popcnt( uint32_t x ) return v; } +#if defined( _M_IX86 ) || defined( _M_X64 ) + +static uint32_t popcnt_intrinsic( uint32_t x ) +{ + return __popcnt( x ); +} + +static uint32_t( *popcnt )( uint32_t ); + +//detecting popcnt availability on msvc intel +static void selectPOPCNT() +{ + int info[4]; + __cpuid( info, 1 ); + if ( ( info[2] & ( (int)1 << 23 ) ) != 0 ) + { + popcnt = &popcnt_intrinsic; + } + else + { + popcnt = &popcnt_generic; + } +} + +#else //defined( _M_IX86 ) || defined( _M_X64 ) + +//MSVC non INTEL should use generic implementation +inline void selectPOPCNT() +{ +} + +#define popcnt popcnt_generic + #endif +#else //defined( _MSC_VER ) + +//non MVSC should use builtin implementation + +inline void selectPOPCNT() +{ +} + +#define popcnt __builtin_popcount + +#endif + +namespace Lynx +{ + +namespace +{ + +static constexpr int64_t CNT_MAX = std::numeric_limits::max() & ~15; + int32_t clamp( int32_t v, int32_t lo, int32_t hi ) { return v < lo ? lo : ( v > hi ? hi : v ); @@ -513,6 +540,7 @@ private: Mikey::Mikey( uint32_t sampleRate ) : mMikey{ std::make_unique() }, mQueue{ std::make_unique() }, mTick{}, mNextTick{}, mSampleRate{ sampleRate }, mSamplesRemainder{}, mTicksPerSample{ 16000000 / mSampleRate, 16000000 % mSampleRate } { + selectPOPCNT(); enqueueSampling(); } diff --git a/src/engine/platform/sound/n163/n163.cpp b/src/engine/platform/sound/n163/n163.cpp index 5d9deab1a..3fd30bc44 100644 --- a/src/engine/platform/sound/n163/n163.cpp +++ b/src/engine/platform/sound/n163/n163.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/sound/n163/n163.hpp b/src/engine/platform/sound/n163/n163.hpp index 800d1ea13..317a33b45 100644 --- a/src/engine/platform/sound/n163/n163.hpp +++ b/src/engine/platform/sound/n163/n163.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/sound/oki/msm6295.cpp b/src/engine/platform/sound/oki/msm6295.cpp index 1b7fe568a..e7f39d27e 100644 --- a/src/engine/platform/sound/oki/msm6295.cpp +++ b/src/engine/platform/sound/oki/msm6295.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: tildearrow diff --git a/src/engine/platform/sound/oki/msm6295.hpp b/src/engine/platform/sound/oki/msm6295.hpp index 5203f4340..ca8b81d41 100644 --- a/src/engine/platform/sound/oki/msm6295.hpp +++ b/src/engine/platform/sound/oki/msm6295.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: tildearrow diff --git a/src/engine/platform/sound/oki/util.hpp b/src/engine/platform/sound/oki/util.hpp index 731772acc..b9c50d7bf 100644 --- a/src/engine/platform/sound/oki/util.hpp +++ b/src/engine/platform/sound/oki/util.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: tildearrow diff --git a/src/engine/platform/sound/oki/vox.hpp b/src/engine/platform/sound/oki/vox.hpp index c085c0b7c..23fbfd78e 100644 --- a/src/engine/platform/sound/oki/vox.hpp +++ b/src/engine/platform/sound/oki/vox.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: tildearrow diff --git a/src/engine/platform/sound/scc/scc.cpp b/src/engine/platform/sound/scc/scc.cpp index 3e230447a..e2ebcf200 100644 --- a/src/engine/platform/sound/scc/scc.cpp +++ b/src/engine/platform/sound/scc/scc.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Contributor(s): Natt Akuma, James Alan Nguyen, Laurens Holst diff --git a/src/engine/platform/sound/scc/scc.hpp b/src/engine/platform/sound/scc/scc.hpp index db99ec877..24a365ccf 100644 --- a/src/engine/platform/sound/scc/scc.hpp +++ b/src/engine/platform/sound/scc/scc.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Contributor(s): Natt Akuma, James Alan Nguyen, Laurens Holst diff --git a/src/engine/platform/sound/su.cpp b/src/engine/platform/sound/su.cpp index b7fa731a6..a87205457 100644 --- a/src/engine/platform/sound/su.cpp +++ b/src/engine/platform/sound/su.cpp @@ -5,9 +5,22 @@ #define minval(a,b) (((a)<(b))?(a):(b)) #define maxval(a,b) (((a)>(b))?(a):(b)) +#define FILVOL chan[4].special1C +#define ILCTRL chan[4].special1D +#define ILSIZE chan[5].special1C +#define FIL1 chan[5].special1D +#define IL1 chan[6].special1C +#define IL2 chan[6].special1D +#define IL0 chan[7].special1C +#define MVOL chan[7].special1D + void SoundUnit::NextSample(short* l, short* r) { + // run channels for (int i=0; i<8; i++) { - if (chan[i].vol==0 && !chan[i].flags.swvol) {fns[i]=0; continue;} + if (chan[i].vol==0 && !chan[i].flags.swvol) { + fns[i]=0; + continue; + } if (chan[i].flags.pcm) { ns[i]=pcm[chan[i].pcmpos]; } else switch (chan[i].flags.shape) { @@ -48,13 +61,12 @@ void SoundUnit::NextSample(short* l, short* r) { pcmdec[i]-=32768; if (chan[i].pcmpos>2; tnsR=(nsR[0]+nsR[1]+nsR[2]+nsR[3]+nsR[4]+nsR[5]+nsR[6]+nsR[7])>>2; - - *l=minval(32767,maxval(-32767,tnsL)); - *r=minval(32767,maxval(-32767,tnsR)); + + IL1=minval(32767,maxval(-32767,tnsL))>>8; + IL2=minval(32767,maxval(-32767,tnsR))>>8; + + // write input lines to sample memory + if (ILSIZE&64) { + if (++ilBufPeriod>=((1+(FIL1>>4))<<2)) { + ilBufPeriod=0; + unsigned short ilLowerBound=pcmSize-((1+(ILSIZE&63))<<7); + short next; + if (ilBufPos>4); + if (next<-128) next=-128; + if (next>127) next=127; + pcm[ilBufPos]=next; + if (++ilBufPos>=pcmSize) ilBufPos=ilLowerBound; + break; + case 1: + ilFeedback0=ilFeedback1=pcm[ilBufPos]; + next=((signed char)IL1)+((pcm[ilBufPos]*(FIL1&15))>>4); + if (next<-128) next=-128; + if (next>127) next=127; + pcm[ilBufPos]=next; + if (++ilBufPos>=pcmSize) ilBufPos=ilLowerBound; + break; + case 2: + ilFeedback0=ilFeedback1=pcm[ilBufPos]; + next=((signed char)IL2)+((pcm[ilBufPos]*(FIL1&15))>>4); + if (next<-128) next=-128; + if (next>127) next=127; + pcm[ilBufPos]=next; + if (++ilBufPos>=pcmSize) ilBufPos=ilLowerBound; + break; + case 3: + ilFeedback0=pcm[ilBufPos]; + next=((signed char)IL1)+((pcm[ilBufPos]*(FIL1&15))>>4); + if (next<-128) next=-128; + if (next>127) next=127; + pcm[ilBufPos]=next; + if (++ilBufPos>=pcmSize) ilBufPos=ilLowerBound; + ilFeedback1=pcm[ilBufPos]; + next=((signed char)IL2)+((pcm[ilBufPos]*(FIL1&15))>>4); + if (next<-128) next=-128; + if (next>127) next=127; + pcm[ilBufPos]=next; + if (++ilBufPos>=pcmSize) ilBufPos=ilLowerBound; + break; + } + } + if (ILCTRL&4) { + if (ILSIZE&128) { + tnsL+=ilFeedback1*(signed char)FILVOL; + tnsR+=ilFeedback0*(signed char)FILVOL; + } else { + tnsL+=ilFeedback0*(signed char)FILVOL; + tnsR+=ilFeedback1*(signed char)FILVOL; + } + } + } + + if (dsOut) { + tnsL=minval(32767,maxval(-32767,tnsL<<1)); + tnsR=minval(32767,maxval(-32767,tnsR<<1)); + + short accumL=0; + short accumR=0; + + for (int i=0; i<4; i++) { + if ((tnsL>>8)==0 && dsCounterL>0) dsCounterL=0; + dsCounterL+=tnsL>>8; + if (dsCounterL>=0) { + accumL+=4095; + dsCounterL-=127; + } else { + accumL+=-4095; + dsCounterL+=127; + } + + if ((tnsR>>8)==0 && dsCounterR>0) dsCounterR=0; + dsCounterR+=tnsR>>8; + if (dsCounterR>=0) { + accumR+=4095; + dsCounterR-=127; + } else { + accumR+=-4095; + dsCounterR+=127; + } + } + + *l=accumL; + *r=accumR; + } else { + *l=minval(32767,maxval(-32767,tnsL)); + *r=minval(32767,maxval(-32767,tnsR)); + } } -void SoundUnit::Init() { +void SoundUnit::Init(int sampleMemSize, bool dsOutMode) { + pcmSize=sampleMemSize; + dsOut=dsOutMode; Reset(); - memset(pcm,0,SOUNDCHIP_PCM_SIZE); + memset(pcm,0,pcmSize); for (int i=0; i<256; i++) { SCsine[i]=sin((i/128.0f)*M_PI)*127; SCtriangle[i]=(i>127)?(255-i):(i); @@ -242,9 +353,6 @@ void SoundUnit::Init() { SCpantabR[128+i]=i-1; } SCpantabR[128]=0; - for (int i=0; i<8; i++) { - muted[i]=false; - } } void SoundUnit::Reset() { @@ -272,8 +380,14 @@ void SoundUnit::Reset() { oldflags[i]=0; pcmdec[i]=0; } + dsCounterL=0; + dsCounterR=0; tnsL=0; tnsR=0; + ilBufPos=0; + ilBufPeriod=0; + ilFeedback0=0; + ilFeedback1=0; memset(chan,0,sizeof(SUChannel)*8); } @@ -282,6 +396,8 @@ void SoundUnit::Write(unsigned char addr, unsigned char data) { } SoundUnit::SoundUnit() { - Init(); - memset(pcm,0,SOUNDCHIP_PCM_SIZE); + Init(65536); // default + for (int i=0; i<8; i++) { + muted[i]=false; + } } diff --git a/src/engine/platform/sound/su.h b/src/engine/platform/sound/su.h index 3152e8568..546acfc9c 100644 --- a/src/engine/platform/sound/su.h +++ b/src/engine/platform/sound/su.h @@ -3,8 +3,6 @@ #include #include -#define SOUNDCHIP_PCM_SIZE 8192 - class SoundUnit { signed char SCsine[256]; signed char SCtriangle[256]; @@ -22,8 +20,15 @@ class SoundUnit { int nshigh[8]; int nsband[8]; int tnsL, tnsR; + unsigned char ilBufPeriod; + unsigned short ilBufPos; + signed char ilFeedback0; + signed char ilFeedback1; unsigned short oldfreq[8]; unsigned short oldflags[8]; + unsigned int pcmSize; + bool dsOut; + short dsCounterL, dsCounterR; public: unsigned short resetfreq[8]; unsigned short voldcycles[8]; @@ -81,11 +86,13 @@ class SoundUnit { unsigned char dir: 1; unsigned char bound; } swcut; - unsigned short wc; + unsigned char special1C; + unsigned char special1D; unsigned short restimer; } chan[8]; - signed char pcm[SOUNDCHIP_PCM_SIZE]; + signed char pcm[65536]; bool muted[8]; + void SetIL0(unsigned char addr); void Write(unsigned char addr, unsigned char data); void NextSample(short* l, short* r); inline int GetSample(int ch) { @@ -94,7 +101,7 @@ class SoundUnit { if (ret>32767) ret=32767; return ret; } - void Init(); + void Init(int sampleMemSize=8192, bool dsOutMode=false); void Reset(); SoundUnit(); }; diff --git a/src/engine/platform/sound/vrcvi/vrcvi.cpp b/src/engine/platform/sound/vrcvi/vrcvi.cpp index 87ff05d7c..a811c2f44 100644 --- a/src/engine/platform/sound/vrcvi/vrcvi.cpp +++ b/src/engine/platform/sound/vrcvi/vrcvi.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/sound/vrcvi/vrcvi.hpp b/src/engine/platform/sound/vrcvi/vrcvi.hpp index 790061c82..4a80f7577 100644 --- a/src/engine/platform/sound/vrcvi/vrcvi.hpp +++ b/src/engine/platform/sound/vrcvi/vrcvi.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/sound/x1_010/x1_010.cpp b/src/engine/platform/sound/x1_010/x1_010.cpp index c047b854a..6b0041bad 100644 --- a/src/engine/platform/sound/x1_010/x1_010.cpp +++ b/src/engine/platform/sound/x1_010/x1_010.cpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/sound/x1_010/x1_010.hpp b/src/engine/platform/sound/x1_010/x1_010.hpp index 3f5d9d4e2..b533b66d8 100644 --- a/src/engine/platform/sound/x1_010/x1_010.hpp +++ b/src/engine/platform/sound/x1_010/x1_010.hpp @@ -1,6 +1,6 @@ /* License: BSD-3-Clause - see https://github.com/cam900/vgsound_emu/blob/vgsound_emu_v1/LICENSE for more details + see https://gitlab.com/cam900/vgsound_emu/-/blob/V1/LICENSE for more details Copyright holder(s): cam900 Modifiers and Contributors for Furnace: cam900, tildearrow diff --git a/src/engine/platform/su.cpp b/src/engine/platform/su.cpp index f3be01a78..afcdba242 100644 --- a/src/engine/platform/su.cpp +++ b/src/engine/platform/su.cpp @@ -26,6 +26,7 @@ #define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } #define chWrite(c,a,v) rWrite(((c)<<5)|(a),v); +#define CHIP_DIVIDER 2 #define CHIP_FREQBASE 524288 const char** DivPlatformSoundUnit::getRegisterSheet() { @@ -98,6 +99,13 @@ const char* DivPlatformSoundUnit::getEffectName(unsigned char effect) { return NULL; } +double DivPlatformSoundUnit::NOTE_SU(int ch, int note) { + if (chan[ch].switchRoles) { + return NOTE_PERIODIC(note); + } + return NOTE_FREQUENCY(note); +} + void DivPlatformSoundUnit::acquire(short* bufL, short* bufR, size_t start, size_t len) { for (size_t h=start; h0); + if (chan[i].switchRoles) { + chWrite(i,0x00,chan[i].syncTimer&0xff); + chWrite(i,0x01,chan[i].syncTimer>>8); + } else { + chWrite(i,0x1e,chan[i].syncTimer&0xff); + chWrite(i,0x1f,chan[i].syncTimer>>8); + } + writeControlUpper(i); + } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_SU); - chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE); + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].switchRoles,2,chan[i].pitch2,chipClock,chan[i].switchRoles?CHIP_DIVIDER:CHIP_FREQBASE); if (chan[i].pcm) { DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_SU); // TODO: sample map? @@ -206,14 +226,19 @@ void DivPlatformSoundUnit::tick(bool sysTick) { } if (chan[i].freq<0) chan[i].freq=0; if (chan[i].freq>65535) chan[i].freq=65535; - chWrite(i,0x00,chan[i].freq&0xff); - chWrite(i,0x01,chan[i].freq>>8); + if (chan[i].switchRoles) { + chWrite(i,0x1e,chan[i].freq&0xff); + chWrite(i,0x1f,chan[i].freq>>8); + } else { + chWrite(i,0x00,chan[i].freq&0xff); + chWrite(i,0x01,chan[i].freq>>8); + } if (chan[i].keyOn) { if (chan[i].pcm) { DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_SU); DivSample* sample=parent->getSample(ins->amiga.getSample(chan[i].note)); if (sample!=NULL) { - unsigned int sampleEnd=sample->offSU+sample->samples; + unsigned int sampleEnd=sample->offSU+(sample->getEndPosition()); unsigned int off=sample->offSU+chan[i].hasOffset; chan[i].hasOffset=0; if (sampleEnd>=getSampleMemCapacity(0)) sampleEnd=getSampleMemCapacity(0)-1; @@ -221,7 +246,7 @@ void DivPlatformSoundUnit::tick(bool sysTick) { chWrite(i,0x0b,off>>8); chWrite(i,0x0c,sampleEnd&0xff); chWrite(i,0x0d,sampleEnd>>8); - if (sample->loopStart>=0 && sample->loopStart<(int)sample->samples) { + if (sample->isLoopable()) { unsigned int sampleLoop=sample->offSU+sample->loopStart; if (sampleLoop>=getSampleMemCapacity(0)) sampleLoop=getSampleMemCapacity(0)-1; chWrite(i,0x0e,sampleLoop&0xff); @@ -249,14 +274,15 @@ int DivPlatformSoundUnit::dispatch(DivCommand c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_SU); - if (chan[c.chan].pcm && ins->type!=DIV_INS_AMIGA) { - chan[c.chan].pcm=(ins->type==DIV_INS_AMIGA); + chan[c.chan].switchRoles=ins->su.switchRoles; + if (chan[c.chan].pcm && !(ins->type==DIV_INS_AMIGA || ins->su.useSample)) { + chan[c.chan].pcm=(ins->type==DIV_INS_AMIGA || ins->su.useSample); writeControl(c.chan); writeControlUpper(c.chan); } - chan[c.chan].pcm=(ins->type==DIV_INS_AMIGA); + chan[c.chan].pcm=(ins->type==DIV_INS_AMIGA || ins->su.useSample); if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].baseFreq=NOTE_SU(c.chan,c.value); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; } @@ -414,7 +440,7 @@ int DivPlatformSoundUnit::dispatch(DivCommand c) { } break; case DIV_CMD_NOTE_PORTA: { - int destFreq=NOTE_FREQUENCY(c.value2); + int destFreq=NOTE_SU(c.chan,c.value2); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value*((parent->song.linearPitch==2)?1:(1+(chan[c.chan].baseFreq>>9))); @@ -446,7 +472,7 @@ int DivPlatformSoundUnit::dispatch(DivCommand c) { chan[c.chan].keyOn=true; break; case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); + chan[c.chan].baseFreq=NOTE_SU(c.chan,c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; break; @@ -454,7 +480,7 @@ int DivPlatformSoundUnit::dispatch(DivCommand c) { if (chan[c.chan].active && c.value2) { if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_SU)); } - if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will) chan[c.chan].baseFreq=NOTE_FREQUENCY(chan[c.chan].note); + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will) chan[c.chan].baseFreq=NOTE_SU(c.chan,chan[c.chan].note); chan[c.chan].inPorta=c.value; break; case DIV_CMD_GET_VOLMAX: @@ -478,6 +504,11 @@ void DivPlatformSoundUnit::forceIns() { for (int i=0; i<8; i++) { chan[i].insChanged=true; chan[i].freqChanged=true; + + // restore channel attributes + chWrite(i,0x03,chan[i].pan); + writeControl(i); + writeControlUpper(i); } } @@ -522,6 +553,16 @@ void DivPlatformSoundUnit::reset() { lfoMode=0; lfoSpeed=255; delay=500; + + // set initial IL status + ilCtrl=initIlCtrl; + ilSize=initIlSize; + fil1=initFil1; + echoVol=initEchoVol; + rWrite(0x9c,echoVol); + rWrite(0x9d,ilCtrl); + rWrite(0xbc,ilSize); + rWrite(0xbd,fil1); } bool DivPlatformSoundUnit::isStereo() { @@ -548,6 +589,15 @@ void DivPlatformSoundUnit::setFlags(unsigned int flags) { for (int i=0; i<8; i++) { oscBuf[i]->rate=rate; } + initIlCtrl=3|(flags&4); + initIlSize=((flags>>8)&63)|((flags&4)?0x40:0)|((flags&8)?0x80:0); + initFil1=flags>>16; + initEchoVol=flags>>24; + + sampleMemSize=flags&16; + + su->Init(sampleMemSize?65536:8192,flags&32); + renderSamples(); } void DivPlatformSoundUnit::poke(unsigned int addr, unsigned short val) { @@ -563,7 +613,7 @@ const void* DivPlatformSoundUnit::getSampleMem(int index) { } size_t DivPlatformSoundUnit::getSampleMemCapacity(int index) { - return (index==0)?8192:0; + return (index==0)?((sampleMemSize?65536:8192)-((initIlSize&64)?((1+(initIlSize&63))<<7):0)):0; } size_t DivPlatformSoundUnit::getSampleMemUsage(int index) { @@ -576,6 +626,7 @@ void DivPlatformSoundUnit::renderSamples() { size_t memPos=0; for (int i=0; isong.sampleLen; i++) { DivSample* s=parent->song.sample[i]; + if (s->data8==NULL) continue; int paddedLen=s->samples; if (memPos>=getSampleMemCapacity(0)) { logW("out of PCM memory for sample %d!",i); @@ -602,9 +653,8 @@ int DivPlatformSoundUnit::init(DivEngine* p, int channels, int sugRate, unsigned isMuted[i]=false; oscBuf[i]=new DivDispatchOscBuffer; } - setFlags(flags); su=new SoundUnit(); - su->Init(); + setFlags(flags); reset(); return 6; } diff --git a/src/engine/platform/su.h b/src/engine/platform/su.h index 1d39854f2..2392624f6 100644 --- a/src/engine/platform/su.h +++ b/src/engine/platform/su.h @@ -31,7 +31,7 @@ class DivPlatformSoundUnit: public DivDispatch { int ins, cutoff, baseCutoff, res, control, hasOffset; signed char pan; unsigned char duty; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, phaseReset, filterPhaseReset; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, phaseReset, filterPhaseReset, switchRoles; bool pcmLoop, timerSync, freqSweep, volSweep, cutSweep; unsigned short freqSweepP, volSweepP, cutSweepP; unsigned char freqSweepB, volSweepB, cutSweepB; @@ -67,6 +67,7 @@ class DivPlatformSoundUnit: public DivDispatch { pcm(false), phaseReset(false), filterPhaseReset(false), + switchRoles(false), pcmLoop(false), timerSync(false), freqSweep(false), @@ -96,6 +97,10 @@ class DivPlatformSoundUnit: public DivDispatch { }; std::queue writes; unsigned char lastPan; + bool sampleMemSize; + unsigned char ilCtrl, ilSize, fil1; + unsigned char initIlCtrl, initIlSize, initFil1; + signed char echoVol, initEchoVol; int cycles, curChan, delay; short tempL; @@ -104,6 +109,7 @@ class DivPlatformSoundUnit: public DivDispatch { SoundUnit* su; size_t sampleMemLen; unsigned char regPool[128]; + double NOTE_SU(int ch, int note); void writeControl(int ch); void writeControlUpper(int ch); diff --git a/src/engine/platform/swan.cpp b/src/engine/platform/swan.cpp index b3e833198..b6da23271 100644 --- a/src/engine/platform/swan.cpp +++ b/src/engine/platform/swan.cpp @@ -83,12 +83,10 @@ void DivPlatformSwan::acquire(short* bufL, short* bufR, size_t start, size_t len continue; } rWrite(0x09,(unsigned char)s->data8[dacPos++]+0x80); - if (dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - dacPos=s->loopStart; - } else { - dacSample=-1; - } + if (s->isLoopable() && dacPos>=s->getEndPosition()) { + dacPos=s->loopStart; + } else if (dacPos>=s->samples) { + dacSample=-1; } dacPeriod-=rate; } diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp index a2dc17da0..0b261a946 100644 --- a/src/engine/platform/vera.cpp +++ b/src/engine/platform/vera.cpp @@ -98,13 +98,11 @@ void DivPlatformVERA::acquire(short* bufL, short* bufR, size_t start, size_t len rWritePCMData(tmp_r&0xff); } chan[16].pcm.pos++; - if (chan[16].pcm.pos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[16].pcm.pos=s->loopStart; - } else { - chan[16].pcm.sample=-1; - break; - } + if (s->isLoopable() && chan[16].pcm.pos>=s->getEndPosition()) { + chan[16].pcm.pos=s->loopStart; + } else if (chan[16].pcm.pos>=s->samples) { + chan[16].pcm.sample=-1; + break; } } } else { @@ -269,12 +267,12 @@ int DivPlatformVERA::dispatch(DivCommand c) { chan[16].pcm.pos=0; DivSample* s=parent->getSample(chan[16].pcm.sample); unsigned char ctrl=0x90|chan[16].vol; // always stereo - if (s->depth==16) { + if (s->depth==DIV_SAMPLE_DEPTH_16BIT) { chan[16].pcm.depth16=true; ctrl|=0x20; } else { chan[16].pcm.depth16=false; - if (s->depth!=8) chan[16].pcm.sample=-1; + if (s->depth!=DIV_SAMPLE_DEPTH_8BIT) chan[16].pcm.sample=-1; } rWritePCMCtrl(ctrl); } diff --git a/src/engine/platform/vrc6.cpp b/src/engine/platform/vrc6.cpp index 88fcb37b4..8a34d9252 100644 --- a/src/engine/platform/vrc6.cpp +++ b/src/engine/platform/vrc6.cpp @@ -77,13 +77,11 @@ void DivPlatformVRC6::acquire(short* bufL, short* bufR, size_t start, size_t len chWrite(i,0,0x80|chan[i].dacOut); } chan[i].dacPos++; - if (chan[i].dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples) { - chan[i].dacPos=s->loopStart; - } else { - chan[i].dacSample=-1; - chWrite(i,0,0); - } + if (s->isLoopable() && chan[i].dacPos>=s->getEndPosition()) { + chan[i].dacPos=s->loopStart; + } else if (chan[i].dacPos>=s->samples) { + chan[i].dacSample=-1; + chWrite(i,0,0); } chan[i].dacPeriod-=rate; } diff --git a/src/engine/platform/ym2608.cpp b/src/engine/platform/ym2608.cpp index b70efaf03..c3cf52a06 100644 --- a/src/engine/platform/ym2608.cpp +++ b/src/engine/platform/ym2608.cpp @@ -761,7 +761,7 @@ int DivPlatformYM2608::dispatch(DivCommand c) { immWrite(0x104,(end>>5)&0xff); immWrite(0x105,(end>>13)&0xff); immWrite(0x101,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|2); - immWrite(0x100,(s->loopStart>=0)?0xb0:0xa0); // start/repeat + immWrite(0x100,(s->isLoopable())?0xb0:0xa0); // start/repeat if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); @@ -796,7 +796,7 @@ int DivPlatformYM2608::dispatch(DivCommand c) { immWrite(0x104,(end>>5)&0xff); immWrite(0x105,(end>>13)&0xff); immWrite(0x101,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|2); - immWrite(0x100,(s->loopStart>=0)?0xb0:0xa0); // start/repeat + immWrite(0x100,(s->isLoopable())?0xb0:0xa0); // start/repeat int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0); immWrite(0x109,freq&0xff); immWrite(0x10a,(freq>>8)&0xff); diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index 1a67c4288..4e01b005c 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -793,7 +793,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { immWrite(0x14,(end>>8)&0xff); immWrite(0x15,end>>16); immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + immWrite(0x10,(s->isLoopable())?0x90:0x80); // start/repeat if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); @@ -828,7 +828,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { immWrite(0x14,(end>>8)&0xff); immWrite(0x15,end>>16); immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + immWrite(0x10,(s->isLoopable())?0x90:0x80); // start/repeat int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0); immWrite(0x19,freq&0xff); immWrite(0x1a,(freq>>8)&0xff); diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index 600c89fd3..1a1f7f0ae 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -775,7 +775,7 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { immWrite(0x14,(end>>8)&0xff); immWrite(0x15,end>>16); immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + immWrite(0x10,(s->isLoopable())?0x90:0x80); // start/repeat if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); @@ -810,7 +810,7 @@ int DivPlatformYM2610B::dispatch(DivCommand c) { immWrite(0x14,(end>>8)&0xff); immWrite(0x15,end>>16); immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat + immWrite(0x10,(s->isLoopable())?0x90:0x80); // start/repeat int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0); immWrite(0x19,freq&0xff); immWrite(0x1a,(freq>>8)&0xff); diff --git a/src/engine/platform/ymz280b.cpp b/src/engine/platform/ymz280b.cpp index d8d98478d..0543a815c 100644 --- a/src/engine/platform/ymz280b.cpp +++ b/src/engine/platform/ymz280b.cpp @@ -23,7 +23,7 @@ #include #include -#define CHIP_FREQBASE 98304 +#define CHIP_FREQBASE 25165824 #define rWrite(a,v) {if(!skipRegisterWrites) {ymz280b.write(0,a); ymz280b.write(1,v); regPool[a]=v; if(dumpWrites) addWrite(a,v); }} @@ -136,50 +136,54 @@ void DivPlatformYMZ280B::tick(bool sysTick) { DivSample* s=parent->getSample(chan[i].sample); unsigned char ctrl; switch (s->depth) { - case 3: ctrl=0x20; break; - case 8: ctrl=0x40; break; - case 16: ctrl=0x60; break; + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: ctrl=0x20; break; + case DIV_SAMPLE_DEPTH_8BIT: ctrl=0x40; break; + case DIV_SAMPLE_DEPTH_16BIT: ctrl=0x60; break; default: ctrl=0; } double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; - chan[i].freq=(int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE))-1; + chan[i].freq=(int)round(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE)/256.0)-1; if (chan[i].freq<0) chan[i].freq=0; if (chan[i].freq>511) chan[i].freq=511; // ADPCM has half the range - if (s->depth==3 && chan[i].freq>255) chan[i].freq=255; - ctrl|=(chan[i].active?0x80:0)|((s->loopStart>=0)?0x10:0)|(chan[i].freq>>8); + if (s->depth==DIV_SAMPLE_DEPTH_YMZ_ADPCM && chan[i].freq>255) chan[i].freq=255; + ctrl|=(chan[i].active?0x80:0)|((s->isLoopable())?0x10:0)|(chan[i].freq>>8); if (chan[i].keyOn) { unsigned int start=s->offYMZ280B; - unsigned int loop=0; + unsigned int loopStart=0; + unsigned int loopEnd=0; unsigned int end=MIN(start+s->getCurBufLen(),getSampleMemCapacity()-1); if (chan[i].audPos>0) { switch (s->depth) { - case 3: start+=chan[i].audPos/2; break; - case 8: start+=chan[i].audPos; break; - case 16: start+=chan[i].audPos*2; break; + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: start+=chan[i].audPos/2; break; + case DIV_SAMPLE_DEPTH_8BIT: start+=chan[i].audPos; break; + case DIV_SAMPLE_DEPTH_16BIT: start+=chan[i].audPos*2; break; + default: break; } start=MIN(start,end); } - if (s->loopStart>=0) { + if (s->isLoopable()) { switch (s->depth) { - case 3: loop=start+s->loopStart/2; break; - case 8: loop=start+s->loopStart; break; - case 16: loop=start+s->loopStart*2; break; + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: loopStart=start+s->loopStart/2; loopEnd=start+s->loopEnd/2; break; + case DIV_SAMPLE_DEPTH_8BIT: loopStart=start+s->loopStart; loopEnd=start+s->loopEnd; break; + case DIV_SAMPLE_DEPTH_16BIT: loopStart=start+s->loopStart*2; loopEnd=start+s->loopEnd*2; break; + default: break; } - loop=MIN(loop,end); + loopEnd=MIN(loopEnd,end); + loopStart=MIN(loopStart,loopEnd); } rWrite(0x01+i*4,ctrl&~0x80); // force keyoff first rWrite(0x20+i*4,(start>>16)&0xff); - rWrite(0x21+i*4,(loop>>16)&0xff); - rWrite(0x22+i*4,(end>>16)&0xff); + rWrite(0x21+i*4,(loopStart>>16)&0xff); + rWrite(0x22+i*4,(loopEnd>>16)&0xff); rWrite(0x23+i*4,(end>>16)&0xff); rWrite(0x40+i*4,(start>>8)&0xff); - rWrite(0x41+i*4,(loop>>8)&0xff); - rWrite(0x42+i*4,(end>>8)&0xff); + rWrite(0x41+i*4,(loopStart>>8)&0xff); + rWrite(0x42+i*4,(loopEnd>>8)&0xff); rWrite(0x43+i*4,(end>>8)&0xff); rWrite(0x60+i*4,start&0xff); - rWrite(0x61+i*4,loop&0xff); - rWrite(0x62+i*4,end&0xff); + rWrite(0x61+i*4,loopStart&0xff); + rWrite(0x62+i*4,loopEnd&0xff); rWrite(0x63+i*4,end&0xff); if (!chan[i].std.vol.had) { chan[i].outVol=chan[i].vol; @@ -263,14 +267,15 @@ int DivPlatformYMZ280B::dispatch(DivCommand c) { case DIV_CMD_NOTE_PORTA: { int destFreq=NOTE_FREQUENCY(c.value2); bool return2=false; + int multiplier=(parent->song.linearPitch==2)?1:256; if (destFreq>chan[c.chan].baseFreq) { - chan[c.chan].baseFreq+=c.value; + chan[c.chan].baseFreq+=c.value*multiplier; if (chan[c.chan].baseFreq>=destFreq) { chan[c.chan].baseFreq=destFreq; return2=true; } } else { - chan[c.chan].baseFreq-=c.value; + chan[c.chan].baseFreq-=c.value*multiplier; if (chan[c.chan].baseFreq<=destFreq) { chan[c.chan].baseFreq=destFreq; return2=true; @@ -327,6 +332,8 @@ void DivPlatformYMZ280B::forceIns() { chan[i].insChanged=true; chan[i].freqChanged=true; chan[i].sample=-1; + + rWrite(0x03+i*4,chan[i].panning); } } diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 329c5b54d..3dbc6b5fb 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -57,6 +57,16 @@ const char* cmdName[]={ "PRE_PORTA", "PRE_NOTE", + "HINT_VIBRATO", + "HINT_VIBRATO_RANGE", + "HINT_VIBRATO_SHAPE", + "HINT_PITCH", + "HINT_ARPEGGIO", + "HINT_VOLUME", + "HINT_VOL_SLIDE", + "HINT_PORTA", + "HINT_LEGATO", + "SAMPLE_MODE", "SAMPLE_FREQ", "SAMPLE_BANK", @@ -201,7 +211,27 @@ const char* formatNote(unsigned char note, unsigned char octave) { int DivEngine::dispatchCmd(DivCommand c) { if (view==DIV_STATUS_COMMANDS) { - printf("%8d | %d: %s(%d, %d)\n",totalTicksR,c.chan,cmdName[c.cmd],c.value,c.value2); + if (!skipping) { + switch (c.cmd) { + // strip away hinted/useless commands + case DIV_ALWAYS_SET_VOLUME: + break; + case DIV_CMD_GET_VOLUME: + break; + case DIV_CMD_VOLUME: + break; + case DIV_CMD_NOTE_PORTA: + break; + case DIV_CMD_LEGATO: + break; + case DIV_CMD_PITCH: + break; + case DIV_CMD_PRE_NOTE: + break; + default: + printf("%8d | %d: %s(%d, %d)\n",totalTicksR,c.chan,cmdName[c.cmd],c.value,c.value2); + } + } } totalCmds++; if (cmdStreamEnabled && cmdStream.size()<2000) { @@ -330,14 +360,15 @@ void DivEngine::processRow(int i, bool afterDelay) { // instrument bool insChanged=false; if (pat->data[whatRow][2]!=-1) { - dispatchCmd(DivCommand(DIV_CMD_INSTRUMENT,i,pat->data[whatRow][2])); if (chan[i].lastIns!=pat->data[whatRow][2]) { + dispatchCmd(DivCommand(DIV_CMD_INSTRUMENT,i,pat->data[whatRow][2])); chan[i].lastIns=pat->data[whatRow][2]; insChanged=true; if (song.legacyVolumeSlides && chan[i].volume==chan[i].volMax+1) { logV("forcing volume"); chan[i].volume=chan[i].volMax; dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } } } @@ -350,11 +381,13 @@ void DivEngine::processRow(int i, bool afterDelay) { if (chan[i].stopOnOff) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].stopOnOff=false; } if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; @@ -371,11 +404,13 @@ void DivEngine::processRow(int i, bool afterDelay) { if (chan[i].stopOnOff) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].stopOnOff=false; } if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; @@ -392,6 +427,7 @@ void DivEngine::processRow(int i, bool afterDelay) { if (!chan[i].keyOn) { if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsArp(dispatchChanOfChan[i])) { chan[i].arp=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); } } chan[i].doNote=true; @@ -408,6 +444,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } chan[i].volume=pat->data[whatRow][3]<<8; dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } } @@ -452,11 +489,13 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].inPorta=false; if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { chan[i].portaNote=song.limitSlides?0x60:255; chan[i].portaSpeed=effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].portaStop=true; chan[i].nowYouCanStop=false; chan[i].stopOnOff=false; @@ -472,11 +511,13 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].inPorta=false; if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { chan[i].portaNote=song.limitSlides?disCont[dispatchOfChan[i]].dispatch->getPortaFloor(dispatchChanOfChan[i]):-60; chan[i].portaSpeed=effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].portaStop=true; chan[i].nowYouCanStop=false; chan[i].stopOnOff=false; @@ -490,6 +531,7 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].inPorta=false; dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); } else { @@ -503,6 +545,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].inPorta=true; chan[i].wasShorthandPorta=false; } + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].portaStop=true; if (chan[i].keyOn) chan[i].doNote=false; chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! @@ -514,6 +557,7 @@ void DivEngine::processRow(int i, bool afterDelay) { case 0x04: // vibrato chan[i].vibratoDepth=effectVal&15; chan[i].vibratoRate=effectVal>>4; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); break; case 0x07: // tremolo @@ -537,12 +581,14 @@ void DivEngine::processRow(int i, bool afterDelay) { } else { chan[i].volSpeed=0; } + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x00: // arpeggio chan[i].arp=effectVal; if (chan[i].arp==0 && song.arp0Reset) { chan[i].resetArp=true; } + dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); break; case 0x0c: // retrigger if (effectVal!=0) { @@ -558,7 +604,7 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0xc0: case 0xc1: case 0xc2: case 0xc3: // set Hz divider=(double)(((effect&0x3)<<8)|effectVal); - if (divider<10) divider=10; + if (divider<1) divider=1; cycles=got.rate*pow(2,MASTER_CLOCK_PREC)/divider; clockDrift=0; subticks=0; @@ -574,6 +620,7 @@ void DivEngine::processRow(int i, bool afterDelay) { case 0xe1: // portamento up chan[i].portaNote=chan[i].note+(effectVal&15); chan[i].portaSpeed=(effectVal>>4)*4; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].portaStop=true; chan[i].nowYouCanStop=false; chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! @@ -592,6 +639,7 @@ void DivEngine::processRow(int i, bool afterDelay) { case 0xe2: // portamento down chan[i].portaNote=chan[i].note-(effectVal&15); chan[i].portaSpeed=(effectVal>>4)*4; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].portaStop=true; chan[i].nowYouCanStop=false; chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! @@ -609,9 +657,11 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0xe3: // vibrato direction chan[i].vibratoDir=effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO_SHAPE,i,chan[i].vibratoDir)); break; case 0xe4: // vibrato fine chan[i].vibratoFine=effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO_RANGE,i,chan[i].vibratoFine)); break; case 0xe5: // pitch chan[i].pitch=effectVal-0x80; @@ -622,6 +672,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } //chan[i].pitch+=globalPitch; dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); + dispatchCmd(DivCommand(DIV_CMD_HINT_PITCH,i,chan[i].pitch)); break; case 0xea: // legato mode chan[i].legato=effectVal; @@ -644,7 +695,7 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0xf0: // set Hz by tempo divider=(double)effectVal*2.0/5.0; - if (divider<10) divider=10; + if (divider<1) divider=1; cycles=got.rate*pow(2,MASTER_CLOCK_PREC)/divider; clockDrift=0; subticks=0; @@ -671,17 +722,21 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0xf3: // fine volume ramp up chan[i].volSpeed=effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0xf4: // fine volume ramp down chan[i].volSpeed=-effectVal; + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0xf8: // single volume ramp up chan[i].volume=MIN(chan[i].volume+effectVal*256,chan[i].volMax); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); break; case 0xf9: // single volume ramp down chan[i].volume=MAX(chan[i].volume-effectVal*256,0); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); break; case 0xfa: // fast volume ramp if (effectVal!=0) { @@ -693,6 +748,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } else { chan[i].volSpeed=0; } + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0xff: // stop song @@ -723,15 +779,18 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); if (chan[i].legato) { dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + dispatchCmd(DivCommand(DIV_CMD_HINT_LEGATO,i,chan[i].note)); } else { if (chan[i].inPorta && chan[i].keyOn && !chan[i].shorthandPorta) { if (song.e1e2StopOnSameNote && chan[i].wasShorthandPorta) { chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); if (!song.brokenShortcutSlides) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); chan[i].wasShorthandPorta=false; chan[i].inPorta=false; } else { chan[i].portaNote=chan[i].note; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); } } else if (!chan[i].noteOnInhibit) { dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,chan[i].note,chan[i].volume>>8)); @@ -742,12 +801,14 @@ void DivEngine::processRow(int i, bool afterDelay) { if (!chan[i].keyOn && chan[i].scheduledSlideReset) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].scheduledSlideReset=false; chan[i].inPorta=false; } if (!chan[i].keyOn && chan[i].volume>chan[i].volMax) { chan[i].volume=chan[i].volMax; dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } chan[i].keyOn=true; chan[i].keyOff=false; @@ -771,7 +832,7 @@ void DivEngine::nextRow() { static char pb1[4096]; static char pb2[4096]; static char pb3[4096]; - if (view==DIV_STATUS_PATTERN) { + if (view==DIV_STATUS_PATTERN && !skipping) { strcpy(pb1,""); strcpy(pb3,""); for (int i=0; idata[curRow][0]==0 && pat->data[curRow][1]==0)) { if (pat->data[curRow][0]!=100 && pat->data[curRow][0]!=101 && pat->data[curRow][0]!=102) { if (!chan[i].legato) { - dispatchCmd(DivCommand(DIV_CMD_PRE_NOTE,i,ticks)); + if (disCont[dispatchOfChan[i]].dispatch!=NULL) { + if (disCont[dispatchOfChan[i]].dispatch->getWantPreNote()) dispatchCmd(DivCommand(DIV_CMD_PRE_NOTE,i,ticks)); + } if (song.oneTickCut) { bool doPrepareCut=true; @@ -896,7 +959,7 @@ void DivEngine::nextRow() { bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { bool ret=false; - if (divider<10) divider=10; + if (divider<1) divider=1; if (lowLatency && !skipping && !inhibitLowLat) { tickMult=1000/divider; @@ -914,7 +977,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { // MIDI clock if (output) if (!skipping && output->midiOut!=NULL) { - output->midiOut->send(TAMidiMessage(TA_MIDI_CLOCK,0,0)); + //output->midiOut->send(TAMidiMessage(TA_MIDI_CLOCK,0,0)); } while (!pendingNotes.empty()) { @@ -985,15 +1048,19 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (chan[i].volume>chan[i].volMax) { chan[i].volume=chan[i].volMax; chan[i].volSpeed=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0)); } else if (chan[i].volume<0) { chan[i].volSpeed=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0)); if (song.legacyVolumeSlides) { chan[i].volume=chan[i].volMax+1; } else { chan[i].volume=0; } dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); } else { dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } @@ -1022,10 +1089,12 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if ((chan[i].keyOn || chan[i].keyOff) && chan[i].portaSpeed>0) { if (dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed*(song.linearPitch==2?song.pitchSlideSpeed:1),chan[i].portaNote))==2 && chan[i].portaStop && song.targetResetsSlides) { chan[i].portaSpeed=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].oldNote=chan[i].note; chan[i].note=chan[i].portaNote; chan[i].inPorta=false; dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + dispatchCmd(DivCommand(DIV_CMD_HINT_LEGATO,i,chan[i].note)); } } } @@ -1039,11 +1108,13 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (chan[i].stopOnOff) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); chan[i].stopOnOff=false; } if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,chan[i].portaNote,chan[i].portaSpeed)); /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; @@ -1057,6 +1128,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { } if (chan[i].resetArp) { dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + dispatchCmd(DivCommand(DIV_CMD_HINT_LEGATO,i,chan[i].note)); chan[i].resetArp=false; } if (song.rowResetsArpPos && firstTick) { @@ -1105,7 +1177,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { } } - if (consoleMode && subticks<=1) fprintf(stderr,"\x1b[2K> %d:%.2d:%.2d.%.2d %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000,curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond); + if (consoleMode && subticks<=1 && !skipping) fprintf(stderr,"\x1b[2K> %d:%.2d:%.2d.%.2d %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000,curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond); } if (haltOn==DIV_HALT_TICK) halted=true; @@ -1204,17 +1276,17 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi blip_add_delta(samp_bb,i,samp_temp-samp_prevSample); samp_prevSample=samp_temp; - if (sPreview.pos>=s->samples || (sPreview.pEnd>=0 && (int)sPreview.pos>=sPreview.pEnd)) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples && (int)sPreview.pos>=s->loopStart) { + if (sPreview.pos>=s->getEndPosition() || (sPreview.pEnd>=0 && (int)sPreview.pos>=sPreview.pEnd)) { + if (s->isLoopable() && (int)sPreview.pos>=s->loopStart) { sPreview.pos=s->loopStart; } } } - if (sPreview.pos>=s->samples || (sPreview.pEnd>=0 && (int)sPreview.pos>=sPreview.pEnd)) { - if (s->loopStart>=0 && s->loopStart<(int)s->samples && (int)sPreview.pos>=s->loopStart) { + if (sPreview.pos>=s->getEndPosition() || (sPreview.pEnd>=0 && (int)sPreview.pos>=sPreview.pEnd)) { + if (s->isLoopable() && (int)sPreview.pos>=s->loopStart) { sPreview.pos=s->loopStart; - } else { + } else if (sPreview.pos>=s->samples) { sPreview.sample=-1; } } @@ -1256,25 +1328,22 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } // logic starts here - size_t runtotal[32]; - size_t runLeft[32]; - size_t runPos[32]; - size_t lastAvail[32]; for (int i=0; i0) { - disCont[i].flush(lastAvail[i]); + disCont[i].lastAvail=blip_samples_avail(disCont[i].bb[0]); + if (disCont[i].lastAvail>0) { + disCont[i].flush(disCont[i].lastAvail); } - runtotal[i]=blip_clocks_needed(disCont[i].bb[0],size-lastAvail[i]); - if (runtotal[i]>disCont[i].bbInLen) { + disCont[i].runtotal=blip_clocks_needed(disCont[i].bb[0],size-disCont[i].lastAvail); + if (disCont[i].runtotal>disCont[i].bbInLen) { + logV("growing dispatch %d bbIn to %d",i,disCont[i].runtotal+256); delete[] disCont[i].bbIn[0]; delete[] disCont[i].bbIn[1]; - disCont[i].bbIn[0]=new short[runtotal[i]+256]; - disCont[i].bbIn[1]=new short[runtotal[i]+256]; - disCont[i].bbInLen=runtotal[i]+256; + disCont[i].bbIn[0]=new short[disCont[i].runtotal+256]; + disCont[i].bbIn[1]=new short[disCont[i].runtotal+256]; + disCont[i].bbInLen=disCont[i].runtotal+256; } - runLeft[i]=runtotal[i]; - runPos[i]=0; + disCont[i].runLeft=disCont[i].runtotal; + disCont[i].runPos=0; } if (metroTickLen>MASTER_CLOCK_PREC); for (int i=0; i1.0) out[0][i]=1.0; + if (out[1][i]<-1.0) out[1][i]=-1.0; + if (out[1][i]>1.0) out[1][i]=1.0; + } + } isBusy.unlock(); std::chrono::steady_clock::time_point ts_processEnd=std::chrono::steady_clock::now(); diff --git a/src/engine/safeWriter.cpp b/src/engine/safeWriter.cpp index 48d7d1227..336793808 100644 --- a/src/engine/safeWriter.cpp +++ b/src/engine/safeWriter.cpp @@ -124,6 +124,9 @@ int SafeWriter::writeWString(WString val, bool pascal) { return 2+val.size()*2; } } +int SafeWriter::writeText(String val) { + return write(val.c_str(),val.size()); +} void SafeWriter::init() { if (operative) return; diff --git a/src/engine/safeWriter.h b/src/engine/safeWriter.h index 3e36f3df7..43f3c67dd 100644 --- a/src/engine/safeWriter.h +++ b/src/engine/safeWriter.h @@ -58,6 +58,7 @@ class SafeWriter { int writeD_BE(double val); int writeWString(WString val, bool pascal); int writeString(String val, bool pascal); + int writeText(String val); void init(); SafeReader* toReader(); diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index 908b9a0d8..60eb35bb2 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -38,6 +38,65 @@ DivSampleHistory::~DivSampleHistory() { if (data!=NULL) delete[] data; } +bool DivSample::isLoopable() { + return (loopStart>=0 && loopStartloopStart && loopEnd<=(int)samples); +} + +unsigned int DivSample::getEndPosition(DivSampleDepth depth) { + int end=loopEnd; + unsigned int len=samples; + switch (depth) { + case DIV_SAMPLE_DEPTH_1BIT: + end=(loopEnd+7)/8; + len=length1; + break; + case DIV_SAMPLE_DEPTH_1BIT_DPCM: + end=(loopEnd+7)/8; + len=lengthDPCM; + break; + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: + end=(loopEnd+1)/2; + len=lengthZ; + break; + case DIV_SAMPLE_DEPTH_QSOUND_ADPCM: + end=(loopEnd+1)/2; + len=lengthQSoundA; + break; + case DIV_SAMPLE_DEPTH_ADPCM_A: + end=(loopEnd+1)/2; + len=lengthA; + break; + case DIV_SAMPLE_DEPTH_ADPCM_B: + end=(loopEnd+1)/2; + len=lengthB; + break; + case DIV_SAMPLE_DEPTH_8BIT: + end=loopEnd; + len=length8; + break; + case DIV_SAMPLE_DEPTH_BRR: + end=9*((loopEnd+15)/16); + len=lengthBRR; + break; + case DIV_SAMPLE_DEPTH_VOX: + end=(loopEnd+1)/2; + len=lengthVOX; + break; + case DIV_SAMPLE_DEPTH_16BIT: + end=loopEnd*2; + len=length16; + break; + default: + break; + } + return isLoopable()?end:len; +} + +void DivSample::setSampleCount(unsigned int count) { + samples=count; + if ((!isLoopable()) || loopEnd<0 || loopEnd>(int)samples) loopEnd=samples; +} + bool DivSample::save(const char* path) { #ifndef HAVE_SNDFILE logE("Furnace was not compiled with libsndfile!"); @@ -53,7 +112,7 @@ bool DivSample::save(const char* path) { si.channels=1; si.samplerate=rate; switch (depth) { - case 8: // 8-bit + case DIV_SAMPLE_DEPTH_8BIT: // 8-bit si.format=SF_FORMAT_PCM_U8|SF_FORMAT_WAV; break; default: // 16-bit @@ -76,17 +135,17 @@ bool DivSample::save(const char* path) { inst.detune = 50 - (pitch % 100); inst.velocity_hi = 0x7f; inst.key_hi = 0x7f; - if(loopStart != -1) + if(isLoopable()) { inst.loop_count = 1; inst.loops[0].mode = SF_LOOP_FORWARD; inst.loops[0].start = loopStart; - inst.loops[0].end = samples; + inst.loops[0].end = loopEnd; } sf_command(f, SFC_SET_INSTRUMENT, &inst, sizeof(inst)); switch (depth) { - case 8: { + case DIV_SAMPLE_DEPTH_8BIT: { // convert from signed to unsigned unsigned char* buf=new unsigned char[length8]; for (size_t i=0; isamples) end=samples; int count=samples-(end-begin); if (count<=0) return resize(0); - if (depth==8) { + if (depth==DIV_SAMPLE_DEPTH_8BIT) { if (data8!=NULL) { signed char* oldData8=data8; data8=NULL; - initInternal(8,count); + initInternal(DIV_SAMPLE_DEPTH_8BIT,count); if (begin>0) { memcpy(data8,oldData8,begin); } @@ -234,13 +293,13 @@ bool DivSample::strip(unsigned int begin, unsigned int end) { // do nothing return true; } - samples=count; + setSampleCount(count); return true; - } else if (depth==16) { + } else if (depth==DIV_SAMPLE_DEPTH_16BIT) { if (data16!=NULL) { short* oldData16=data16; data16=NULL; - initInternal(16,count); + initInternal(DIV_SAMPLE_DEPTH_16BIT,count); if (begin>0) { memcpy(data16,oldData16,sizeof(short)*begin); } @@ -252,7 +311,7 @@ bool DivSample::strip(unsigned int begin, unsigned int end) { // do nothing return true; } - samples=count; + setSampleCount(count); return true; } return false; @@ -262,31 +321,31 @@ bool DivSample::trim(unsigned int begin, unsigned int end) { int count=end-begin; if (count==0) return true; if (begin==0 && end==samples) return true; - if (depth==8) { + if (depth==DIV_SAMPLE_DEPTH_8BIT) { if (data8!=NULL) { signed char* oldData8=data8; data8=NULL; - initInternal(8,count); + initInternal(DIV_SAMPLE_DEPTH_8BIT,count); memcpy(data8,oldData8+begin,count); delete[] oldData8; } else { // do nothing return true; } - samples=count; + setSampleCount(count); return true; - } else if (depth==16) { + } else if (depth==DIV_SAMPLE_DEPTH_16BIT) { if (data16!=NULL) { short* oldData16=data16; data16=NULL; - initInternal(16,count); + initInternal(DIV_SAMPLE_DEPTH_16BIT,count); memcpy(data16,&(oldData16[begin]),sizeof(short)*count); delete[] oldData16; } else { // do nothing return true; } - samples=count; + setSampleCount(count); return true; } return false; @@ -294,11 +353,11 @@ bool DivSample::trim(unsigned int begin, unsigned int end) { bool DivSample::insert(unsigned int pos, unsigned int length) { unsigned int count=samples+length; - if (depth==8) { + if (depth==DIV_SAMPLE_DEPTH_8BIT) { if (data8!=NULL) { signed char* oldData8=data8; data8=NULL; - initInternal(8,count); + initInternal(DIV_SAMPLE_DEPTH_8BIT,count); if (pos>0) { memcpy(data8,oldData8,pos); } @@ -307,15 +366,15 @@ bool DivSample::insert(unsigned int pos, unsigned int length) { } delete[] oldData8; } else { - initInternal(8,count); + initInternal(DIV_SAMPLE_DEPTH_8BIT,count); } - samples=count; + setSampleCount(count); return true; - } else if (depth==16) { + } else if (depth==DIV_SAMPLE_DEPTH_16BIT) { if (data16!=NULL) { short* oldData16=data16; data16=NULL; - initInternal(16,count); + initInternal(DIV_SAMPLE_DEPTH_16BIT,count); if (pos>0) { memcpy(data16,oldData16,sizeof(short)*pos); } @@ -324,9 +383,9 @@ bool DivSample::insert(unsigned int pos, unsigned int length) { } delete[] oldData16; } else { - initInternal(16,count); + initInternal(DIV_SAMPLE_DEPTH_16BIT,count); } - samples=count; + setSampleCount(count); return true; } return false; @@ -337,15 +396,15 @@ bool DivSample::insert(unsigned int pos, unsigned int length) { int finalCount=(double)samples*(r/(double)rate); \ signed char* oldData8=data8; \ short* oldData16=data16; \ - if (depth==16) { \ + if (depth==DIV_SAMPLE_DEPTH_16BIT) { \ if (data16!=NULL) { \ data16=NULL; \ - initInternal(16,finalCount); \ + initInternal(DIV_SAMPLE_DEPTH_16BIT,finalCount); \ } \ - } else if (depth==8) { \ + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { \ if (data8!=NULL) { \ data8=NULL; \ - initInternal(8,finalCount); \ + initInternal(DIV_SAMPLE_DEPTH_8BIT,finalCount); \ } \ } else { \ return false; \ @@ -353,19 +412,20 @@ bool DivSample::insert(unsigned int pos, unsigned int length) { #define RESAMPLE_END \ if (loopStart>=0) loopStart=(double)loopStart*(r/(double)rate); \ + if (loopEnd>=0) loopEnd=(double)loopEnd*(r/(double)rate); \ centerRate=(int)((double)centerRate*(r/(double)rate)); \ rate=r; \ samples=finalCount; \ - if (depth==16) { \ + if (depth==DIV_SAMPLE_DEPTH_16BIT) { \ delete[] oldData16; \ - } else if (depth==8) { \ + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { \ delete[] oldData8; \ } bool DivSample::resampleNone(double r) { RESAMPLE_BEGIN; - if (depth==16) { + if (depth==DIV_SAMPLE_DEPTH_16BIT) { for (int i=0; i=samples) { @@ -374,7 +434,7 @@ bool DivSample::resampleNone(double r) { data16[i]=oldData16[pos]; } } - } else if (depth==8) { + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { for (int i=0; i=samples) { @@ -396,7 +456,7 @@ bool DivSample::resampleLinear(double r) { unsigned int posInt=0; double factor=(double)rate/r; - if (depth==16) { + if (depth==DIV_SAMPLE_DEPTH_16BIT) { for (int i=0; i=samples)?0:oldData16[posInt]; short s2=(posInt+1>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData16[loopStart]:0):oldData16[posInt+1]; @@ -409,7 +469,7 @@ bool DivSample::resampleLinear(double r) { posInt++; } } - } else if (depth==8) { + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { for (int i=0; i=samples)?0:oldData8[posInt]; short s2=(posInt+1>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData8[loopStart]:0):oldData8[posInt+1]; @@ -436,7 +496,7 @@ bool DivSample::resampleCubic(double r) { double factor=(double)rate/r; float* cubicTable=DivFilterTables::getCubicTable(); - if (depth==16) { + if (depth==DIV_SAMPLE_DEPTH_16BIT) { for (int i=0; i=samples)?0:oldData16[posInt]; } } - } else if (depth==8) { + } else if (depth==DIV_SAMPLE_DEPTH_8BIT) { for (int i=0; i>3]>>(i&7))&1)?0x7fff:-0x7fff; } break; - case 1: { // DPCM + case DIV_SAMPLE_DEPTH_1BIT_DPCM: { // DPCM int accum=0; for (unsigned int i=0; i>3]>>(i&7))&1)?1:-1; @@ -687,27 +747,27 @@ void DivSample::render() { } break; } - case 3: // YMZ ADPCM + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: // YMZ ADPCM ymz_decode(dataZ,data16,samples); break; - case 4: // QSound ADPCM + case DIV_SAMPLE_DEPTH_QSOUND_ADPCM: // QSound ADPCM bs_decode(dataQSoundA,data16,samples); break; - case 5: // ADPCM-A + case DIV_SAMPLE_DEPTH_ADPCM_A: // ADPCM-A yma_decode(dataA,data16,samples); break; - case 6: // ADPCM-B + case DIV_SAMPLE_DEPTH_ADPCM_B: // ADPCM-B ymb_decode(dataB,data16,samples); break; - case 8: // 8-bit PCM + case DIV_SAMPLE_DEPTH_8BIT: // 8-bit PCM for (unsigned int i=0; i0) { data1[i>>3]|=1<<(i&7); } } } - if (depth!=1) { // DPCM - if (!initInternal(1,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_1BIT_DPCM) { // DPCM + if (!initInternal(DIV_SAMPLE_DEPTH_1BIT_DPCM,samples)) return; int accum=63; for (unsigned int i=0; i>9; @@ -739,84 +799,88 @@ void DivSample::render() { if (accum>127) accum=127; } } - if (depth!=3) { // YMZ ADPCM - if (!initInternal(3,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_YMZ_ADPCM) { // YMZ ADPCM + if (!initInternal(DIV_SAMPLE_DEPTH_YMZ_ADPCM,samples)) return; ymz_encode(data16,dataZ,(samples+7)&(~0x7)); } - if (depth!=4) { // QSound ADPCM - if (!initInternal(4,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_QSOUND_ADPCM) { // QSound ADPCM + if (!initInternal(DIV_SAMPLE_DEPTH_QSOUND_ADPCM,samples)) return; bs_encode(data16,dataQSoundA,samples); } // TODO: pad to 256. - if (depth!=5) { // ADPCM-A - if (!initInternal(5,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_ADPCM_A) { // ADPCM-A + if (!initInternal(DIV_SAMPLE_DEPTH_ADPCM_A,samples)) return; yma_encode(data16,dataA,(samples+511)&(~0x1ff)); } - if (depth!=6) { // ADPCM-B - if (!initInternal(6,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_ADPCM_B) { // ADPCM-B + if (!initInternal(DIV_SAMPLE_DEPTH_ADPCM_B,samples)) return; ymb_encode(data16,dataB,(samples+511)&(~0x1ff)); } - if (depth!=8) { // 8-bit PCM - if (!initInternal(8,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_8BIT) { // 8-bit PCM + if (!initInternal(DIV_SAMPLE_DEPTH_8BIT,samples)) return; for (unsigned int i=0; i>8; } } // TODO: BRR! - if (depth!=10) { // VOX - if (!initInternal(10,samples)) return; + if (depth!=DIV_SAMPLE_DEPTH_VOX) { // VOX + if (!initInternal(DIV_SAMPLE_DEPTH_VOX,samples)) return; oki_encode(data16,dataVOX,samples); } } void* DivSample::getCurBuf() { switch (depth) { - case 0: + case DIV_SAMPLE_DEPTH_1BIT: return data1; - case 1: + case DIV_SAMPLE_DEPTH_1BIT_DPCM: return dataDPCM; - case 3: + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: return dataZ; - case 4: + case DIV_SAMPLE_DEPTH_QSOUND_ADPCM: return dataQSoundA; - case 5: + case DIV_SAMPLE_DEPTH_ADPCM_A: return dataA; - case 6: + case DIV_SAMPLE_DEPTH_ADPCM_B: return dataB; - case 8: + case DIV_SAMPLE_DEPTH_8BIT: return data8; - case 9: + case DIV_SAMPLE_DEPTH_BRR: return dataBRR; - case 10: + case DIV_SAMPLE_DEPTH_VOX: return dataVOX; - case 16: + case DIV_SAMPLE_DEPTH_16BIT: return data16; + default: + return NULL; } return NULL; } unsigned int DivSample::getCurBufLen() { switch (depth) { - case 0: + case DIV_SAMPLE_DEPTH_1BIT: return length1; - case 1: + case DIV_SAMPLE_DEPTH_1BIT_DPCM: return lengthDPCM; - case 3: + case DIV_SAMPLE_DEPTH_YMZ_ADPCM: return lengthZ; - case 4: + case DIV_SAMPLE_DEPTH_QSOUND_ADPCM: return lengthQSoundA; - case 5: + case DIV_SAMPLE_DEPTH_ADPCM_A: return lengthA; - case 6: + case DIV_SAMPLE_DEPTH_ADPCM_B: return lengthB; - case 8: + case DIV_SAMPLE_DEPTH_8BIT: return length8; - case 9: + case DIV_SAMPLE_DEPTH_BRR: return lengthBRR; - case 10: + case DIV_SAMPLE_DEPTH_VOX: return lengthVOX; - case 16: + case DIV_SAMPLE_DEPTH_16BIT: return length16; + default: + return 0; } return 0; } @@ -831,9 +895,9 @@ DivSampleHistory* DivSample::prepareUndo(bool data, bool doNotPush) { duplicate=new unsigned char[getCurBufLen()]; memcpy(duplicate,getCurBuf(),getCurBufLen()); } - h=new DivSampleHistory(duplicate,getCurBufLen(),samples,depth,rate,centerRate,loopStart); + h=new DivSampleHistory(duplicate,getCurBufLen(),samples,depth,rate,centerRate,loopStart,loopEnd); } else { - h=new DivSampleHistory(depth,rate,centerRate,loopStart); + h=new DivSampleHistory(depth,rate,centerRate,loopStart,loopEnd); } if (!doNotPush) { while (!redoHist.empty()) { @@ -863,7 +927,8 @@ DivSampleHistory* DivSample::prepareUndo(bool data, bool doNotPush) { } \ rate=h->rate; \ centerRate=h->centerRate; \ - loopStart=h->loopStart; + loopStart=h->loopStart; \ + loopEnd=h->loopEnd; int DivSample::undo() { diff --git a/src/engine/sample.h b/src/engine/sample.h index 70f8418cf..103bcaa27 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -17,9 +17,28 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#ifndef _SAMPLE_H +#define _SAMPLE_H + +#pragma once + #include "../ta-utils.h" #include +enum DivSampleDepth: unsigned char { + DIV_SAMPLE_DEPTH_1BIT=0, + DIV_SAMPLE_DEPTH_1BIT_DPCM=1, + DIV_SAMPLE_DEPTH_YMZ_ADPCM=3, + DIV_SAMPLE_DEPTH_QSOUND_ADPCM=4, + DIV_SAMPLE_DEPTH_ADPCM_A=5, + DIV_SAMPLE_DEPTH_ADPCM_B=6, + DIV_SAMPLE_DEPTH_8BIT=8, + DIV_SAMPLE_DEPTH_BRR=9, + DIV_SAMPLE_DEPTH_VOX=10, + DIV_SAMPLE_DEPTH_16BIT=16, + DIV_SAMPLE_DEPTH_MAX // boundary for sample depth +}; + enum DivResampleFilters { DIV_RESAMPLE_NONE=0, DIV_RESAMPLE_LINEAR, @@ -32,10 +51,10 @@ enum DivResampleFilters { struct DivSampleHistory { unsigned char* data; unsigned int length, samples; - unsigned char depth; - int rate, centerRate, loopStart; + DivSampleDepth depth; + int rate, centerRate, loopStart, loopEnd; bool hasSample; - DivSampleHistory(void* d, unsigned int l, unsigned int s, unsigned char de, int r, int cr, int ls): + DivSampleHistory(void* d, unsigned int l, unsigned int s, DivSampleDepth de, int r, int cr, int ls, int le): data((unsigned char*)d), length(l), samples(s), @@ -43,8 +62,9 @@ struct DivSampleHistory { rate(r), centerRate(cr), loopStart(ls), + loopEnd(le), hasSample(true) {} - DivSampleHistory(unsigned char de, int r, int cr, int ls): + DivSampleHistory(DivSampleDepth de, int r, int cr, int ls, int le): data(NULL), length(0), samples(0), @@ -52,13 +72,14 @@ struct DivSampleHistory { rate(r), centerRate(cr), loopStart(ls), + loopEnd(le), hasSample(false) {} ~DivSampleHistory(); }; struct DivSample { String name; - int rate, centerRate, loopStart, loopOffP; + int rate, centerRate, loopStart, loopEnd, loopOffP; // valid values are: // - 0: ZX Spectrum overlay drum (1-bit) // - 1: 1-bit NES DPCM (1-bit) @@ -70,7 +91,7 @@ struct DivSample { // - 9: BRR (SNES) // - 10: VOX ADPCM // - 16: 16-bit PCM - unsigned char depth; + DivSampleDepth depth; // these are the new data structures. signed char* data8; // 8 @@ -93,6 +114,23 @@ struct DivSample { std::deque undoHist; std::deque redoHist; + /** + * check if sample is loopable. + * @return whether it is loopable. + */ + bool isLoopable(); + + /** + * get sample end position + * @return the samples end position. + */ + unsigned int getEndPosition(DivSampleDepth depth=DIV_SAMPLE_DEPTH_MAX); + + /** + * @warning DO NOT USE - internal functions + */ + void setSampleCount(unsigned int count); + /** * @warning DO NOT USE - internal functions */ @@ -116,7 +154,7 @@ struct DivSample { * @param count number of samples. * @return whether it was successful. */ - bool initInternal(unsigned char d, int count); + bool initInternal(DivSampleDepth d, int count); /** * initialize sample data. make sure you have set `depth` before doing so. @@ -212,8 +250,9 @@ struct DivSample { rate(32000), centerRate(8363), loopStart(-1), + loopEnd(-1), loopOffP(0), - depth(16), + depth(DIV_SAMPLE_DEPTH_16BIT), data8(NULL), data16(NULL), data1(NULL), @@ -253,3 +292,5 @@ struct DivSample { samples(0) {} ~DivSample(); }; + +#endif diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 216a1bc4d..1adb33ebb 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -18,6 +18,7 @@ */ #include "song.h" +#include "../ta-log.h" void DivSubSong::clearData() { for (int i=0; i> clearOuts=pat[i].optimize(); + for (auto& j: clearOuts) { + for (int k=0; k<256; k++) { + if (orders.ord[i][k]==j.first) { + orders.ord[i][k]=j.second; + } + } + } + } +} + +void DivSubSong::rearrangePatterns() { + for (int i=0; i> clearOuts=pat[i].rearrange(); + for (auto& j: clearOuts) { + for (int k=0; k<256; k++) { + if (orders.ord[i][k]==j.first) { + orders.ord[i][k]=j.second; + } + } + } + } +} + void DivSong::clearSongData() { for (DivSubSong* i: subsong) { i->clearData(); diff --git a/src/engine/song.h b/src/engine/song.h index 26640fdd4..de422a24a 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -138,6 +138,8 @@ struct DivSubSong { String chanShortName[DIV_MAX_CHANS]; void clearData(); + void optimizePatterns(); + void rearrangePatterns(); DivSubSong(): hilightA(4), @@ -428,7 +430,7 @@ struct DivSong { unsigned int systemFlags[32]; // song information - String name, author; + String name, author, systemName; // legacy song information // those will be stored in .fur and mapped to VGM as: @@ -438,7 +440,7 @@ struct DivSong { String carrier, composer, vendor, category, writer, arranger, copyright, manGroup, manInfo, createdDate, revisionDate; // more VGM specific stuff - String nameJ, authorJ, categoryJ; + String nameJ, authorJ, categoryJ, systemNameJ; // other things String notes; @@ -541,6 +543,7 @@ struct DivSong { systemLen(2), name(""), author(""), + systemName(""), carrier(""), composer(""), vendor(""), @@ -561,7 +564,7 @@ struct DivSong { linearPitch(2), pitchSlideSpeed(4), loopModality(0), - properNoiseLayout(false), + properNoiseLayout(true), waveDutyIsVol(false), resetMacroOnPorta(false), legacyVolumeSlides(false), diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index a5b689b2b..e3c356cf7 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -54,14 +54,14 @@ std::vector& DivEngine::getPossibleInsTypes() { return possibleInsTypes; } -// TODO: rewrite this function (again). it's an unreliable mess. -String DivEngine::getSongSystemName(bool isMultiSystemAcceptable) { - switch (song.systemLen) { +// for pre-dev103 modules +String DivEngine::getSongSystemLegacyName(DivSong& ds, bool isMultiSystemAcceptable) { + switch (ds.systemLen) { case 0: return "help! what's going on!"; case 1: - if (song.system[0]==DIV_SYSTEM_AY8910) { - switch (song.systemFlags[0]&0x3f) { + if (ds.system[0]==DIV_SYSTEM_AY8910) { + switch (ds.systemFlags[0]&0x3f) { case 0: // AY-3-8910, 1.79MHz case 1: // AY-3-8910, 1.77MHz case 2: // AY-3-8910, 1.75MHz @@ -88,114 +88,116 @@ String DivEngine::getSongSystemName(bool isMultiSystemAcceptable) { return "Intellivision (PAL)"; default: - if ((song.systemFlags[0]&0x30)==0x00) { + if ((ds.systemFlags[0]&0x30)==0x00) { return "AY-3-8910"; - } else if ((song.systemFlags[0]&0x30)==0x10) { + } else if ((ds.systemFlags[0]&0x30)==0x10) { return "Yamaha YM2149"; - } else if ((song.systemFlags[0]&0x30)==0x20) { + } else if ((ds.systemFlags[0]&0x30)==0x20) { return "Overclocked Sunsoft 5B"; - } else if ((song.systemFlags[0]&0x30)==0x30) { + } else if ((ds.systemFlags[0]&0x30)==0x30) { return "Intellivision"; } } - } else if (song.system[0]==DIV_SYSTEM_SMS) { - switch (song.systemFlags[0]&0x0f) { + } else if (ds.system[0]==DIV_SYSTEM_SMS) { + switch (ds.systemFlags[0]&0x0f) { case 0: case 1: return "Sega Master System"; case 6: return "BBC Micro"; } - } else if (song.system[0]==DIV_SYSTEM_YM2612) { - switch (song.systemFlags[0]&3) { + } else if (ds.system[0]==DIV_SYSTEM_YM2612) { + switch (ds.systemFlags[0]&3) { case 2: return "FM Towns"; } - } else if (song.system[0]==DIV_SYSTEM_YM2151) { - switch (song.systemFlags[0]&3) { + } else if (ds.system[0]==DIV_SYSTEM_YM2151) { + switch (ds.systemFlags[0]&3) { case 2: return "Sharp X68000"; } - } else if (song.system[0]==DIV_SYSTEM_SAA1099) { - switch (song.systemFlags[0]&3) { + } else if (ds.system[0]==DIV_SYSTEM_SAA1099) { + switch (ds.systemFlags[0]&3) { case 0: return "SAM Coupé"; } } - return getSystemName(song.system[0]); + return getSystemName(ds.system[0]); case 2: - if (song.system[0]==DIV_SYSTEM_YM2612 && song.system[1]==DIV_SYSTEM_SMS) { + if (ds.system[0]==DIV_SYSTEM_YM2612 && ds.system[1]==DIV_SYSTEM_SMS) { return "Sega Genesis/Mega Drive"; } - if (song.system[0]==DIV_SYSTEM_YM2612_EXT && song.system[1]==DIV_SYSTEM_SMS) { + if (ds.system[0]==DIV_SYSTEM_YM2612_EXT && ds.system[1]==DIV_SYSTEM_SMS) { return "Sega Genesis Extended Channel 3"; } - if (song.system[0]==DIV_SYSTEM_OPLL && song.system[1]==DIV_SYSTEM_SMS) { + if (ds.system[0]==DIV_SYSTEM_OPLL && ds.system[1]==DIV_SYSTEM_SMS) { return "NTSC-J Sega Master System"; } - if (song.system[0]==DIV_SYSTEM_OPLL_DRUMS && song.system[1]==DIV_SYSTEM_SMS) { + if (ds.system[0]==DIV_SYSTEM_OPLL_DRUMS && ds.system[1]==DIV_SYSTEM_SMS) { return "NTSC-J Sega Master System + drums"; } - if (song.system[0]==DIV_SYSTEM_OPLL && song.system[1]==DIV_SYSTEM_AY8910) { + if (ds.system[0]==DIV_SYSTEM_OPLL && ds.system[1]==DIV_SYSTEM_AY8910) { return "MSX-MUSIC"; } - if (song.system[0]==DIV_SYSTEM_OPLL_DRUMS && song.system[1]==DIV_SYSTEM_AY8910) { + if (ds.system[0]==DIV_SYSTEM_OPLL_DRUMS && ds.system[1]==DIV_SYSTEM_AY8910) { return "MSX-MUSIC + drums"; } - if (song.system[0]==DIV_SYSTEM_C64_6581 && song.system[1]==DIV_SYSTEM_C64_6581) { + if (ds.system[0]==DIV_SYSTEM_C64_6581 && ds.system[1]==DIV_SYSTEM_C64_6581) { return "Commodore 64 with dual 6581"; } - if (song.system[0]==DIV_SYSTEM_C64_8580 && song.system[1]==DIV_SYSTEM_C64_8580) { + if (ds.system[0]==DIV_SYSTEM_C64_8580 && ds.system[1]==DIV_SYSTEM_C64_8580) { return "Commodore 64 with dual 8580"; } - if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_SEGAPCM_COMPAT) { + if (ds.system[0]==DIV_SYSTEM_YM2151 && ds.system[1]==DIV_SYSTEM_SEGAPCM_COMPAT) { return "YM2151 + SegaPCM Arcade (compatibility)"; } - if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_SEGAPCM) { + if (ds.system[0]==DIV_SYSTEM_YM2151 && ds.system[1]==DIV_SYSTEM_SEGAPCM) { return "YM2151 + SegaPCM Arcade"; } - if (song.system[0]==DIV_SYSTEM_SAA1099 && song.system[1]==DIV_SYSTEM_SAA1099) { + if (ds.system[0]==DIV_SYSTEM_SAA1099 && ds.system[1]==DIV_SYSTEM_SAA1099) { return "Creative Music System"; } - if (song.system[0]==DIV_SYSTEM_GB && song.system[1]==DIV_SYSTEM_AY8910) { + if (ds.system[0]==DIV_SYSTEM_GB && ds.system[1]==DIV_SYSTEM_AY8910) { return "Game Boy with AY expansion"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_VRC6) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_VRC6) { return "Famicom + Konami VRC6"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_VRC7) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_VRC7) { return "Famicom + Konami VRC7"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_OPLL) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_OPLL) { return "Family Noraebang"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_FDS) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_FDS) { return "Famicom Disk System"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_N163) { - return "Famicom + Namco C163"; + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_N163) { + String ret="Famicom + "; + ret+=getConfString("c163Name",DIV_C163_DEFAULT_NAME); + return ret; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_MMC5) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_MMC5) { return "Famicom + MMC5"; } - if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_AY8910) { + if (ds.system[0]==DIV_SYSTEM_NES && ds.system[1]==DIV_SYSTEM_AY8910) { return "Famicom + Sunsoft 5B"; } - if (song.system[0]==DIV_SYSTEM_AY8910 && song.system[1]==DIV_SYSTEM_AY8910) { + if (ds.system[0]==DIV_SYSTEM_AY8910 && ds.system[1]==DIV_SYSTEM_AY8910) { return "Bally Midway MCR"; } - if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_VERA) { + if (ds.system[0]==DIV_SYSTEM_YM2151 && ds.system[1]==DIV_SYSTEM_VERA) { return "Commander X16"; } break; case 3: - if (song.system[0]==DIV_SYSTEM_AY8910 && song.system[1]==DIV_SYSTEM_AY8910 && song.system[2]==DIV_SYSTEM_BUBSYS_WSG) { + if (ds.system[0]==DIV_SYSTEM_AY8910 && ds.system[1]==DIV_SYSTEM_AY8910 && ds.system[2]==DIV_SYSTEM_BUBSYS_WSG) { return "Konami Bubble System"; } break; @@ -203,9 +205,13 @@ String DivEngine::getSongSystemName(bool isMultiSystemAcceptable) { if (isMultiSystemAcceptable) return "multi-system"; String ret=""; - for (int i=0; i0) ret+=" + "; - ret+=getSystemName(song.system[i]); + if (ds.system[i]==DIV_SYSTEM_N163) { + ret+=getConfString("c163Name",DIV_C163_DEFAULT_NAME); + } else { + ret+=getSystemName(ds.system[i]); + } } return ret; @@ -213,6 +219,11 @@ String DivEngine::getSongSystemName(bool isMultiSystemAcceptable) { const char* DivEngine::getSystemName(DivSystem sys) { if (sysDefs[sys]==NULL) return "Unknown"; + if (sys==DIV_SYSTEM_N163) { + String c1=getConfString("c163Name",DIV_C163_DEFAULT_NAME); + strncpy(c163NameCS,c1.c_str(),1023); + return c163NameCS; + } return sysDefs[sys]->name; } @@ -1235,7 +1246,7 @@ void DivEngine::registerSystems() { ); sysDefs[DIV_SYSTEM_N163]=new DivSysDef( - "Namco C163", NULL, 0x8c, 0, 8, false, true, 0, false, + "Namco 163/C163/129/160/106/whatever", NULL, 0x8c, 0, 8, false, true, 0, false, "an expansion chip for the Famicom, with full wavetable.", {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8"}, {"CH1", "CH2", "CH3", "CH4", "CH5", "CH6", "CH7", "CH8"}, @@ -1829,7 +1840,7 @@ void DivEngine::registerSystems() { sysDefs[DIV_SYSTEM_ES5506]=new DivSysDef( "Ensoniq ES5506", NULL, 0xb1, 0, 32, false, true, 0, false, - "a sample chip used in the Gravis Ultrasound, popular in the PC (DOS) demoscene.", + "a sample chip used in the Ensoniq's unique TransWave synthesizers, and SoundScape series PC ISA soundcards (which are yet another (partially) Sound Blaster compatible ones with emulated OPL3 and MIDI ROMpler).", {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "Channel 17", "Channel 18", "Channel 19", "Channel 20", "Channel 21", "Channel 22", "Channel 23", "Channel 24", "Channel 25", "Channel 26", "Channel 27", "Channel 28", "Channel 29", "Channel 30", "Channel 31", "Channel 32"}, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32"}, {DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM}, @@ -1839,8 +1850,8 @@ void DivEngine::registerSystems() { sysDefs[DIV_SYSTEM_Y8950]=new DivSysDef( "Yamaha Y8950", NULL, 0xb2, 0, 10, true, false, 0x151, false, "like OPL but with an ADPCM channel.", - {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "PCM"}, - {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "PCM"}, + {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "ADPCM"}, + {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "P"}, {DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_PCM}, {DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_AMIGA}, {}, @@ -1851,8 +1862,8 @@ void DivEngine::registerSystems() { sysDefs[DIV_SYSTEM_Y8950_DRUMS]=new DivSysDef( "Yamaha Y8950 with drums", NULL, 0xb3, 0, 12, true, false, 0x151, false, "the Y8950 chip, in drums mode.", - {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Kick/FM 7", "Snare", "Tom", "Top", "HiHat", "PCM"}, - {"F1", "F2", "F3", "F4", "F5", "F6", "BD", "SD", "TM", "TP", "HH", "PCM"}, + {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Kick/FM 7", "Snare", "Tom", "Top", "HiHat", "ADPCM"}, + {"F1", "F2", "F3", "F4", "F5", "F6", "BD", "SD", "TM", "TP", "HH", "P"}, {DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_NOISE, DIV_CH_NOISE, DIV_CH_NOISE, DIV_CH_NOISE, DIV_CH_NOISE, DIV_CH_PCM}, {DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL_DRUMS, DIV_INS_OPL_DRUMS, DIV_INS_OPL_DRUMS, DIV_INS_OPL_DRUMS, DIV_INS_AMIGA}, {DIV_INS_NULL, DIV_INS_NULL, DIV_INS_NULL, DIV_INS_NULL, DIV_INS_NULL, DIV_INS_NULL, DIV_INS_NULL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_OPL, DIV_INS_NULL}, diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 0d1ad4f52..15de0156f 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -512,7 +512,7 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(streamID); w->writeS(write.val); // sample number w->writeC((sample->loopStart==0)|(sampleDir[streamID]?0x10:0)); // flags - if (sample->loopStart>0 && !sampleDir[streamID]) { + if (sample->isLoopable() && !sampleDir[streamID]) { loopTimer[streamID]=sample->length8; loopSample[streamID]=write.val; } @@ -802,7 +802,7 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write } } -SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { +SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool patternHints) { if (version<0x150) { lastError="VGM version is too low"; return NULL; @@ -1549,7 +1549,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { size_t memPos=0; for (int i=0; ilength8+0xff)&(~0xff); + unsigned int alignedSize=(sample->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)+0xff)&(~0xff); if (alignedSize>65536) alignedSize=65536; if ((memPos&0xff0000)!=((memPos+alignedSize)&0xff0000)) { memPos=(memPos+0xffff)&0xff0000; @@ -1559,8 +1559,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { sample->offSegaPCM=memPos; unsigned int readPos=0; for (unsigned int j=0; j=sample->length8) { - if (sample->loopStart>=0 && sample->loopStart<(int)sample->length8) { + if (readPos>=sample->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)) { + if (sample->isLoopable()) { readPos=sample->loopStart; pcmMem[memPos++]=((unsigned char)sample->data8[readPos]+0x80); } else { @@ -1663,7 +1663,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { memcpy(sampleMem,writeZ280[i]->getSampleMem(),sampleMemLen); for (int i=0; idepth==16) { + if (s->depth==DIV_SAMPLE_DEPTH_16BIT) { unsigned int pos=s->offYMZ280B; for (unsigned int j=0; jsamples; j++) { unsigned char lo=sampleMem[pos+j*2]; @@ -1795,6 +1795,12 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { playSub(false); size_t tickCount=0; bool writeLoop=false; + int ord=-1; + int exportChans=0; + for (int i=0; iwriteC(0x67); + w->writeC(0x66); + w->writeC(0xfe); + w->writeI(3+exportChans); + w->writeC(0x01); + w->writeC(prevOrder); + w->writeC(prevRow); + for (int i=0; iwriteC(curSubSong->orders.ord[i][prevOrder]); + } + } + } } // get register dumps for (int i=0; iloopStart<(int)sample->length8) { + if (sample->loopStart<(int)sample->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)) { w->writeC(0x93); w->writeC(nextToTouch); w->writeI(sample->off8+sample->loopStart); w->writeC(0x81); - w->writeI(sample->length8-sample->loopStart); + w->writeI(sample->getEndPosition(DIV_SAMPLE_DEPTH_8BIT)-sample->loopStart); } } loopSample[nextToTouch]=-1; @@ -1920,24 +1946,20 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { WString ws; ws=utf8To16(song.name.c_str()); w->writeWString(ws,false); // name - w->writeS(0); // japanese name - w->writeS(0); // game name - w->writeS(0); // japanese game name - if (song.systemLen>1) { - ws=L"Multiple Systems"; - } else { - ws=utf8To16(getSystemName(song.system[0])); - } + ws=utf8To16(song.nameJ.c_str()); + w->writeWString(ws,false); // japanese name + ws=utf8To16(song.category.c_str()); + w->writeWString(ws,false); // game name + ws=utf8To16(song.categoryJ.c_str()); + w->writeWString(ws,false); // japanese game name + ws=utf8To16(song.systemName.c_str()); w->writeWString(ws,false); // system name - if (song.systemLen>1) { - ws=L"複数システム"; - } else { - ws=utf8To16(getSystemNameJ(song.system[0])); - } + ws=utf8To16(song.systemNameJ.c_str()); w->writeWString(ws,false); // japanese system name ws=utf8To16(song.author.c_str()); w->writeWString(ws,false); // author name - w->writeS(0); // japanese author name + ws=utf8To16(song.authorJ.c_str()); + w->writeWString(ws,false); // japanese author name w->writeS(0); // date w->writeWString(L"Furnace Tracker",false); // ripper w->writeS(0); // notes diff --git a/src/engine/wavetable.cpp b/src/engine/wavetable.cpp index 953400ee1..0b13497ef 100644 --- a/src/engine/wavetable.cpp +++ b/src/engine/wavetable.cpp @@ -92,3 +92,65 @@ bool DivWavetable::save(const char* path) { w->finish(); return true; } + +bool DivWavetable::saveDMW(const char* path) { + SafeWriter* w=new SafeWriter(); + w->init(); + + // write width + w->writeI(len); + + // check height + w->writeC(max); + if (max==255) { + // write as new format (because 0xff means that) + w->writeC(1); // format version + w->writeC(max); // actual height + + // waveform data + for (int i=0; iwriteI(data[i]&0xff); + } + } else { + // write as old format + for (int i=0; iwriteC(data[i]); + } + } + + FILE* outFile=ps_fopen(path,"wb"); + if (outFile==NULL) { + logE("could not save wavetable: %s!",strerror(errno)); + w->finish(); + return false; + } + if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) { + logW("did not write entire wavetable!"); + } + fclose(outFile); + w->finish(); + return true; +} + +bool DivWavetable::saveRaw(const char* path) { + SafeWriter* w=new SafeWriter(); + w->init(); + + // waveform data + for (int i=0; iwriteC(data[i]); + } + + FILE* outFile=ps_fopen(path,"wb"); + if (outFile==NULL) { + logE("could not save wavetable: %s!",strerror(errno)); + w->finish(); + return false; + } + if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) { + logW("did not write entire wavetable!"); + } + fclose(outFile); + w->finish(); + return true; +} diff --git a/src/engine/wavetable.h b/src/engine/wavetable.h index 616b048cf..0f518ab53 100644 --- a/src/engine/wavetable.h +++ b/src/engine/wavetable.h @@ -46,6 +46,20 @@ struct DivWavetable { * @return whether it was successful. */ bool save(const char* path); + + /** + * save this wavetable to a file in .dmw format. + * @param path file path. + * @return whether it was successful. + */ + bool saveDMW(const char* path); + + /** + * save this wavetable to a file in raw format. + * @param path file path. + * @return whether it was successful. + */ + bool saveRaw(const char* path); DivWavetable(): len(32), min(0), @@ -56,4 +70,4 @@ struct DivWavetable { } }; -#endif \ No newline at end of file +#endif diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 70e09e727..ae0792aef 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -27,8 +27,8 @@ const char* aboutLine[]={ "", ("Furnace " DIV_VERSION), "", - "the free software multi-system chiptune tracker,", - "compatible with DefleMask modules.", + "the biggest multi-system chiptune tracker!", + "featuring DefleMask song compatibility.", "", "zero disassembly.", "just clean-room design,", @@ -67,6 +67,7 @@ const char* aboutLine[]={ "AURORA*FIELDS", "BlueElectric05", "breakthetargets", + "brickblock369", "CaptainMalware", "DeMOSic", "DevEd", @@ -78,12 +79,14 @@ const char* aboutLine[]={ "kleeder", "jaezu", "Laggy", + "LovelyA72", "LunaMoth", + "LVintageNerd", "Mahbod Karamoozian", "Miker", "nicco1690", "NikonTeen", - "SnugglyValeria", + "psdominator", "SuperJet Spade", "TheDuccinator", "theloredev", @@ -96,6 +99,7 @@ const char* aboutLine[]={ "-- additional feedback/fixes --", "fd", "GENATARi", + "host12prog", "plane", "TheEssem", "", @@ -132,7 +136,7 @@ const char* aboutLine[]={ "VICE VIC-20 sound core by Rami Rasanen and viznut", "VERA sound core by Frank van den Hoef", "K005289 emulator by cam900", - "Namco C163 emulator by cam900", + "Namco 163 emulator by cam900", "Seta X1-010 emulator by cam900", "Konami VRC6 emulator by cam900", "Konami SCC emulator by cam900", diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index 62b93479b..10dda6aaf 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -184,6 +184,7 @@ void FurnaceGUI::drawInsList() { if (i>=0) { DivInstrument* ins=e->song.ins[i]; insType=(ins->type>DIV_INS_MAX)?"Unknown":insTypes[ins->type]; + if (ins->type==DIV_INS_N163) insType=settings.c163Name.c_str(); switch (ins->type) { case DIV_INS_FM: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FM]); @@ -419,6 +420,12 @@ void FurnaceGUI::drawWaveList() { if (ImGui::Button(ICON_FA_FOLDER_OPEN "##WaveLoad")) { doAction(GUI_ACTION_WAVE_LIST_OPEN); } + if (ImGui::BeginPopupContextItem("WaveOpenOpt")) { + if (ImGui::MenuItem("replace...")) { + doAction((curWave>=0 && curWave<(int)e->song.wave.size())?GUI_ACTION_WAVE_LIST_OPEN_REPLACE:GUI_ACTION_WAVE_LIST_OPEN); + } + ImGui::EndPopup(); + } ImGui::SameLine(); if (ImGui::Button(ICON_FA_FLOPPY_O "##WaveSave")) { doAction(GUI_ACTION_WAVE_LIST_SAVE); @@ -469,6 +476,19 @@ void FurnaceGUI::drawSampleList() { if (ImGui::Button(ICON_FA_FOLDER_OPEN "##SampleLoad")) { doAction(GUI_ACTION_SAMPLE_LIST_OPEN); } + if (ImGui::BeginPopupContextItem("SampleOpenOpt")) { + if (ImGui::MenuItem("replace...")) { + doAction((curSample>=0 && curSample<(int)e->song.sample.size())?GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE:GUI_ACTION_SAMPLE_LIST_OPEN); + } + ImGui::Separator(); + if (ImGui::MenuItem("import raw...")) { + doAction(GUI_ACTION_SAMPLE_LIST_OPEN_RAW); + } + if (ImGui::MenuItem("import raw (replace)...")) { + doAction((curSample>=0 && curSample<(int)e->song.sample.size())?GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE_RAW:GUI_ACTION_SAMPLE_LIST_OPEN_RAW); + } + ImGui::EndPopup(); + } ImGui::SameLine(); if (ImGui::Button(ICON_FA_FLOPPY_O "##SampleSave")) { doAction(GUI_ACTION_SAMPLE_LIST_SAVE); diff --git a/src/gui/debug.cpp b/src/gui/debug.cpp index f1c974e8b..5ae596bcd 100644 --- a/src/gui/debug.cpp +++ b/src/gui/debug.cpp @@ -334,7 +334,7 @@ void putDispatchChan(void* data, int chanNum, int type) { break; } default: - ImGui::Text("Unknown system! Help!"); + ImGui::Text("Unimplemented chip! Help!"); break; } } diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 22527a7b8..8fe02b116 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -18,6 +18,7 @@ */ #include "gui.h" +#include "guiConst.h" #include "debug.h" #include "IconsFontAwesome4.h" #include @@ -154,12 +155,19 @@ void FurnaceGUI::drawDebug() { ImGui::Text("rate: %d",sample->rate); ImGui::Text("centerRate: %d",sample->centerRate); ImGui::Text("loopStart: %d",sample->loopStart); + ImGui::Text("loopEnd: %d", sample->loopEnd); ImGui::Text("loopOffP: %d",sample->loopOffP); - ImGui::Text("depth: %d",sample->depth); + if (sampleDepths[sample->depth]!=NULL) { + ImGui::Text("depth: %d (%s)",(unsigned char)sample->depth,sampleDepths[sample->depth]); + } else { + ImGui::Text("depth: %d ()",(unsigned char)sample->depth); + } + ImGui::Text("length8: %d",sample->length8); ImGui::Text("length16: %d",sample->length16); ImGui::Text("length1: %d",sample->length1); ImGui::Text("lengthDPCM: %d",sample->lengthDPCM); + ImGui::Text("lengthZ: %d",sample->lengthZ); ImGui::Text("lengthQSoundA: %d",sample->lengthQSoundA); ImGui::Text("lengthA: %d",sample->lengthA); ImGui::Text("lengthB: %d",sample->lengthB); @@ -170,6 +178,7 @@ void FurnaceGUI::drawDebug() { ImGui::Text("off16: %x",sample->off16); ImGui::Text("off1: %x",sample->off1); ImGui::Text("offDPCM: %x",sample->offDPCM); + ImGui::Text("offZ: %x",sample->offZ); ImGui::Text("offQSoundA: %x",sample->offQSoundA); ImGui::Text("offA: %x",sample->offA); ImGui::Text("offB: %x",sample->offB); @@ -179,6 +188,8 @@ void FurnaceGUI::drawDebug() { ImGui::Text("offQSound: %x",sample->offQSound); ImGui::Text("offX1_010: %x",sample->offX1_010); ImGui::Text("offSU: %x",sample->offSU); + ImGui::Text("offYMZ280B: %x",sample->offYMZ280B); + ImGui::Text("offRF5C68: %x",sample->offRF5C68); ImGui::Text("samples: %d",sample->samples); ImGui::TreePop(); @@ -209,24 +220,34 @@ void FurnaceGUI::drawDebug() { ImGui::Text("Data"); for (int j=0; jgetChannelCount(system); j++, c++) { + DivDispatchOscBuffer* oscBuf=e->getOscBuffer(c); + if (oscBuf==NULL) { + ImGui::TableNextRow(); + // channel + ImGui::TableNextColumn(); + ImGui::Text("%d",j); + ImGui::TableNextColumn(); + ImGui::Text(""); + continue; + } ImGui::TableNextRow(); // channel ImGui::TableNextColumn(); ImGui::Text("%d",j); // follow ImGui::TableNextColumn(); - ImGui::Checkbox(fmt::sprintf("##%d_OSCFollow_%d",i,c).c_str(),&e->getOscBuffer(c)->follow); + ImGui::Checkbox(fmt::sprintf("##%d_OSCFollow_%d",i,c).c_str(),&oscBuf->follow); // address ImGui::TableNextColumn(); - int needle=e->getOscBuffer(c)->follow?e->getOscBuffer(c)->needle:e->getOscBuffer(c)->followNeedle; - ImGui::BeginDisabled(e->getOscBuffer(c)->follow); + int needle=oscBuf->follow?oscBuf->needle:oscBuf->followNeedle; + ImGui::BeginDisabled(oscBuf->follow); if (ImGui::InputInt(fmt::sprintf("##%d_OSCFollowNeedle_%d",i,c).c_str(),&needle,1,100)) { - e->getOscBuffer(c)->followNeedle=MIN(MAX(needle,0),65535); + oscBuf->followNeedle=MIN(MAX(needle,0),65535); } ImGui::EndDisabled(); // data ImGui::TableNextColumn(); - ImGui::Text("%d",e->getOscBuffer(c)->data[needle]); + ImGui::Text("%d",oscBuf->data[needle]); } ImGui::EndTable(); } @@ -260,9 +281,23 @@ void FurnaceGUI::drawDebug() { ImGui::Unindent(); ImGui::TreePop(); } + if (ImGui::TreeNode("File Selection Test")) { + if (ImGui::Button("Test Open")) { + openFileDialog(GUI_FILE_TEST_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button("Test Open Multi")) { + openFileDialog(GUI_FILE_TEST_OPEN_MULTI); + } + ImGui::SameLine(); + if (ImGui::Button("Test Save")) { + openFileDialog(GUI_FILE_TEST_SAVE); + } + ImGui::TreePop(); + } if (ImGui::TreeNode("Playground")) { if (pgSys<0 || pgSys>=e->song.systemLen) pgSys=0; - if (ImGui::BeginCombo("System",fmt::sprintf("%d. %s",pgSys+1,e->getSystemName(e->song.system[pgSys])).c_str())) { + if (ImGui::BeginCombo("Chip",fmt::sprintf("%d. %s",pgSys+1,e->getSystemName(e->song.system[pgSys])).c_str())) { for (int i=0; isong.systemLen; i++) { if (ImGui::Selectable(fmt::sprintf("%d. %s",i+1,e->getSystemName(e->song.system[i])).c_str())) { pgSys=i; @@ -323,7 +358,7 @@ void FurnaceGUI::drawDebug() { if (ImGui::TreeNode("Register Cheatsheet")) { const char** sheet=e->getRegisterSheet(pgSys); if (sheet==NULL) { - ImGui::Text("no cheatsheet available for this system."); + ImGui::Text("no cheatsheet available for this chip."); } else { if (ImGui::BeginTable("RegisterSheet",2,ImGuiTableFlags_SizingFixedSame)) { ImGui::TableNextRow(); diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index 60c9d5c3a..5c840df4e 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -238,6 +238,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WINDOW_CHANNELS: nextWindow=GUI_WINDOW_CHANNELS; break; + case GUI_ACTION_WINDOW_PAT_MANAGER: + nextWindow=GUI_WINDOW_PAT_MANAGER; + break; case GUI_ACTION_WINDOW_REGISTER_VIEW: nextWindow=GUI_WINDOW_REGISTER_VIEW; break; @@ -322,6 +325,9 @@ void FurnaceGUI::doAction(int what) { case GUI_WINDOW_CHANNELS: channelsOpen=false; break; + case GUI_WINDOW_PAT_MANAGER: + patManagerOpen=false; + break; case GUI_WINDOW_REGISTER_VIEW: regViewOpen=false; break; @@ -648,6 +654,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WAVE_LIST_OPEN: openFileDialog(GUI_FILE_WAVE_OPEN); break; + case GUI_ACTION_WAVE_LIST_OPEN_REPLACE: + openFileDialog(GUI_FILE_WAVE_OPEN_REPLACE); + break; case GUI_ACTION_WAVE_LIST_SAVE: if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE); break; @@ -709,6 +718,7 @@ void FurnaceGUI::doAction(int what) { sample->centerRate=prevSample->centerRate; sample->name=prevSample->name; sample->loopStart=prevSample->loopStart; + sample->loopEnd=prevSample->loopEnd; sample->depth=prevSample->depth; if (sample->init(prevSample->samples)) { if (prevSample->getCurBuf()!=NULL) { @@ -727,6 +737,15 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_SAMPLE_LIST_OPEN: openFileDialog(GUI_FILE_SAMPLE_OPEN); break; + case GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE: + openFileDialog(GUI_FILE_SAMPLE_OPEN_REPLACE); + break; + case GUI_ACTION_SAMPLE_LIST_OPEN_RAW: + openFileDialog(GUI_FILE_SAMPLE_OPEN_RAW); + break; + case GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE_RAW: + openFileDialog(GUI_FILE_SAMPLE_OPEN_REPLACE_RAW); + break; case GUI_ACTION_SAMPLE_LIST_SAVE: if (curSample>=0 && curSample<(int)e->song.sample.size()) openFileDialog(GUI_FILE_SAMPLE_SAVE); break; @@ -838,7 +857,7 @@ void FurnaceGUI::doAction(int what) { if (!sample->insert(pos,sampleClipboardLen)) { showError("couldn't paste! make sure your sample is 8 or 16-bit."); } else { - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (size_t i=0; idata8[pos+i]=sampleClipboard[i]>>8; } @@ -864,7 +883,7 @@ void FurnaceGUI::doAction(int what) { if (pos<0) pos=0; e->lockEngine([this,sample,pos]() { - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (size_t i=0; i=sample->samples) break; sample->data8[pos+i]=sampleClipboard[i]>>8; @@ -894,7 +913,7 @@ void FurnaceGUI::doAction(int what) { if (pos<0) pos=0; e->lockEngine([this,sample,pos]() { - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (size_t i=0; i=sample->samples) break; int val=sample->data8[pos+i]+(sampleClipboard[i]>>8); @@ -948,7 +967,7 @@ void FurnaceGUI::doAction(int what) { SAMPLE_OP_BEGIN; float maxVal=0.0f; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]/32767.0f); if (val>maxVal) maxVal=val; @@ -963,7 +982,7 @@ void FurnaceGUI::doAction(int what) { sample->data16[i]=val; } } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]/127.0f); if (val>maxVal) maxVal=val; @@ -994,14 +1013,14 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]*float(i-start)/float(end-start); if (val<-32768) val=-32768; if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]*float(i-start)/float(end-start); if (val<-128) val=-128; @@ -1024,14 +1043,14 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]*float(end-i)/float(end-start); if (val<-32768) val=-32768; if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]*float(end-i)/float(end-start); if (val<-128) val=-128; @@ -1058,11 +1077,11 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]=0; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]=0; } @@ -1116,7 +1135,7 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[ri]^=sample->data16[i]; sample->data16[i]^=sample->data16[ri]; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; ilockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]=-sample->data16[i]; if (sample->data16[i]==-32768) sample->data16[i]=32767; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]=-sample->data8[i]; if (sample->data16[i]==-128) sample->data16[i]=127; @@ -1174,11 +1193,11 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]^=0x8000; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]^=0x80; } @@ -1261,8 +1280,8 @@ void FurnaceGUI::doAction(int what) { e->lockEngine([this,sample]() { SAMPLE_OP_BEGIN; - sample->trim(0,end); sample->loopStart=start; + sample->loopEnd=end; updateSampleTex=true; e->renderSamples(); diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 9bd827951..21b1a72eb 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -24,7 +24,7 @@ #include "actionUtil.h" -const char* noteNameNormal(short note, short octave) { +const char* FurnaceGUI::noteNameNormal(short note, short octave) { if (note==100) { // note cut return "OFF"; } else if (note==101) { // note off and envelope release diff --git a/src/gui/fileDialog.cpp b/src/gui/fileDialog.cpp index f64f72cbb..8d524ab46 100644 --- a/src/gui/fileDialog.cpp +++ b/src/gui/fileDialog.cpp @@ -10,13 +10,14 @@ #ifdef USE_NFD struct NFDState { - bool isSave; + bool isSave, allowMultiple; String header; std::vector filter; String path; FileDialogSelectCallback clickCallback; - NFDState(bool save, String h, std::vector filt, String pa, FileDialogSelectCallback cc): + NFDState(bool save, String h, std::vector filt, String pa, FileDialogSelectCallback cc, bool multi): isSave(save), + allowMultiple(multi), header(h), filter(filt), path(pa), @@ -25,41 +26,55 @@ struct NFDState { }; // TODO: filter -void _nfdThread(const NFDState state, std::atomic* ok, String* result) { +void _nfdThread(const NFDState state, std::atomic* ok, std::vector* result, bool* errorOutput) { nfdchar_t* out=NULL; nfdresult_t ret=NFD_CANCEL; + (*errorOutput)=false; + nfdpathset_t paths; + + result->clear(); if (state.isSave) { ret=NFD_SaveDialog(state.filter,state.path.c_str(),&out,state.clickCallback); } else { - ret=NFD_OpenDialog(state.filter,state.path.c_str(),&out,state.clickCallback); + if (state.allowMultiple) { + ret=NFD_OpenDialogMultiple(state.filter,state.path.c_str(),&paths,state.clickCallback); + } else { + ret=NFD_OpenDialog(state.filter,state.path.c_str(),&out,state.clickCallback); + } } switch (ret) { case NFD_OKAY: - if (out!=NULL) { - (*result)=out; + if (state.allowMultiple) { + logD("pushing multi path"); + for (size_t i=0; ipush_back(String(NFD_PathSet_GetPath(&paths,i))); + } + NFD_PathSet_Free(&paths); } else { - (*result)=""; + logD("pushing single path"); + if (out!=NULL) { + logD("we have it"); + result->push_back(String(out)); + } } break; case NFD_CANCEL: - (*result)=""; break; case NFD_ERROR: - (*result)=""; logE("NFD error! %s\n",NFD_GetError()); + (*errorOutput)=true; break; default: logE("NFD unknown return code %d!\n",ret); - (*result)=""; break; } (*ok)=true; } #endif -bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback) { +bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback, bool allowMultiple) { if (opened) return false; saving=false; curPath=path; @@ -68,16 +83,18 @@ bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, c #ifdef USE_NFD dialogOK=false; #ifdef NFD_NON_THREADED - _nfdThread(NFDState(false,header,filter,path,clickCallback),&dialogOK,&nfdResult); + _nfdThread(NFDState(false,header,filter,path,clickCallback,allowMultiple),&dialogOK,&nfdResult,&hasError); #else - dialogO=new std::thread(_nfdThread,NFDState(false,header,filter,path,clickCallback),&dialogOK,&nfdResult); + dialogO=new std::thread(_nfdThread,NFDState(false,header,filter,path,clickCallback,allowMultiple),&dialogOK,&nfdResult,&hasError); #endif #else - dialogO=new pfd::open_file(header,path,filter); + dialogO=new pfd::open_file(header,path,filter,allowMultiple?(pfd::opt::multiselect):(pfd::opt::none)); + hasError=!pfd::settings::available(); #endif } else { + hasError=false; ImGuiFileDialog::Instance()->DpiScale=dpiScale; - ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,1,nullptr,0,clickCallback); + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,allowMultiple?999:1,nullptr,0,clickCallback); } opened=true; return true; @@ -92,14 +109,16 @@ bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, c #ifdef USE_NFD dialogOK=false; #ifdef NFD_NON_THREADED - _nfdThread(NFDState(true,header,filter,path,NULL),&dialogOK,&nfdResult); + _nfdThread(NFDState(true,header,filter,path,NULL,false),&dialogOK,&nfdResult,&hasError); #else - dialogS=new std::thread(_nfdThread,NFDState(true,header,filter,path,NULL),&dialogOK,&nfdResult); + dialogS=new std::thread(_nfdThread,NFDState(true,header,filter,path,NULL,false),&dialogOK,&nfdResult,&hasError); #endif #else dialogS=new pfd::save_file(header,path,filter); + hasError=!pfd::settings::available(); #endif } else { + hasError=false; ImGuiFileDialog::Instance()->DpiScale=dpiScale; ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); } @@ -109,7 +128,7 @@ bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, c bool FurnaceGUIFileDialog::accepted() { if (sysDialog) { - return (fileName!=""); + return (!fileName.empty()); } else { return ImGuiFileDialog::Instance()->IsOk(); } @@ -147,10 +166,15 @@ bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { if (sysDialog) { #ifdef USE_NFD if (dialogOK) { + fileName.clear(); fileName=nfdResult; - size_t dsPos=fileName.rfind(DIR_SEPARATOR); - if (dsPos!=String::npos) curPath=fileName.substr(0,dsPos); - logD("returning %s",fileName.c_str()); + if (!fileName.empty()) { + size_t dsPos=fileName[0].rfind(DIR_SEPARATOR); + if (dsPos!=String::npos) curPath=fileName[0].substr(0,dsPos); + } + for (String& i: fileName) { + logD("- returning %s",i); + } dialogOK=false; return true; } @@ -159,10 +183,11 @@ bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { if (saving) { if (dialogS!=NULL) { if (dialogS->ready(0)) { - fileName=dialogS->result(); - size_t dsPos=fileName.rfind(DIR_SEPARATOR); - if (dsPos!=String::npos) curPath=fileName.substr(0,dsPos); - logD("returning %s",fileName.c_str()); + fileName.clear(); + fileName.push_back(dialogS->result()); + size_t dsPos=fileName[0].rfind(DIR_SEPARATOR); + if (dsPos!=String::npos) curPath=fileName[0].substr(0,dsPos); + logD("returning %s",fileName[0]); return true; } } @@ -170,13 +195,19 @@ bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { if (dialogO!=NULL) { if (dialogO->ready(0)) { if (dialogO->result().empty()) { - fileName=""; + fileName.clear(); logD("returning nothing"); } else { - fileName=dialogO->result()[0]; - size_t dsPos=fileName.rfind(DIR_SEPARATOR); - if (dsPos!=String::npos) curPath=fileName.substr(0,dsPos); - logD("returning %s",fileName.c_str()); + fileName=dialogO->result(); + if (fileName.empty()) { + // don't touch + } else { + size_t dsPos=fileName[0].rfind(DIR_SEPARATOR); + if (dsPos!=String::npos) curPath=fileName[0].substr(0,dsPos); + for (String& i: fileName) { + logD("- returning %s",i); + } + } } return true; } @@ -193,6 +224,10 @@ bool FurnaceGUIFileDialog::isOpen() { return opened; } +bool FurnaceGUIFileDialog::isError() { + return hasError; +} + String FurnaceGUIFileDialog::getPath() { if (sysDialog) { if (curPath.size()>1) { @@ -207,10 +242,19 @@ String FurnaceGUIFileDialog::getPath() { } } -String FurnaceGUIFileDialog::getFileName() { +std::vector& FurnaceGUIFileDialog::getFileName() { if (sysDialog) { return fileName; } else { - return ImGuiFileDialog::Instance()->GetFilePathName(); + fileName.clear(); + if (saving) { + fileName.push_back(ImGuiFileDialog::Instance()->GetFilePathName()); + } else { + for (auto& i: ImGuiFileDialog::Instance()->GetSelection()) { + fileName.push_back(i.second); + } + } + // + return fileName; } } diff --git a/src/gui/fileDialog.h b/src/gui/fileDialog.h index 6724eb951..7990b0370 100644 --- a/src/gui/fileDialog.h +++ b/src/gui/fileDialog.h @@ -28,30 +28,33 @@ class FurnaceGUIFileDialog { bool sysDialog; bool opened; bool saving; + bool hasError; String curPath; - String fileName; + std::vector fileName; #ifdef USE_NFD std::thread* dialogO; std::thread* dialogS; std::atomic dialogOK; - String nfdResult; + std::vector nfdResult; #else pfd::open_file* dialogO; pfd::save_file* dialogS; #endif public: - bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL); + bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL, bool allowMultiple=false); bool openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); bool accepted(); void close(); bool render(const ImVec2& min, const ImVec2& max); bool isOpen(); + bool isError(); String getPath(); - String getFileName(); + std::vector& getFileName(); explicit FurnaceGUIFileDialog(bool system): sysDialog(system), opened(false), saving(false), + hasError(false), dialogO(NULL), dialogS(NULL) {} }; diff --git a/src/gui/findReplace.cpp b/src/gui/findReplace.cpp index e982882d4..428fce5a4 100644 --- a/src/gui/findReplace.cpp +++ b/src/gui/findReplace.cpp @@ -39,6 +39,7 @@ const char* queryReplaceModes[GUI_QUERY_REPLACE_MAX]={ "set", "add", "add (overflow)", + "scale", "clear" }; @@ -292,6 +293,8 @@ void FurnaceGUI::doReplace() { } } break; + case GUI_QUERY_REPLACE_SCALE: + break; case GUI_QUERY_REPLACE_CLEAR: p->data[i.y][0]=0; p->data[i.y][1]=0; @@ -314,6 +317,13 @@ void FurnaceGUI::doReplace() { case GUI_QUERY_REPLACE_ADD_OVERFLOW: if (p->data[i.y][2]>=0) p->data[i.y][2]=(p->data[i.y][2]+queryReplaceIns)&0xff; break; + case GUI_QUERY_REPLACE_SCALE: + if (p->data[i.y][2]>=0) { + p->data[i.y][2]=(p->data[i.y][2]*queryReplaceIns)/100; + if (p->data[i.y][2]<0) p->data[i.y][2]=0; + if (p->data[i.y][2]>255) p->data[i.y][2]=255; + } + break; case GUI_QUERY_REPLACE_CLEAR: p->data[i.y][2]=-1; break; @@ -335,6 +345,13 @@ void FurnaceGUI::doReplace() { case GUI_QUERY_REPLACE_ADD_OVERFLOW: if (p->data[i.y][3]>=0) p->data[i.y][3]=(p->data[i.y][3]+queryReplaceVol)&0xff; break; + case GUI_QUERY_REPLACE_SCALE: + if (p->data[i.y][3]>=0) { + p->data[i.y][3]=(p->data[i.y][3]*queryReplaceVol)/100; + if (p->data[i.y][3]<0) p->data[i.y][3]=0; + if (p->data[i.y][3]>255) p->data[i.y][3]=255; + } + break; case GUI_QUERY_REPLACE_CLEAR: p->data[i.y][3]=-1; break; @@ -402,6 +419,13 @@ void FurnaceGUI::doReplace() { case GUI_QUERY_REPLACE_ADD_OVERFLOW: if (p->data[i.y][4+pos*2]>=0) p->data[i.y][4+pos*2]=(p->data[i.y][4+pos*2]+queryReplaceEffect[j])&0xff; break; + case GUI_QUERY_REPLACE_SCALE: + if (p->data[i.y][4+pos*2]>=0) { + p->data[i.y][4+pos*2]=(p->data[i.y][4+pos*2]*queryReplaceEffect[j])/100; + if (p->data[i.y][4+pos*2]<0) p->data[i.y][4+pos*2]=0; + if (p->data[i.y][4+pos*2]>255) p->data[i.y][4+pos*2]=255; + } + break; case GUI_QUERY_REPLACE_CLEAR: p->data[i.y][4+pos*2]=-1; break; @@ -423,6 +447,13 @@ void FurnaceGUI::doReplace() { case GUI_QUERY_REPLACE_ADD_OVERFLOW: if (p->data[i.y][5+pos*2]>=0) p->data[i.y][5+pos*2]=(p->data[i.y][5+pos*2]+queryReplaceEffectVal[j])&0xff; break; + case GUI_QUERY_REPLACE_SCALE: + if (p->data[i.y][5+pos*2]>=0) { + p->data[i.y][5+pos*2]=(p->data[i.y][5+pos*2]*queryReplaceEffectVal[j])/100; + if (p->data[i.y][5+pos*2]<0) p->data[i.y][5+pos*2]=0; + if (p->data[i.y][5+pos*2]>255) p->data[i.y][5+pos*2]=255; + } + break; case GUI_QUERY_REPLACE_CLEAR: p->data[i.y][5+pos*2]=-1; break; @@ -562,11 +593,11 @@ void FurnaceGUI::drawFindReplace() { i.note=0; } if (i.note==130) { - snprintf(tempID,1024,"REL"); + snprintf(tempID,1024,"%s##MREL",macroRelLabel); } else if (i.note==129) { - snprintf(tempID,1024,"==="); + snprintf(tempID,1024,"%s##NREL",noteRelLabel); } else if (i.note==128) { - snprintf(tempID,1024,"OFF"); + snprintf(tempID,1024,"%s##NOFF",noteOffLabel); } else if (i.note>=-60 && i.note<120) { snprintf(tempID,1024,"%s",noteNames[i.note+60]); } else { @@ -582,13 +613,13 @@ void FurnaceGUI::drawFindReplace() { } } if (i.noteMode!=GUI_QUERY_RANGE && i.noteMode!=GUI_QUERY_RANGE_NOT) { - if (ImGui::Selectable("OFF",i.note==128)) { + if (ImGui::Selectable(noteOffLabel,i.note==128)) { i.note=128; } - if (ImGui::Selectable("===",i.note==129)) { + if (ImGui::Selectable(noteRelLabel,i.note==129)) { i.note=129; } - if (ImGui::Selectable("REL",i.note==130)) { + if (ImGui::Selectable(macroRelLabel,i.note==130)) { i.note=130; } } @@ -885,11 +916,11 @@ void FurnaceGUI::drawFindReplace() { ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (queryReplaceNoteMode==GUI_QUERY_REPLACE_SET) { if (queryReplaceNote==130) { - snprintf(tempID,1024,"REL"); + snprintf(tempID,1024,"%s##MREL",macroRelLabel); } else if (queryReplaceNote==129) { - snprintf(tempID,1024,"==="); + snprintf(tempID,1024,"%s##NREL",noteRelLabel); } else if (queryReplaceNote==128) { - snprintf(tempID,1024,"OFF"); + snprintf(tempID,1024,"%s##NOFF",noteOffLabel); } else if (queryReplaceNote>=-60 && queryReplaceNote<120) { snprintf(tempID,1024,"%s",noteNames[queryReplaceNote+60]); } else { @@ -903,13 +934,13 @@ void FurnaceGUI::drawFindReplace() { queryReplaceNote=j-60; } } - if (ImGui::Selectable("OFF",queryReplaceNote==128)) { + if (ImGui::Selectable(noteOffLabel,queryReplaceNote==128)) { queryReplaceNote=128; } - if (ImGui::Selectable("===",queryReplaceNote==129)) { + if (ImGui::Selectable(noteRelLabel,queryReplaceNote==129)) { queryReplaceNote=129; } - if (ImGui::Selectable("REL",queryReplaceNote==130)) { + if (ImGui::Selectable(macroRelLabel,queryReplaceNote==130)) { queryReplaceNote=130; } ImGui::EndCombo(); @@ -919,6 +950,8 @@ void FurnaceGUI::drawFindReplace() { if (queryReplaceNote<-180) queryReplaceNote=-180; if (queryReplaceNote>180) queryReplaceNote=180; } + } else if (queryReplaceNoteMode==GUI_QUERY_REPLACE_SCALE) { + ImGui::Text("INVALID"); } ImGui::EndDisabled(); @@ -941,6 +974,13 @@ void FurnaceGUI::drawFindReplace() { if (queryReplaceIns<-255) queryReplaceIns=-255; if (queryReplaceIns>255) queryReplaceIns=255; } + } else if (queryReplaceInsMode==GUI_QUERY_REPLACE_SCALE) { + if (queryReplaceIns<0) queryReplaceIns=0; + if (queryReplaceIns>400) queryReplaceIns=400; + if (ImGui::InputInt("##IRValue",&queryReplaceIns,1,12)) { + if (queryReplaceIns<0) queryReplaceIns=0; + if (queryReplaceIns>400) queryReplaceIns=400; + } } ImGui::EndDisabled(); @@ -963,6 +1003,13 @@ void FurnaceGUI::drawFindReplace() { if (queryReplaceVol<-255) queryReplaceVol=-255; if (queryReplaceVol>255) queryReplaceVol=255; } + } else if (queryReplaceVolMode==GUI_QUERY_REPLACE_SCALE) { + if (queryReplaceVol<0) queryReplaceVol=0; + if (queryReplaceVol>400) queryReplaceVol=400; + if (ImGui::InputInt("##VRValue",&queryReplaceVol,1,12)) { + if (queryReplaceVol<0) queryReplaceVol=0; + if (queryReplaceVol>400) queryReplaceVol=400; + } } ImGui::EndDisabled(); @@ -987,6 +1034,13 @@ void FurnaceGUI::drawFindReplace() { if (queryReplaceEffect[i]<-255) queryReplaceEffect[i]=-255; if (queryReplaceEffect[i]>255) queryReplaceEffect[i]=255; } + } else if (queryReplaceEffectMode[i]==GUI_QUERY_REPLACE_SCALE) { + if (queryReplaceEffect[i]<0) queryReplaceEffect[i]=0; + if (queryReplaceEffect[i]>400) queryReplaceEffect[i]=400; + if (ImGui::InputInt("##ERValue",&queryReplaceEffect[i],1,12)) { + if (queryReplaceEffect[i]<0) queryReplaceEffect[i]=0; + if (queryReplaceEffect[i]>400) queryReplaceEffect[i]=400; + } } ImGui::EndDisabled(); @@ -1009,10 +1063,16 @@ void FurnaceGUI::drawFindReplace() { if (queryReplaceEffectVal[i]<-255) queryReplaceEffectVal[i]=-255; if (queryReplaceEffectVal[i]>255) queryReplaceEffectVal[i]=255; } + } else if (queryReplaceEffectValMode[i]==GUI_QUERY_REPLACE_SCALE) { + if (queryReplaceEffectVal[i]<0) queryReplaceEffectVal[i]=0; + if (queryReplaceEffectVal[i]>400) queryReplaceEffectVal[i]=400; + if (ImGui::InputInt("##ERValueV",&queryReplaceEffectVal[i],1,12)) { + if (queryReplaceEffectVal[i]<0) queryReplaceEffectVal[i]=0; + if (queryReplaceEffectVal[i]>400) queryReplaceEffectVal[i]=400; + } } ImGui::EndDisabled(); - ImGui::PopID(); } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 773d6f7fe..3fcfed8eb 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -84,13 +84,13 @@ void FurnaceGUI::bindEngine(DivEngine* eng) { const char* FurnaceGUI::noteName(short note, short octave) { if (note==100) { - return "OFF"; + return noteOffLabel; } else if (note==101) { // note off and envelope release - return "==="; + return noteRelLabel; } else if (note==102) { // envelope release only - return "REL"; + return macroRelLabel; } else if (octave==0 && note==0) { - return "..."; + return emptyLabel; } else if (note==0 && octave!=0) { return "BUG"; } @@ -551,7 +551,9 @@ void FurnaceGUI::updateWindowTitle() { } if (settings.titleBarSys) { - title+=fmt::sprintf(" (%s)",e->getSongSystemName(!settings.noMultiSystem)); + if (e->song.systemName!="") { + title+=fmt::sprintf(" (%s)",e->song.systemName); + } } if (sdlWin!=NULL) SDL_SetWindowTitle(sdlWin,title.c_str()); @@ -1224,9 +1226,9 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); hasOpened=fileDialog->openLoad( "Open File", - {"compatible files", "*.fur *.dmf *.mod", + {"compatible files", "*.fur *.dmf *.mod *.fc13 *.fc14 *.smod", "all files", ".*"}, - "compatible files{.fur,.dmf,.mod},.*", + "compatible files{.fur,.dmf,.mod,.fc13,.fc14,.smod},.*", workingDirSong, dpiScale ); @@ -1265,6 +1267,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Load Instrument", // TODO supply loadable formats in a dynamic, scalable, "DRY" way. + // thank the author of IGFD for making things impossible {"all compatible files", "*.fui *.dmp *.tfi *.vgi *.s3i *.sbi *.opli *.opni *.y12 *.bnk *.ff *.gyb *.opm *.wopl *.wopn", "Furnace instrument", "*.fui", "DefleMask preset", "*.dmp", @@ -1311,13 +1314,15 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); hasOpened=fileDialog->openSave( "Save Instrument", - {"Furnace instrument", "*.fui"}, - "Furnace instrument{.fui}", + {"Furnace instrument", "*.fui", + "DefleMask preset", "*.dmp"}, + "Furnace instrument{.fui},DefleMask preset{.dmp}", workingDirIns, dpiScale ); break; case GUI_FILE_WAVE_OPEN: + case GUI_FILE_WAVE_OPEN_REPLACE: if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); hasOpened=fileDialog->openLoad( "Load Wavetable", @@ -1332,13 +1337,16 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); hasOpened=fileDialog->openSave( "Save Wavetable", - {"Furnace wavetable", ".fuw"}, - "Furnace wavetable{.fuw}", + {"Furnace wavetable", ".fuw", + "DefleMask wavetable", ".dmw", + "raw data", ".raw"}, + "Furnace wavetable{.fuw},DefleMask wavetable{.dmw},raw data{.raw}", workingDirWave, dpiScale ); break; case GUI_FILE_SAMPLE_OPEN: + case GUI_FILE_SAMPLE_OPEN_REPLACE: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); hasOpened=fileDialog->openLoad( "Load Sample", @@ -1349,6 +1357,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_SAMPLE_OPEN_RAW: + case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW: + if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Load Raw Sample", + {"all files", ".*"}, + ".*", + workingDirSample, + dpiScale + ); + break; case GUI_FILE_SAMPLE_SAVE: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); hasOpened=fileDialog->openSave( @@ -1409,6 +1428,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_EXPORT_CMDSTREAM: + if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Command Stream", + {"text file", "*.txt", + "binary file", "*.bin"}, + "text file{.txt},binary file{.bin}", + workingDirROMExport, + dpiScale + ); + break; case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; @@ -1505,6 +1535,43 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_TEST_OPEN: + if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Open Test", + {"compatible files", "*.fur *.dmf *.mod", + "another option", "*.wav *.ttf", + "all files", ".*"}, + "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*", + workingDirTest, + dpiScale + ); + break; + case GUI_FILE_TEST_OPEN_MULTI: + if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Open Test (Multi)", + {"compatible files", "*.fur *.dmf *.mod", + "another option", "*.wav *.ttf", + "all files", ".*"}, + "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*", + workingDirTest, + dpiScale, + NULL, + true + ); + break; + case GUI_FILE_TEST_SAVE: + if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Test", + {"Furnace song", "*.fur", + "DefleMask module", "*.dmf"}, + "Furnace song{.fur},DefleMask module{.dmf}", + workingDirTest, + dpiScale + ); + break; } if (hasOpened) curFileDialog=type; //ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NavEnableKeyboard; @@ -1839,7 +1906,7 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { #define sysAddOption(x) \ if (ImGui::MenuItem(getSystemName(x))) { \ if (!e->addSystem(x)) { \ - showError("cannot add system! ("+e->getLastError()+")"); \ + showError("cannot add chip! ("+e->getLastError()+")"); \ } \ updateWindowTitle(); \ } @@ -1868,6 +1935,15 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { fileName+=fallback; \ } +#define checkExtensionTriple(x,y,z,fallback) \ + String lowerCase=fileName; \ + for (char& i: lowerCase) { \ + if (i>='A' && i<='Z') i+='a'-'A'; \ + } \ + if (lowerCase.size()<4 || (lowerCase.rfind(x)!=lowerCase.size()-4 && lowerCase.rfind(y)!=lowerCase.size()-4 && lowerCase.rfind(z)!=lowerCase.size()-4)) { \ + fileName+=fallback; \ + } + #define drawOpMask(m) \ ImGui::PushFont(patFont); \ ImGui::PushID("om_" #m); \ @@ -2452,7 +2528,13 @@ void FurnaceGUI::processPoint(SDL_Event& ev) { } bool FurnaceGUI::loop() { - SDL_SetEventFilter(_processEvent,this); + bool doThreadedInput=!settings.noThreadedInput; + if (doThreadedInput) { + logD("key input: event filter"); + SDL_SetEventFilter(_processEvent,this); + } else { + logD("key input: main thread"); + } while (!quit) { SDL_Event ev; @@ -2468,6 +2550,7 @@ bool FurnaceGUI::loop() { WAKE_UP; ImGui_ImplSDL2_ProcessEvent(&ev); processPoint(ev); + if (!doThreadedInput) processEvent(&ev); switch (ev.type) { case SDL_MOUSEMOTION: { int motionX=ev.motion.x; @@ -2583,6 +2666,8 @@ bool FurnaceGUI::loop() { case SDL_DROPFILE: if (ev.drop.file!=NULL) { std::vector instruments=e->instrumentFromFile(ev.drop.file); + DivWavetable* droppedWave=NULL; + DivSample* droppedSample=NULL;; if (!instruments.empty()) { if (!e->getWarnings().empty()) { showWarning(e->getWarnings(),GUI_WARN_GENERIC); @@ -2592,10 +2677,12 @@ bool FurnaceGUI::loop() { } nextWindow=GUI_WINDOW_INS_LIST; MARK_MODIFIED; - } else if (e->addWaveFromFile(ev.drop.file,false)) { + } else if ((droppedWave=e->waveFromFile(ev.drop.file,false))!=NULL) { + e->addWavePtr(droppedWave); nextWindow=GUI_WINDOW_WAVE_LIST; MARK_MODIFIED; - } else if (e->addSampleFromFile(ev.drop.file)!=-1) { + } else if ((droppedSample=e->sampleFromFile(ev.drop.file))!=NULL) { + e->addSamplePtr(droppedSample); nextWindow=GUI_WINDOW_SAMPLE_LIST; MARK_MODIFIED; } else if (modified) { @@ -2840,7 +2927,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("one file")) { openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE); } - if (ImGui::MenuItem("multiple files (one per system)")) { + if (ImGui::MenuItem("multiple files (one per chip)")) { openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS); } if (ImGui::MenuItem("multiple files (one per channel)")) { @@ -2865,7 +2952,23 @@ bool FurnaceGUI::loop() { ImGui::EndCombo(); } ImGui::Checkbox("loop",&vgmExportLoop); - ImGui::Text("systems to export:"); + ImGui::Checkbox("add pattern change hints",&vgmExportPatternHints); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "inserts data blocks on pattern changes.\n" + "useful if you are writing a playback routine.\n\n" + + "the format of a pattern change data block is:\n" + "67 66 FE ll ll ll ll 01 oo rr pp pp pp ...\n" + "- ll: length, a 32-bit little-endian number\n" + "- oo: order\n" + "- rr: initial row (a 0Dxx effect is able to select a different row)\n" + "- pp: pattern index (one per channel)\n\n" + + "pattern indexes are ordered as they appear in the song." + ); + } + ImGui::Text("chips to export:"); bool hasOneAtLeast=false; for (int i=0; isong.systemLen; i++) { int minVersion=e->minVGMVersion(e->song.system[i]); @@ -2874,17 +2977,17 @@ bool FurnaceGUI::loop() { ImGui::EndDisabled(); if (minVersion>vgmExportVersion) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { - ImGui::SetTooltip("this system is only available in VGM %d.%.2x and higher!",minVersion>>8,minVersion&0xff); + ImGui::SetTooltip("this chip is only available in VGM %d.%.2x and higher!",minVersion>>8,minVersion&0xff); } } else if (minVersion==0) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { - ImGui::SetTooltip("this system is not supported by the VGM format!"); + ImGui::SetTooltip("this chip is not supported by the VGM format!"); } } else { if (willExport[i]) hasOneAtLeast=true; } } - ImGui::Text("select the systems you wish to export,"); + ImGui::Text("select the chip you wish to export,"); ImGui::Text("but only up to %d of each type.",(vgmExportVersion>=0x151)?2:1); if (hasOneAtLeast) { if (ImGui::MenuItem("click to export")) { @@ -2915,15 +3018,28 @@ bool FurnaceGUI::loop() { ImGui::EndMenu(); } } + if (ImGui::BeginMenu("export command stream...")) { + ImGui::Text( + "this option exports a text or binary file which\n" + "contains a dump of the internal command stream\n" + "produced when playing the song.\n\n" + + "technical/development use only!" + ); + if (ImGui::Button("export")) { + openFileDialog(GUI_FILE_EXPORT_CMDSTREAM); + } + ImGui::EndMenu(); + } ImGui::Separator(); - if (ImGui::BeginMenu("add system...")) { + if (ImGui::BeginMenu("add chip...")) { for (int j=0; availableSystems[j]; j++) { if (!settings.hiddenSystems && (availableSystems[j]==DIV_SYSTEM_YMU759 || availableSystems[j]==DIV_SYSTEM_DUMMY)) continue; sysAddOption((DivSystem)availableSystems[j]); } ImGui::EndMenu(); } - if (ImGui::BeginMenu("configure system...")) { + if (ImGui::BeginMenu("configure chip...")) { for (int i=0; isong.systemLen; i++) { if (ImGui::TreeNode(fmt::sprintf("%d. %s##_SYSP%d",i+1,getSystemName(e->song.system[i]),i).c_str())) { drawSysConf(i,e->song.system[i],e->song.systemFlags[i],true); @@ -2932,7 +3048,7 @@ bool FurnaceGUI::loop() { } ImGui::EndMenu(); } - if (ImGui::BeginMenu("change system...")) { + if (ImGui::BeginMenu("change chip...")) { ImGui::Checkbox("Preserve channel positions",&preserveChanPos); for (int i=0; isong.systemLen; i++) { if (ImGui::BeginMenu(fmt::sprintf("%d. %s##_SYSC%d",i+1,getSystemName(e->song.system[i]),i).c_str())) { @@ -2945,12 +3061,12 @@ bool FurnaceGUI::loop() { } ImGui::EndMenu(); } - if (ImGui::BeginMenu("remove system...")) { + if (ImGui::BeginMenu("remove chip...")) { ImGui::Checkbox("Preserve channel positions",&preserveChanPos); for (int i=0; isong.systemLen; i++) { if (ImGui::MenuItem(fmt::sprintf("%d. %s##_SYSR%d",i+1,getSystemName(e->song.system[i]),i).c_str())) { if (!e->removeSystem(i,preserveChanPos)) { - showError("cannot remove system! ("+e->getLastError()+")"); + showError("cannot remove chip! ("+e->getLastError()+")"); } } } @@ -3021,6 +3137,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("pattern",BIND_FOR(GUI_ACTION_WINDOW_PATTERN),patternOpen)) patternOpen=!patternOpen; if (ImGui::MenuItem("mixer",BIND_FOR(GUI_ACTION_WINDOW_MIXER),mixerOpen)) mixerOpen=!mixerOpen; if (ImGui::MenuItem("channels",BIND_FOR(GUI_ACTION_WINDOW_CHANNELS),channelsOpen)) channelsOpen=!channelsOpen; + if (ImGui::MenuItem("pattern manager",BIND_FOR(GUI_ACTION_WINDOW_PAT_MANAGER),patManagerOpen)) patManagerOpen=!patManagerOpen; if (ImGui::MenuItem("compatibility flags",BIND_FOR(GUI_ACTION_WINDOW_COMPAT_FLAGS),compatFlagsOpen)) compatFlagsOpen=!compatFlagsOpen; if (ImGui::MenuItem("song comments",BIND_FOR(GUI_ACTION_WINDOW_NOTES),notesOpen)) notesOpen=!notesOpen; ImGui::Separator(); @@ -3037,7 +3154,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("log viewer",BIND_FOR(GUI_ACTION_WINDOW_LOG),logOpen)) logOpen=!logOpen; if (ImGui::MenuItem("statistics",BIND_FOR(GUI_ACTION_WINDOW_STATS),statsOpen)) statsOpen=!statsOpen; if (spoilerOpen) if (ImGui::MenuItem("spoiler",NULL,spoilerOpen)) spoilerOpen=!spoilerOpen; - + ImGui::EndMenu(); } if (ImGui::BeginMenu("help")) { @@ -3155,6 +3272,7 @@ bool FurnaceGUI::loop() { drawPiano(); drawNotes(); drawChannels(); + drawPatManager(); drawRegView(); drawLog(); drawEffectList(); @@ -3213,10 +3331,14 @@ bool FurnaceGUI::loop() { workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_WAVE_OPEN: + case GUI_FILE_WAVE_OPEN_REPLACE: case GUI_FILE_WAVE_SAVE: workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_SAMPLE_OPEN: + case GUI_FILE_SAMPLE_OPEN_RAW: + case GUI_FILE_SAMPLE_OPEN_REPLACE: + case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW: case GUI_FILE_SAMPLE_SAVE: workingDirSample=fileDialog->getPath()+DIR_SEPARATOR_STR; break; @@ -3226,12 +3348,15 @@ bool FurnaceGUI::loop() { workingDirAudioExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_VGM: - case GUI_FILE_EXPORT_ROM: workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_EXPORT_ZSM: workingDirZSMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_EXPORT_ROM: + case GUI_FILE_EXPORT_CMDSTREAM: + workingDirROMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; case GUI_FILE_LOAD_MAIN_FONT: case GUI_FILE_LOAD_PAT_FONT: workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR; @@ -3253,9 +3378,25 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_TEST_OPEN: + case GUI_FILE_TEST_OPEN_MULTI: + case GUI_FILE_TEST_SAVE: + workingDirTest=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + } + if (fileDialog->isError()) { +#if defined(_WIN32) || defined(__APPLE__) + showError("there was an error in the file dialog! you may want to report this issue to:\nhttps://github.com/tildearrow/furnace/issues\ncheck the Log Viewer (window > log viewer) for more information.\n\nfor now please disable the system file picker in Settings > General."); +#else + showError("Zenity/KDialog not available!\nplease install one of these, or disable the system file picker in Settings > General."); +#endif } if (fileDialog->accepted()) { - fileName=fileDialog->getFileName(); + if (fileDialog->getFileName().empty()) { + fileName=""; + } else { + fileName=fileDialog->getFileName()[0]; + } if (fileName!="") { if (curFileDialog==GUI_FILE_SAVE) { // we can't tell whether the user chose .dmf or .fur in the system file picker @@ -3272,10 +3413,21 @@ bool FurnaceGUI::loop() { checkExtension(".wav"); } if (curFileDialog==GUI_FILE_INS_SAVE) { - checkExtension(".fui"); + // we can't tell whether the user chose .fui or .dmp in the system file picker + const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace instrument")?".fui":".dmp"; + checkExtensionDual(".fui",".dmp",fallbackExt); } if (curFileDialog==GUI_FILE_WAVE_SAVE) { - checkExtension(".fuw"); + // same thing here + const char* fallbackExt=".fuw"; + if (!settings.sysFileDialog) { + if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="raw data") { + fallbackExt=".raw"; + } else if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="DefleMask wavetable") { + fallbackExt=".dmw"; + } + } + checkExtensionTriple(".fuw",".dmw",".raw",fallbackExt); } if (curFileDialog==GUI_FILE_EXPORT_VGM) { checkExtension(".vgm"); @@ -3283,6 +3435,11 @@ bool FurnaceGUI::loop() { if (curFileDialog==GUI_FILE_EXPORT_ZSM) { checkExtension(".zsm"); } + if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM) { + // we can't tell whether the user chose .txt or .bin in the system file picker + const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="text file")?".txt":".bin"; + checkExtensionDual(".txt",".bin",fallbackExt); + } if (curFileDialog==GUI_FILE_EXPORT_COLORS) { checkExtension(".cfgc"); } @@ -3356,21 +3513,75 @@ bool FurnaceGUI::loop() { break; case GUI_FILE_INS_SAVE: if (curIns>=0 && curIns<(int)e->song.ins.size()) { - e->song.ins[curIns]->save(copyOfName.c_str()); + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + if ((lowerCase.size()<4 || lowerCase.rfind(".dmp")!=lowerCase.size()-4)) { + e->song.ins[curIns]->save(copyOfName.c_str()); + } else { + if (!e->song.ins[curIns]->saveDMP(copyOfName.c_str())) { + showError("error while saving instrument! make sure your instrument is compatible."); + } + } } break; case GUI_FILE_WAVE_SAVE: if (curWave>=0 && curWave<(int)e->song.wave.size()) { - e->song.wave[curWave]->save(copyOfName.c_str()); + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + if (lowerCase.size()<4) { + e->song.wave[curWave]->save(copyOfName.c_str()); + } else if (lowerCase.rfind(".dmw")==lowerCase.size()-4) { + e->song.wave[curWave]->saveDMW(copyOfName.c_str()); + } else if (lowerCase.rfind(".raw")==lowerCase.size()-4) { + e->song.wave[curWave]->saveRaw(copyOfName.c_str()); + } else { + e->song.wave[curWave]->save(copyOfName.c_str()); + } } break; - case GUI_FILE_SAMPLE_OPEN: - if (e->addSampleFromFile(copyOfName.c_str())==-1) { + case GUI_FILE_SAMPLE_OPEN: { + DivSample* s=e->sampleFromFile(copyOfName.c_str()); + if (s==NULL) { showError(e->getLastError()); } else { - MARK_MODIFIED; + if (e->addSamplePtr(s)==-1) { + showError(e->getLastError()); + } else { + MARK_MODIFIED; + } } break; + } + case GUI_FILE_SAMPLE_OPEN_REPLACE: { + DivSample* s=e->sampleFromFile(copyOfName.c_str()); + if (s==NULL) { + showError(e->getLastError()); + } else { + if (curSample>=0 && curSample<(int)e->song.sample.size()) { + e->lockEngine([this,s]() { + // if it crashes here please tell me... + DivSample* oldSample=e->song.sample[curSample]; + e->song.sample[curSample]=s; + delete oldSample; + e->renderSamples(); + MARK_MODIFIED; + }); + } else { + showError("...but you haven't selected a sample!"); + delete s; + } + } + break; + } + case GUI_FILE_SAMPLE_OPEN_RAW: + case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW: + pendingRawSample=copyOfName; + displayPendingRawSample=true; + break; case GUI_FILE_SAMPLE_SAVE: if (curSample>=0 && curSample<(int)e->song.sample.size()) { e->song.sample[curSample]->save(copyOfName.c_str()); @@ -3434,15 +3645,38 @@ bool FurnaceGUI::loop() { } break; } - case GUI_FILE_WAVE_OPEN: - if (!e->addWaveFromFile(copyOfName.c_str())) { + case GUI_FILE_WAVE_OPEN: { + DivWavetable* wave=e->waveFromFile(copyOfName.c_str()); + if (wave==NULL) { showError("cannot load wavetable! ("+e->getLastError()+")"); } else { - MARK_MODIFIED; + if (e->addWavePtr(wave)==-1) { + showError("cannot load wavetable! ("+e->getLastError()+")"); + } else { + MARK_MODIFIED; + } } break; + } + case GUI_FILE_WAVE_OPEN_REPLACE: { + DivWavetable* wave=e->waveFromFile(copyOfName.c_str()); + if (wave==NULL) { + showError("cannot load wavetable! ("+e->getLastError()+")"); + } else { + if (curWave>=0 && curWave<(int)e->song.wave.size()) { + e->lockEngine([this,wave]() { + *e->song.wave[curWave]=*wave; + MARK_MODIFIED; + }); + } else { + showError("...but you haven't selected a wavetable!"); + } + delete wave; + } + break; + } case GUI_FILE_EXPORT_VGM: { - SafeWriter* w=e->saveVGM(willExport,vgmExportLoop,vgmExportVersion); + SafeWriter* w=e->saveVGM(willExport,vgmExportLoop,vgmExportVersion,vgmExportPatternHints); if (w!=NULL) { FILE* f=ps_fopen(copyOfName.c_str(),"wb"); if (f!=NULL) { @@ -3484,6 +3718,35 @@ bool FurnaceGUI::loop() { case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; + case GUI_FILE_EXPORT_CMDSTREAM: { + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + bool isBinary=true; + if ((lowerCase.size()<4 || lowerCase.rfind(".bin")!=lowerCase.size()-4)) { + isBinary=false; + } + + SafeWriter* w=e->saveCommand(isBinary); + if (w!=NULL) { + FILE* f=ps_fopen(copyOfName.c_str(),"wb"); + if (f!=NULL) { + fwrite(w->getFinalBuf(),1,w->size(),f); + fclose(f); + } else { + showError("could not open file!"); + } + w->finish(); + delete w; + if (!e->getWarnings().empty()) { + showWarning(e->getWarnings(),GUI_WARN_GENERIC); + } + } else { + showError(fmt::sprintf("could not write command stream! (%s)",e->getLastError())); + } + break; + } case GUI_FILE_LOAD_MAIN_FONT: settings.mainFontPath=copyOfName; break; @@ -3517,6 +3780,20 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: settings.mu5Path=copyOfName; break; + case GUI_FILE_TEST_OPEN: + showWarning(fmt::sprintf("You opened: %s",copyOfName),GUI_WARN_GENERIC); + break; + case GUI_FILE_TEST_OPEN_MULTI: { + String msg="You opened:"; + for (String i: fileDialog->getFileName()) { + msg+=fmt::sprintf("\n- %s",i); + } + showWarning(msg,GUI_WARN_GENERIC); + break; + } + case GUI_FILE_TEST_SAVE: + showWarning(fmt::sprintf("You saved: %s",copyOfName),GUI_WARN_GENERIC); + break; } curFileDialog=GUI_FILE_OPEN; } @@ -3544,6 +3821,11 @@ bool FurnaceGUI::loop() { ImGui::OpenPopup("Select Instrument"); } + if (displayPendingRawSample) { + displayPendingRawSample=false; + ImGui::OpenPopup("Import Raw Sample"); + } + if (displayExporting) { displayExporting=false; ImGui::OpenPopup("Rendering..."); @@ -3974,6 +4256,53 @@ bool FurnaceGUI::loop() { ImGui::EndPopup(); } + if (ImGui::BeginPopupModal("Import Raw Sample",NULL,ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Data type:"); + for (int i=0; isampleFromFileRaw(pendingRawSample.c_str(),(DivSampleDepth)pendingRawSampleDepth,pendingRawSampleChannels,pendingRawSampleBigEndian,pendingRawSampleUnsigned); + if (s==NULL) { + showError(e->getLastError()); + } else { + if (e->addSamplePtr(s)==-1) { + showError(e->getLastError()); + } else { + MARK_MODIFIED; + } + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + layoutTimeEnd=SDL_GetPerformanceCounter(); // backup trigger @@ -4064,10 +4393,12 @@ bool FurnaceGUI::init() { workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir); workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir); workingDirZSMExport=e->getConfString("lastDirZSMExport",workingDir); + workingDirROMExport=e->getConfString("lastDirROMExport",workingDir); workingDirFont=e->getConfString("lastDirFont",workingDir); workingDirColors=e->getConfString("lastDirColors",workingDir); workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir); workingDirLayout=e->getConfString("lastDirLayout",workingDir); + workingDirTest=e->getConfString("lastDirTest",workingDir); editControlsOpen=e->getConfBool("editControlsOpen",true); ordersOpen=e->getConfBool("ordersOpen",true); @@ -4089,6 +4420,7 @@ bool FurnaceGUI::init() { pianoOpen=e->getConfBool("pianoOpen",false); notesOpen=e->getConfBool("notesOpen",false); channelsOpen=e->getConfBool("channelsOpen",false); + patManagerOpen=e->getConfBool("patManagerOpen",false); regViewOpen=e->getConfBool("regViewOpen",false); logOpen=e->getConfBool("logOpen",false); effectListOpen=e->getConfBool("effectListOpen",false); @@ -4098,6 +4430,8 @@ bool FurnaceGUI::init() { tempoView=e->getConfBool("tempoView",true); waveHex=e->getConfBool("waveHex",false); + waveGenVisible=e->getConfBool("waveGenVisible",false); + waveEditStyle=e->getConfInt("waveEditStyle",0); lockLayout=e->getConfBool("lockLayout",false); #ifdef IS_MOBILE fullScreen=true; @@ -4302,10 +4636,12 @@ bool FurnaceGUI::finish() { e->setConf("lastDirAudioExport",workingDirAudioExport); e->setConf("lastDirVGMExport",workingDirVGMExport); e->setConf("lastDirZSMExport",workingDirZSMExport); + e->setConf("lastDirROMExport",workingDirROMExport); e->setConf("lastDirFont",workingDirFont); e->setConf("lastDirColors",workingDirColors); e->setConf("lastDirKeybinds",workingDirKeybinds); e->setConf("lastDirLayout",workingDirLayout); + e->setConf("lastDirTest",workingDirTest); // commit last open windows e->setConf("editControlsOpen",editControlsOpen); @@ -4328,6 +4664,7 @@ bool FurnaceGUI::finish() { e->setConf("pianoOpen",pianoOpen); e->setConf("notesOpen",notesOpen); e->setConf("channelsOpen",channelsOpen); + e->setConf("patManagerOpen",patManagerOpen); e->setConf("regViewOpen",regViewOpen); e->setConf("logOpen",logOpen); e->setConf("effectListOpen",effectListOpen); @@ -4341,6 +4678,8 @@ bool FurnaceGUI::finish() { e->setConf("tempoView",tempoView); e->setConf("waveHex",waveHex); + e->setConf("waveGenVisible",waveGenVisible); + e->setConf("waveEditStyle",waveEditStyle); e->setConf("lockLayout",lockLayout); e->setConf("fullScreen",fullScreen); e->setConf("mobileUI",mobileUI); @@ -4408,6 +4747,7 @@ FurnaceGUI::FurnaceGUI(): displayExporting(false), vgmExportLoop(true), zsmExportLoop(true), + vgmExportPatternHints(false), wantCaptureKeyboard(false), oldWantCaptureKeyboard(false), displayMacroMenu(false), @@ -4418,10 +4758,16 @@ FurnaceGUI::FurnaceGUI(): noteInputPoly(true), displayPendingIns(false), pendingInsSingle(false), + displayPendingRawSample(false), vgmExportVersion(0x171), drawHalt(10), zsmExportTickRate(60), macroPointSize(16), + waveEditStyle(0), + pendingRawSampleDepth(8), + pendingRawSampleChannels(1), + pendingRawSampleUnsigned(false), + pendingRawSampleBigEndian(false), globalWinFlags(0), curFileDialog(GUI_FILE_OPEN), warnAction(GUI_WARN_OPEN), @@ -4500,6 +4846,7 @@ FurnaceGUI::FurnaceGUI(): subSongsOpen(true), findOpen(false), spoilerOpen(false), + patManagerOpen(false), selecting(false), selectingFull(false), dragging(false), @@ -4516,6 +4863,7 @@ FurnaceGUI::FurnaceGUI(): firstFrame(true), tempoView(true), waveHex(false), + waveGenVisible(false), lockLayout(false), editOptsVisible(false), latchNibble(false), @@ -4692,7 +5040,12 @@ FurnaceGUI::FurnaceGUI(): pianoView(0), pianoInputPadMode(0), #endif - hasACED(false) { + hasACED(false), + waveGenBaseShape(0), + waveGenDuty(0.5f), + waveGenPower(1), + waveGenInvertPoint(1.0f), + waveGenFM(false) { // value keys valueKeys[SDLK_0]=0; valueKeys[SDLK_1]=1; @@ -4754,6 +5107,20 @@ FurnaceGUI::FurnaceGUI(): memset(acedData,0,23); + memset(waveGenAmp,0,sizeof(float)*16); + memset(waveGenPhase,0,sizeof(float)*16); + memset(waveGenTL,0,sizeof(float)*4); + memset(waveGenMult,0,sizeof(int)*4); + memset(waveGenFB,0,sizeof(float)*4); + memset(waveGenFMCon1,0,sizeof(bool)*4); + memset(waveGenFMCon2,0,sizeof(bool)*3); + memset(waveGenFMCon3,0,sizeof(bool)*2); + + waveGenAmp[0]=1.0f; + waveGenFMCon1[0]=true; + waveGenFMCon2[0]=true; + waveGenFMCon3[0]=true; + memset(pianoKeyHit,0,sizeof(float)*180); memset(pianoKeyPressed,0,sizeof(bool)*180); @@ -4765,4 +5132,16 @@ FurnaceGUI::FurnaceGUI(): memset(queryReplaceEffectValDo,0,sizeof(bool)*8); chanOscGrad.bgColor=ImVec4(0.0f,0.0f,0.0f,1.0f); + + memset(noteOffLabel,0,32); + memset(noteRelLabel,0,32); + memset(macroRelLabel,0,32); + memset(emptyLabel,0,32); + memset(emptyLabel2,0,32); + + strncpy(noteOffLabel,"OFF",32); + strncpy(noteRelLabel,"===",32); + strncpy(macroRelLabel,"REL",32); + strncpy(emptyLabel,"...",32); + strncpy(emptyLabel2,"..",32); } diff --git a/src/gui/gui.h b/src/gui/gui.h index 30f059d25..8e0c791a7 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -201,6 +201,13 @@ enum FurnaceGUIColors { GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY, GUI_COLOR_PATTERN_EFFECT_MISC, + GUI_COLOR_PAT_MANAGER_NULL, + GUI_COLOR_PAT_MANAGER_USED, + GUI_COLOR_PAT_MANAGER_OVERUSED, + GUI_COLOR_PAT_MANAGER_EXTREMELY_OVERUSED, + GUI_COLOR_PAT_MANAGER_COMBO_BREAKER, + GUI_COLOR_PAT_MANAGER_UNUSED, + GUI_COLOR_PIANO_BACKGROUND, GUI_COLOR_PIANO_KEY_BOTTOM, GUI_COLOR_PIANO_KEY_TOP, @@ -243,6 +250,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_PIANO, GUI_WINDOW_NOTES, GUI_WINDOW_CHANNELS, + GUI_WINDOW_PAT_MANAGER, GUI_WINDOW_REGISTER_VIEW, GUI_WINDOW_LOG, GUI_WINDOW_EFFECT_LIST, @@ -260,14 +268,19 @@ enum FurnaceGUIFileDialogs { GUI_FILE_INS_OPEN_REPLACE, GUI_FILE_INS_SAVE, GUI_FILE_WAVE_OPEN, + GUI_FILE_WAVE_OPEN_REPLACE, GUI_FILE_WAVE_SAVE, GUI_FILE_SAMPLE_OPEN, + GUI_FILE_SAMPLE_OPEN_RAW, + GUI_FILE_SAMPLE_OPEN_REPLACE, + GUI_FILE_SAMPLE_OPEN_REPLACE_RAW, GUI_FILE_SAMPLE_SAVE, GUI_FILE_EXPORT_AUDIO_ONE, GUI_FILE_EXPORT_AUDIO_PER_SYS, GUI_FILE_EXPORT_AUDIO_PER_CHANNEL, GUI_FILE_EXPORT_VGM, GUI_FILE_EXPORT_ZSM, + GUI_FILE_EXPORT_CMDSTREAM, GUI_FILE_EXPORT_ROM, GUI_FILE_LOAD_MAIN_FONT, GUI_FILE_LOAD_PAT_FONT, @@ -279,7 +292,11 @@ enum FurnaceGUIFileDialogs { GUI_FILE_EXPORT_LAYOUT, GUI_FILE_YRW801_ROM_OPEN, GUI_FILE_TG100_ROM_OPEN, - GUI_FILE_MU5_ROM_OPEN + GUI_FILE_MU5_ROM_OPEN, + + GUI_FILE_TEST_OPEN, + GUI_FILE_TEST_OPEN_MULTI, + GUI_FILE_TEST_SAVE }; enum FurnaceGUIWarnings { @@ -354,6 +371,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_PIANO, GUI_ACTION_WINDOW_NOTES, GUI_ACTION_WINDOW_CHANNELS, + GUI_ACTION_WINDOW_PAT_MANAGER, GUI_ACTION_WINDOW_REGISTER_VIEW, GUI_ACTION_WINDOW_LOG, GUI_ACTION_WINDOW_EFFECT_LIST, @@ -448,6 +466,7 @@ enum FurnaceGUIActions { GUI_ACTION_WAVE_LIST_ADD, GUI_ACTION_WAVE_LIST_DUPLICATE, GUI_ACTION_WAVE_LIST_OPEN, + GUI_ACTION_WAVE_LIST_OPEN_REPLACE, GUI_ACTION_WAVE_LIST_SAVE, GUI_ACTION_WAVE_LIST_MOVE_UP, GUI_ACTION_WAVE_LIST_MOVE_DOWN, @@ -461,6 +480,9 @@ enum FurnaceGUIActions { GUI_ACTION_SAMPLE_LIST_ADD, GUI_ACTION_SAMPLE_LIST_DUPLICATE, GUI_ACTION_SAMPLE_LIST_OPEN, + GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE, + GUI_ACTION_SAMPLE_LIST_OPEN_RAW, + GUI_ACTION_SAMPLE_LIST_OPEN_REPLACE_RAW, GUI_ACTION_SAMPLE_LIST_SAVE, GUI_ACTION_SAMPLE_LIST_MOVE_UP, GUI_ACTION_SAMPLE_LIST_MOVE_DOWN, @@ -883,6 +905,7 @@ enum FurnaceGUIFindQueryReplaceModes { GUI_QUERY_REPLACE_SET=0, GUI_QUERY_REPLACE_ADD, GUI_QUERY_REPLACE_ADD_OVERFLOW, + GUI_QUERY_REPLACE_SCALE, GUI_QUERY_REPLACE_CLEAR, GUI_QUERY_REPLACE_MAX @@ -945,18 +968,26 @@ class FurnaceGUI { bool updateSampleTex; String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile; - String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport, workingDirVGMExport, workingDirZSMExport, workingDirFont, workingDirColors, workingDirKeybinds, workingDirLayout, workingDirROM; + String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport; + String workingDirVGMExport, workingDirZSMExport, workingDirROMExport, workingDirFont, workingDirColors, workingDirKeybinds; + String workingDirLayout, workingDirROM, workingDirTest; String mmlString[32]; String mmlStringW; - bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, zsmExportLoop, wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; + bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, zsmExportLoop, vgmExportPatternHints; + bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; bool displayNew, fullScreen, preserveChanPos, wantScrollList, noteInputPoly; - bool displayPendingIns, pendingInsSingle; + bool displayPendingIns, pendingInsSingle, displayPendingRawSample; bool willExport[32]; int vgmExportVersion; int drawHalt; int zsmExportTickRate; int macroPointSize; + int waveEditStyle; + + String pendingRawSample; + int pendingRawSampleDepth, pendingRawSampleChannels; + bool pendingRawSampleUnsigned, pendingRawSampleBigEndian; ImGuiWindowFlags globalWinFlags; @@ -998,6 +1029,12 @@ class FurnaceGUI { ImU32 sysCmd1Grad[256]; ImU32 sysCmd2Grad[256]; + char noteOffLabel[32]; + char noteRelLabel[32]; + char macroRelLabel[32]; + char emptyLabel[32]; + char emptyLabel2[32]; + struct Settings { int mainFontSize, patFontSize, iconSize; int audioEngine; @@ -1090,12 +1127,22 @@ class FurnaceGUI { int blankIns; int dragMovesSelection; int unsignedDetune; + int noThreadedInput; + int clampSamples; + int saveUnusedPatterns; unsigned int maxUndoSteps; String mainFontPath; String patFontPath; String audioDevice; String midiInDevice; String midiOutDevice; + String c163Name; + String initialSysName; + String noteOffLabel; + String noteRelLabel; + String macroRelLabel; + String emptyLabel; + String emptyLabel2; std::vector initialSys; Settings(): @@ -1192,12 +1239,22 @@ class FurnaceGUI { blankIns(0), dragMovesSelection(1), unsignedDetune(0), + noThreadedInput(0), + clampSamples(0), + saveUnusedPatterns(0), maxUndoSteps(100), mainFontPath(""), patFontPath(""), audioDevice(""), midiInDevice(""), - midiOutDevice("") {} + midiOutDevice(""), + c163Name(""), + initialSysName("Sega Genesis/Mega Drive"), + noteOffLabel("OFF"), + noteRelLabel("==="), + macroRelLabel("REL"), + emptyLabel("..."), + emptyLabel2("..") {} } settings; char finalLayoutPath[4096]; @@ -1214,16 +1271,17 @@ class FurnaceGUI { bool waveListOpen, waveEditOpen, sampleListOpen, sampleEditOpen, aboutOpen, settingsOpen; bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen; - bool subSongsOpen, findOpen, spoilerOpen; + bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen; SelectionPoint selStart, selEnd, cursor, cursorDrag, dragStart, dragEnd; bool selecting, selectingFull, dragging, curNibble, orderNibble, followOrders, followPattern, changeAllOrders, mobileUI; - bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, lockLayout, editOptsVisible, latchNibble, nonLatchNibble; + bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, waveGenVisible, lockLayout, editOptsVisible, latchNibble, nonLatchNibble; FurnaceGUIWindows curWindow, nextWindow, curWindowLast; float peak[2]; float patChanX[DIV_MAX_CHANS+1]; float patChanSlideY[DIV_MAX_CHANS+1]; const int* nextDesc; + String nextDescName; OperationMask opMaskDelete, opMaskPullDelete, opMaskInsert, opMaskPaste, opMaskTransposeNote, opMaskTransposeValue; OperationMask opMaskInterpolate, opMaskFade, opMaskInvertVal, opMaskScale; @@ -1454,6 +1512,21 @@ class FurnaceGUI { bool hasACED; unsigned char acedData[23]; + // wave generator + int waveGenBaseShape; + float waveGenDuty; + int waveGenPower; + float waveGenInvertPoint; + float waveGenAmp[16]; + float waveGenPhase[16]; + float waveGenTL[4]; + int waveGenMult[4]; + float waveGenFB[4]; + bool waveGenFMCon1[4]; + bool waveGenFMCon2[3]; + bool waveGenFMCon3[2]; + bool waveGenFM; + void drawSSGEnv(unsigned char type, const ImVec2& size); void drawWaveform(unsigned char type, bool opz, const ImVec2& size); void drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, const ImVec2& size); @@ -1507,6 +1580,7 @@ class FurnaceGUI { void drawPiano(); void drawNotes(); void drawChannels(); + void drawPatManager(); void drawRegView(); void drawAbout(); void drawSettings(); @@ -1577,6 +1651,8 @@ class FurnaceGUI { void noteInput(int num, int key, int vol=-1); void valueInput(int num, bool direct=false, int target=-1); + void doGenerateWave(); + void doUndoSample(); void doRedoSample(); @@ -1612,6 +1688,7 @@ class FurnaceGUI { public: void showWarning(String what, FurnaceGUIWarnings type); void showError(String what); + const char* noteNameNormal(short note, short octave); const char* noteName(short note, short octave); bool decodeNote(const char* what, short& note, short& octave); void bindEngine(DivEngine* eng); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index b514d3c21..7c38237a5 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -97,7 +97,7 @@ const char* insTypes[DIV_INS_MAX+1]={ "FM (OPL)", "FDS", "Virtual Boy", - "Namco C163", + "Namco 163", "Konami SCC/Bubble System WSG", "FM (OPZ)", "POKEY", @@ -116,7 +116,7 @@ const char* insTypes[DIV_INS_MAX+1]={ NULL }; -const char* sampleDepths[17]={ +const char* sampleDepths[DIV_SAMPLE_DEPTH_MAX]={ "1-bit PCM", "1-bit DPCM", NULL, @@ -487,11 +487,12 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WINDOW_PIANO", "Piano", 0), D("WINDOW_NOTES", "Song Comments", 0), D("WINDOW_CHANNELS", "Channels", 0), + D("WINDOW_PAT_MANAGER", "Pattern Manager", 0), D("WINDOW_REGISTER_VIEW", "Register View", 0), D("WINDOW_LOG", "Log Viewer", 0), - D("WINDOW_SUBSONGS", "Subsongs", 0), D("EFFECT_LIST", "Effect List", 0), D("WINDOW_CHAN_OSC", "Oscilloscope (per-channel)", 0), + D("WINDOW_SUBSONGS", "Subsongs", 0), D("WINDOW_FIND", "Find/Replace", FURKMOD_CMD|SDLK_f), D("COLLAPSE_WINDOW", "Collapse/expand current window", 0), @@ -581,6 +582,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WAVE_LIST_ADD", "Add", SDLK_INSERT), D("WAVE_LIST_DUPLICATE", "Duplicate", FURKMOD_CMD|SDLK_d), D("WAVE_LIST_OPEN", "Open", 0), + D("WAVE_LIST_OPEN_REPLACE", "Open (replace current)", 0), D("WAVE_LIST_SAVE", "Save", 0), D("WAVE_LIST_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), D("WAVE_LIST_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), @@ -594,6 +596,9 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("SAMPLE_LIST_ADD", "Add", SDLK_INSERT), D("SAMPLE_LIST_DUPLICATE", "Duplicate", FURKMOD_CMD|SDLK_d), D("SAMPLE_LIST_OPEN", "Open", 0), + D("SAMPLE_LIST_OPEN_REPLACE", "Open (replace current)", 0), + D("SAMPLE_LIST_OPEN_RAW", "Import raw data", 0), + D("SAMPLE_LIST_OPEN_REPLACE_RAW", "Import raw data (replace current)", 0), D("SAMPLE_LIST_SAVE", "Save", 0), D("SAMPLE_LIST_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), D("SAMPLE_LIST_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), @@ -808,6 +813,13 @@ const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={ D(GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY,"",ImVec4(0.0f,1.0f,0.5f,1.0f)), D(GUI_COLOR_PATTERN_EFFECT_MISC,"",ImVec4(0.3f,0.3f,1.0f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_NULL,"",ImVec4(0.15f,0.15f,0.15f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_USED,"",ImVec4(0.15f,1.0f,0.15f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_OVERUSED,"",ImVec4(1.0f,1.0f,0.15f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_EXTREMELY_OVERUSED,"",ImVec4(1.0f,0.5f,0.15f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_COMBO_BREAKER,"",ImVec4(1.0f,0.15f,1.0f,1.0f)), + D(GUI_COLOR_PAT_MANAGER_UNUSED,"",ImVec4(1.0f,0.15f,0.15f,1.0f)), + D(GUI_COLOR_PIANO_BACKGROUND,"",ImVec4(0.0f,0.0f,0.0f,1.0f)), D(GUI_COLOR_PIANO_KEY_BOTTOM,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), D(GUI_COLOR_PIANO_KEY_TOP,"",ImVec4(0.0f,0.0f,0.0f,1.0f)), @@ -895,6 +907,7 @@ const int availableSystems[]={ DIV_SYSTEM_MSM6258, DIV_SYSTEM_MSM6295, DIV_SYSTEM_RF5C68, + DIV_SYSTEM_PCM_DAC, 0 // don't remove this last one! }; diff --git a/src/gui/guiConst.h b/src/gui/guiConst.h index 69085c324..a6df68f62 100644 --- a/src/gui/guiConst.h +++ b/src/gui/guiConst.h @@ -40,7 +40,7 @@ extern const char* noteNames[180]; extern const char* noteNamesG[180]; extern const char* pitchLabel[11]; extern const char* insTypes[]; -extern const char* sampleDepths[17]; +extern const char* sampleDepths[]; extern const char* resampleStrats[]; extern const int availableSystems[]; extern const FurnaceGUIActionDef guiActions[]; diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 45f7c6757..d4e76611f 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -43,24 +43,91 @@ const char* fmParamShortNames[3][32]={ {"ALG", "FB", "FMS", "AMS", "A", "D", "D2", "R", "S", "TL", "RS", "ML", "DT", "DT2", "SSG", "AM", "DAM", "DVB", "EGT", "EGS", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM", "EGS", "REV", "Fine", "FMS2", "AMS2"} }; -const char* opllInsNames[17]={ - "User", - "Violin", - "Guitar", - "Piano", - "Flute", - "Clarinet", - "Oboe", - "Trumpet", - "Organ", - "Horn", - "Synth", - "Harpsichord", - "Vibraphone", - "Synth Bass", - "Acoustic Bass", - "Electric Guitar", - "Drums" +const char* opllVariants[4]={ + "OPLL", + "YMF281", + "YM2423", + "VRC7" +}; + +const char* opllInsNames[4][17]={ + /* YM2413 */ { + "User", + "Violin", + "Guitar", + "Piano", + "Flute", + "Clarinet", + "Oboe", + "Trumpet", + "Organ", + "Horn", + "Synth", + "Harpsichord", + "Vibraphone", + "Synth Bass", + "Acoustic Bass", + "Electric Guitar", + "Drums" + }, + /* YMF281 */ { + "User", + "Electric String", + "Bow wow", + "Electric Guitar", + "Organ", + "Clarinet", + "Saxophone", + "Trumpet", + "Street Organ", + "Synth Brass", + "Electric Piano", + "Bass", + "Vibraphone", + "Chime", + "Tom Tom II", + "Noise", + "Drums" + }, + /* YM2423 */ { + "User", + "Strings", + "Guitar", + "Electric Guitar", + "Electric Piano", + "Flute", + "Marimba", + "Trumpet", + "Harmonica", + "Tuba", + "Synth Brass", + "Short Saw", + "Vibraphone", + "Electric Guitar 2", + "Synth Bass", + "Sitar", + "Drums" + }, + // stolen from FamiTracker + /* VRC7 */ { + "User", + "Bell", + "Guitar", + "Piano", + "Flute", + "Clarinet", + "Rattling Bell", + "Trumpet", + "Reed Organ", + "Soft Bell", + "Xylophone", + "Vibraphone", + "Brass", + "Bass Guitar", + "Synth", + "Chorus", + "Drums" + } }; const char* oplWaveforms[8]={ @@ -217,6 +284,15 @@ const char* dualWSEffects[9]={ "Phase Modulation" }; +const char* gbHWSeqCmdTypes[6]={ + "Envelope", + "Sweep", + "Wait", + "Wait for Release", + "Loop", + "Loop until Release" +}; + const char* macroAbsoluteMode="Fixed"; const char* macroRelativeMode="Relative"; const char* macroQSoundMode="QSound"; @@ -1416,7 +1492,7 @@ void FurnaceGUI::drawInsEdit() { ins->type=(DivInstrumentType)insType; } */ - if (ImGui::BeginCombo("##Type",insTypes[insType])) { + if (ImGui::BeginCombo("##Type",insType==DIV_INS_N163?settings.c163Name.c_str():insTypes[insType])) { std::vector insTypeList; if (settings.displayAllInsTypes) { for (int i=0; insTypes[i]; i++) { @@ -1426,7 +1502,7 @@ void FurnaceGUI::drawInsEdit() { insTypeList=e->getPossibleInsTypes(); } for (DivInstrumentType i: insTypeList) { - if (ImGui::Selectable(insTypes[i],insType==i)) { + if (ImGui::Selectable(i==DIV_INS_N163?settings.c163Name.c_str():insTypes[i],insType==i)) { ins->type=i; // reset macro zoom @@ -1572,10 +1648,59 @@ void FurnaceGUI::drawInsEdit() { ImGui::TableNextColumn(); drawAlgorithm(0,FM_ALGS_2OP_OPL,ImVec2(ImGui::GetContentRegionAvail().x,24.0*dpiScale)); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::BeginCombo("##LLPreset",opllInsNames[ins->fm.opllPreset])) { - for (int i=0; i<17; i++) { - if (ImGui::Selectable(opllInsNames[i])) { - ins->fm.opllPreset=i; + + bool isPresent[4]; + int isPresentCount=0; + memset(isPresent,0,4*sizeof(bool)); + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i]==DIV_SYSTEM_VRC7) { + isPresent[3]=true; + } else if (e->song.system[i]==DIV_SYSTEM_OPLL || e->song.system[i]==DIV_SYSTEM_OPLL_DRUMS) { + isPresent[(e->song.systemFlags[i]>>4)&3]=true; + } + } + if (!isPresent[0] && !isPresent[1] && !isPresent[2] && !isPresent[3]) { + isPresent[0]=true; + } + for (int i=0; i<4; i++) { + if (isPresent[i]) isPresentCount++; + } + int presentWhich=0; + for (int i=0; i<4; i++) { + if (isPresent[i]) { + presentWhich=i; + break; + } + } + + if (ImGui::BeginCombo("##LLPreset",opllInsNames[presentWhich][ins->fm.opllPreset])) { + if (isPresentCount>1) { + if (ImGui::BeginTable("LLPresetList",isPresentCount)) { + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int i=0; i<4; i++) { + if (!isPresent[i]) continue; + ImGui::TableNextColumn(); + ImGui::Text("%s name",opllVariants[i]); + } + for (int i=0; i<17; i++) { + ImGui::TableNextRow(); + for (int j=0; j<4; j++) { + if (!isPresent[j]) continue; + ImGui::TableNextColumn(); + ImGui::PushID(j*17+i); + if (ImGui::Selectable(opllInsNames[j][i])) { + ins->fm.opllPreset=i; + } + ImGui::PopID(); + } + } + ImGui::EndTable(); + } + } else { + for (int i=0; i<17; i++) { + if (ImGui::Selectable(opllInsNames[presentWhich][i])) { + ins->fm.opllPreset=i; + } } } ImGui::EndCombo(); @@ -2840,6 +2965,10 @@ void FurnaceGUI::drawInsEdit() { } } if (ins->type==DIV_INS_GB) if (ImGui::BeginTabItem("Game Boy")) { + P(ImGui::Checkbox("Use software envelope",&ins->gb.softEnv)); + P(ImGui::Checkbox("Initialize envelope on every note",&ins->gb.alwaysInit)); + + ImGui::BeginDisabled(ins->gb.softEnv); P(CWSliderScalar("Volume",ImGuiDataType_U8,&ins->gb.envVol,&_ZERO,&_FIFTEEN)); rightClickable P(CWSliderScalar("Envelope Length",ImGuiDataType_U8,&ins->gb.envLen,&_ZERO,&_SEVEN)); rightClickable P(CWSliderScalar("Sound Length",ImGuiDataType_U8,&ins->gb.soundLen,&_ZERO,&_SIXTY_FOUR,ins->gb.soundLen>63?"Infinity":"%d")); rightClickable @@ -2858,6 +2987,195 @@ void FurnaceGUI::drawInsEdit() { } drawGBEnv(ins->gb.envVol,ins->gb.envLen,ins->gb.soundLen,ins->gb.envDir,ImVec2(ImGui::GetContentRegionAvail().x,100.0f*dpiScale)); + + if (ImGui::BeginChild("HWSeq",ImGui::GetContentRegionAvail(),true,ImGuiWindowFlags_MenuBar)) { + ImGui::BeginMenuBar(); + ImGui::Text("Hardware Sequence"); + ImGui::EndMenuBar(); + + if (ins->gb.hwSeqLen>0) if (ImGui::BeginTable("HWSeqList",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); + int curFrame=0; + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Tick"); + ImGui::TableNextColumn(); + ImGui::Text("Command"); + ImGui::TableNextColumn(); + ImGui::Text("Move/Remove"); + for (int i=0; igb.hwSeqLen; i++) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d (#%d)",curFrame,i); + ImGui::TableNextColumn(); + ImGui::PushID(i); + if (ins->gb.hwSeq[i].cmd>=DivInstrumentGB::DIV_GB_HWCMD_MAX) { + ins->gb.hwSeq[i].cmd=0; + } + int cmd=ins->gb.hwSeq[i].cmd; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::Combo("##HWSeqCmd",&cmd,gbHWSeqCmdTypes,DivInstrumentGB::DIV_GB_HWCMD_MAX)) { + if (ins->gb.hwSeq[i].cmd!=cmd) { + ins->gb.hwSeq[i].cmd=cmd; + ins->gb.hwSeq[i].data=0; + } + } + bool somethingChanged=false; + switch (ins->gb.hwSeq[i].cmd) { + case DivInstrumentGB::DIV_GB_HWCMD_ENVELOPE: { + int hwsVol=(ins->gb.hwSeq[i].data&0xf0)>>4; + bool hwsDir=ins->gb.hwSeq[i].data&8; + int hwsLen=ins->gb.hwSeq[i].data&7; + int hwsSoundLen=ins->gb.hwSeq[i].data>>8; + + if (CWSliderInt("Volume",&hwsVol,0,15)) { + somethingChanged=true; + } + if (CWSliderInt("Env Length",&hwsLen,0,7)) { + somethingChanged=true; + } + if (CWSliderInt("Sound Length",&hwsSoundLen,0,64,hwsSoundLen>63?"Infinity":"%d")) { + somethingChanged=true; + } + if (ImGui::RadioButton("Up",hwsDir)) { PARAMETER + hwsDir=true; + somethingChanged=true; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Down",!hwsDir)) { PARAMETER + hwsDir=false; + somethingChanged=true; + } + + if (somethingChanged) { + ins->gb.hwSeq[i].data=(hwsLen&7)|(hwsDir?8:0)|(hwsVol<<4)|(hwsSoundLen<<8); + PARAMETER; + } + break; + } + case DivInstrumentGB::DIV_GB_HWCMD_SWEEP: { + int hwsShift=ins->gb.hwSeq[i].data&7; + int hwsSpeed=(ins->gb.hwSeq[i].data&0x70)>>4; + bool hwsDir=ins->gb.hwSeq[i].data&8; + + if (CWSliderInt("Shift",&hwsShift,0,7)) { + somethingChanged=true; + } + if (CWSliderInt("Speed",&hwsSpeed,0,7)) { + somethingChanged=true; + } + + if (ImGui::RadioButton("Up",!hwsDir)) { PARAMETER + hwsDir=false; + somethingChanged=true; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Down",hwsDir)) { PARAMETER + hwsDir=true; + somethingChanged=true; + } + + if (somethingChanged) { + ins->gb.hwSeq[i].data=(hwsShift&7)|(hwsDir?8:0)|(hwsSpeed<<4); + PARAMETER; + } + break; + } + case DivInstrumentGB::DIV_GB_HWCMD_WAIT: { + int len=ins->gb.hwSeq[i].data+1; + curFrame+=ins->gb.hwSeq[i].data+1; + + if (ImGui::InputInt("Ticks",&len)) { + if (len<1) len=1; + if (len>255) len=256; + somethingChanged=true; + } + + if (somethingChanged) { + ins->gb.hwSeq[i].data=len-1; + PARAMETER; + } + break; + } + case DivInstrumentGB::DIV_GB_HWCMD_WAIT_REL: + curFrame++; + break; + case DivInstrumentGB::DIV_GB_HWCMD_LOOP: + case DivInstrumentGB::DIV_GB_HWCMD_LOOP_REL: { + int pos=ins->gb.hwSeq[i].data; + + if (ImGui::InputInt("Position",&pos)) { + if (pos<0) pos=0; + if (pos>(ins->gb.hwSeqLen-1)) pos=(ins->gb.hwSeqLen-1); + somethingChanged=true; + } + + if (somethingChanged) { + ins->gb.hwSeq[i].data=pos; + PARAMETER; + } + break; + } + default: + break; + } + ImGui::PopID(); + ImGui::TableNextColumn(); + ImGui::PushID(i+512); + if (ImGui::Button(ICON_FA_CHEVRON_UP "##HWCmdUp")) { + if (i>0) { + e->lockEngine([ins,i]() { + ins->gb.hwSeq[i-1].cmd^=ins->gb.hwSeq[i].cmd; + ins->gb.hwSeq[i].cmd^=ins->gb.hwSeq[i-1].cmd; + ins->gb.hwSeq[i-1].cmd^=ins->gb.hwSeq[i].cmd; + + ins->gb.hwSeq[i-1].data^=ins->gb.hwSeq[i].data; + ins->gb.hwSeq[i].data^=ins->gb.hwSeq[i-1].data; + ins->gb.hwSeq[i-1].data^=ins->gb.hwSeq[i].data; + }); + } + MARK_MODIFIED; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_CHEVRON_DOWN "##HWCmdDown")) { + if (igb.hwSeqLen-1) { + e->lockEngine([ins,i]() { + ins->gb.hwSeq[i-1].cmd^=ins->gb.hwSeq[i].cmd; + ins->gb.hwSeq[i].cmd^=ins->gb.hwSeq[i-1].cmd; + ins->gb.hwSeq[i-1].cmd^=ins->gb.hwSeq[i].cmd; + + ins->gb.hwSeq[i-1].data^=ins->gb.hwSeq[i].data; + ins->gb.hwSeq[i].data^=ins->gb.hwSeq[i-1].data; + ins->gb.hwSeq[i-1].data^=ins->gb.hwSeq[i].data; + }); + } + MARK_MODIFIED; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TIMES "##HWCmdDel")) { + for (int j=i; jgb.hwSeqLen-1; j++) { + ins->gb.hwSeq[j].cmd=ins->gb.hwSeq[j+1].cmd; + ins->gb.hwSeq[j].data=ins->gb.hwSeq[j+1].data; + } + ins->gb.hwSeqLen--; + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + + if (ImGui::Button(ICON_FA_PLUS "##HWCmdAdd")) { + if (ins->gb.hwSeqLen<255) { + ins->gb.hwSeq[ins->gb.hwSeqLen].cmd=0; + ins->gb.hwSeq[ins->gb.hwSeqLen].data=0; + ins->gb.hwSeqLen++; + } + } + } + ImGui::EndChild(); + ImGui::EndDisabled(); ImGui::EndTabItem(); } if (ins->type==DIV_INS_C64) if (ImGui::BeginTabItem("C64")) { @@ -2977,13 +3295,17 @@ void FurnaceGUI::drawInsEdit() { P(ImGui::Checkbox("Don't test/gate before new note",&ins->c64.noTest)); ImGui::EndTabItem(); } - if (ins->type==DIV_INS_AMIGA) if (ImGui::BeginTabItem("Sample")) { + if (ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SU) if (ImGui::BeginTabItem((ins->type==DIV_INS_SU)?"Sound Unit":"Sample")) { String sName; if (ins->amiga.initSample<0 || ins->amiga.initSample>=e->song.sampleLen) { sName="none selected"; } else { sName=e->song.sample[ins->amiga.initSample]->name; } + if (ins->type==DIV_INS_SU) { + P(ImGui::Checkbox("Use sample",&ins->su.useSample)); + P(ImGui::Checkbox("Switch roles of frequency and phase reset timer",&ins->su.switchRoles)); + } if (ImGui::BeginCombo("Initial Sample",sName.c_str())) { String id; for (int i=0; isong.sampleLen; i++) { @@ -2994,14 +3316,16 @@ void FurnaceGUI::drawInsEdit() { } ImGui::EndCombo(); } - P(ImGui::Checkbox("Use wavetable (Amiga only)",&ins->amiga.useWave)); - if (ins->amiga.useWave) { - int len=ins->amiga.waveLen+1; - if (ImGui::InputInt("Width",&len,2,16)) { - if (len<2) len=2; - if (len>256) len=256; - ins->amiga.waveLen=(len&(~1))-1; - PARAMETER + if (ins->type==DIV_INS_AMIGA) { + P(ImGui::Checkbox("Use wavetable (Amiga only)",&ins->amiga.useWave)); + if (ins->amiga.useWave) { + int len=ins->amiga.waveLen+1; + if (ImGui::InputInt("Width",&len,2,16)) { + if (len<2) len=2; + if (len>256) len=256; + ins->amiga.waveLen=(len&(~1))-1; + PARAMETER + } } } ImGui::BeginDisabled(ins->amiga.useWave); @@ -3011,7 +3335,7 @@ void FurnaceGUI::drawInsEdit() { if (ImGui::BeginTable("NoteMap",2,ImGuiTableFlags_ScrollY|ImGuiTableFlags_Borders|ImGuiTableFlags_SizingStretchSame)) { ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); + //ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupScrollFreeze(0,1); @@ -3022,37 +3346,38 @@ void FurnaceGUI::drawInsEdit() { /*ImGui::TableNextColumn(); ImGui::Text("Frequency");*/ for (int i=0; i<120; i++) { + DivInstrumentAmiga::SampleMap& sampleMap=ins->amiga.noteMap[i]; ImGui::TableNextRow(); ImGui::PushID(fmt::sprintf("NM_%d",i).c_str()); ImGui::TableNextColumn(); ImGui::Text("%s",noteNames[60+i]); ImGui::TableNextColumn(); - if (ins->amiga.noteMap[i]<0 || ins->amiga.noteMap[i]>=e->song.sampleLen) { + if (sampleMap.map<0 || sampleMap.map>=e->song.sampleLen) { sName="-- empty --"; - ins->amiga.noteMap[i]=-1; + sampleMap.map=-1; } else { - sName=e->song.sample[ins->amiga.noteMap[i]]->name; + sName=e->song.sample[sampleMap.map]->name; } ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::BeginCombo("##SM",sName.c_str())) { String id; - if (ImGui::Selectable("-- empty --",ins->amiga.noteMap[i]==-1)) { PARAMETER - ins->amiga.noteMap[i]=-1; + if (ImGui::Selectable("-- empty --",sampleMap.map==-1)) { PARAMETER + sampleMap.map=-1; } for (int j=0; jsong.sampleLen; j++) { id=fmt::sprintf("%d: %s",j,e->song.sample[j]->name); - if (ImGui::Selectable(id.c_str(),ins->amiga.noteMap[i]==j)) { PARAMETER - ins->amiga.noteMap[i]=j; - if (ins->amiga.noteFreq[i]<=0) ins->amiga.noteFreq[i]=(int)((double)e->song.sample[j]->centerRate*pow(2.0,((double)i-48.0)/12.0)); + if (ImGui::Selectable(id.c_str(),sampleMap.map==j)) { PARAMETER + sampleMap.map=j; + if (sampleMap.freq<=0) sampleMap.freq=(int)((double)e->song.sample[j]->centerRate*pow(2.0,((double)i-48.0)/12.0)); } } ImGui::EndCombo(); } /*ImGui::TableNextColumn(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##SF",&ins->amiga.noteFreq[i],50,500)) { PARAMETER - if (ins->amiga.noteFreq[i]<0) ins->amiga.noteFreq[i]=0; - if (ins->amiga.noteFreq[i]>262144) ins->amiga.noteFreq[i]=262144; + if (ImGui::InputInt("##SF",&sampleMap.freq,50,500)) { PARAMETER + if (sampleMap.freq<0) sampleMap.freq=0; + if (sampleMap.freq>262144) sampleMap.freq=262144; }*/ ImGui::PopID(); } @@ -3062,7 +3387,7 @@ void FurnaceGUI::drawInsEdit() { ImGui::EndDisabled(); ImGui::EndTabItem(); } - if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem("Namco 163")) { + if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem(settings.c163Name.c_str())) { if (ImGui::InputInt("Waveform##WAVE",&ins->n163.wave,1,10)) { PARAMETER if (ins->n163.wave<0) ins->n163.wave=0; if (ins->n163.wave>=e->song.waveLen) ins->n163.wave=e->song.waveLen-1; @@ -3408,11 +3733,15 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_AMIGA) { volMax=64; } - if (ins->type==DIV_INS_FM || ins->type==DIV_INS_MIKEY || ins->type==DIV_INS_MULTIPCM || ins->type==DIV_INS_SU) { + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_MIKEY || ins->type==DIV_INS_MULTIPCM || ins->type==DIV_INS_SU || ins->type==DIV_INS_OPZ) { volMax=127; } if (ins->type==DIV_INS_GB) { - volMax=0; + if (ins->gb.softEnv) { + volMax=15; + } else { + volMax=0; + } } if (ins->type==DIV_INS_PET || ins->type==DIV_INS_BEEPER) { volMax=1; @@ -3677,6 +4006,7 @@ void FurnaceGUI::drawInsEdit() { } if (ins->type==DIV_INS_SU) { macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.ex3Macro,0,4,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,suControlBits)); + macroList.push_back(FurnaceGUIMacroDesc("Phase Reset Timer",&ins->std.ex4Macro,0,65535,160,uiColors[GUI_COLOR_MACRO_OTHER])); // again reuse code from resonance macro but use ex4 instead } drawMacros(macroList); diff --git a/src/gui/newSong.cpp b/src/gui/newSong.cpp index 0dea772b7..26fddf9fb 100644 --- a/src/gui/newSong.cpp +++ b/src/gui/newSong.cpp @@ -65,6 +65,7 @@ void FurnaceGUI::drawNewSong() { ImGui::TableNextColumn(); if (ImGui::Selectable(i.name,false,ImGuiSelectableFlags_DontClosePopups)) { nextDesc=i.definition.data(); + nextDescName=i.name; accepted=true; } } @@ -97,7 +98,7 @@ void FurnaceGUI::drawNewSong() { } if (accepted) { - e->createNew(nextDesc); + e->createNew(nextDesc,nextDescName); undoHist.clear(); redoHist.clear(); curFileName=""; diff --git a/src/gui/patManager.cpp b/src/gui/patManager.cpp new file mode 100644 index 000000000..d597bd9c2 --- /dev/null +++ b/src/gui/patManager.cpp @@ -0,0 +1,109 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "gui.h" +#include "misc/cpp/imgui_stdlib.h" +#include "IconsFontAwesome4.h" +#include + +void FurnaceGUI::drawPatManager() { + if (nextWindow==GUI_WINDOW_PAT_MANAGER) { + patManagerOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!patManagerOpen) return; + char id[1024]; + unsigned char isUsed[256]; + bool isNull[256]; + if (ImGui::Begin("Pattern Manager",&patManagerOpen,globalWinFlags)) { + ImGui::Text("Global Tasks"); + + if (ImGui::Button("De-duplicate patterns")) { + e->lockEngine([this]() { + e->curSubSong->optimizePatterns(); + }); + } + ImGui::SameLine(); + if (ImGui::Button("Re-arrange patterns")) { + e->lockEngine([this]() { + e->curSubSong->rearrangePatterns(); + }); + } + + for (int i=0; igetTotalChannelCount(); i++) { + memset(isUsed,0,256); + memset(isNull,0,256*sizeof(bool)); + for (int j=0; jcurSubSong->ordersLen; j++) { + isUsed[e->curSubSong->orders.ord[i][j]]++; + } + for (int j=0; j<256; j++) { + isNull[j]=(e->curSubSong->pat[i].data[j]==NULL); + } + ImGui::Text("%d. %s",i+1,e->getChannelName(i)); + ImGui::PushID(1000+i); + ImGui::PushFont(patFont); + if (ImGui::BeginTable("PatManTable",32)) { + for (int k=0; k<256; k++) { + if ((k&31)==0) ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + snprintf(id,1023,"%.2X",k); + if (isNull[k]) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_NULL]); + } else if (isUsed[k]>=e->curSubSong->ordersLen) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_COMBO_BREAKER]); + } else if (isUsed[k]>=0.7*(double)e->curSubSong->ordersLen) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_EXTREMELY_OVERUSED]); + } else if (isUsed[k]>=0.4*(double)e->curSubSong->ordersLen) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_OVERUSED]); + } else if (isUsed[k]) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_USED]); + } else { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PAT_MANAGER_UNUSED]); + } + ImGui::Selectable(id,isUsed[k]); + if (ImGui::IsItemHovered()) { + ImGui::PushFont(mainFont); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); + if (isNull[k]) { + ImGui::SetTooltip("Pattern %.2X\n- not allocated",k); + } else { + ImGui::SetTooltip("Pattern %.2X\n- use count: %d (%.0f%%)\n\nright-click to erase",k,isUsed[k],100.0*(double)isUsed[k]/(double)e->curSubSong->ordersLen); + } + ImGui::PopStyleColor(); + ImGui::PopFont(); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + e->lockEngine([this,i,k]() { + delete e->curSubSong->pat[i].data[k]; + e->curSubSong->pat[i].data[k]=NULL; + }); + } + ImGui::PopStyleColor(); + } + ImGui::EndTable(); + } + ImGui::PopFont(); + ImGui::PopID(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_PAT_MANAGER; + ImGui::End(); +} diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index b7940a9e8..254c53bed 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -56,7 +56,7 @@ void FurnaceGUI::popPartBlend() { // draw a pattern row inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord, const DivPattern** patCache, bool inhibitSel) { - static char id[32]; + static char id[64]; bool selectedRow=(i>=sel1.y && i<=sel2.y && !inhibitSel); ImGui::TableNextRow(0,lineHeight); ImGui::TableNextColumn(); @@ -114,9 +114,9 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int ImGui::PushStyleColor(ImGuiCol_Text,rowIndexColor); if (settings.patRowsBase==1) { - snprintf(id,31," %.2X ##PR_%d",i,i); + snprintf(id,63," %.2X ##PR_%d",i,i); } else { - snprintf(id,31,"%3d ##PR_%d",i,i); + snprintf(id,63,"%3d ##PR_%d",i,i); } ImGui::Selectable(id,false,ImGuiSelectableFlags_NoPadWithHalfSpacing,fourChars); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) { @@ -151,7 +151,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int bool cursorVol=(cursor.y==i && cursor.xCoarse==j && cursor.xFine==2 && curWindowLast==GUI_WINDOW_PATTERN); // note - sprintf(id,"%s##PN_%d_%d",noteName(pat->data[i][0],pat->data[i][1]),i,j); + snprintf(id,63,"%.31s##PN_%d_%d",noteName(pat->data[i][0],pat->data[i][1]),i,j); if (pat->data[i][0]==0 && pat->data[i][1]==0) { ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { @@ -182,7 +182,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int // instrument if (pat->data[i][2]==-1) { ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); - sprintf(id,"..##PI_%d_%d",i,j); + snprintf(id,63,"%.31s##PI_%d_%d",emptyLabel2,i,j); } else { if (pat->data[i][2]<0 || pat->data[i][2]>=e->song.insLen) { ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS_ERROR]); @@ -194,7 +194,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); } } - sprintf(id,"%.2X##PI_%d_%d",pat->data[i][2],i,j); + snprintf(id,63,"%.2X##PI_%d_%d",pat->data[i][2],i,j); } ImGui::SameLine(0.0f,0.0f); if (cursorIns) { @@ -221,13 +221,13 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int if (e->curSubSong->chanCollapse[j]<2) { // volume if (pat->data[i][3]==-1) { - sprintf(id,"..##PV_%d_%d",i,j); + snprintf(id,63,"%.31s##PV_%d_%d",emptyLabel2,i,j); ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { int volColor=(pat->data[i][3]*127)/chanVolMax; if (volColor>127) volColor=127; if (volColor<0) volColor=0; - sprintf(id,"%.2X##PV_%d_%d",pat->data[i][3],i,j); + snprintf(id,63,"%.2X##PV_%d_%d",pat->data[i][3],i,j); ImGui::PushStyleColor(ImGuiCol_Text,volColors[volColor]); } ImGui::SameLine(0.0f,0.0f); @@ -263,15 +263,15 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int // effect if (pat->data[i][index]==-1) { - sprintf(id,"..##PE%d_%d_%d",k,i,j); + snprintf(id,63,"%.31s##PE%d_%d_%d",emptyLabel2,k,i,j); ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { if (pat->data[i][index]>0xff) { - sprintf(id,"??##PE%d_%d_%d",k,i,j); + snprintf(id,63,"??##PE%d_%d_%d",k,i,j); ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); } else { const unsigned char data=pat->data[i][index]; - sprintf(id,"%.2X##PE%d_%d_%d",data,k,i,j); + snprintf(id,63,"%.2X##PE%d_%d_%d",data,k,i,j); ImGui::PushStyleColor(ImGuiCol_Text,uiColors[fxColors[data]]); } } @@ -297,9 +297,9 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int // effect value if (pat->data[i][index+1]==-1) { - sprintf(id,"..##PF%d_%d_%d",k,i,j); + snprintf(id,63,"%.31s##PF%d_%d_%d",emptyLabel2,k,i,j); } else { - sprintf(id,"%.2X##PF%d_%d_%d",pat->data[i][index+1],k,i,j); + snprintf(id,63,"%.2X##PF%d_%d_%d",pat->data[i][index+1],k,i,j); } ImGui::SameLine(0.0f,0.0f); if (cursorEffectVal) { @@ -701,6 +701,15 @@ void FurnaceGUI::drawPattern() { if (i.cmd==DIV_CMD_SAMPLE_BANK) continue; if (i.cmd==DIV_CMD_GET_VOLUME) continue; if (i.cmd==DIV_ALWAYS_SET_VOLUME) continue; + if (i.cmd==DIV_CMD_HINT_VOLUME || + i.cmd==DIV_CMD_HINT_PORTA || + i.cmd==DIV_CMD_HINT_LEGATO || + i.cmd==DIV_CMD_HINT_VOL_SLIDE || + i.cmd==DIV_CMD_HINT_ARPEGGIO || + i.cmd==DIV_CMD_HINT_PITCH || + i.cmd==DIV_CMD_HINT_VIBRATO || + i.cmd==DIV_CMD_HINT_VIBRATO_RANGE || + i.cmd==DIV_CMD_HINT_VIBRATO_SHAPE) continue; float width=patChanX[i.chan+1]-patChanX[i.chan]; float speedX=0.0f; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 3774f232c..571cda441 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -409,7 +409,7 @@ void FurnaceGUI::initSystemPresets() { } )); cat.systems.push_back(FurnaceGUISysDef( - "Namco C163", { + "Namco 163", { DIV_SYSTEM_N163, 64, 0, 0, 0 } @@ -617,7 +617,7 @@ void FurnaceGUI::initSystemPresets() { } )); cat.systems.push_back(FurnaceGUISysDef( - "Famicom with Namco C163", { + "Famicom with Namco 163", { DIV_SYSTEM_NES, 64, 0, 0, DIV_SYSTEM_N163, 64, 0, 112, 0 @@ -1241,6 +1241,15 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Williams/Midway Y/T unit w/ADPCM sound board", { + // ADPCM sound board + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_PCM_DAC, 64, 0, 15624|(7<<16), // variable via OPM timer? + DIV_SYSTEM_MSM6295, 64, 0, 0, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "Konami Gyruss", { DIV_SYSTEM_AY8910, 64, 0, 0, @@ -1261,6 +1270,34 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Konami Battlantis", { + DIV_SYSTEM_OPL2, 64, 0, 3, // 3MHz + DIV_SYSTEM_OPL2, 64, 0, 3, // "" + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Konami Battlantis (drums mode on first OPL2)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 3, // 3MHz + DIV_SYSTEM_OPL2, 64, 0, 3, // "" + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Konami Battlantis (drums mode on second OPL2)", { + DIV_SYSTEM_OPL2, 64, 0, 3, // 3MHz + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 3, // "" + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Konami Battlantis (drums mode on both OPL2s)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 3, // 3MHz + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 3, // "" + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "Konami Hexion", { DIV_SYSTEM_SCC, 64, 0, 2, // 1.5MHz (3MHz input) @@ -1326,6 +1363,13 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega System 24", { + DIV_SYSTEM_YM2151, 64, 0, 2, // 4MHz + DIV_SYSTEM_PCM_DAC, 64, 0, 61499|(7<<16), // software controlled, variable rate via configurable timers + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "Sega System 18", { DIV_SYSTEM_YM2612, 64, 0, 2, // discrete 8MHz YM3438 @@ -1863,7 +1907,7 @@ void FurnaceGUI::initSystemPresets() { "Alpha denshi Alpha-68K", { DIV_SYSTEM_OPN, 64, 0, 3, // 3MHz DIV_SYSTEM_OPLL, 64, 0, 0, // 3.58MHz - // software controlled 8 bit DAC + DIV_SYSTEM_PCM_DAC, 64, 0, 7613|(7<<16), // software controlled 8 bit DAC 0 } )); @@ -1871,7 +1915,7 @@ void FurnaceGUI::initSystemPresets() { "Alpha denshi Alpha-68K (extended channel 3)", { DIV_SYSTEM_OPN_EXT, 64, 0, 3, // 3MHz DIV_SYSTEM_OPLL, 64, 0, 0, // 3.58MHz - // software controlled 8 bit DAC + DIV_SYSTEM_PCM_DAC, 64, 0, 7613|(7<<16), // software controlled 8 bit DAC 0 } )); @@ -1879,7 +1923,7 @@ void FurnaceGUI::initSystemPresets() { "Alpha denshi Alpha-68K (drums mode)", { DIV_SYSTEM_OPN, 64, 0, 3, // 3MHz DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, // 3.58MHz - // software controlled 8 bit DAC + DIV_SYSTEM_PCM_DAC, 64, 0, 7613|(7<<16), // software controlled 8 bit DAC 0 } )); @@ -1887,7 +1931,7 @@ void FurnaceGUI::initSystemPresets() { "Alpha denshi Alpha-68K (extended channel 3; drums mode)", { DIV_SYSTEM_OPN_EXT, 64, 0, 3, // 3MHz DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, // 3.58MHz - // software controlled 8 bit DAC + DIV_SYSTEM_PCM_DAC, 64, 0, 7613|(7<<16), // software controlled 8 bit DAC 0 } )); @@ -1929,10 +1973,27 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Namco System 86", { // without expansion board case; Hopping Mappy, etc + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_NAMCO_CUS30, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Namco Thunder Ceptor", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_NAMCO_CUS30, 64, 0, 0, + DIV_SYSTEM_PCM_DAC, 64, 0, 7999|(7<<16), // M65C02 software driven, correct sample rate? + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "Namco System 1", { DIV_SYSTEM_YM2151, 64, 0, 0, DIV_SYSTEM_NAMCO_CUS30, 64, 0, 0, + DIV_SYSTEM_PCM_DAC, 64, 0, 5999|(7<<16), // sample rate verified from https://github.com/mamedev/mame/blob/master/src/devices/sound/n63701x.cpp + DIV_SYSTEM_PCM_DAC, 64, 0, 5999|(7<<16), // "" 0 } )); @@ -2045,6 +2106,13 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Irem M72", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_PCM_DAC, 64, 0, 7811|(7<<16), + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("DefleMask-compatible","these configurations are compatible with DefleMask.\nselect this if you need to save as .dmf or work with that program."); diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 7264baed5..e9b5cfb35 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -41,7 +41,7 @@ void FurnaceGUI::drawSampleEdit() { } else { DivSample* sample=e->song.sample[curSample]; String sampleType="Invalid"; - if (sample->depth<17) { + if (sample->depthdepth]!=NULL) { sampleType=sampleDepths[sample->depth]; } @@ -61,11 +61,11 @@ void FurnaceGUI::drawSampleEdit() { ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::BeginCombo("##SampleType",sampleType.c_str())) { - for (int i=0; i<17; i++) { + for (int i=0; iprepareUndo(true); - sample->depth=i; + sample->depth=(DivSampleDepth)i; e->renderSamplesP(); updateSampleTex=true; MARK_MODIFIED; @@ -93,22 +93,43 @@ void FurnaceGUI::drawSampleEdit() { } ImGui::TableNextColumn(); - bool doLoop=(sample->loopStart>=0); + bool doLoop=(sample->isLoopable()); if (ImGui::Checkbox("Loop",&doLoop)) { MARK_MODIFIED if (doLoop) { sample->loopStart=0; + sample->loopEnd=sample->samples; } else { sample->loopStart=-1; + sample->loopEnd=sample->samples; } updateSampleTex=true; } if (doLoop) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Loop Start"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopPosition",&sample->loopStart,1,10)) { MARK_MODIFIED - if (sample->loopStart<0 || sample->loopStart>=(int)sample->samples) { + if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,10)) { MARK_MODIFIED + if (sample->loopStart<0) { sample->loopStart=0; } + if (sample->loopStart>sample->loopEnd) { + sample->loopStart=sample->loopEnd; + } + updateSampleTex=true; + } + ImGui::TableNextColumn(); + ImGui::Text("Loop End"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,10)) { MARK_MODIFIED + if (sample->loopEndloopStart) { + sample->loopEnd=sample->loopStart; + } + if (sample->loopEnd>=(int)sample->samples) { + sample->loopEnd=sample->samples; + } updateSampleTex=true; } } @@ -123,7 +144,7 @@ void FurnaceGUI::drawSampleEdit() { */ ImGui::Separator(); - ImGui::BeginDisabled(sample->depth!=8 && sample->depth!=16); + ImGui::BeginDisabled(sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT); ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(!sampleDragMode)); if (ImGui::Button(ICON_FA_I_CURSOR "##SSelect")) { @@ -275,14 +296,14 @@ void FurnaceGUI::drawSampleEdit() { SAMPLE_OP_BEGIN; float vol=amplifyVol/100.0f; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]*vol; if (val<-32768) val=-32768; if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]*vol; if (val<-128) val=-128; @@ -335,7 +356,7 @@ void FurnaceGUI::drawSampleEdit() { if (silenceSize<0) silenceSize=0; if (silenceSize>16777215) silenceSize=16777215; } - if (ImGui::Button("Resize")) { + if (ImGui::Button("Go")) { int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?sample->samples:sampleSelStart; sample->prepareUndo(true); e->lockEngine([this,sample,pos]() { @@ -466,7 +487,7 @@ void FurnaceGUI::drawSampleEdit() { double power=(sampleFilterCutStart>sampleFilterCutEnd)?0.5:2.0; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; irate))*M_PI); @@ -482,7 +503,7 @@ void FurnaceGUI::drawSampleEdit() { if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; irate))*M_PI); @@ -512,14 +533,14 @@ void FurnaceGUI::drawSampleEdit() { ImGui::SameLine(); ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_UP "##PreviewSample")) { + if (ImGui::Button(ICON_FA_PLAY "##PreviewSample")) { e->previewSample(curSample); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Preview sample"); } ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_OFF "##StopSample")) { + if (ImGui::Button(ICON_FA_STOP "##StopSample")) { e->stopSamplePreview(); } if (ImGui::IsItemHovered()) { @@ -574,11 +595,11 @@ void FurnaceGUI::drawSampleEdit() { ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::BeginCombo("##SampleType",sampleType.c_str())) { - for (int i=0; i<17; i++) { + for (int i=0; iprepareUndo(true); - sample->depth=i; + sample->depth=(DivSampleDepth)i; e->renderSamplesP(); updateSampleTex=true; MARK_MODIFIED; @@ -607,22 +628,43 @@ void FurnaceGUI::drawSampleEdit() { } ImGui::TableNextColumn(); - bool doLoop=(sample->loopStart>=0); + bool doLoop=(sample->isLoopable()); if (ImGui::Checkbox("Loop",&doLoop)) { MARK_MODIFIED if (doLoop) { sample->loopStart=0; + sample->loopEnd=sample->samples; } else { sample->loopStart=-1; + sample->loopEnd=sample->samples; } updateSampleTex=true; } if (doLoop) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Loop Start"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopPosition",&sample->loopStart,1,10)) { MARK_MODIFIED - if (sample->loopStart<0 || sample->loopStart>=(int)sample->samples) { + if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,10)) { MARK_MODIFIED + if (sample->loopStart<0) { sample->loopStart=0; } + if (sample->loopStart>sample->loopEnd) { + sample->loopStart=sample->loopEnd; + } + updateSampleTex=true; + } + ImGui::TableNextColumn(); + ImGui::Text("Loop End"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,10)) { MARK_MODIFIED + if (sample->loopEndloopStart) { + sample->loopEnd=sample->loopStart; + } + if (sample->loopEnd>=(int)sample->samples) { + sample->loopEnd=sample->samples; + } updateSampleTex=true; } } @@ -637,7 +679,7 @@ void FurnaceGUI::drawSampleEdit() { */ ImGui::Separator(); - ImGui::BeginDisabled(sample->depth!=8 && sample->depth!=16); + ImGui::BeginDisabled(sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT); ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(!sampleDragMode)); if (ImGui::Button(ICON_FA_I_CURSOR "##SSelect")) { @@ -820,7 +862,7 @@ void FurnaceGUI::drawSampleEdit() { double power=(sampleFilterCutStart>sampleFilterCutEnd)?0.5:2.0; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; irate))*M_PI); @@ -836,7 +878,7 @@ void FurnaceGUI::drawSampleEdit() { if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; irate))*M_PI); @@ -890,14 +932,14 @@ void FurnaceGUI::drawSampleEdit() { SAMPLE_OP_BEGIN; float vol=amplifyVol/100.0f; - if (sample->depth==16) { + if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { for (unsigned int i=start; idata16[i]*vol; if (val<-32768) val=-32768; if (val>32767) val=32767; sample->data16[i]=val; } - } else if (sample->depth==8) { + } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { for (unsigned int i=start; idata8[i]*vol; if (val<-128) val=-128; @@ -991,7 +1033,7 @@ void FurnaceGUI::drawSampleEdit() { if (silenceSize<0) silenceSize=0; if (silenceSize>16777215) silenceSize=16777215; } - if (ImGui::Button("Resize")) { + if (ImGui::Button("Go")) { int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?sample->samples:sampleSelStart; sample->prepareUndo(true); e->lockEngine([this,sample,pos]() { @@ -1138,7 +1180,7 @@ void FurnaceGUI::drawSampleEdit() { for (int i=0; iloopStart>=0 && sample->loopStart<(int)sample->samples && scaledPos>=sample->loopStart) { + if (sample->isLoopable() && (scaledPos>=sample->loopStart && scaledPos<=sample->loopEnd)) { data[i*availX+j]=bgColorLoop; } else { data[i*availX+j]=bgColor; @@ -1158,7 +1200,7 @@ void FurnaceGUI::drawSampleEdit() { if (xCoarse>=sample->samples) break; int y1, y2; int totalAdvance=0; - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { y1=((unsigned char)sample->data8[xCoarse]^0x80)*availY/256; } else { y1=((unsigned short)sample->data16[xCoarse]^0x8000)*availY/65536; @@ -1171,7 +1213,7 @@ void FurnaceGUI::drawSampleEdit() { totalAdvance+=xAdvanceCoarse; if (xCoarse>=sample->samples) break; do { - if (sample->depth==8) { + if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { y2=((unsigned char)sample->data8[xCoarse]^0x80)*availY/256; } else { y2=((unsigned short)sample->data16[xCoarse]^0x8000)*availY/65536; @@ -1209,11 +1251,11 @@ void FurnaceGUI::drawSampleEdit() { sampleSelStart=0; sampleSelEnd=sample->samples; } else { - if (sample->samples>0 && (sample->depth==16 || sample->depth==8)) { + if (sample->samples>0 && (sample->depth==DIV_SAMPLE_DEPTH_16BIT || sample->depth==DIV_SAMPLE_DEPTH_8BIT)) { sampleDragStart=rectMin; sampleDragAreaSize=rectSize; - sampleDrag16=(sample->depth==16); - sampleDragTarget=(sample->depth==16)?((void*)sample->data16):((void*)sample->data8); + sampleDrag16=(sample->depth==DIV_SAMPLE_DEPTH_16BIT); + sampleDragTarget=(sample->depth==DIV_SAMPLE_DEPTH_16BIT)?((void*)sample->data16):((void*)sample->data8); sampleDragLen=sample->samples; sampleDragActive=true; sampleSelStart=-1; @@ -1312,7 +1354,7 @@ void FurnaceGUI::drawSampleEdit() { posX=samplePos+pos.x*sampleZoom; if (posX>(int)sample->samples) posX=-1; } - posY=(0.5-pos.y/rectSize.y)*((sample->depth==8)?255:32767); + posY=(0.5-pos.y/rectSize.y)*((sample->depth==DIV_SAMPLE_DEPTH_8BIT)?255:32767); if (posX>=0) { statusBar+=fmt::sprintf(" | (%d, %d)",posX,posY); } @@ -1362,7 +1404,7 @@ void FurnaceGUI::drawSampleEdit() { } } - if (sample->depth!=8 && sample->depth!=16) { + if (sample->depth!=DIV_SAMPLE_DEPTH_8BIT && sample->depth!=DIV_SAMPLE_DEPTH_16BIT) { statusBar="Non-8/16-bit samples cannot be edited without prior conversion."; } diff --git a/src/gui/sampleUtil.h b/src/gui/sampleUtil.h index 981a56046..187f5ebf6 100644 --- a/src/gui/sampleUtil.h +++ b/src/gui/sampleUtil.h @@ -20,6 +20,10 @@ #define SAMPLE_OP_BEGIN \ unsigned int start=0; \ unsigned int end=sample->samples; \ + if (sampleSelStart<0) sampleSelStart=0; \ + if (sampleSelStart>(int)sample->samples) sampleSelStart=sample->samples; \ + if (sampleSelEnd<0) sampleSelEnd=0; \ + if (sampleSelEnd>(int)sample->samples) sampleSelEnd=sample->samples; \ if (sampleSelStart!=-1 && sampleSelEnd!=-1 && sampleSelStart!=sampleSelEnd) { \ start=sampleSelStart; \ end=sampleSelEnd; \ diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 579d8715c..b2859f7ff 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -21,6 +21,7 @@ #include "fonts.h" #include "../ta-log.h" #include "../fileutils.h" +#include "../utfutils.h" #include "util.h" #include "guiConst.h" #include "intConst.h" @@ -39,6 +40,13 @@ #define POWER_SAVE_DEFAULT 0 #endif +#ifdef __HAIKU__ +// NFD doesn't support Haiku +#define SYS_FILE_DIALOG_DEFAULT 0 +#else +#define SYS_FILE_DIALOG_DEFAULT 1 +#endif + const char* mainFonts[]={ "IBM Plex Sans", "Liberation Sans", @@ -252,9 +260,9 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); - ImGui::Text("Initial system/chips:"); + ImGui::Text("Initial system:"); ImGui::SameLine(); - if (ImGui::Button("Current systems")) { + if (ImGui::Button("Current system")) { settings.initialSys.clear(); for (int i=0; isong.systemLen; i++) { settings.initialSys.push_back(e->song.system[i]); @@ -262,6 +270,7 @@ void FurnaceGUI::drawSettings() { settings.initialSys.push_back(e->song.systemPan[i]); settings.initialSys.push_back(e->song.systemFlags[i]); } + settings.initialSysName=e->song.systemName; } ImGui::SameLine(); if (ImGui::Button("Randomize")) { @@ -282,6 +291,31 @@ void FurnaceGUI::drawSettings() { settings.initialSys.push_back(0); settings.initialSys.push_back(0); } + // randomize system name + std::vector wordPool[6]; + for (size_t i=0; igetSystemName((DivSystem)settings.initialSys[i*4]); + String nameWord; + sName+=" "; + for (char& i: sName) { + if (i==' ') { + if (nameWord!="") { + wordPool[wpPos++].push_back(nameWord); + if (wpPos>=6) break; + nameWord=""; + } + } else { + nameWord+=i; + } + } + } + settings.initialSysName=""; + for (int i=0; i<6; i++) { + if (wordPool[i].empty()) continue; + settings.initialSysName+=wordPool[i][rand()%wordPool[i].size()]; + settings.initialSysName+=" "; + } } ImGui::SameLine(); if (ImGui::Button("Reset to defaults")) { @@ -294,7 +328,14 @@ void FurnaceGUI::drawSettings() { settings.initialSys.push_back(32); settings.initialSys.push_back(0); settings.initialSys.push_back(0); + settings.initialSysName="Sega Genesis/Mega Drive"; } + + ImGui::Text("Name"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("##InitSysName",&settings.initialSysName); + for (size_t i=0; igetAudioDescWant(); TAAudioDesc& audioGot=e->getAudioDescGot(); @@ -1028,6 +1087,15 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); + ImGui::Text("Pattern view labels:"); + ImGui::InputTextWithHint("Note off (3-char)","OFF",&settings.noteOffLabel); + ImGui::InputTextWithHint("Note release (3-char)","===",&settings.noteRelLabel); + ImGui::InputTextWithHint("Macro release (3-char)","REL",&settings.macroRelLabel); + ImGui::InputTextWithHint("Empty field (3-char)","...",&settings.emptyLabel); + ImGui::InputTextWithHint("Empty field (2-char)","..",&settings.emptyLabel2); + + ImGui::Separator(); + ImGui::Text("Orders row number format:"); if (ImGui::RadioButton("Decimal##orbD",settings.orderRowsBase==0)) { settings.orderRowsBase=0; @@ -1148,6 +1216,12 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); + ImGui::Text("Namco 163 chip name"); + ImGui::SameLine(); + ImGui::InputTextWithHint("##C163Name",DIV_C163_DEFAULT_NAME,&settings.c163Name); + + ImGui::Separator(); + bool insEditColorizeB=settings.insEditColorize; if (ImGui::Checkbox("Colorize instrument editor using instrument type",&insEditColorizeB)) { settings.insEditColorize=insEditColorizeB; @@ -1173,11 +1247,6 @@ void FurnaceGUI::drawSettings() { } ImGui::EndDisabled(); - bool chipNamesB=settings.chipNames; - if (ImGui::Checkbox("Use chip names instead of system names",&chipNamesB)) { - settings.chipNames=chipNamesB; - } - bool oplStandardWaveNamesB=settings.oplStandardWaveNames; if (ImGui::Checkbox("Use standard OPL waveform names",&oplStandardWaveNamesB)) { settings.oplStandardWaveNames=oplStandardWaveNamesB; @@ -1446,7 +1515,11 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_OPL,"FM (OPL)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_FDS,"FDS"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_VBOY,"Virtual Boy"); - UI_COLOR_CONFIG(GUI_COLOR_INSTR_N163,"Namco 163"); + // special case + String c163Label=fmt::sprintf("%s##CC_GUI_COLOR_INSTR_N163",settings.c163Name); + if (ImGui::ColorEdit4(c163Label.c_str(),(float*)&uiColors[GUI_COLOR_INSTR_N163])) { + applyUISettings(false); + } UI_COLOR_CONFIG(GUI_COLOR_INSTR_SCC,"Konami SCC"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_OPZ,"FM (OPZ)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_POKEY,"POKEY"); @@ -1505,12 +1578,21 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SONG,"Song effect"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_TIME,"Time effect"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SPEED,"Speed effect"); - UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY,"Primary system effect"); - UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY,"Secondary system effect"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY,"Primary specific effect"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY,"Secondary specific effect"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_EFFECT_MISC,"Miscellaneous"); UI_COLOR_CONFIG(GUI_COLOR_EE_VALUE,"External command output"); ImGui::TreePop(); } + if (ImGui::TreeNode("Pattern Manager")) { + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_NULL,"Unallocated"); + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_UNUSED,"Unused"); + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_USED,"Used"); + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_OVERUSED,"Overused"); + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_EXTREMELY_OVERUSED,"Really overused"); + UI_COLOR_CONFIG(GUI_COLOR_PAT_MANAGER_COMBO_BREAKER,"Combo Breaker"); + ImGui::TreePop(); + } if (ImGui::TreeNode("Piano")) { UI_COLOR_CONFIG(GUI_COLOR_PIANO_BACKGROUND,"Background"); UI_COLOR_CONFIG(GUI_COLOR_PIANO_KEY_TOP,"Upper key"); @@ -1603,6 +1685,7 @@ void FurnaceGUI::drawSettings() { UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_DEBUG); UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_OSCILLOSCOPE); UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_CHAN_OSC); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_EFFECT_LIST); UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_VOL_METER); UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_STATS); UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_COMPAT_FLAGS); @@ -1649,7 +1732,7 @@ void FurnaceGUI::drawSettings() { ImGui::Text("%s",SDL_GetScancodeName((SDL_Scancode)i.scan)); ImGui::TableNextColumn(); if (i.val==102) { - snprintf(id,4095,"Envelope release##SNType_%d",i.scan); + snprintf(id,4095,"Macro release##SNType_%d",i.scan); if (ImGui::Button(id)) { noteKeys[i.scan]=0; } @@ -1965,6 +2048,7 @@ void FurnaceGUI::syncSettings() { settings.audioDevice=e->getConfString("audioDevice",""); settings.midiInDevice=e->getConfString("midiInDevice",""); settings.midiOutDevice=e->getConfString("midiOutDevice",""); + settings.c163Name=e->getConfString("c163Name",DIV_C163_DEFAULT_NAME); settings.audioQuality=e->getConfInt("audioQuality",0); settings.audioBufSize=e->getConfInt("audioBufSize",1024); settings.audioRate=e->getConfInt("audioRate",44100); @@ -2009,7 +2093,7 @@ void FurnaceGUI::syncSettings() { settings.insFocusesPattern=e->getConfInt("insFocusesPattern",1); settings.stepOnInsert=e->getConfInt("stepOnInsert",0); settings.unifiedDataView=e->getConfInt("unifiedDataView",0); - settings.sysFileDialog=e->getConfInt("sysFileDialog",1); + settings.sysFileDialog=e->getConfInt("sysFileDialog",SYS_FILE_DIALOG_DEFAULT); settings.roundedWindows=e->getConfInt("roundedWindows",1); settings.roundedButtons=e->getConfInt("roundedButtons",1); settings.roundedMenus=e->getConfInt("roundedMenus",0); @@ -2056,6 +2140,15 @@ void FurnaceGUI::syncSettings() { settings.blankIns=e->getConfInt("blankIns",0); settings.dragMovesSelection=e->getConfInt("dragMovesSelection",2); settings.unsignedDetune=e->getConfInt("unsignedDetune",0); + settings.noThreadedInput=e->getConfInt("noThreadedInput",0); + settings.initialSysName=e->getConfString("initialSysName",""); + settings.clampSamples=e->getConfInt("clampSamples",0); + settings.noteOffLabel=e->getConfString("noteOffLabel","OFF"); + settings.noteRelLabel=e->getConfString("noteRelLabel","==="); + settings.macroRelLabel=e->getConfString("macroRelLabel","REL"); + settings.emptyLabel=e->getConfString("emptyLabel","..."); + settings.emptyLabel2=e->getConfString("emptyLabel2",".."); + settings.saveUnusedPatterns=e->getConfInt("saveUnusedPatterns",0); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -2142,6 +2235,9 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.blankIns,0,1); clampSetting(settings.dragMovesSelection,0,2); clampSetting(settings.unsignedDetune,0,1); + clampSetting(settings.noThreadedInput,0,1); + clampSetting(settings.clampSamples,0,1); + clampSetting(settings.saveUnusedPatterns,0,1); settings.initialSys=e->decodeSysDesc(e->getConfString("initialSys","")); if (settings.initialSys.size()<4) { @@ -2185,6 +2281,7 @@ void FurnaceGUI::commitSettings() { e->setConf("audioDevice",settings.audioDevice); e->setConf("midiInDevice",settings.midiInDevice); e->setConf("midiOutDevice",settings.midiOutDevice); + e->setConf("c163Name",settings.c163Name); e->setConf("audioQuality",settings.audioQuality); e->setConf("audioBufSize",settings.audioBufSize); e->setConf("audioRate",settings.audioRate); @@ -2264,6 +2361,7 @@ void FurnaceGUI::commitSettings() { e->setConf("moveWindowTitle",settings.moveWindowTitle); e->setConf("hiddenSystems",settings.hiddenSystems); e->setConf("initialSys",e->encodeSysDesc(settings.initialSys)); + e->setConf("initialSysName",settings.initialSysName); e->setConf("horizontalDataView",settings.horizontalDataView); e->setConf("noMultiSystem",settings.noMultiSystem); e->setConf("oldMacroVSlider",settings.oldMacroVSlider); @@ -2277,6 +2375,14 @@ void FurnaceGUI::commitSettings() { e->setConf("blankIns",settings.blankIns); e->setConf("dragMovesSelection",settings.dragMovesSelection); e->setConf("unsignedDetune",settings.unsignedDetune); + e->setConf("noThreadedInput",settings.noThreadedInput); + e->setConf("clampSamples",settings.clampSamples); + e->setConf("noteOffLabel",settings.noteOffLabel); + e->setConf("noteRelLabel",settings.noteRelLabel); + e->setConf("macroRelLabel",settings.macroRelLabel); + e->setConf("emptyLabel",settings.emptyLabel); + e->setConf("emptyLabel2",settings.emptyLabel2); + e->setConf("saveUnusedPatterns",settings.saveUnusedPatterns); // colors for (int i=0; i=0.5f) dpiScale=settings.dpiScale; // colors diff --git a/src/gui/songInfo.cpp b/src/gui/songInfo.cpp index e90043b7a..44982abaf 100644 --- a/src/gui/songInfo.cpp +++ b/src/gui/songInfo.cpp @@ -64,6 +64,25 @@ void FurnaceGUI::drawSongInfo() { if (ImGui::InputText("##Author",&e->song.author)) { MARK_MODIFIED; } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Album"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputText("##Category",&e->song.category)) { + MARK_MODIFIED; + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("System"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputText("##SystemName",&e->song.systemName)) { + MARK_MODIFIED; + updateWindowTitle(); + } + ImGui::EndTable(); } @@ -176,7 +195,7 @@ void FurnaceGUI::drawSongInfo() { float setHz=tempoView?e->curSubSong->hz*2.5:e->curSubSong->hz; if (ImGui::InputFloat("##Rate",&setHz,1.0f,1.0f,"%g")) { MARK_MODIFIED if (tempoView) setHz/=2.5; - if (setHz<10) setHz=10; + if (setHz<1) setHz=1; if (setHz>999) setHz=999; e->setSongRate(setHz,setHz<52); } diff --git a/src/gui/subSongs.cpp b/src/gui/subSongs.cpp index b13b84204..c56bf6405 100644 --- a/src/gui/subSongs.cpp +++ b/src/gui/subSongs.cpp @@ -2,6 +2,7 @@ #include "imgui.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include "intConst.h" void FurnaceGUI::drawSubSongs() { if (nextWindow==GUI_WINDOW_SUBSONGS) { @@ -11,7 +12,7 @@ void FurnaceGUI::drawSubSongs() { } if (!subSongsOpen) return; ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - if (ImGui::Begin("Subsongs",&subSongsOpen,ImGuiWindowFlags_NoScrollWithMouse|ImGuiWindowFlags_NoScrollbar|globalWinFlags)) { + if (ImGui::Begin("Subsongs",&subSongsOpen,globalWinFlags)) { char id[1024]; ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::GetFrameHeightWithSpacing()*2.0f-ImGui::GetStyle().ItemSpacing.x); if (e->curSubSong->name.empty()) { diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 9cd963cc3..a6bbcb921 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -109,6 +109,104 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool } break; } + case DIV_SYSTEM_PCE: { + sysPal=flags&1; + if (ImGui::Checkbox("Pseudo-PAL",&sysPal)) { + copyOfFlags=(flags&(~1))|(unsigned int)sysPal; + } + bool antiClick=flags&8; + if (ImGui::Checkbox("Disable anti-click",&antiClick)) { + copyOfFlags=(flags&(~8))|(antiClick<<3); + } + ImGui::Text("Chip revision:"); + if (ImGui::RadioButton("HuC6280 (original)",(flags&4)==0)) { + copyOfFlags=(flags&(~4))|0; + } + if (ImGui::RadioButton("HuC6280A (SuperGrafx)",(flags&4)==4)) { + copyOfFlags=(flags&(~4))|4; + } + break; + } + case DIV_SYSTEM_SOUND_UNIT: { + ImGui::Text("CPU rate:"); + if (ImGui::RadioButton("6.18MHz (NTSC)",(flags&3)==0)) { + copyOfFlags=(flags&(~3))|0; + } + if (ImGui::RadioButton("5.95MHz (PAL)",(flags&3)==1)) { + copyOfFlags=(flags&(~3))|1; + } + ImGui::Text("Sample memory:"); + if (ImGui::RadioButton("8K (rev A/B/E)",(flags&16)==0)) { + copyOfFlags=(flags&(~16))|0; + } + if (ImGui::RadioButton("64K (rev D/F)",(flags&16)==16)) { + copyOfFlags=(flags&(~16))|16; + } + ImGui::Text("DAC resolution"); + if (ImGui::RadioButton("16-bit (rev A/B/D/F)",(flags&32)==0)) { + copyOfFlags=(flags&(~32))|0; + } + if (ImGui::RadioButton("1-bit PDM (rev C/E)",(flags&32)==32)) { + copyOfFlags=(flags&(~32))|32; + } + bool echo=flags&4; + if (ImGui::Checkbox("Enable echo",&echo)) { + copyOfFlags=(flags&(~4))|(echo<<2); + } + bool flipEcho=flags&8; + if (ImGui::Checkbox("Swap echo channels",&flipEcho)) { + copyOfFlags=(flags&(~8))|(flipEcho<<3); + } + ImGui::Text("Echo delay:"); + int echoBufSize=(flags&0x3f00)>>8; + if (CWSliderInt("##EchoBufSize",&echoBufSize,0,63)) { + if (echoBufSize<0) echoBufSize=0; + if (echoBufSize>63) echoBufSize=63; + copyOfFlags=(flags&~0x3f00)|(echoBufSize<<8); + } rightClickable + ImGui::Text("Echo resolution:"); + int echoResolution=(flags&0xf00000)>>20; + if (CWSliderInt("##EchoResolution",&echoResolution,0,15)) { + if (echoResolution<0) echoResolution=0; + if (echoResolution>15) echoResolution=15; + copyOfFlags=(flags&(~0xf00000))|(echoResolution<<20); + } rightClickable + ImGui::Text("Echo feedback:"); + int echoFeedback=(flags&0xf0000)>>16; + if (CWSliderInt("##EchoFeedback",&echoFeedback,0,15)) { + if (echoFeedback<0) echoFeedback=0; + if (echoFeedback>15) echoFeedback=15; + copyOfFlags=(flags&(~0xf0000))|(echoFeedback<<16); + } rightClickable + ImGui::Text("Echo volume:"); + int echoVolume=(signed char)((flags&0xff000000)>>24); + if (CWSliderInt("##EchoVolume",&echoVolume,-128,127)) { + if (echoVolume<-128) echoVolume=-128; + if (echoVolume>127) echoVolume=127; + copyOfFlags=(flags&(~0xff000000))|(((unsigned char)echoVolume)<<24); + } rightClickable + break; + } + case DIV_SYSTEM_GB: { + bool antiClick=flags&8; + if (ImGui::Checkbox("Disable anti-click",&antiClick)) { + copyOfFlags=(flags&(~8))|(antiClick<<3); + } + ImGui::Text("Chip revision:"); + if (ImGui::RadioButton("Original (DMG)",(flags&7)==0)) { + copyOfFlags=(flags&(~7))|0; + } + if (ImGui::RadioButton("Game Boy Color (rev C)",(flags&7)==1)) { + copyOfFlags=(flags&(~7))|1; + } + if (ImGui::RadioButton("Game Boy Color (rev E)",(flags&7)==2)) { + copyOfFlags=(flags&(~7))|2; + } + if (ImGui::RadioButton("Game Boy Advance",(flags&7)==3)) { + copyOfFlags=(flags&(~7))|3; + } + break; + } case DIV_SYSTEM_OPLL: case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_VRC7: { @@ -594,7 +692,7 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool if (ImGui::RadioButton("14.32MHz (NTSC)",(flags&255)==1)) { copyOfFlags=(flags&(~255))|1; } - if (ImGui::RadioButton("14.19MHz (PAL)",(flags&255)==3)) { + if (ImGui::RadioButton("14.19MHz (PAL)",(flags&255)==2)) { copyOfFlags=(flags&(~255))|2; } if (ImGui::RadioButton("16MHz",(flags&255)==3)) { @@ -609,16 +707,18 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool break; } case DIV_SYSTEM_PCM_DAC: { + // default to 44100Hz 16-bit stereo + if (!flags) copyOfFlags=flags=0x1f0000|44099; int sampRate=(flags&65535)+1; int bitDepth=((flags>>16)&15)+1; bool stereo=(flags>>20)&1; ImGui::Text("Output rate:"); - if (CWSliderInt("##SampRate",&sampRate,1,65536)) { - if (sampRate<1) sampRate=1; + if (CWSliderInt("##SampRate",&sampRate,1000,65536)) { + if (sampRate<1000) sampRate=1000; if (sampRate>65536) sampRate=65536; copyOfFlags=(flags&(~65535))|(sampRate-1); } rightClickable - ImGui::Text("Output depth:"); + ImGui::Text("Output bit depth:"); if (CWSliderInt("##BitDepth",&bitDepth,1,16)) { if (bitDepth<1) bitDepth=1; if (bitDepth>16) bitDepth=16; @@ -629,7 +729,6 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool } break; } - case DIV_SYSTEM_GB: case DIV_SYSTEM_SWAN: case DIV_SYSTEM_VERA: case DIV_SYSTEM_BUBSYS_WSG: diff --git a/src/gui/waveEdit.cpp b/src/gui/waveEdit.cpp index 4ff2182e2..7e453ef58 100644 --- a/src/gui/waveEdit.cpp +++ b/src/gui/waveEdit.cpp @@ -17,12 +17,93 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#define _USE_MATH_DEFINES #include "gui.h" #include "plot_nolerp.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include #include +const char* waveGenBaseShapes[4]={ + "Sine", + "Triangle", + "Saw", + "Pulse" +}; + +void FurnaceGUI::doGenerateWave() { + float finalResult[256]; + if (curWave<0 || curWave>=(int)e->song.wave.size()) return; + + DivWavetable* wave=e->song.wave[curWave]; + memset(finalResult,0,sizeof(float)*256); + + if (wave->len<2) return; + + if (waveGenFM) { + + } else { + switch (waveGenBaseShape) { + case 0: // sine + for (int i=0; ilen; i++) { + for (int j=0; j<16; j++) { + float pos=fmod((waveGenPhase[j]*wave->len)+(i*(j+1)),wave->len); + float partial=sin((0.5+pos)*2.0*M_PI/(double)wave->len); + partial=pow(partial,waveGenPower); + partial*=waveGenAmp[j]; + finalResult[i]+=partial; + } + } + break; + case 1: // triangle + for (int i=0; ilen; i++) { + for (int j=0; j<16; j++) { + float pos=fmod((waveGenPhase[j]*wave->len)+(i*(j+1)),wave->len); + float partial=4.0*(0.5-fabs(0.5-(pos/(double)(wave->len-1))))-1.0; + partial=pow(partial,waveGenPower); + partial*=waveGenAmp[j]; + finalResult[i]+=partial; + } + } + break; + case 2: // saw + for (int i=0; ilen; i++) { + for (int j=0; j<16; j++) { + float pos=fmod((waveGenPhase[j]*wave->len)+(i*(j+1)),wave->len); + float partial=((2*pos)/(double)(wave->len-1))-1.0; + partial=pow(partial,waveGenPower); + partial*=waveGenAmp[j]; + finalResult[i]+=partial; + } + } + break; + case 3: // pulse + for (int i=0; ilen; i++) { + for (int j=0; j<16; j++) { + float pos=fmod((waveGenPhase[j]*wave->len)+(i*(j+1)),wave->len); + float partial=(pos>=(waveGenDuty*wave->len))?1:-1; + partial=pow(partial,waveGenPower); + partial*=waveGenAmp[j]; + finalResult[i]+=partial; + } + } + break; + } + } + + for (int i=waveGenInvertPoint*wave->len; ilen; i++) { + finalResult[i]=-finalResult[i]; + } + + for (int i=0; ilen; i++) { + finalResult[i]=(1.0+finalResult[i])*0.5; + if (finalResult[i]<0.0f) finalResult[i]=0.0f; + if (finalResult[i]>1.0f) finalResult[i]=1.0f; + wave->data[i]=round(finalResult[i]*wave->max); + } +} + void FurnaceGUI::drawWaveEdit() { if (nextWindow==GUI_WINDOW_WAVE_EDIT) { waveEditOpen=true; @@ -36,31 +117,44 @@ void FurnaceGUI::drawWaveEdit() { if (curWave<0 || curWave>=(int)e->song.wave.size()) { ImGui::Text("no wavetable selected"); } else { - ImGui::SetNextItemWidth(80.0f*dpiScale); - if (ImGui::InputInt("##CurWave",&curWave,1,1)) { - if (curWave<0) curWave=0; - if (curWave>=(int)e->song.wave.size()) curWave=e->song.wave.size()-1; - } - ImGui::SameLine(); - // TODO: load replace - if (ImGui::Button(ICON_FA_FOLDER_OPEN "##WELoad")) { - doAction(GUI_ACTION_WAVE_LIST_OPEN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FLOPPY_O "##WESave")) { - doAction(GUI_ACTION_WAVE_LIST_SAVE); - } - ImGui::SameLine(); - DivWavetable* wave=e->song.wave[curWave]; - - if (!settings.waveLayout){ - ImGui::Text("Width"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a width of:\n- any on Amiga/N163\n- 32 on Game Boy, PC Engine and WonderSwan\n- 64 on FDS\n- 128 on X1-010\nany other widths will be scaled during playback."); + + if (ImGui::BeginTable("WEProps",2)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(80.0f*dpiScale); + if (ImGui::InputInt("##CurWave",&curWave,1,1)) { + if (curWave<0) curWave=0; + if (curWave>=(int)e->song.wave.size()) curWave=e->song.wave.size()-1; } ImGui::SameLine(); - ImGui::SetNextItemWidth(128.0f*dpiScale); + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##WELoad")) { + doAction(GUI_ACTION_WAVE_LIST_OPEN_REPLACE); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FLOPPY_O "##WESave")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE); + } + ImGui::SameLine(); + + if (ImGui::RadioButton("Steps",waveEditStyle==0)) { + waveEditStyle=0; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Lines",waveEditStyle==1)) { + waveEditStyle=1; + } + + ImGui::TableNextColumn(); + ImGui::Text("Width"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("use a width of:\n- any on Amiga/N163\n- 32 on Game Boy, PC Engine, SCC, Konami Bubble System, Namco WSG and WonderSwan\n- 64 on FDS\n- 128 on X1-010\nany other widths will be scaled during playback."); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(96.0f*dpiScale); if (ImGui::InputInt("##_WTW",&wave->len,1,2)) { if (wave->len>256) wave->len=256; if (wave->len<1) wave->len=1; @@ -71,59 +165,23 @@ void FurnaceGUI::drawWaveEdit() { ImGui::SameLine(); ImGui::Text("Height"); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a height of:\n- 15 for Game Boy, WonderSwan, X1-010 Envelope shape and N163\n- 31 for PC Engine\n- 63 for FDS\n- 255 for X1-010\nany other heights will be scaled during playback."); + ImGui::SetTooltip("use a height of:\n- 15 for Game Boy, WonderSwan, Namco WSG, Konami Bubble System, X1-010 Envelope shape and N163\n- 31 for PC Engine\n- 63 for FDS\n- 255 for X1-010 and SCC\nany other heights will be scaled during playback."); } ImGui::SameLine(); - ImGui::SetNextItemWidth(128.0f*dpiScale); + ImGui::SetNextItemWidth(96.0f*dpiScale); if (ImGui::InputInt("##_WTH",&wave->max,1,2)) { if (wave->max>255) wave->max=255; if (wave->max<1) wave->max=1; e->notifyWaveChange(curWave); MARK_MODIFIED; } - } - ImGui::SameLine(); - if (ImGui::RadioButton("Dec",!waveHex)) { - waveHex=false; - } - ImGui::SameLine(); - if (ImGui::RadioButton("Hex",waveHex)) { - waveHex=true; - } - if (settings.waveLayout){ - if (ImGui::BeginTable("WaveProps",2,ImGuiTableFlags_SizingStretchSame)) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Width"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a width of:\n- any on Amiga/N163\n- 32 on Game Boy, PC Engine and WonderSwan\n- 64 on FDS\n- 128 on X1-010\nany other widths will be scaled during playback."); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##_WTW",&wave->len,1,2)) { - if (wave->len>256) wave->len=256; - if (wave->len<1) wave->len=1; - e->notifyWaveChange(curWave); - if (wavePreviewOn) e->previewWave(curWave,wavePreviewNote); - MARK_MODIFIED; - } - ImGui::TableNextColumn(); - ImGui::SameLine(); - ImGui::Text("Height"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a height of:\n- 15 for Game Boy, WonderSwan, X1-010 Envelope shape and N163\n- 31 for PC Engine\n- 63 for FDS\n- 255 for X1-010\nany other heights will be scaled during playback."); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##_WTH",&wave->max,1,2)) { - if (wave->max>255) wave->max=255; - if (wave->max<1) wave->max=1; - e->notifyWaveChange(curWave); - MARK_MODIFIED; - } - ImGui::EndTable(); + ImGui::SameLine(); + if (ImGui::Button(waveGenVisible?(ICON_FA_CHEVRON_RIGHT "##WEWaveGen"):(ICON_FA_CHEVRON_LEFT "##WEWaveGen"))) { + waveGenVisible=!waveGenVisible; } + + ImGui::EndTable(); } for (int i=0; ilen; i++) { @@ -131,6 +189,149 @@ void FurnaceGUI::drawWaveEdit() { wavePreview[i]=wave->data[i]; } if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + + if (ImGui::BeginTable("WEWaveSection",waveGenVisible?2:1)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); + if (waveGenVisible) ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,250.0f*dpiScale); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); + + ImVec2 contentRegion=ImGui::GetContentRegionAvail(); // wavetable graph size determined here + contentRegion.y-=ImGui::GetFrameHeightWithSpacing()+ImGui::GetStyle().WindowPadding.y; + if (waveEditStyle) { + PlotNoLerp("##Waveform",wavePreview,wave->len+1,0,NULL,0,wave->max,contentRegion); + } else { + PlotCustom("##Waveform",wavePreview,wave->len,0,NULL,0,wave->max,contentRegion,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,true); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + waveDragStart=ImGui::GetItemRectMin(); + waveDragAreaSize=contentRegion; + waveDragMin=0; + waveDragMax=wave->max; + waveDragLen=wave->len; + waveDragActive=true; + waveDragTarget=wave->data; + processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); + e->notifyWaveChange(curWave); + modified=true; + } + ImGui::PopStyleVar(); + + if (waveGenVisible) { + ImGui::TableNextColumn(); + + if (ImGui::BeginTabBar("WaveGenOpt")) { + if (ImGui::BeginTabItem("Shapes")) { + waveGenFM=false; + + if (waveGenBaseShape<0) waveGenBaseShape=0; + if (waveGenBaseShape>3) waveGenBaseShape=3; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##WGShape",&waveGenBaseShape,0,3,waveGenBaseShapes[waveGenBaseShape])) { + if (waveGenBaseShape<0) waveGenBaseShape=0; + if (waveGenBaseShape>3) waveGenBaseShape=3; + doGenerateWave(); + } + + if (ImGui::BeginTable("WGShapeProps",2)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Duty"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderFloat("##WGDuty",&waveGenDuty,0.0f,1.0f)) { + doGenerateWave(); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Exponent"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##WGExp",&waveGenPower,1,8)) { + doGenerateWave(); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("XOR Point"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderFloat("##WGXOR",&waveGenInvertPoint,0.0f,1.0f)) { + doGenerateWave(); + } + + ImGui::EndTable(); + } + + if (ImGui::TreeNode("Amplitude/Phase")) { + if (ImGui::BeginTable("WGShapeProps",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.6f); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.4f); + + for (int i=0; i<16; i++) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d",i+1); + ImGui::TableNextColumn(); + ImGui::PushID(140+i); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderFloat("##WGAmp",&waveGenAmp[i],-1.0f,1.0f)) { + doGenerateWave(); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { + waveGenAmp[i]=0.0f; + doGenerateWave(); + } + ImGui::PopID(); + ImGui::TableNextColumn(); + ImGui::PushID(140+i); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderFloat("##WGPhase",&waveGenPhase[i],0.0f,1.0f)) { + doGenerateWave(); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { + waveGenPhase[i]=0.0f; + doGenerateWave(); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + } + ImGui::TreePop(); + } + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("FM")) { + waveGenFM=true; + + ImGui::Text("FM stuff here"); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Mangle")) { + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + ImGui::EndTable(); + } + + if (ImGui::RadioButton("Dec",!waveHex)) { + waveHex=false; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Hex",waveHex)) { + waveHex=true; + } + ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); // wavetable text input size found here if (ImGui::InputText("##MMLWave",&mmlStringW)) { decodeMMLStrW(mmlStringW,wave->data,wave->len,wave->max,waveHex); @@ -138,27 +339,6 @@ void FurnaceGUI::drawWaveEdit() { if (!ImGui::IsItemActive()) { encodeMMLStr(mmlStringW,wave->data,wave->len,-1,-1,waveHex); } - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - - ImVec2 contentRegion=ImGui::GetContentRegionAvail(); // wavetable graph size determined here - if (ImGui::GetContentRegionAvail().y > (ImGui::GetContentRegionAvail().x / 2.0f)) { - contentRegion=ImVec2(ImGui::GetContentRegionAvail().x,ImGui::GetContentRegionAvail().x / 2.0f); - } - PlotNoLerp("##Waveform",wavePreview,wave->len+1,0,NULL,0,wave->max,contentRegion); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - waveDragStart=ImGui::GetItemRectMin(); - waveDragAreaSize=contentRegion; - waveDragMin=0; - waveDragMax=wave->max; - waveDragLen=wave->len; - waveDragActive=true; - waveDragTarget=wave->data; - processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); - e->notifyWaveChange(curWave); - modified=true; - } - ImGui::PopStyleVar(); } } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_WAVE_EDIT; diff --git a/src/main.cpp b/src/main.cpp index e89f6bdc5..484de5451 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,7 @@ #include #include #include -#ifdef HAVE_GUI +#ifdef HAVE_SDL2 #include "SDL_events.h" #endif #include "ta-log.h" @@ -36,6 +36,8 @@ #include #endif +#include "cli/cli.h" + #ifdef HAVE_GUI #include "gui/gui.h" #endif @@ -46,10 +48,14 @@ DivEngine e; FurnaceGUI g; #endif +FurnaceCLI cli; + String outName; String vgmOutName; String zsmOutName; +String cmdOutName; int loops=1; +int benchMode=0; DivAudioExportModes outMode=DIV_EXPORT_MODE_ONE; #ifdef HAVE_GUI @@ -59,6 +65,7 @@ bool consoleMode=true; #endif bool displayEngineFailError=false; +bool cmdOutBinary=false; std::vector params; @@ -110,6 +117,11 @@ TAParamResult pConsole(String val) { return TA_PARAM_SUCCESS; } +TAParamResult pBinary(String val) { + cmdOutBinary=true; + return TA_PARAM_SUCCESS; +} + TAParamResult pLogLevel(String val) { if (val=="trace") { logLevel=LOGLEVEL_TRACE; @@ -221,6 +233,19 @@ TAParamResult pOutMode(String val) { return TA_PARAM_SUCCESS; } +TAParamResult pBenchmark(String val) { + if (val=="render") { + benchMode=1; + } else if (val=="seek") { + benchMode=2; + } else { + logE("invalid value for benchmark! valid values are: render and seek."); + return TA_PARAM_ERROR; + } + e.setAudio(DIV_AUDIO_DUMMY); + return TA_PARAM_SUCCESS; +} + TAParamResult pOutput(String val) { outName=val; e.setAudio(DIV_AUDIO_DUMMY); @@ -239,6 +264,12 @@ TAParamResult pZSMOut(String val) { return TA_PARAM_SUCCESS; } +TAParamResult pCmdOut(String val) { + cmdOutName=val; + e.setAudio(DIV_AUDIO_DUMMY); + return TA_PARAM_SUCCESS; +} + bool needsValue(String param) { for (size_t i=0; i","output audio to file")); params.push_back(TAParam("O","vgmout",true,pVGMOut,"","output .vgm data")); params.push_back(TAParam("Z","zsmout",true,pZSMOut,"","output .zsm data for Commander X16 Zsound")); + params.push_back(TAParam("C","cmdout",true,pCmdOut,"","output command stream")); + params.push_back(TAParam("b","binary",false,pBinary,"","set command stream output format to binary")); params.push_back(TAParam("L","loglevel",true,pLogLevel,"debug|info|warning|error","set the log level (info by default)")); params.push_back(TAParam("v","view",true,pView,"pattern|commands|nothing","set visualization (pattern by default)")); params.push_back(TAParam("c","console",false,pConsole,"","enable console mode")); @@ -262,6 +295,8 @@ void initParams() { params.push_back(TAParam("l","loops",true,pLoops,"","set number of loops (-1 means loop forever)")); params.push_back(TAParam("o","outmode",true,pOutMode,"one|persys|perchan","set file output mode")); + params.push_back(TAParam("B","benchmark",true,pBenchmark,"render|seek","run performance test")); + params.push_back(TAParam("V","version",false,pVersion,"","view information about Furnace.")); params.push_back(TAParam("W","warranty",false,pWarranty,"","view warranty disclaimer.")); } @@ -287,7 +322,7 @@ int main(int argc, char** argv) { logE("CoInitializeEx failed!"); } #endif -#if !(defined(__APPLE__) || defined(_WIN32) || defined(ANDROID)) +#if !(defined(__APPLE__) || defined(_WIN32) || defined(ANDROID) || defined(__HAIKU__)) // workaround for Wayland HiDPI issue if (getenv("SDL_VIDEODRIVER")==NULL) { setenv("SDL_VIDEODRIVER","x11",1); @@ -296,6 +331,7 @@ int main(int argc, char** argv) { outName=""; vgmOutName=""; zsmOutName=""; + cmdOutName=""; initParams(); @@ -423,7 +459,32 @@ int main(int argc, char** argv) { displayEngineFailError=true; } } - if (outName!="" || vgmOutName!="") { + if (benchMode) { + logI("starting benchmark!"); + if (benchMode==2) { + e.benchmarkSeek(); + } else { + e.benchmarkPlayback(); + } + return 0; + } + if (outName!="" || vgmOutName!="" || cmdOutName!="") { + if (cmdOutName!="") { + SafeWriter* w=e.saveCommand(cmdOutBinary); + if (w!=NULL) { + FILE* f=fopen(cmdOutName.c_str(),"wb"); + if (f!=NULL) { + fwrite(w->getFinalBuf(),1,w->size(),f); + fclose(f); + } else { + reportError(fmt::sprintf("could not open file! (%s)",e.getLastError())); + } + w->finish(); + delete w; + } else { + reportError("could not write command stream!"); + } + } if (vgmOutName!="") { SafeWriter* w=e.saveVGM(); if (w!=NULL) { @@ -449,25 +510,39 @@ int main(int argc, char** argv) { } if (consoleMode) { + bool cliSuccess=false; + cli.bindEngine(&e); + if (!cli.init()) { + reportError("error while starting CLI!"); + } else { + cliSuccess=true; + } logI("playing..."); e.play(); -#ifdef HAVE_GUI - SDL_Event ev; - while (true) { - SDL_WaitEvent(&ev); - if (ev.type==SDL_QUIT) break; - } - e.quit(); - return 0; + if (cliSuccess) { + cli.loop(); + cli.finish(); + e.quit(); + return 0; + } else { +#ifdef HAVE_SDL2 + SDL_Event ev; + while (true) { + SDL_WaitEvent(&ev); + if (ev.type==SDL_QUIT) break; + } + e.quit(); + return 0; #else - while (true) { + while (true) { #ifdef _WIN32 - Sleep(500); + Sleep(500); #else - usleep(500000); + usleep(500000); +#endif + } #endif } -#endif } #ifdef HAVE_GUI diff --git a/src/utfutils.cpp b/src/utfutils.cpp index c99528986..4c727777b 100644 --- a/src/utfutils.cpp +++ b/src/utfutils.cpp @@ -19,7 +19,7 @@ #include "utfutils.h" -int decodeUTF8(const unsigned char* data, char& len) { +int decodeUTF8(const unsigned char* data, signed char& len) { int ret=0xfffd; if (data[0]<0x80) { ret=data[0]; @@ -66,7 +66,7 @@ int decodeUTF8(const unsigned char* data, char& len) { size_t utf8len(const char* s) { size_t p=0; size_t r=0; - char cl; + signed char cl; while (s[p]!=0) { r++; decodeUTF8((const unsigned char*)&s[p],cl); @@ -76,7 +76,7 @@ size_t utf8len(const char* s) { } char utf8csize(const unsigned char* c) { - char ret; + signed char ret; decodeUTF8(c,ret); return ret; } @@ -84,7 +84,7 @@ char utf8csize(const unsigned char* c) { WString utf8To16(const char* s) { WString ret; int ch, p; - char chs; + signed char chs; p=0; while (s[p]!=0) { ch=decodeUTF8((const unsigned char*)&s[p],chs); diff --git a/src/utfutils.h b/src/utfutils.h index 087913a43..76c894708 100644 --- a/src/utfutils.h +++ b/src/utfutils.h @@ -21,6 +21,8 @@ #define _UTFUTILS_H #include "ta-utils.h" +int decodeUTF8(const unsigned char* data, signed char& len); + size_t utf8len(const char* s); size_t utf8clen(const char* s); size_t utf8pos(const char* s, size_t inpos);