diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..6b5659d36 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4dce04678..293c4cd0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,17 +18,16 @@ jobs: strategy: matrix: config: - - { name: 'Windows MSVC', os: windows-latest, compiler: msvc, shell: bash } - - { name: 'Windows MinGW', os: windows-latest, compiler: mingw, shell: 'msys2 {0}' } - - { name: 'macOS', os: macos-latest, shell: bash } - - { name: 'Ubuntu', os: ubuntu-18.04, shell: bash } + - { name: 'Windows MSVC x86', os: windows-latest, compiler: msvc, arch: x86 } + - { 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 } fail-fast: false name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} - defaults: - run: - shell: ${{ matrix.config.shell }} steps: - name: Checkout @@ -36,53 +35,123 @@ jobs: with: submodules: recursive + - name: Set Windows arch identifiers + id: windows-identify + if: ${{ matrix.config.compiler == 'msvc' || matrix.config.compiler == 'mingw' }} + run: | + vswhere_target="${{ matrix.config.arch }}" + msvc_target="${{ matrix.config.arch }}" + mingw_target="${{ matrix.config.arch }}" + + if [ '${{ matrix.config.arch }}' == 'x86' ]; then + msvc_target="Win32" + elif [ '${{ matrix.config.arch }}' == 'x86_64' ]; then + vswhere_target="amd64" + msvc_target="x64" + fi + + if [ '${{ matrix.config.compiler }}' == 'msvc' ]; then + echo "vswhere target: ${vswhere_target}" + echo "MSVC target: ${msvc_target}" + else + echo "MinGW cross target: ${mingw_target}" + fi + + echo "::set-output name=vswhere-target::${vswhere_target}" + echo "::set-output name=msvc-target::${msvc_target}" + echo "::set-output name=mingw-target::${mingw_target}" + + - name: Set package identifier + id: package-identify + run: | + package_name="furnace-${GITHUB_SHA}" + package_ext="" + if [ '${{ runner.os }}' == 'Windows' ] || [ '${{ matrix.config.compiler }}' == 'mingw' ]; then + package_name="${package_name}-Windows" + if [ '${{ matrix.config.compiler }}' == 'mingw' ]; then + package_name="${package_name}-MinGW" + else + package_name="${package_name}-MSVC" + fi + 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_ext=".dmg" + else + package_name="${package_name}-Linux" + package_ext=".AppImage" + fi + + echo "Package identifier: ${package_name}" + echo "Package file: ${package_name}${package_ext}" + + echo "::set-output name=id::${package_name}" + echo "::set-output name=filename::${package_name}${package_ext}" + + - name: Set build cores amount + id: build-cores + run: | + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + set amount=2 + if [ '${{ runner.os }}' == 'macOS' ]; then + amount=3 + fi + + echo "Amount of cores we can build with: ${amount}" + + echo "::set-output name=amount::${amount}" + - name: Setup Toolchain [Windows MSVC] - if: ${{ runner.os == 'Windows' && matrix.config.compiler == 'msvc' }} + if: ${{ matrix.config.compiler == 'msvc' }} uses: seanmiddleditch/gha-setup-vsdevenv@v3 + with: + arch: ${{ steps.windows-identify.outputs.vswhere-target }} - name: Setup Toolchain [Windows MinGW] - if: ${{ runner.os == 'Windows' && matrix.config.compiler == 'mingw' }} - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - update: true - install: | - mingw-w64-x86_64-toolchain - mingw-w64-x86_64-cmake - make - - - name: Install Dependencies [macOS] - if: ${{ runner.os == 'macOS' }} + if: ${{ matrix.config.compiler == 'mingw' }} run: | - export HOMEBREW_NO_INSTALL_CLEANUP=1 - brew update - brew install \ - pkg-config \ - sdl2 \ - libsndfile \ - zlib \ - jack + sudo apt update + sudo apt install \ + mingw-w64 \ + mingw-w64-tools - name: Install Dependencies [Ubuntu] - if: ${{ runner.os == 'Linux' }} + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} run: | sudo apt update sudo apt install \ libsdl2-dev \ + libfmt-dev \ + librtmidi-dev \ libsndfile1-dev \ zlib1g-dev \ - libjack-jackd2-dev + libjack-jackd2-dev \ + appstream + wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x appimagetool-x86_64.AppImage - - name: Configure + - name: Configure (System Libraries) + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} run: | export USE_WAE=ON export CMAKE_EXTRA_ARGS=() - if [ '${{ runner.os }}' == 'Windows' ]; then - if [ '${{ matrix.config.compiler }}' == 'mingw' ]; then - CMAKE_EXTRA_ARGS+=('-G' 'MSYS Makefiles') - elif [ '${{ matrix.config.compiler }}' == 'msvc' ]; then - # We don't want all the MSVC warnings to cause errors yet - export USE_WAE=OFF + if [ '${{ matrix.config.compiler }}' == 'msvc' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_GENERATOR_PLATFORM=${{ steps.windows-identify.outputs.msvc-target }}') + elif [ '${{ matrix.config.compiler }}' == 'mingw' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_TOOLCHAIN_FILE=scripts/Cross-MinGW-${{ steps.windows-identify.outputs.mingw-target }}.cmake') + else + # Test with system libs + CMAKE_EXTRA_ARGS+=( + '-DSYSTEM_FMT=OFF' + '-DSYSTEM_LIBSNDFILE=ON' + '-DSYSTEM_RTMIDI=ON' + '-DSYSTEM_ZLIB=ON' + '-DWITH_JACK=ON' + ) + # Too old on Ubuntu + if [ '${{ runner.os }}' == 'macOS' ]; then + CMAKE_EXTRA_ARGS+=('-DSYSTEM_SDL2=ON') fi fi @@ -93,16 +162,122 @@ jobs: -DWARNINGS_ARE_ERRORS=${USE_WAE} \ "${CMAKE_EXTRA_ARGS[@]}" - - name: Build + - name: Build (System Libraries) + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} run: | - export VERBOSE=1 cmake \ --build ${PWD}/build \ --config ${{ env.BUILD_TYPE }} \ - --parallel 2 + --parallel ${{ steps.build-cores.outputs.amount }} - - name: Install + - name: Install (System Libraries) + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} run: | cmake \ --install ${PWD}/build \ --config ${{ env.BUILD_TYPE }} + + - name: Cleanup (System Libraries) + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + run: | + rm -rf build/ target/ + + - name: Configure + run: | + export USE_WAE=ON + export CMAKE_EXTRA_ARGS=() + if [ '${{ matrix.config.compiler }}' == 'msvc' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_GENERATOR_PLATFORM=${{ steps.windows-identify.outputs.msvc-target }}') + + # Force static linking + # 1. Make MSVC runtime configurable + CMAKE_EXTRA_ARGS+=('-DCMAKE_POLICY_DEFAULT_CMP0091=NEW') + # 2. Use static (debug) runtime + if [ '${{ env.BUILD_TYPE }}' == 'Debug' ]; then + CMAKE_EXTRA_ARGS+=('-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug') + else + CMAKE_EXTRA_ARGS+=('-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded') + fi + 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"') + fi + + cmake \ + -B ${PWD}/build \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DWARNINGS_ARE_ERRORS=${USE_WAE} \ + "${CMAKE_EXTRA_ARGS[@]}" + + - name: Build + run: | + cmake \ + --build ${PWD}/build \ + --config ${{ env.BUILD_TYPE }} \ + --parallel ${{ steps.build-cores.outputs.amount }} + + - name: Package [Windows] + if: ${{ runner.os == 'Windows' || matrix.config.compiler == 'mingw' }} + run: | + binPath=build + if [ '${{ matrix.config.compiler }}' == 'msvc' ]; then + binPath="${binPath}/${{ env.BUILD_TYPE }}" + fi + if [ '${{ matrix.config.compiler }}' == 'mingw' ] && [ '${{ env.BUILD_TYPE }}' == 'Release' ]; then + # arch-specific strip prefix + # TODO maybe extract from cross toolchain files? + toolPrefix="-w64-mingw32-" + if [ '${{ matrix.config.arch }}' == 'x86_64' ]; then + toolPrefix="x86_64${toolPrefix}" + else + toolPrefix="i686${toolPrefix}" + fi + ${toolPrefix}strip -s "${binPath}/furnace.exe" + fi + + mkdir ${{ steps.package-identify.outputs.filename }} + pushd ${{ steps.package-identify.outputs.filename }} + + cp -v ../LICENSE LICENSE.txt + cp -v ../README.md README.txt + cp -vr ../{papers,demos} ../${binPath}/furnace.exe ./ + + popd + + - name: Package [macOS] + if: ${{ runner.os == 'macOS' }} + run: | + pushd build + cpack + mv Furnace-*-Darwin.dmg ../${{ steps.package-identify.outputs.filename }} + popd + + - name: Package [Ubuntu] + if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' }} + run: | + if [ '${{ env.BUILD_TYPE }}' == 'Release' ]; then + strip -s build/furnace + fi + + mkdir -p target/furnace.AppDir + make -C ${PWD}/build DESTDIR=${PWD}/target/furnace.AppDir install + pushd target + + pushd furnace.AppDir + cp -v usr/share/{icons/hicolor/1024x1024/apps/furnace.png,applications/furnace.desktop} ./ + ln -s furnace.png .DirIcon + mv -v usr/share/metainfo/{furnace.appdata,org.tildearrow.furnace.metainfo}.xml + cp -v ../../res/AppRun ./ + popd + + ../appimagetool-x86_64.AppImage furnace.AppDir + mv Furnace-*.AppImage ../${{ steps.package-identify.outputs.filename }} + popd + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.package-identify.outputs.id }} + path: ${{ steps.package-identify.outputs.filename }} diff --git a/.gitmodules b/.gitmodules index 435f43c75..d63fd70b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "extern/adpcm"] path = extern/adpcm url = https://github.com/superctr/adpcm +[submodule "extern/Nuked-OPL3"] + path = extern/Nuked-OPL3 + url = https://github.com/nukeykt/Nuked-OPL3.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a96fe7d9..498f3df69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,8 @@ endif() set(CMAKE_CXX_STANDARD 14) set(CMAKE_PROJECT_VERSION_MAJOR 0) -set(CMAKE_PROJECT_VERSION_MINOR 5) -set(CMAKE_PROJECT_VERSION_PATCH 7) +set(CMAKE_PROJECT_VERSION_MINOR 6) +set(CMAKE_PROJECT_VERSION_PATCH 0) if (ANDROID) set(BUILD_GUI_DEFAULT OFF) @@ -65,7 +65,7 @@ list(APPEND DEPENDENCIES_LIBRARIES ${CMAKE_THREAD_LIBS_INIT}) if (SYSTEM_FMT) if (PKG_CONFIG_FOUND) - pkg_check_modules(FMT fmt) + pkg_check_modules(FMT fmt>=7.1.0) if (FMT_FOUND) list(APPEND DEPENDENCIES_INCLUDE_DIRS ${FMT_INCLUDE_DIRS}) list(APPEND DEPENDENCIES_COMPILE_OPTIONS ${FMT_CFLAGS_OTHER}) @@ -150,52 +150,43 @@ endif() if (SYSTEM_SDL2) if (PKG_CONFIG_FOUND) - pkg_check_modules(SDL sdl>=${SYSTEM_SDL_MIN_VER}) - if (SDL_FOUND) - list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL_INCLUDE_DIRS}) - list(APPEND DEPENDENCIES_COMPILE_OPTIONS ${SDL_CFLAGS_OTHER}) - list(APPEND DEPENDENCIES_LIBRARIES ${SDL_LIBRARIES}) - list(APPEND DEPENDENCIES_LIBRARY_DIRS ${SDL_LIBRARY_DIRS}) - list(APPEND DEPENDENCIES_LINK_OPTIONS ${SDL_LDFLAGS_OTHER}) - list(APPEND DEPENDENCIES_LEGACY_LDFLAGS ${SDL_LDFLAGS}) + pkg_check_modules(SDL2 sdl2>=${SYSTEM_SDL_MIN_VER}) + if (SDL2_FOUND) + list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL2_INCLUDE_DIRS}) + list(APPEND DEPENDENCIES_COMPILE_OPTIONS ${SDL2_CFLAGS_OTHER}) + list(APPEND DEPENDENCIES_LIBRARIES ${SDL2_LIBRARIES}) + list(APPEND DEPENDENCIES_LIBRARY_DIRS ${SDL2_LIBRARY_DIRS}) + list(APPEND DEPENDENCIES_LINK_OPTIONS ${SDL2_LDFLAGS_OTHER}) + list(APPEND DEPENDENCIES_LEGACY_LDFLAGS ${SDL2_LDFLAGS}) endif() endif() - if (NOT SDL_FOUND) - find_package(SDL ${SYSTEM_SDL_MIN_VER}) - if (SDL_FOUND) - list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL_INCLUDE_DIR}) - list(APPEND DEPENDENCIES_LIBRARIES ${SDL_LIBRARY}) - else() - if (PKG_CONFIG_FOUND) - pkg_check_modules(SDL2 sdl2>=${SYSTEM_SDL_MIN_VER}) - if (SDL2_FOUND) - list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL2_INCLUDE_DIRS}) - list(APPEND DEPENDENCIES_COMPILE_OPTIONS ${SDL2_CFLAGS_OTHER}) - list(APPEND DEPENDENCIES_LIBRARIES ${SDL2_LIBRARIES}) - list(APPEND DEPENDENCIES_LIBRARY_DIRS ${SDL2_LIBRARY_DIRS}) - list(APPEND DEPENDENCIES_LINK_OPTIONS ${SDL2_LDFLAGS_OTHER}) - list(APPEND DEPENDENCIES_LEGACY_LDFLAGS ${SDL2_LDFLAGS}) - endif() - endif() - if (NOT SDL2_FOUND) - find_package(SDL2 ${SYSTEM_SDL_MIN_VER} REQUIRED) - list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL2_INCLUDE_DIR}) - list(APPEND DEPENDENCIES_LIBRARIES ${SDL2_LIBRARY}) - endif() - endif() + if (NOT SDL2_FOUND) + find_package(SDL2 ${SYSTEM_SDL_MIN_VER} REQUIRED) + list(APPEND DEPENDENCIES_INCLUDE_DIRS ${SDL2_INCLUDE_DIR}) + list(APPEND DEPENDENCIES_LIBRARIES ${SDL2_LIBRARY}) endif() message(STATUS "Using system-installed SDL2") else() - set(SDL_SHARED OFF) - set(SDL_STATIC ON) + set(SDL_SHARED OFF CACHE BOOL "Force no dynamically-linked SDL" FORCE) + set(SDL_STATIC ON CACHE BOOL "Force statically-linked SDL" 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. + # This should probably go in a FAQ. + set(SDL_LIBC ON CACHE BOOL "Tell SDL that we want it to use our C runtime (required for proper static linking)" FORCE) add_subdirectory(extern/SDL EXCLUDE_FROM_ALL) list(APPEND DEPENDENCIES_INCLUDE_DIRS extern/SDL/include) list(APPEND DEPENDENCIES_LIBRARIES SDL2-static) + # Work around add_subdirectory'd SDL not propagating HAVE_LIBC to MSVC furnace build + if (MSVC) + list(APPEND DEPENDENCIES_COMPILE_OPTIONS "/DHAVE_LIBC") + endif() message(STATUS "Using vendored SDL2") endif() set(AUDIO_SOURCES src/audio/abstract.cpp +src/audio/midi.cpp src/audio/sdl.cpp ) @@ -218,6 +209,7 @@ endif() if (USE_RTMIDI) list(APPEND AUDIO_SOURCES src/audio/rtmidi.cpp) message(STATUS "Building with RtMidi") + list(APPEND DEPENDENCIES_DEFINES HAVE_RTMIDI) else() message(STATUS "Building without RtMidi") endif() @@ -245,6 +237,7 @@ extern/adpcm/ymz_codec.c extern/Nuked-OPN2/ym3438.c extern/opm/opm.c extern/Nuked-OPLL/opll.c +extern/Nuked-OPL3/opl3.c src/engine/platform/sound/sn76496.cpp src/engine/platform/sound/ay8910.cpp src/engine/platform/sound/saa1099.cpp @@ -252,6 +245,10 @@ src/engine/platform/sound/gb/apu.c src/engine/platform/sound/gb/timing.c src/engine/platform/sound/pce_psg.cpp src/engine/platform/sound/nes/apu.c +src/engine/platform/sound/nes/fds.c +src/engine/platform/sound/nes/mmc5.c +src/engine/platform/sound/vera_psg.c +src/engine/platform/sound/vera_pcm.c src/engine/platform/sound/c64/sid.cc src/engine/platform/sound/c64/voice.cc @@ -283,6 +280,18 @@ src/engine/platform/sound/lynx/Mikey.cpp src/engine/platform/sound/qsound.c +src/engine/platform/sound/x1_010/x1_010.cpp + +src/engine/platform/sound/swan.cpp + +src/engine/platform/sound/k005289/k005289.cpp + +src/engine/platform/sound/n163/n163.cpp + +src/engine/platform/sound/vic20sound.c + +src/engine/platform/sound/vrcvi/vrcvi.cpp + src/engine/platform/ym2610Interface.cpp src/engine/blip_buf.c @@ -292,6 +301,8 @@ src/engine/config.cpp src/engine/dispatchContainer.cpp src/engine/engine.cpp src/engine/fileOps.cpp +src/engine/fileOpsIns.cpp +src/engine/filter.cpp src/engine/instrument.cpp src/engine/macroInt.cpp src/engine/pattern.cpp @@ -300,6 +311,7 @@ src/engine/sample.cpp src/engine/song.cpp src/engine/sysDef.cpp src/engine/wavetable.cpp +src/engine/waveSynth.cpp src/engine/vgmOps.cpp src/engine/platform/abstract.cpp src/engine/platform/genesis.cpp @@ -308,20 +320,35 @@ src/engine/platform/sms.cpp src/engine/platform/opll.cpp src/engine/platform/gb.cpp src/engine/platform/pce.cpp +src/engine/platform/mmc5.cpp src/engine/platform/nes.cpp src/engine/platform/c64.cpp src/engine/platform/arcade.cpp +src/engine/platform/tx81z.cpp src/engine/platform/ym2610.cpp src/engine/platform/ym2610ext.cpp +src/engine/platform/ym2610b.cpp +src/engine/platform/ym2610bext.cpp src/engine/platform/ay.cpp src/engine/platform/ay8930.cpp +src/engine/platform/opl.cpp +src/engine/platform/fds.cpp src/engine/platform/tia.cpp src/engine/platform/saa.cpp src/engine/platform/amiga.cpp +src/engine/platform/pcspkr.cpp src/engine/platform/segapcm.cpp src/engine/platform/qsound.cpp -src/engine/platform/dummy.cpp +src/engine/platform/x1_010.cpp src/engine/platform/lynx.cpp +src/engine/platform/swan.cpp +src/engine/platform/vera.cpp +src/engine/platform/bubsyswsg.cpp +src/engine/platform/n163.cpp +src/engine/platform/pet.cpp +src/engine/platform/vic20.cpp +src/engine/platform/vrc6.cpp +src/engine/platform/dummy.cpp ) if (WIN32) @@ -352,15 +379,40 @@ src/gui/font_unifont.cpp src/gui/font_icon.cpp src/gui/fonts.cpp src/gui/debug.cpp +src/gui/fileDialog.cpp src/gui/intConst.cpp src/gui/guiConst.cpp +src/gui/about.cpp +src/gui/channels.cpp +src/gui/compatFlags.cpp +src/gui/cursor.cpp +src/gui/dataList.cpp +src/gui/debugWindow.cpp +src/gui/doAction.cpp +src/gui/editing.cpp +src/gui/editControls.cpp src/gui/insEdit.cpp +src/gui/log.cpp +src/gui/mixer.cpp +src/gui/midiMap.cpp +src/gui/newSong.cpp src/gui/orders.cpp +src/gui/osc.cpp src/gui/pattern.cpp +src/gui/piano.cpp +src/gui/presets.cpp +src/gui/regView.cpp +src/gui/sampleEdit.cpp src/gui/settings.cpp +src/gui/songInfo.cpp +src/gui/songNotes.cpp +src/gui/stats.cpp +src/gui/sysConf.cpp src/gui/util.cpp +src/gui/waveEdit.cpp +src/gui/volMeter.cpp src/gui/gui.cpp ) @@ -403,13 +455,20 @@ endif() if (NOT MSVC) set(WARNING_FLAGS -Wall -Wextra -Wno-unused-parameter) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND WARNING_FLAGS -Wno-cast-function-type) + endif() if (WARNINGS_ARE_ERRORS) list(APPEND WARNING_FLAGS -Werror) endif() else() - # /wd4100 == -Wno-unused-parameter - add_compile_options("/source-charset:utf-8") - set(WARNING_FLAGS /W4 /wd4100 /D_CRT_SECURE_NO_WARNINGS) + add_compile_options("/utf-8") + set(WARNING_FLAGS /W2 /D_CRT_SECURE_NO_WARNINGS) + list(APPEND WARNING_FLAGS + /wd4244 # implicit type conversions + /wd4305 # truncations + /wd4309 # truncations of constant values + ) if (WARNINGS_ARE_ERRORS) list(APPEND WARNING_FLAGS /WX) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f57b2025..ad608e4cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,23 @@ the coding style is described here: - indent switch cases - preprocessor directives not intended - if macro comprises more than one line, indent +- prefer built-in types: + - `bool` + - `signed char` or `unsigned char` are 8-bit + - when the type is `char`, **always** specify whether it is signed or not. + - unspecified `char` is signed on x86 and unsigned on ARM, so yeah. + - the only situation in where unspecified `char` is allowed is for C strings (`const char*`). + - `short` or `unsigned short` are 16-bit + - `int` or `unsigned int` are 32-bit + - `float` is 32-bit + - `double` is 64-bit + - `long long int` or `unsigned long long int` are 64-bit + - avoid using 64-bit numbers as I still build for 32-bit systems. + - two `long`s are required to make Windows happy. + - `size_t` are 32-bit or 64-bit, depending on architecture. +- in float/double operations, always use decimal and `f` if single-precision. + - e.g. `1.0f` or `1.0` instead of `1`. +- don't use `auto` unless needed. 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 e0db0e05e..f8ea1aec5 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,14 @@ ![screenshot](papers/screenshot1.png) -this is a work-in-progress chiptune tracker compatible with DefleMask modules (.dmf). +this is a multi-system chiptune tracker. -[downloads](#downloads) | [discussion](#discussion) | [help](#help) | [developer info](#developer-info) +[downloads](#downloads) | [discussion/help](#quick-references) | [developer info](#developer-info) | [unofficial packages](#unofficial-packages) | [FAQ](#frequently-asked-questions) + +*** +## downloads + +check out the [Releases](https://github.com/tildearrow/furnace/releases) page. available for Windows, macOS and Linux (AppImage). ## features @@ -23,12 +28,14 @@ this is a work-in-progress chiptune tracker compatible with DefleMask modules (. - Amiga - TIA (Atari 2600/7800) - multiple sound chips in a single song! +- DefleMask compatibility - loads .dmf modules, .dmp instruments and .dmw wavetables - clean-room design (guesswork and ABX tests only, no decompilation involved) - bug/quirk implementation for increased playback accuracy - VGM and audio file export - accurate emulation cores whether possible (Nuked, MAME, SameBoy, Mednafen PCE, puNES, reSID, Stella, SAASound and ymfm) - additional features on top: - FM macros! + - negative octaves - arbitrary pitch samples - sample loop points - SSG envelopes in Neo Geo @@ -36,21 +43,28 @@ this is a work-in-progress chiptune tracker compatible with DefleMask modules (. - ability to change tempo mid-song with `Cxxx` effect (`xxx` between `000` and `3ff`) - open-source under GPLv2 or later. -## downloads +*** +# quick references -check out the [Releases](https://github.com/tildearrow/furnace/releases) page. available for Windows, macOS and Linux (AppImage). + - **discussion**: see the [Discussions](https://github.com/tildearrow/furnace/discussions) section, or (preferably) the [official Discord server](https://discord.gg/EfrwT2wq7z). + - **help**: check out the [documentation](papers/doc/README.md). it's mostly incomplete, but has details on effects. -## discussion +## unofficial packages -see the [Discussions](https://github.com/tildearrow/furnace/discussions) section, or (preferably) the [official Discord server](https://discord.gg/EfrwT2wq7z). +[![Packaging status](https://repology.org/badge/tiny-repos/furnace.svg)](https://repology.org/project/furnace/versions) -## help - -check out the [documentation](papers/doc/README.md). it's mostly incomplete, but has details on effects. +some people have provided packages for Unix/Unix-like distributions. here's a list. + - **Arch Linux**: [furnace-git is in the AUR.](https://aur.archlinux.org/packages/furnace-git) thank you Essem! + - **FreeBSD**: [a package in ports](https://www.freshports.org/audio/furnace/) is available courtesy of ehaupt. + - **Nix**: [package](https://search.nixos.org/packages?channel=unstable&show=furnace&from=0&size=50&sort=relevance&type=packages&query=furnace) thanks to OPNA2608. + - **OpenSUSE**: [a package](https://software.opensuse.org/package/furnace) is available, courtesy of fpesari. +*** # developer info -**NOTE: do not download the project's source as a .zip or .tar.gz as these do not include the project's submodules which are necessary to proceed with building.** +[![Build furnace](https://github.com/tildearrow/furnace/actions/workflows/build.yml/badge.svg)](https://github.com/tildearrow/furnace/actions/workflows/build.yml) + +**NOTE: do not download the project's source as a .zip or .tar.gz as these do not include the project's submodules which are necessary to proceed with building. please instead use Git as shown below.** ## dependencies @@ -97,6 +111,7 @@ cd build cmake .. make ``` +Alternatively, build scripts are provided in the `scripts/` folder in the root of the repository. ### CMake options @@ -135,6 +150,9 @@ this will play a compatible file. this will play a compatible file and enable the commands view. +**note that these commands only actually work in Linux environments. on other command lines, such as Windows' Command Prompt, or MacOS Terminal, it may not work correctly.** + +*** # notes > how do I use Neo Geo SSG envelopes? @@ -161,6 +179,9 @@ the following effects are provided: a lower envelope period will make the envelope run faster. +*** +# frequently asked questions + > how do I use C64 absolute filter/duty? on Instrument Editor in the C64 tab there are two options to toggle these. @@ -168,25 +189,29 @@ also provided are two effects: - `3xxx`: set fine duty. - `4xxx`: set fine cutoff. `xxx` range is 000-7ff. +additionally, you can change the cutoff and/or duty as a macro inside an instrument by clicking the `absolute cutoff macro` and/or `absolute duty macro` checkbox at the bottom of the instrument. (for the filter, you also need to click the checkbox that says `volume macro is cutoff macro`.) -> my song sounds very odd at a certain point +> Q: how do I use PCM on a PCM-capable system? -file a bug report. use the Issues page. +A: Two possibilities: the recommended way is via creating the "Amiga/Sample" type instrument and assigning sample to it, or via old, Deflemask-compatible method, using `17xx` effect -it's probably another playback inaccuracy. +> Q: my song sounds very odd at a certain point -> my song sounds correct, but it doesn't in DefleMask +A: file a bug report. use the Issues page. it's probably another playback inaccuracy. -file a bug report **here**. it still is a playback inaccuracy. +> Q: my song sounds correct, but it doesn't in DefleMask -> my C64 song sounds terrible after saving as .dmf! +A: file a bug report **here**. it still is a playback inaccuracy. -that's a limitation of the DefleMask format. save in Furnace song format instead (.fur). +> Q: my C64 song sounds terrible after saving as .dmf! -> how do I solo channels? +A: that's a limitation of the DefleMask format. save in Furnace song format instead (.fur). -right click on the channel name. +> Q: how do I solo channels? +A: right click on the channel name. + +*** # footnotes copyright (C) 2021-2022 tildearrow and contributors. diff --git a/demos/Another_winter.fur b/demos/Another_winter.fur new file mode 100644 index 000000000..b0ccceac1 Binary files /dev/null and b/demos/Another_winter.fur differ diff --git a/demos/Coconut_Mall.fur b/demos/Coconut_Mall.fur new file mode 100644 index 000000000..471fcbec3 Binary files /dev/null and b/demos/Coconut_Mall.fur differ diff --git a/demos/silverlining.fur b/demos/silverlining.fur new file mode 100644 index 000000000..9ad4a8815 Binary files /dev/null and b/demos/silverlining.fur differ diff --git a/demos/yky.fur b/demos/yky.fur new file mode 100644 index 000000000..106ed1004 Binary files /dev/null and b/demos/yky.fur differ diff --git a/extern/Nuked-OPL3 b/extern/Nuked-OPL3 new file mode 160000 index 000000000..bb5c8d08a --- /dev/null +++ b/extern/Nuked-OPL3 @@ -0,0 +1 @@ +Subproject commit bb5c8d08a85779c42b75c79d7b84f365a1b93b66 diff --git a/extern/Nuked-OPLL/opll.c b/extern/Nuked-OPLL/opll.c index fd2aa239f..ecff69fd3 100644 --- a/extern/Nuked-OPLL/opll.c +++ b/extern/Nuked-OPLL/opll.c @@ -290,6 +290,28 @@ static void OPLL_DoModeWrite(opll_t *chip) { } } +const opll_patch_t* OPLL_GetPatchROM(uint32_t chip_type) { + switch (chip_type) { + case opll_type_ds1001: + return patch_ds1001; + break; + case opll_type_ymf281: + case opll_type_ymf281b: + return patch_ymf281; + break; + case opll_type_ym2423: + return patch_ym2423; + break; + case opll_type_ym2413: + case opll_type_ym2413b: + case opll_type_ym2420: + default: + return patch_ym2413; + break; + } + return patch_ym2413; +} + void OPLL_Reset(opll_t *chip, uint32_t chip_type) { uint32_t i; memset(chip, 0, sizeof(opll_t)); diff --git a/extern/Nuked-OPLL/opll.h b/extern/Nuked-OPLL/opll.h index 706eb9f3b..85c721a78 100644 --- a/extern/Nuked-OPLL/opll.h +++ b/extern/Nuked-OPLL/opll.h @@ -193,6 +193,8 @@ typedef struct { } opll_t; +const opll_patch_t* OPLL_GetPatchROM(uint32_t chip_type); + void OPLL_Reset(opll_t *chip, uint32_t chip_type); void OPLL_Clock(opll_t *chip, int32_t *buffer); void OPLL_Write(opll_t *chip, uint32_t port, uint8_t data); diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index 5be457930..3980bcd3e 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -3877,6 +3877,7 @@ namespace IGFD static ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SpanAvailWidth; + // TODO BUG?! va_list args; va_start(args, vFmt); vsnprintf(fdi.puVariadicBuffer, MAX_FILE_DIALOG_NAME_BUFFER, vFmt, args); @@ -4074,6 +4075,7 @@ namespace IGFD if (ImGui::TableNextColumn()) // file name { + // TODO BUG?!?!?! needToBreakTheloop = prSelectableItem(i, infos, selected, _str.c_str()); if (needToBreakTheloop==2) escape=true; } diff --git a/extern/imgui_patched/imgui_impl_sdlrenderer.cpp b/extern/imgui_patched/imgui_impl_sdlrenderer.cpp index ae034179c..89c87f62b 100644 --- a/extern/imgui_patched/imgui_impl_sdlrenderer.cpp +++ b/extern/imgui_patched/imgui_impl_sdlrenderer.cpp @@ -26,6 +26,7 @@ #include "imgui.h" #include "imgui_impl_sdlrenderer.h" +#include #if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier #include // intptr_t #else @@ -184,6 +185,7 @@ void ImGui_ImplSDLRenderer_RenderDrawData(ImDrawData* draw_data) // Bind texture, Draw SDL_Texture* tex = (SDL_Texture*)pcmd->GetTexID(); + SDL_SetTextureScaleMode(tex, SDL_ScaleModeBest); // ??? SDL_RenderGeometryRaw(bd->SDLRenderer, tex, xy, (int)sizeof(ImDrawVert), color, (int)sizeof(ImDrawVert), diff --git a/extern/pfd-fixed/.gitignore b/extern/pfd-fixed/.gitignore new file mode 100644 index 000000000..ec121a999 --- /dev/null +++ b/extern/pfd-fixed/.gitignore @@ -0,0 +1,4 @@ +CMakeCache.txt +CMakeFiles +Makefile +cmake_install.cmake diff --git a/extern/pfd-fixed/.lgtm.yml b/extern/pfd-fixed/.lgtm.yml new file mode 100644 index 000000000..b4c4985b9 --- /dev/null +++ b/extern/pfd-fixed/.lgtm.yml @@ -0,0 +1,5 @@ +extraction: + cpp: + index: + build_command: + - make -C examples diff --git a/extern/pfd-fixed/CMakeLists.txt b/extern/pfd-fixed/CMakeLists.txt new file mode 100644 index 000000000..3be61ae5a --- /dev/null +++ b/extern/pfd-fixed/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(portable_file_dialogs VERSION 1.00 LANGUAGES CXX) + +add_library(${PROJECT_NAME} INTERFACE) +target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/extern/pfd-fixed/COPYING b/extern/pfd-fixed/COPYING new file mode 100644 index 000000000..8b014d64a --- /dev/null +++ b/extern/pfd-fixed/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/extern/pfd-fixed/README.md b/extern/pfd-fixed/README.md new file mode 100644 index 000000000..ceb36a405 --- /dev/null +++ b/extern/pfd-fixed/README.md @@ -0,0 +1,64 @@ +# Portable File Dialogs + +A free C++11 file dialog library. + +- works on Windows, Mac OS X, Linux +- **single-header**, no extra library dependencies +- **synchronous *or* asynchronous** (does not block the rest of your program!) +- **cancelable** (kill asynchronous dialogues without user interaction) +- **secure** (immune to shell-quote vulnerabilities) + +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a25d3fd6959a4333871f630ac70b6e09)](https://www.codacy.com/manual/samhocevar/portable-file-dialogs?utm_source=github.com&utm_medium=referral&utm_content=samhocevar/portable-file-dialogs&utm_campaign=Badge_Grade) + +## Status + +The library is now pretty robust. It is not as feature-complete as +[Tiny File Dialogs](https://sourceforge.net/projects/tinyfiledialogs/), +but has asynchonous dialogs, more maintainable code, and fewer potential +security issues. + +The currently available backends are: + +- Win32 API (all known versions of Windows) +- Mac OS X (using AppleScript) +- GNOME desktop (using [Zenity](https://en.wikipedia.org/wiki/Zenity) or its clones Matedialog and Qarma) +- KDE desktop (using [KDialog](https://github.com/KDE/kdialog)) + +Experimental support for Emscripten is on its way. + +## Documentation + +- [`pfd`](doc/pfd.md) general documentation +- [`pfd::message`](doc/message.md) message box +- [`pfd::notify`](doc/notify.md) notification +- [`pfd::open_file`](doc/open_file.md) file open +- [`pfd::save_file`](doc/save_file.md) file save +- [`pfd::select_folder`](doc/select_folder.md) folder selection + +## History + +- 0.1.0 (July 16, 2020): first public release + +## Screenshots (Windows 10) + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +## Screenshots (Mac OS X, dark theme) + +![warning-osxdark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) +![notify-osxdark](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) +![open-osxdark](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +## Screenshots (Linux, GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47136608-772a3080-d2b4-11e8-9e1d-60a7e743e908.png) +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +## Screenshots (Linux, KDE Plasma desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/message.md b/extern/pfd-fixed/doc/message.md new file mode 100644 index 000000000..e54c44771 --- /dev/null +++ b/extern/pfd-fixed/doc/message.md @@ -0,0 +1,97 @@ +## Message Box API + +Displaying a message box is done using the `pfd::message` class. It can be provided a title, a +message text, a `choice` representing which buttons need to be rendered, and an `icon` for the +message: + +```cpp +pfd::message::message(std::string const &title, + std::string const &text, + pfd::choice choice = pfd::choice::ok_cancel, + pfd::icon icon = pfd::icon::info); + +enum class pfd::choice { ok, ok_cancel, yes_no, yes_no_cancel }; + +enum class pfd::icon { info, warning, error, question }; +``` + +The pressed button is queried using `pfd::message::result()`. If the dialog box is closed by any +other means, the `pfd::button::cancel` is assumed: + +```cpp +pfd::button pfd::message::result(); + +enum class pfd::button { ok, cancel, yes, no }; +``` + +It is possible to ask the dialog box whether the user took action using the `pfd::message::ready()` +method, with an optional `timeout` argument. If the user did not press a button within `timeout` +milliseconds, the function will return `false`: + +```cpp +bool pfd::message::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple notification + +The `pfd::message` destructor waits for user action, so this operation will block until the user +closes the message box: + +```cpp +pfd::message("Problem", "An error occurred while doing things", + pfd::choice::ok, pfd::icon::error); +``` + +## Example 2: retrieving the pressed button + +Using `pfd::message::result()` will also wait for user action before returning. This operation will block and return the user choice: + +```cpp +// Ask for user opinion +auto button = pfd::message("Action requested", "Do you want to proceed with things?", + pfd::choice::yes_no, pfd::icon::question).result(); +// Do something with button… +``` + +## Example 3: asynchronous message box + +Using `pfd::message::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// Message box with nice message +auto box = pfd::message("Unsaved Files", "Do you want to save the current " + "document before closing the application?", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + +// Do something while waiting for user input +while (!box.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the selected button +switch (box.result()) +{ + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; +} +``` + +## Screenshots + +#### Windows 10 + +![warning-win32](https://user-images.githubusercontent.com/245089/47136607-76919a00-d2b4-11e8-8f42-e2d62c4f9570.png) + +#### Mac OS X + +![warning-osx-dark](https://user-images.githubusercontent.com/245089/56053001-22dba700-5d53-11e9-8233-ca7a2c58188d.png) ![warning-osx-light](https://user-images.githubusercontent.com/245089/56053055-49014700-5d53-11e9-8306-e9a03a25e044.png) + +#### Linux (GNOME desktop) + +![warning-gnome](https://user-images.githubusercontent.com/245089/47140824-8662ab80-d2bf-11e8-9c87-2742dd5b58af.png) + +#### Linux (KDE desktop) + +![warning-kde](https://user-images.githubusercontent.com/245089/47149255-4dcccd00-d2d3-11e8-84c9-f85612784680.png) diff --git a/extern/pfd-fixed/doc/notify.md b/extern/pfd-fixed/doc/notify.md new file mode 100644 index 000000000..b140e2619 --- /dev/null +++ b/extern/pfd-fixed/doc/notify.md @@ -0,0 +1,40 @@ +## Notification API + +Displaying a desktop notification is done using the `pfd::notify` class. It can be provided a +title, a message text, and an `icon` for the notification style: + +```cpp +pfd::notify::notify(std::string const &title, + std::string const &text, + pfd::icon icon = pfd::icon::info); + +enum class pfd::icon { info, warning, error }; +``` + +## Example + +Displaying a notification is straightforward. Emoji are supported: + +```cpp +pfd::notify("System event", "Something might be on fire 🔥", + pfd::icon::warning); +``` + +The `pfd::notify` object needs not be kept around, letting the object clean up itself is enough. + +## Screenshots + +Windows 10: +![notify-win32](https://user-images.githubusercontent.com/245089/47142453-2ff76c00-d2c3-11e8-871a-1a110ac91eb2.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053188-bc0abd80-5d53-11e9-8298-68aa96315c6c.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053137-92ea2d00-5d53-11e9-8cf2-049486c45713.png) + +Linux (GNOME desktop): +![notify-gnome](https://user-images.githubusercontent.com/245089/47142455-30900280-d2c3-11e8-8b76-ea16c7e502d4.png) + +Linux (KDE desktop): +![notify-kde](https://user-images.githubusercontent.com/245089/47149206-27a72d00-d2d3-11e8-8f1b-96e462f08c2b.png) diff --git a/extern/pfd-fixed/doc/open_file.md b/extern/pfd-fixed/doc/open_file.md new file mode 100644 index 000000000..db3158ef2 --- /dev/null +++ b/extern/pfd-fixed/doc/open_file.md @@ -0,0 +1,90 @@ +## File Open API + +The `pfd::open_file` class handles file opening dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::open_file::open_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::multiselect` to allow selecting multiple files. + +The selected files are queried using `pfd::open_file::result()`. If the user canceled the +operation, the returned list is empty: + +```cpp +std::vector pfd::open_file::result(); +``` + +It is possible to ask the file open dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::open_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::open_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto selection = pfd::open_file("Select a file").result(); +if (!selection.empty()) + std::cout << "User selected file " << selection[0] << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto selection = pfd::open_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::multiselect).result(); +// Do something with selection +for (auto const &filename : dialog.result()) + std::cout << "Selected file: " << filename << "\n"; +``` + +## Example 3: asynchronous file open + +Using `pfd::open_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File open dialog +auto dialog = pfd::open_file("Select file to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Number of selected files: " << dialog.result().size() << "\n"; +``` + +## Screenshots + +Windows 10: +![open-win32](https://user-images.githubusercontent.com/245089/47155865-0f8cd900-d2e6-11e8-8041-1e20b6f77dee.png) + +Mac OS X (dark theme): +![image](https://user-images.githubusercontent.com/245089/56053378-39363280-5d54-11e9-9583-9f1c978fa0db.png) + +Mac OS X (light theme): +![image](https://user-images.githubusercontent.com/245089/56053413-4fdc8980-5d54-11e9-85e3-e9e5d0e10772.png) + +Linux (GNOME desktop): +![open-gnome](https://user-images.githubusercontent.com/245089/47155867-0f8cd900-d2e6-11e8-93af-275636491ec4.png) + +Linux (KDE desktop): +![open-kde](https://user-images.githubusercontent.com/245089/47155866-0f8cd900-d2e6-11e8-8006-f14b948afc55.png) diff --git a/extern/pfd-fixed/doc/pfd.md b/extern/pfd-fixed/doc/pfd.md new file mode 100644 index 000000000..f62799efc --- /dev/null +++ b/extern/pfd-fixed/doc/pfd.md @@ -0,0 +1,120 @@ +## Portable File Dialogs documentation + +The library can be used either as a [header-only library](https://en.wikipedia.org/wiki/Header-only), +or as a [single file library](https://github.com/nothings/single_file_libs). + +### Use as header-only library + +Just include the main header file wherever needed: + +```cpp +#include "portable-file-dialogs.h" + +/* ... */ + + pfd::message::message("Hello", "This is a test"); + +/* ... */ +``` + +### Use as a single-file library + +Defining the `PFD_SKIP_IMPLEMENTATION` macro before including `portable-file-dialogs.h` will +skip all the implementation code and reduce compilation times. You still need to include the +header without the macro at least once, typically in a `pfd-impl.cpp` file. + +```cpp +// In pfd-impl.cpp +#include "portable-file-dialogs.h" +``` + +```cpp +// In all other files +#define PFD_SKIP_IMPLEMENTATION 1 +#include "portable-file-dialogs.h" +``` + +### General concepts + +Dialogs inherit from `pfd::dialog` and are created by calling their class constructor. Their +destructor will block until the window is closed by user interaction. So for instance this +will block until the end of the line: + +```cpp +pfd::message::message("Hi", "there"); +``` + +Whereas this will only block until the end of the scope, allowing the program to perform +additional operations while the dialog is open: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + // ... perform asynchronous operations here +} +``` + +It is possible to call `bool pfd::dialog::ready(timeout)` on the dialog in order to query its +status and perform asynchronous operations as long as the user has not interacted: + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + } +} +``` + +If necessary, a dialog can be forcibly closed using `bool pfd::dialog::kill()`. Note that this +may be confusing to the user and should only be used in very specific situations. It is also not +possible to close a Windows message box that provides no _Cancel_ button. + +```cpp +{ + auto m = pfd::message::message("Hi", "there"); + + while (!m.ready()) + { + // ... perform asynchronous operations here + + if (too_much_time_has_passed()) + m.kill(); + } +} +``` + +Finally, the user response can be retrieved using `pfd::dialog::result()`. The return value of +this function depends on which dialog is being used. See their respective documentation for more +information: + + * [`pfd::message`](message.md) (message box) + * [`pfd::notify`](notify.md) (notification) + * [`pfd::open_file`](open_file.md) (file open) + * [`pfd::save_file`](save_file.md) (file save) + * [`pfd::select_folder`](select_folder.md) (folder selection) + +### Settings + +The library can be queried and configured through the `pfd::settings` class. + +```cpp +bool pfd::settings::available(); +void pfd::settings::verbose(bool value); +void pfd::settings::rescan(); +``` + +The return value of `pfd::settings::available()` indicates whether a suitable dialog backend (such +as Zenity or KDialog on Linux) has been found. If not, the library will not work and all dialog +invocations will be no-ops. The program will not crash but you should account for this situation +and add a fallback mechanism or exit gracefully. + +Calling `pfd::settings::rescan()` will force a rescan of available backends. This may change the +result of `pfd::settings::available()` if a backend was installed on the system in the meantime. +This is probably only useful for debugging purposes. + +Calling `pfd::settings::verbose(true)` may help debug the library. It will output debug information +to `std::cout` about some operations being performed. diff --git a/extern/pfd-fixed/doc/save_file.md b/extern/pfd-fixed/doc/save_file.md new file mode 100644 index 000000000..5c10badb6 --- /dev/null +++ b/extern/pfd-fixed/doc/save_file.md @@ -0,0 +1,73 @@ +## File Open API + +The `pfd::save_file` class handles file saving dialogs. It can be provided a title, a starting +directory and/or pre-selected file, an optional filter for recognised file types, and an optional +flag to allow multiple selection: + +```cpp +pfd::save_file::save_file(std::string const &title, + std::string const &initial_path, + std::vector filters = { "All Files", "*" }, + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_overwrite` to disable a potential warning when +saving to an existing file. + +The selected file is queried using `pfd::save_file::result()`. If the user canceled the +operation, the returned file name is empty: + +```cpp +std::string pfd::save_file::result(); +``` + +It is possible to ask the file save dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::save_file::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple file selection + +Using `pfd::save_file::result()` will wait for user action before returning. This operation will +block and return the user choice: + +```cpp +auto destination = pfd::save_file("Select a file").result(); +if (!destination.empty()) + std::cout << "User selected file " << destination << "\n"; +``` + +## Example 2: filters + +The filter list enumerates filter names and corresponded space-separated wildcard lists. It +defaults to `{ "All Files", "*" }`, but here is how to use other options: + +```cpp +auto destination = pfd::save_file("Select a file", ".", + { "Image Files", "*.png *.jpg *.jpeg *.bmp", + "Audio Files", "*.wav *.mp3", + "All Files", "*" }, + pfd::opt::force_overwrite).result(); +// Do something with destination +std::cout << "Selected file: " << destination << "\n"; +``` + +## Example 3: asynchronous file save + +Using `pfd::save_file::ready()` allows the application to perform other tasks while waiting for +user input: + +```cpp +// File save dialog +auto dialog = pfd::save_file("Select file to save"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "User selected file: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/doc/select_folder.md b/extern/pfd-fixed/doc/select_folder.md new file mode 100644 index 000000000..28f5f63fa --- /dev/null +++ b/extern/pfd-fixed/doc/select_folder.md @@ -0,0 +1,55 @@ +## Folder Selection API + +The `pfd::select_folder` class handles folder opening dialogs. It can be provided a title, and an +optional starting directory: + +```cpp +pfd::select_folder::select_folder(std::string const &title, + std::string const &default_path = "", + pfd::opt option = pfd::opt::none); +``` + +The `option` parameter can be `pfd::opt::force_path` to force the operating system to use the +provided path. Some systems default to the most recently used path, if applicable. + +The selected folder is queried using `pfd::select_folder::result()`. If the user canceled the +operation, the returned string is empty: + +```cpp +std::string pfd::select_folder::result(); +``` + +It is possible to ask the folder selection dialog whether the user took action using the +`pfd::message::ready()` method, with an optional `timeout` argument. If the user did not validate +the dialog within `timeout` milliseconds, the function will return `false`: + +```cpp +bool pfd::select_folder::ready(int timeout = pfd::default_wait_timeout); +``` + +## Example 1: simple folder selection + +Using `pfd::select_folder::result()` will wait for user action before returning. This operation +will block and return the user choice: + +```cpp +auto selection = pfd::select_folder("Select a folder").result(); +if (!selection.empty()) + std::cout << "User selected folder " << selection << "\n"; +``` + +## Example 2: asynchronous folder open + +Using `pfd::select_folder::ready()` allows the application to perform other tasks while waiting for user input: + +```cpp +// Folder selection dialog +auto dialog = pfd::select_folder("Select folder to open"); + +// Do something while waiting for user input +while (!dialog.ready(1000)) + std::cout << "Waited 1 second for user input...\n"; + +// Act depending on the user choice +std::cout << "Selected folder: " << dialog.result() << "\n"; +``` diff --git a/extern/pfd-fixed/examples/.gitignore b/extern/pfd-fixed/examples/.gitignore new file mode 100644 index 000000000..bda6acf70 --- /dev/null +++ b/extern/pfd-fixed/examples/.gitignore @@ -0,0 +1,11 @@ +example +example.exe +kill +kill.exe + +Debug +Release +*.vcxproj.user + +.idea +cmake-build-* diff --git a/extern/pfd-fixed/examples/example.cpp b/extern/pfd-fixed/examples/example.cpp new file mode 100644 index 000000000..a216ae4b3 --- /dev/null +++ b/extern/pfd-fixed/examples/example.cpp @@ -0,0 +1,110 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +#if _WIN32 +#define DEFAULT_PATH "C:\\" +#else +#define DEFAULT_PATH "/tmp" +#endif + +int main() +{ + // Check that a backend is available + if (!pfd::settings::available()) + { + std::cout << "Portable File Dialogs are not available on this platform.\n"; + return 1; + } + + // Set verbosity to true + pfd::settings::verbose(true); + + // Notification + pfd::notify("Important Notification", + "This is ' a message, pay \" attention \\ to it!", + pfd::icon::info); + + // Message box with nice message + auto m = pfd::message("Personal Message", + "You are an amazing person, don’t let anyone make you think otherwise.", + pfd::choice::yes_no_cancel, + pfd::icon::warning); + + // Optional: do something while waiting for user action + for (int i = 0; i < 10 && !m.ready(1000); ++i) + std::cout << "Waited 1 second for user input...\n"; + + // Do something according to the selected button + switch (m.result()) + { + case pfd::button::yes: std::cout << "User agreed.\n"; break; + case pfd::button::no: std::cout << "User disagreed.\n"; break; + case pfd::button::cancel: std::cout << "User freaked out.\n"; break; + default: break; // Should not happen + } + + // Directory selection + auto dir = pfd::select_folder("Select any directory", DEFAULT_PATH).result(); + std::cout << "Selected dir: " << dir << "\n"; + + // File open + auto f = pfd::open_file("Choose files to read", DEFAULT_PATH, + { "Text Files (.txt .text)", "*.txt *.text", + "All Files", "*" }, + pfd::opt::multiselect); + std::cout << "Selected files:"; + for (auto const &name : f.result()) + std::cout << " " + name; + std::cout << "\n"; +} + +// Unused function that just tests the whole API +void api() +{ + // pfd::settings + pfd::settings::verbose(true); + pfd::settings::rescan(); + + // pfd::notify + pfd::notify("", ""); + pfd::notify("", "", pfd::icon::info); + pfd::notify("", "", pfd::icon::warning); + pfd::notify("", "", pfd::icon::error); + pfd::notify("", "", pfd::icon::question); + + pfd::notify a("", ""); + (void)a.ready(); + (void)a.ready(42); + + // pfd::message + pfd::message("", ""); + pfd::message("", "", pfd::choice::ok); + pfd::message("", "", pfd::choice::ok_cancel); + pfd::message("", "", pfd::choice::yes_no); + pfd::message("", "", pfd::choice::yes_no_cancel); + pfd::message("", "", pfd::choice::retry_cancel); + pfd::message("", "", pfd::choice::abort_retry_ignore); + pfd::message("", "", pfd::choice::ok, pfd::icon::info); + pfd::message("", "", pfd::choice::ok, pfd::icon::warning); + pfd::message("", "", pfd::choice::ok, pfd::icon::error); + pfd::message("", "", pfd::choice::ok, pfd::icon::question); + + pfd::message b("", ""); + (void)b.ready(); + (void)b.ready(42); + (void)b.result(); +} + diff --git a/extern/pfd-fixed/examples/example.vcxproj b/extern/pfd-fixed/examples/example.vcxproj new file mode 100644 index 000000000..d7e914928 --- /dev/null +++ b/extern/pfd-fixed/examples/example.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {10F4364D-27C4-4C74-8079-7C42971E81E7} + Win32Proj + example + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + \ No newline at end of file diff --git a/extern/pfd-fixed/examples/kill.cpp b/extern/pfd-fixed/examples/kill.cpp new file mode 100644 index 000000000..787edbeb3 --- /dev/null +++ b/extern/pfd-fixed/examples/kill.cpp @@ -0,0 +1,42 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#include "portable-file-dialogs.h" + +#include + +int main() +{ + // Set verbosity to true + pfd::settings::verbose(true); + + // Message box with nice message + auto m = pfd::message("Upgrade software?", + "Press OK to upgrade this software.\n" + "\n" + "By default, the software will update itself\n" + "automatically in 10 seconds.", + pfd::choice::ok_cancel, + pfd::icon::warning); + + // Wait for an answer for up to 10 seconds + for (int i = 0; i < 10 && !m.ready(1000); ++i) + ; + + // Upgrade software if user clicked OK, or if user didn’t interact + bool upgrade = m.ready() ? m.result() == pfd::button::ok : m.kill(); + if (upgrade) + std::cout << "Upgrading software!\n"; + else + std::cout << "Not upgrading software.\n"; +} + diff --git a/extern/pfd-fixed/examples/kill.vcxproj b/extern/pfd-fixed/examples/kill.vcxproj new file mode 100644 index 000000000..b1ee3c9c4 --- /dev/null +++ b/extern/pfd-fixed/examples/kill.vcxproj @@ -0,0 +1,96 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 15.0 + {B94D26B1-7EF7-43A2-A973-9A96A08E2E17} + Win32Proj + kill + 10.0 + + + + Application + v142 + Unicode + + + true + + + false + true + + + + + + + + + + + + true + + + false + + + + .. + NotUsing + Level3 + true + true + + + Console + true + + + + + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + + + MaxSpeed + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + + + true + true + + + + + + diff --git a/extern/pfd-fixed/portable-file-dialogs.h b/extern/pfd-fixed/portable-file-dialogs.h new file mode 100644 index 000000000..fc6df0a14 --- /dev/null +++ b/extern/pfd-fixed/portable-file-dialogs.h @@ -0,0 +1,1733 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#ifdef __APPLE__ +# ifndef _DARWIN_C_SOURCE +# define _DARWIN_C_SOURCE +# endif +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2() +#include // ::kill, std::signal +#include // waitpid() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog +#ifndef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 1 +# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION +# if __GXX_ABI_VERSION <= 1014 +# undef PFD_HAS_IFILEDIALOG +# define PFD_HAS_IFILEDIALOG 0 +# endif +# endif +#endif + +namespace pfd +{ + +enum class button +{ + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice +{ + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon +{ + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t +{ + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } +inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings +{ +public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + +protected: + explicit settings(bool resync = false); + + bool check_program(std::string const &program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag + { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const &flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool &flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal +{ + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor +{ + friend class dialog; + +public: + // High level function to get the result of a command + std::string result(int *exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const &fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const &command); +#endif + + ~executor(); + +protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + +private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform +{ +protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast(::GetProcAddress(lib.handle, sym.c_str()))) + {} + + operator bool() const { return m_proc != nullptr; } + operator T *() const { return m_proc; } + + private: + T *m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll + { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform +{ +public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + +protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const &str) const; + std::string osascript_quote(std::string const &str) const; + std::string shell_quote(std::string const &str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog +{ +protected: + enum type + { + open, + save, + folder, + }; + + file_dialog(type in_type, + std::string const &title, + std::string const &default_path = "", + std::vector const &filters = {}, + opt options = opt::none); + +protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); +#if PFD_HAS_IFILEDIALOG + std::string select_folder_vista(IFileDialog *ifd, bool force_path); +#endif + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The notify widget +// + +class notify : public internal::dialog +{ +public: + notify(std::string const &title, + std::string const &message, + icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog +{ +public: + message(std::string const &title, + std::string const &text, + choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + +private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog +{ +public: + open_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog +{ +public: + save_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog +{ +public: + select_folder(std::string const &title, + std::string const &default_path = "", + opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal +{ + +#if _WIN32 +static inline std::wstring str2wstr(std::string const &str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const &str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() +{ + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = VerSetConditionMask( + VerSetConditionMask( + VerSetConditionMask( + 0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const &str, std::string const &suffix) +{ + return suffix.size() <= str.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const &str, std::string const &prefix) +{ + return prefix.size() <= str.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) + { + auto desktop_name = std::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name && desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name && desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || + tmp.flags(flag::has_matedialog) || + tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const &program) +{ +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({"/bin/sh", "-c", "which " + program}); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const +{ +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const +{ + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const +{ + return flags(flag::has_kdialog); +} + +inline bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// executor implementation + +inline std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() +{ +#if _WIN32 + if (m_future.valid()) + { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) + SendMessage(hwnd, WM_CLOSE, 0, 0); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) +{ + auto that = (executor *)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const &fun) +{ + stop(); + + auto trampoline = [fun, this]() + { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const &command) +{ + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) + { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const &s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() +{ + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) +{ + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) + { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) + { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) + { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) + { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +inline internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() + : dll("ole32.dll") +{ + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() +{ + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() +{ + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() +{ + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() +{ + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = + { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, NULL, NULL + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +inline internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +inline std::vector internal::dialog::desktop_helper() const +{ +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) +{ + switch (_choice) + { + case choice::ok_cancel: return "okcancel"; + case choice::yes_no: return "yesno"; + case choice::yes_no_cancel: return "yesnocancel"; + case choice::retry_cancel: return "retrycancel"; + case choice::abort_retry_ignore: return "abortretryignore"; + /* case choice::ok: */ default: return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) +{ + switch (_icon) + { + case icon::warning: return "warning"; + case icon::error: return "error"; + case icon::question: return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// THis is only used for debugging purposes +inline std::ostream& operator <<(std::ostream &s, std::vector const &v) +{ + int not_first = 0; + for (auto &e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const &str) const +{ + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, + std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = {} */, + opt options /* = opt::none */) +{ +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) + { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, + options](int *exit_code) -> std::string + { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) + { +#if PFD_HAS_IFILEDIALOG + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = dll::proc(ole32, "CoCreateInstance") + (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } +#endif + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) + { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto *list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) + { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) + { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + else + { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const *p = woutput.c_str(); *p; ) + { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) + { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "set ret to choose"; + switch (in_type) + { + case type::save: + script += " file name"; + break; + case type::open: default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) + script += " default location " + osascript_quote(default_path); + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) + { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for ( ; iter != end; ++iter) + { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += (filter_list.size() == 0 ? "" : ",") + + osascript_quote(pat.substr(2, pat.size() - 2)); + } + if (has_filter && filter_list.size() > 0) + script += " of type {" + filter_list + "}"; + } + + if (in_type == type::open && (options & opt::multiselect)) + { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } + else + { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + command.push_back("--file-selection"); + command.push_back("--filename=" + default_path); + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) + { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } + else if (is_kdialog()) + { + switch (in_type) + { + case type::save: command.push_back("--getsavefilename"); break; + case type::open: command.push_back("--getopenfilename"); break; + case type::folder: command.push_back("--getexistingdirectory"); break; + } + if (options & opt::multiselect) + { + command.push_back("--multiple"); + command.push_back("--separate-output"); + } + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() +{ +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/')) + ret.pop_back(); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) + { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +#if PFD_HAS_IFILEDIALOG +inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) +{ + std::string result; + + IShellItem *folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc + create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), + nullptr, + IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) + { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wselected = nullptr; + item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); + item->Release(); + + if (wselected) + { + result = internal::wstr2str(std::wstring(wselected)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wselected); + } + } + } + + ifd->Release(); + + return result; +} +#endif +#endif + +// notify implementation + +#if _WIN32 +inline BOOL WINAPI icon_enum_callback(HMODULE hModule, LPCTSTR lpType, LPTSTR lpName, LONG_PTR lParam) +{ + ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; +}; +#endif + +inline notify::notify(std::string const &title, + std::string const &message, + icon _icon /* = icon::info */) +{ + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW + { + ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) + { + case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; + case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; + /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; + } + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, (ENUMRESNAMEPROC)icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + + " with title " + osascript_quote(title)); + } + else if (is_zenity()) + { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } + else if (is_kdialog()) + { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought + // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52 + UINT style = MB_SYSTEMMODAL; + switch (_icon) + { + case icon::warning: style |= MB_ICONWARNING; break; + case icon::error: style |= MB_ICONERROR; break; + case icon::question: style |= MB_ICONQUESTION; break; + /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; + } + + switch (_choice) + { + case choice::ok_cancel: style |= MB_OKCANCEL; break; + case choice::yes_no: style |= MB_YESNO; break; + case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; + case choice::retry_cancel: style |= MB_RETRYCANCEL; break; + case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; + /* case choice::ok: */ default: style |= MB_OK; break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) + { + case icon::warning: full_message = "⚠️"; break; + case icon::error: full_message = "⛔"; break; + case icon::question: full_message = "❓"; break; + /* case icon::info: */ default: full_message = "ℹ"; break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "display dialog " + osascript_quote(text) + + " with title " + osascript_quote(title); + switch (_choice) + { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + m_mappings[256] = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Retry\"" + " cancel button \"Retry\""; + m_mappings[256] = button::cancel; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + m_mappings[256] = button::ok; + break; + } + script += " with icon "; + switch (_icon) + { + #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; + case icon::warning: script += "caution"; break; + case icon::error: script += "stop"; break; + case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; + #undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + switch (_choice) + { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::retry_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; + case choice::ok: + default: + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--warning"); break; + default: command.push_back("--info"); break; + } + } + + command.insert(command.end(), { "--title", title, + "--width=300", "--height=0", // sensible defaults + "--text", text, + "--icon-name=dialog-" + get_icon_name(_icon) }); + } + else if (is_kdialog()) + { + if (_choice == choice::ok) + { + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--sorry"); break; + default: command.push_back("--msgbox"); break; + } + } + else + { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) + { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() +{ + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (exit_code < 0 || // this means cancel + internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) +{ +} + +inline open_file::open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect) + : open_file(title, default_path, filters, + (allow_multiselect ? opt::multiselect : opt::none)) +{ +} + +inline std::vector open_file::result() +{ + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) +{ +} + +inline save_file::save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite) + : save_file(title, default_path, filters, + (confirm_overwrite ? opt::none : opt::force_overwrite)) +{ +} + +inline std::string save_file::result() +{ + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const &title, + std::string const &default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) +{ +} + +inline std::string select_folder::result() +{ + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd + diff --git a/papers/doc/2-interface/components.md b/papers/doc/2-interface/components.md index 8ff73e3fe..0243662b8 100644 --- a/papers/doc/2-interface/components.md +++ b/papers/doc/2-interface/components.md @@ -80,4 +80,4 @@ TODO: image sliders are used for controlling values in a quick manner by being dragged. -alternatively, Ctrl-clicking a slider (Command-click on macOS) will turn it into a number input field for a short period of time, allowing you to input fine values. +alternatively, right-clicking or Ctrl-clicking or a slider (Command-click on macOS) will turn it into a number input field for a short period of time, allowing you to input fine values. diff --git a/papers/doc/3-pattern/effects.md b/papers/doc/3-pattern/effects.md index ce47e4849..773d30373 100644 --- a/papers/doc/3-pattern/effects.md +++ b/papers/doc/3-pattern/effects.md @@ -10,6 +10,8 @@ however, effects are continuous, which means you only need to type it once and t - a note must be present for this effect to work. - `04xy`: vibrato. `x` is the speed, while `y` is the depth. - maximum vibrato depth is ±1 semitone. +- `07xy`: tremolo. `x` is the speed, while `y` is the depth. + - maximum tremolo depth is -60 volume steps. - `08xy`: set panning. `x` is the left channel and `y` is the right one. - not all systems support this effect. - `09xx`: set speed 1. @@ -22,6 +24,9 @@ however, effects are continuous, which means you only need to type it once and t - `0Dxx`: jump to next pattern. - `0Fxx`: set speed 2. +- `9xxx`: set sample position to `xxx`\*0x100. + - not all systems support this effect. + - `Cxxx`: change song Hz. - `xxx` may be from `000` to `3ff`. @@ -42,10 +47,20 @@ however, effects are continuous, which means you only need to type it once and t - `ECxx`: note off after `xx` ticks. - `EDxx`: delay note by `xx` ticks. - `EExx`: send external command. - - currently not used, but this eventually will allow you to do special things after I add VGM export. + - this effect is currently incomplete. - `EFxx`: add or subtract global pitch. - this effect is rather weird. use with caution. - `80` is center. +- `F0xx`: change song Hz by BPM value. +- `F1xx`: single tick slide up. +- `F2xx`: single tick slide down. +- `F3xx`: fine volume slide up (64x slower than `0Axy`). +- `F4xx`: fine volume slide down (64x slower than `0Axy`). +- `F8xx`: single tick volume slide up. +- `F9xx`: single tick volume slide down. +- `FAxy`: fast volume slide (4x faster than `0Axy`). + - if `x` is 0 then this is a slide down. + - if `y` is 0 then this is a slide up. - `FFxx`: end of song/stop playback. additionally each system has its own effects. [click here for more details](../7-systems/README.md). diff --git a/papers/doc/4-instrument/README.md b/papers/doc/4-instrument/README.md index b9d4b8f40..1ffff7e54 100644 --- a/papers/doc/4-instrument/README.md +++ b/papers/doc/4-instrument/README.md @@ -10,19 +10,25 @@ double-click to open the instrument editor. every instrument can be renamed and have its type changed. -depending on the instrument type, there are currently 10 different types of an instrument editor: +depending on the instrument type, there are currently 13 different types of an instrument editor: - [FM synthesis](fm.md) - for use with YM2612, YM2151 and FM block portion of YM2610. - [Standard](standard.md) - for use with NES and Sega Master System's PSG sound source and its derivatives. - [Game Boy](game-boy.md) - for use with Game Boy APU. - [PC Engine/TurboGrafx-16](pce.md) - for use with PC Engine's wavetable synthesizer. +- [WonderSwan](wonderswan.md) - for use with WonderSwan's wavetable synthesizer. - [AY8930](8930.md) - for use with Microchip AY8930 E-PSG sound source. - [Commodore 64](c64.md) - for use with Commodore 64 SID. - [SAA1099](saa.md) - for use with Philips SAA1099 PSG sound source. - [TIA](tia.md) - for use with Atari 2600 system. - [AY-3-8910](ay8910.md) - for use with AY-3-8910 PSG sound source and SSG portion in YM2610. -- [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM and PC Engine's sample playback mode. -- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console. +- [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM, X1-010 and PC Engine's sample playback mode. +- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console. +- [VERA](vera.md) - for use with Commander X16 VERA. +- [Seta/Allumer X1-010](x1_010.md) - for use with Wavetable portion in Seta/Allumer X1-010. +- [Konami SCC/Bubble System WSG](scc.md) - for use with Konami SCC and Wavetable portion in Bubble System's sound hardware. +- [Namco 163](n163.md) - for use with Namco 163. +- [Konami VRC6](vrc6.md) - for use with VRC6's PSG sound source. # macros diff --git a/papers/doc/4-instrument/n163.md b/papers/doc/4-instrument/n163.md new file mode 100644 index 000000000..0bd914453 --- /dev/null +++ b/papers/doc/4-instrument/n163.md @@ -0,0 +1,23 @@ +# Namco 163 instrument editor + +Namco 163 instrument editor consists of two tabs: one controlling various parameters for waveform initialize and macro tab containing 10 macros. + +## N163 +- [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. +- [Load waveform before playback] - Determines the load initial waveform into RAM before playback. +- [Update waveforms into RAM when every waveform changes] - Determines the update every different waveform changes in playback. + + +## Macros +- [Volume] - volume levels sequence +- [Arpeggio]- pitch sequence +- [Waveform pos.] - sets the waveform source address in RAM for playback (single nibble unit) +- [Waveform] - sets waveform source for playback immediately or update later +- [Waveform len.] - sets the waveform source length for playback (4 nibble unit) +- [Waveform update] - sets the waveform update trigger behavior for playback +- [Waveform to load] - sets waveform source for load to RAM immediately or later +- [Wave pos. to load] - sets address of waveform for load to RAM (single nibble unit) +- [Wave len. to load] - sets length of waveform for load to RAM (4 nibble unit) +- [Waveform load] - sets the waveform load trigger behavior diff --git a/papers/doc/4-instrument/pce.md b/papers/doc/4-instrument/pce.md index 7bff37c70..50ac335bf 100644 --- a/papers/doc/4-instrument/pce.md +++ b/papers/doc/4-instrument/pce.md @@ -3,5 +3,5 @@ PCE instrument editor consists of only three macros, almost like TIA: - [Volume] - volume sequence -- [Arpeggio] - pitch sequencr +- [Arpeggio] - pitch sequence - [Waveform] - spicifies wavetables sequence diff --git a/papers/doc/4-instrument/scc.md b/papers/doc/4-instrument/scc.md new file mode 100644 index 000000000..2650e0a30 --- /dev/null +++ b/papers/doc/4-instrument/scc.md @@ -0,0 +1,7 @@ +# Konami SCC/Bubble System WSG instrument editor + +SCC/Bubble System WSG instrument editor consists of only three macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequence +- [Waveform] - spicifies wavetables sequence diff --git a/papers/doc/4-instrument/vera.md b/papers/doc/4-instrument/vera.md new file mode 100644 index 000000000..b577ccd23 --- /dev/null +++ b/papers/doc/4-instrument/vera.md @@ -0,0 +1,8 @@ +# VERA instrument editor + +VERA instrument editor consists of only four macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequence +- [Duty cycle] - pulse duty cycle sequence +- [Waveform] - select the waveform used by instrument diff --git a/papers/doc/4-instrument/vrc6.md b/papers/doc/4-instrument/vrc6.md new file mode 100644 index 000000000..a4ea64a18 --- /dev/null +++ b/papers/doc/4-instrument/vrc6.md @@ -0,0 +1,7 @@ +# VRC6 instrument editor + +VRC6 instrument editor consists of only three macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequence +- [Duty cycle] - specifies duty cycle for pulse wave channels diff --git a/papers/doc/4-instrument/wonderswan.md b/papers/doc/4-instrument/wonderswan.md new file mode 100644 index 000000000..6ad0e8c97 --- /dev/null +++ b/papers/doc/4-instrument/wonderswan.md @@ -0,0 +1,8 @@ +# WonderSwan instrument editor + +WS instrument editor consists of only four macros, similar to PCE but with different volume and noise range: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequencr +- [Noise] - noise LFSR tap sequence +- [Waveform] - spicifies wavetables sequence diff --git a/papers/doc/4-instrument/x1_010.md b/papers/doc/4-instrument/x1_010.md new file mode 100644 index 000000000..8f3691ee6 --- /dev/null +++ b/papers/doc/4-instrument/x1_010.md @@ -0,0 +1,11 @@ +# X1-010 instrument editor + +X1-010 instrument editor consists of 7 macros. + +- [Volume] - volume levels sequence +- [Arpeggio]- pitch sequence +- [Waveform] - spicifies wavetables sequence +- [Envelope Mode] - allows shaping an envelope +- [Envelope] - spicifies envelope shape sequence, it's also wavetable. +- [Auto envelope numerator] - sets the envelope to the channel's frequency multiplied by numerator +- [Auto envelope denominator] - ets the envelope to the channel's frequency multiplied by denominator diff --git a/papers/doc/5-wave/README.md b/papers/doc/5-wave/README.md index 91fe92656..0668c0bf2 100644 --- a/papers/doc/5-wave/README.md +++ b/papers/doc/5-wave/README.md @@ -1,5 +1,5 @@ # wavetable editor -Wavetable synthizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.5.5, wavetable editor affects PC Engine and channel 3 of Game Boy. +Wavetable synthizers, in context of Furnace, are sound sources that operate on extremely short n-bit PCM streams. By extremely short, no more than 256 bytes. This amount of space is nowhere near enough to store an actual sampled sound, it allows certain amount of freedom to define a waveform shape. As of Furnace 0.5.8, wavetable editor affects PC Engine, WonderSwan and channel 3 of Game Boy. -Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: both Game Boy and PCE can handle max 32 byte waveforms as of now, width 16-level height for GB and 32-level height for PCE. If larger wave will be defined for these two systems, it will be squashed to fit in the constrains of the system. +Furnace's wavetable editor is rather simple, you can draw the waveform using mouse or by pasting an MML bit stream in the input field. Maximum wave width (length) is 256 bytes, and maximum wave height (depth) is 256. NOTE: Game Boy, PCE, WonderSwan and Bubble System can handle max 32 byte waveforms, X1-010 can handle max 128 byte waveforms as of now, with 16-level height for GB, X1-010 Envelope, WS, Bubble System and N163, and 32-level height for PCE. If larger wave will be defined for these systems, it will be squashed to fit within the constraints of the system. diff --git a/papers/doc/6-sample/README.md b/papers/doc/6-sample/README.md index 78eb322c3..87ddf0f8f 100644 --- a/papers/doc/6-sample/README.md +++ b/papers/doc/6-sample/README.md @@ -12,7 +12,8 @@ As of Furnace 0.5.5, the following sound chips have sample support: - PC Engine/TurboGrafx 16/Huc6280 (same conditions as above) - Amiga/Paula (on all channels AND resamplable, but you need to make an instrument with the Amiga format and tie it to a sample first) - Arcade/SEGA PCM (same as above but you don't need to make an instrument for it and you have to use the `20xx` effect command to resample your samples) - - Neo Geo/Neo Geo EXT-Ch2 (on the last 5 channels only and can be resampled the same way as above) + - Neo Geo/Neo Geo EXT-Ch2 (on the last 7 channels only and can be resampled the same way as above) + - Seta/Allumer X1-010 (same as above, and both `1701` and `20xx` effect commands are affected on all 16 channels) 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. diff --git a/papers/doc/7-systems/README.md b/papers/doc/7-systems/README.md index 90a17ee2d..fe5af1aba 100644 --- a/papers/doc/7-systems/README.md +++ b/papers/doc/7-systems/README.md @@ -4,18 +4,33 @@ this is a list of systems that Furnace supports, including each system's effects - [Sega Genesis/Mega Drive](genesis.md) - [Sega Master System](sms.md) +- [Yamaha OPLL](opll.md) - [Game Boy](game-boy.md) - [PC Engine/TurboGrafx-16](pce.md) - [NES](nes.md) - [Commodore 64](c64.md) - [Arcade (YM2151/PCM)](arcade.md) - [Neo Geo/YM2610](ym2610.md) +- [Taito Arcade/YM2610B](ym2610b.md) - [AY-3-8910](ay8910.md) - [Amiga](amiga.md) - [Yamaha YM2612 standalone](ym2612.md) - [Yamaha YM2151 standalone](ym2151.md) +- [SegaPCM](segapcm.md) - [Atari 2600](tia.md) - [Philips SAA1099](saa1099.md) - [Microchip AY8930](ay8930.md) +- [VERA](vera.md) +- [Seta/Allumer X1-010](x1-010.md) +- [WonderSwan](wonderswan.md) +- [Bubble System WSG](bubblesystem.md) +- [Namco 163](n163.md) +- [Yamaha OPL](opl.md) +- [PC Speaker](pcspkr.md) +- [Commodore PET](pet.md) +- [Commodore VIC-20](vic20.md) +- [Konami VRC6](vrc6.md) +- [Famicom Disk System](fds.md) +- [Nintendo MMC5](mmc5.md) -Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but does not emulate the chip at all. +Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but... diff --git a/papers/doc/7-systems/amiga.md b/papers/doc/7-systems/amiga.md index fba62c430..97c1151f6 100644 --- a/papers/doc/7-systems/amiga.md +++ b/papers/doc/7-systems/amiga.md @@ -6,4 +6,8 @@ in this very computer music trackers were born... # effects -none. as of this moment the Amiga doesn't need any effects in particular, but some may be added in a future. +- `10xx`: toggle low-pass filter. `0` turns it off and `1` turns it on. +- `11xx`: toggle amplitude modulation with the next channel. + - does not work on the last channel. +- `12xx`: toggle period (frequency) modulation with the next channel. + - does not work on the last channel. \ No newline at end of file diff --git a/papers/doc/7-systems/arcade.md b/papers/doc/7-systems/arcade.md index fd17f91c1..dc9496e6b 100644 --- a/papers/doc/7-systems/arcade.md +++ b/papers/doc/7-systems/arcade.md @@ -1,7 +1,7 @@ # Arcade (Yamaha YM2151/PCM) this chip combination was used in the Sega OutRun, X and Y arcade boards, and perhaps a few others. -the actual PCM chip had 16 channels, but the number has been cut to 5 in DefleMask for seemingly arbitrary reasons. a system with all 16 channels may be coming soon. +the actual PCM chip had 16 channels, but the number has been cut to 5 in DefleMask for seemingly arbitrary reasons. # effects diff --git a/papers/doc/7-systems/ay8910.md b/papers/doc/7-systems/ay8910.md index 3dd1049f6..5df69eb65 100644 --- a/papers/doc/7-systems/ay8910.md +++ b/papers/doc/7-systems/ay8910.md @@ -1,8 +1,10 @@ # General Instrument AY-3-8910 -this chip was used in several home computers (ZX Spectrum, MSX, Amstrad CPC, Atari ST, etc.), video game consoles (Intellivision and Vectrex), arcade boards and even slot machines! +this chip was used in many home computers (ZX Spectrum, MSX, Amstrad CPC, Atari ST, etc.), video game consoles (Intellivision and Vectrex), arcade boards and even slot machines! -the chip's powerful sound comes from the envelope... +It is a 3-channel PSG sound source. The chip's powerful sound comes from the envelope... + +AY-3-8914 variant was used in Intellivision, it's basically original AY with 4 level envelope volume per channel and different register format. # effects @@ -37,4 +39,8 @@ the chip's powerful sound comes from the envelope... - in this mode the envelope period is set to the channel's notes, multiplied by a fraction. - `x` is the numerator. - `y` is the denominator. - - if `x` or `y` are 0 this will disable auto-envelope mode. \ No newline at end of file + - if `x` or `y` are 0 this will disable auto-envelope mode. +- `2Exx`: write to I/O port A. + - this changes the port's mode to "write". make sure you have connected something to it. +- `2Fxx`: write to I/O port B. + - this changes the port's mode to "write". make sure you have connected something to it. \ No newline at end of file diff --git a/papers/doc/7-systems/ay8930.md b/papers/doc/7-systems/ay8930.md index 7344b8f0b..6d9db10a9 100644 --- a/papers/doc/7-systems/ay8930.md +++ b/papers/doc/7-systems/ay8930.md @@ -5,7 +5,7 @@ a backwards-compatible successor to the AY-3-8910, with increased volume resolut sadly, this soundchip has only ever observed minimal success, and has remained rather obscure since. it is known for being used in the Covox Sound Master, which didn't sell well either. -while emulation of this chip is mostly complete, the additional noise setup registers are not emulated (yet). whether it ever has been emulated at some point in a now-abandoned tracker with similar goal as Furnace is unknown. +while emulation of this chip is mostly complete, hardware comparison hasn't been performed yet due to its scarcity. it also was emulated in a now-abandoned tracker with similar goal as Furnace, which sparked interest on the chip. # effects diff --git a/papers/doc/7-systems/bubblesystem.md b/papers/doc/7-systems/bubblesystem.md new file mode 100644 index 000000000..0c363163e --- /dev/null +++ b/papers/doc/7-systems/bubblesystem.md @@ -0,0 +1,15 @@ +# Bubble System WSG + +a Konami's 2 channel wavetable sound generator logic used at their arcade hardware Bubble System. + +It's configured with K005289, 4 bit PROM and DAC. + +Also known as K005289, but that's just part of the logic used for pitch and wavetable ROM address. +Waveform select and Volume control are tied with single AY-3-8910 IO for both channels. +Another AY-3-8910 IO is used for reading sound hardware status. + +Furnace emulates this configurations as single system, waveform format is 15 level and 32 width. + +# effects + +- `10xx`: change wave. diff --git a/papers/doc/7-systems/fds.md b/papers/doc/7-systems/fds.md new file mode 100644 index 000000000..1174b3f3b --- /dev/null +++ b/papers/doc/7-systems/fds.md @@ -0,0 +1,26 @@ +# Famicom Disk System + +the Famicom Disk System is an expansion device for the Famicom (known as NES outside Japan), a popular console from the '80's. +as it name implies, it allowed people to play games on specialized floppy disks that could be rewritten on vending machines, therefore reducing the cost of ownership and manufacturing. + +it also offers an additional 6-bit, 64-byte wavetable sound channel with (somewhat limited) FM capabilities, which is what Furnace supports. + +# effects + +- `10xx`: change wave. +- `11xx`: set modulation depth. +- `12xy`: set modulation speed high byte and toggle on/off. + - `x` is the toggle. a value of 1 turns on the modulator. + - `y` is the speed. +- `13xx`: set modulation speed low byte. +- `14xx`: set modulator position. +- `15xx`: set modulator wave. + - `xx` points to a wavetable. it should (preferably) have a height of 7 with the values mapping to: + - 0: +0 + - 1: +1 + - 2: +2 + - 3: +3 + - 4: reset + - 5: -3 + - 6: -2 + - 7: -1 diff --git a/papers/doc/7-systems/lynx.md b/papers/doc/7-systems/lynx.md new file mode 100644 index 000000000..a92b25806 --- /dev/null +++ b/papers/doc/7-systems/lynx.md @@ -0,0 +1,22 @@ +# Atari Lynx/MIKEY + +The Atari Lynx is a 16 bit handheld console developed by (obviously) Atari Corporation, and initially released in September of 1989, with the worldwide release being in 1990. + +The Lynx, while being an incredible handheld for the time (and a lot more powerful than a Game Boy), unfortunately meant nothing in the end due to the Lynx being a market failure, and ending up as one of the things that contributed to the downfall of Atari. + +Although the Lynx is still getting (rather impressive) homebrew developed for it, it does not mean that the Lynx is a popular system at all. + +The Atari Lynx's custom sound chip and CPU (MIKEY) is a 6502-based 8 bit CPU running at 16MHz, however this information is generally not useful in the context of Furnace. + +## Sound capabilities + + - The MIKEY has 4 channels of square wave-based sound, which can be modulated with different frequencies (×0, ×1, ×2, ×3, ×4, ×5, ×7, ×10, and ×11) to create wavetable-like results. + - Likewise, when a lot of the modulators are activated, this can provide a "pseudo-white noise"-like effect, whoch can be useful for drums and sound effects. + - The MIKEY also has hard stereo panning capabilities via the `08xx` effect command. + - The MIKEY has four 8-bit DACs (Digital to Analog Converter) — one for each voice — that essentially mean you can play samples on the MIKEY (at the cost of CPU time and memory). + - The MIKEY also has a variety of pitches to choose from, and they go from 32Hz to "above the range of human hearing", according to Atari. + +## Effect commands + - `3xxx`: Load LFSR (0 to FFF). + - this is a bitmask. + - for it to work, duty macro in instrument editor must be set to some value, without it LFSR will not be fed with any bits. diff --git a/papers/doc/7-systems/mmc5.md b/papers/doc/7-systems/mmc5.md new file mode 100644 index 000000000..faf305c60 --- /dev/null +++ b/papers/doc/7-systems/mmc5.md @@ -0,0 +1,12 @@ +# Nintendo MMC5 + +a mapper chip which made NES cartridges exceeding 1MB possible. + +it has two pulse channels which are very similar to the ones found in the NES, but lacking the sweep unit. + +additionally, it offers an 8-bit DAC which can be used to play samples. only one game is known to use it, though. + +# effects + +- `12xx`: set duty cycle or noise mode of channel. + - may be 0-3 for the pulse channels and 0-1 for the noise channel. diff --git a/papers/doc/7-systems/n163.md b/papers/doc/7-systems/n163.md new file mode 100644 index 000000000..68774801c --- /dev/null +++ b/papers/doc/7-systems/n163.md @@ -0,0 +1,25 @@ +# Namco 163 + +This is Namco's one of NES mapper, with up to 8 wavetable channels. It has also 128 byte of internal RAM, both channel register and wavetables are stored here. Wavetables are variable size and freely allocable anywhere in RAM, it means it can be uses part of or continuously pre-loaded waveform and/or its sequences in RAM. But waveform RAM area becomes smaller as much as activating more channels; Channel register consumes 8 byte for each channels. You must avoid conflict with channel register area and waveform for avoid channel playback broken. + +It has can be outputs only single channel at clock; so it's sound quality is more crunchy as much as activating more channels. + +Furnace supports both load waveform into RAM and waveform playback simultaneously, and channel limit is dynamically changeable with effect commands. +You must load waveform to RAM first for playback or do something, its load behavior is changeable to auto-update when every waveform changes or manual update. +Both waveform playback and load command is works independently per each channel columns, (Global) commands are don't care about the channel columns for work commands and its load behavior is independent with per-channel column load commands. + +# effects + +- `10xx`: set waveform for playback. +- `11xx`: set waveform position in RAM for playback. (single nibble unit) +- `12xx`: set waveform length in RAM for playback. (04 to FC, 4 nibble unit) +- `130x`: set playback waveform update behavior. (0: off, bit 0: update now, bit 1: update when every waveform is changed) +- `14xx`: set waveform for load to RAM. +- `15xx`: set waveform position for load to RAM. (single nibble unit) +- `16xx`: set waveform length for load to RAM. (04 to FC, 4 nibble unit) +- `170x`: set waveform load behavior. (0: off, bit 0: load now, bit 1: load when every waveform is changed) +- `180x`: set channel limit (0 to 7, x + 1) +- `20xx`: (Global) set waveform for load to RAM. +- `21xx`: (Global) set waveform position for load to RAM. (single nibble unit) +- `22xx`: (Global) set waveform length for load to RAM. (04 to FC, 4 nibble unit) +- `230x`: (Global) set waveform load behavior. (0: off, bit 0: load now, bit 1: load when every waveform is changed) diff --git a/papers/doc/7-systems/nes.md b/papers/doc/7-systems/nes.md index dc6eb20af..3de0e3076 100644 --- a/papers/doc/7-systems/nes.md +++ b/papers/doc/7-systems/nes.md @@ -2,7 +2,7 @@ the console from Nintendo that plays Super Mario Bros. and helped revive the agonizing video game market in the US during mid-80s. -also known as Famicom. +also known as Famicom. It is a five-channel PSG: first two channels play pulse wave with three different duty cycles, third is a fixed-volume triangle channel, fourth is a noise channel (can work in both pseudo-random and periodic modes) and fifth is a (D)PCM sample channel # effects @@ -15,4 +15,4 @@ also known as Famicom. - `14xy`: setup sweep down. - `x` is the time. - `y` is the shift. - - set to 0 to disable it. \ No newline at end of file + - set to 0 to disable it. diff --git a/papers/doc/7-systems/opl.md b/papers/doc/7-systems/opl.md new file mode 100644 index 000000000..b019e3f98 --- /dev/null +++ b/papers/doc/7-systems/opl.md @@ -0,0 +1,46 @@ +# Yamaha OPL + +a series of FM sound chips which were very popular in DOS land. it was so popular that even Yamaha made a logo for it! + +essentially a downgraded version of Yamaha's other FM chips, with only 2 operators per channel. +however, it also had a drums mode, and later chips in the series added more waveforms (than just the typical sine) and even a 4-operator mode. + +the original OPL (Yamaha YM3526) was present as an expansion for the Commodore 64 and MSX computers (erm, a variant of it). it only had 9 two-operator channels and drums mode. + +its successor, the OPL2 (Yamaha YM3812), added 3 more waveforms and was one of the more popular chips because it was present on the AdLib card for PC. +later Creative would borrow the chip to make the Sound Blaster, and totally destroyed AdLib's dominance. + +the OPL3 (Yamaha YMF262) added 9 more channels, 4 more waveforms, rudimentary 4-operator mode (pairing up to 12 channels to make up to six 4-operator channels), quadraphonic output (sadly Furnace only supports stereo) and some other things. + +afterwards everyone moved to Windows and software mixed PCM streaming... + +# effects + +- 10xx: set AM depth. the following values are accepted: + - 0: 1dB (shallow) + - 1: 4.8dB (deep) + - this effect applies to all channels. +- `11xx`: set feedback of channel. +- `12xx`: set operator 1 level. +- `13xx`: set operator 2 level. +- `14xx`: set operator 3 level. + - only in 4-op mode (OPL3). +- `15xx`: set operator 4 level. + - only in 4-op mode (OPL3). +- `16xy`: set multiplier of operator. + - `x` is the operator (1-4; last 2 operators only in 4-op mode). + - `y` is the mutliplier. +- 17xx: set vibrato depth. the following values are accepted: + - 0: normal + - 1: double + - this effect applies to all channels. +- `18xx`: toggle drums mode. + - 0 disables it and 1 enables it. + - only in drums system. +- `19xx`: set attack of all operators. +- `1Axx`: set attack of operator 1. +- `1Bxx`: set attack of operator 2. +- `1Cxx`: set attack of operator 3. + - only in 4-op mode (OPL3). +- `1Dxx`: set attack of operator 4. + - only in 4-op mode (OPL3). diff --git a/papers/doc/7-systems/opll.md b/papers/doc/7-systems/opll.md index d760d413e..2e92171ba 100644 --- a/papers/doc/7-systems/opll.md +++ b/papers/doc/7-systems/opll.md @@ -1,21 +1,39 @@ # Yamaha YM2413/OPLL -The YM2413, otherwise known as OPLL, is a cost-reduced FM synthesis sound chip manufactured by Yamaha Corporation and based on the Yamaha YM3812 sound chip (OPL2). -As of Furnace version 0.5.7pre4, the OPLL sound chip is not yet emulated. It is, however, emulated in Deflemask as of version 1.1.0. Support for loading .DMFs which contain the YM2413 was added in Furnace version 0.5.7pre4. +the YM2413, otherwise known as OPLL, is a cost-reduced FM synthesis sound chip, based on the Yamaha YM3812 (OPL2). thought OPL was downgraded enough? :p -## Technical specifications -The YM2413 is equipped with the following features: - - 9 channels of 2 operator FM synthesis - - A drum/percussion mode, replacing the last 3 voices with 3 rhythm channels - - 1 user-definable patch (this patch can be changed throughout the course of the song) - - 15 pre-defined patches which can all be used at the same time - - Support for ADSR on both the modulator and the carrier - - Sine and half-sine based FM synthesis - - 9 octave note control - - 4096 different frequencies for channels - - 16 unique volume levels (NOTE: Volume 0 is NOT silent.) - - Modulator and carrier key scaling - - Built-in hardware vibrato support +OPLL spawned also a few derivative chips, the best known of these is: +- the myth. the legend. THE VRC7. 6 channels, *rather interesting* instruments sound bank, no drums mode +- Yamaha YM2423, same chip as YM2413, just a different patch set +- Yamaha YMF281, ditto -## Effect commands -TODO: Add effect commands here when YM2413 emulation is added. +# technical specifications + +the YM2413 is equipped with the following features: + +- 9 channels of 2 operator FM synthesis +- A drum/percussion mode, replacing the last 3 voices with 3 rhythm channels +- 1 user-definable patch (this patch can be changed throughout the course of the song) +- 15 pre-defined patches which can all be used at the same time +- Support for ADSR on both the modulator and the carrier +- Sine and half-sine based FM synthesis +- 9 octave note control +- 4096 different frequencies for channels +- 16 unique volume levels (NOTE: Volume 0 is NOT silent.) +- Modulator and carrier key scaling +- Built-in hardware vibrato support + +# effects + +- `11xx`: set feedback of channel. +- `12xx`: set operator 1 level. +- `13xx`: set operator 2 level. +- `16xy`: set multiplier of operator. + - `x` is the operator (1 or 2). + - `y` is the mutliplier. +- `18xx`: toggle drums mode. + - 0 disables it and 1 enables it. + - only in drums system. +- `19xx`: set attack of all operators. +- `1Axx`: set attack of operator 1. +- `1Bxx`: set attack of operator 2. diff --git a/papers/doc/7-systems/pce.md b/papers/doc/7-systems/pce.md index 5034e83fd..c061a9b7a 100644 --- a/papers/doc/7-systems/pce.md +++ b/papers/doc/7-systems/pce.md @@ -1,9 +1,11 @@ # PC Engine/TurboGrafx-16 -a console from NEC that attempted to enter the fierce battle between Nintendo and Sega, but because its capabilities are a mix of third and fourth generation, it failed to last long. +a console from NEC that, depending on a region: + attempted to enter the fierce battle between Nintendo and Sega, but because its capabilities are a mix of third and fourth generation, it failed to last long. (US and Europe) + was Nintendo's most fearsome rival, completely defeating Sega Mega Drive and defending itself against Super Famicom (Japan) it has 6 wavetable channels and the last two ones also double as noise channels. -furthermore, it has some PCM! +furthermore, it has some PCM and LFO! # effects diff --git a/papers/doc/7-systems/pcspkr.md b/papers/doc/7-systems/pcspkr.md new file mode 100644 index 000000000..6c2e1a94c --- /dev/null +++ b/papers/doc/7-systems/pcspkr.md @@ -0,0 +1,7 @@ +# PC Speaker + +40 years of one square beep - and still going! Single channel, no volume control... + +# effects + +ha! effects... diff --git a/papers/doc/7-systems/pet.md b/papers/doc/7-systems/pet.md new file mode 100644 index 000000000..f2c2c1c61 --- /dev/null +++ b/papers/doc/7-systems/pet.md @@ -0,0 +1,11 @@ +# Commodore PET + +a computer from 1977 which was leader on US schools back then. subsequently the Apple II took its throne. + +maybe no better than a computer terminal, but somebody discovered a way to update the screen at turbo rate - and eventually its sound "chip" (it was nothing more than an 8-bit shift register) was abused as well. + +some of these didn't even have sound... + +# effects + +- 10xx: set waveform. `xx` is a bitmask. diff --git a/papers/doc/7-systems/saa1099.md b/papers/doc/7-systems/saa1099.md index 093eb603e..9cf7b6431 100644 --- a/papers/doc/7-systems/saa1099.md +++ b/papers/doc/7-systems/saa1099.md @@ -1,6 +1,9 @@ # Philips SAA1099 -this was used by the Game Blaster and SAM Coupé. it's pretty similar to the AY-3-8910, but has stereo sound, twice the channels and two envelopes, both of which are highly flexible. +this was used by the Game Blaster and SAM Coupé. it's pretty similar to the AY-3-8910, but has stereo sound, twice the channels and two envelopes, both of which are highly flexible. The envelopes work like this: + +an instrument with envelope settings is placed on channel 2 or channel 5 +an instrument that is used as an "envelope output", is placed on channel 3 or channel 6. You may want to disable wave output on the output channel. # effects diff --git a/papers/doc/7-systems/segapcm.md b/papers/doc/7-systems/segapcm.md new file mode 100644 index 000000000..1b3ed63fb --- /dev/null +++ b/papers/doc/7-systems/segapcm.md @@ -0,0 +1,12 @@ +# SegaPCM + +16 channels of PCM? no way! + +yep, that's right! 16 channels of PCM! + +a chip used in the Sega OutRun/X/Y arcade boards. eventually the MultiPCM surpassed it with 24 channels, and later they joined the software mixing gang. + +# effects +- `20xx`: set PCM frequency. + - `xx` is a 256th fraction of 31250Hz. + - this effect exists for mostly DefleMask compatibility - it is otherwise recommended to use Sample type instruments. diff --git a/papers/doc/7-systems/sms.md b/papers/doc/7-systems/sms.md index 61650ecf9..53560a976 100644 --- a/papers/doc/7-systems/sms.md +++ b/papers/doc/7-systems/sms.md @@ -2,7 +2,7 @@ the predecessor to Genesis. -surely had better graphics than NES, but its sound is subject of several jokes. +surely had better graphics than NES, but its sound (fairly weak, 4ch PSG with A-3 is a lowest tone) is subject of several jokes. this console is powered by a derivative of the Texas Instruments SN76489. diff --git a/papers/doc/7-systems/vera.md b/papers/doc/7-systems/vera.md new file mode 100644 index 000000000..be610a809 --- /dev/null +++ b/papers/doc/7-systems/vera.md @@ -0,0 +1,15 @@ +# VERA + +this is a video and sound generator chip used in the Commander X16, a modern 8-bit computer created by The 8-Bit Guy. +it has 16 channels of pulse/triangle/saw/noise and one stereo PCM channel. + +currently Furnace does not support the PCM channel's stereo mode, though (except for panning). + +# effects + +- `20xx`: set waveform. the following values are accepted: + - 0: pulse + - 1: saw + - 2: triangle + - 3: noise +- `22xx`: set duty cycle. `xx` may go from 0 to 3F. diff --git a/papers/doc/7-systems/vic20.md b/papers/doc/7-systems/vic20.md new file mode 100644 index 000000000..0c8186318 --- /dev/null +++ b/papers/doc/7-systems/vic20.md @@ -0,0 +1,11 @@ +# Commodore VIC-20 + +The Commodore VIC-20 was Commodore's major attempt at making a personal home computer, and is the percursor to the Commodore 64. The VIC-20 was also known as the VC-20 in Germany, and the VIC-1001 in Japan. + +It has 4 PSG voices that has a limited but wide tuning range, and like the SN76489, the last voice is dedicated to playing pseudo-white noise. + +The 3 pulse wave channels also have different octaves that they can play notes on. The first channel is the bass channel, and it can play notes from octave 1. The next is the 'mid/chord' channel, and it plays notes from octave 2. And rather obviously, the 3rd pulse channel is typically the lead channel, can play notes from octave 3. + +## effect commands + + - `10xx` Switch waveform (`xx` from `00` to `0F`) \ No newline at end of file diff --git a/papers/doc/7-systems/vrc6.md b/papers/doc/7-systems/vrc6.md new file mode 100644 index 000000000..c89678b47 --- /dev/null +++ b/papers/doc/7-systems/vrc6.md @@ -0,0 +1,18 @@ +# Konami VRC6 + +the most popular expansion chip to the NES' sound system. + +the chip has 2 pulse wave channels and one sawtooth channel. +volume register is 4 bit for pulse wave and 6 bit for sawtooth, but sawtooth output is corrupted when volume register value is too high. because this register is actually an 8 bit accumulator, its output may wrap around. + +For that reason, the sawtooth channel has it's own instrument type. Setting volume macro and pattern editor volume setting too high (above 42/2A) will distort the waveform. + +pulse wave duty cycle is 8-level. it can be ignored and it has potential for DAC at this case: volume register in this mode is DAC output and it can be PCM playback through this mode. +Furnace supports this routine for PCM playback, but it consumes a lot of CPU time in real hardware (even if conjunction with VRC6's integrated IRQ timer). + +# effects + +these effects only are effective in the pulse channels. + +- `12xx`: set duty cycle (0 to 7). +- `17xx`: toggle PCM mode. diff --git a/papers/doc/7-systems/wonderswan.md b/papers/doc/7-systems/wonderswan.md new file mode 100644 index 000000000..44657decb --- /dev/null +++ b/papers/doc/7-systems/wonderswan.md @@ -0,0 +1,17 @@ +# WonderSwan + +A handheld console released only in Japan by Bandai. Designed by the same people behind Game Boy and Virtual Boy, it has lots of similar elements from those two systems in the sound department. + +It has 4 wavetable channels, channel #2 could play PCM, channel #3 has hardware sweep and channel #4 could play noise. + +# effects + +- `10xx`: change wave. +- `11xx`: setup noise mode (channel 4 only). + - 0: disable. + - 1-8: enable and set tap preset. +- `12xx`: setup sweep period (channel 3 only). + - 0: disable. + - 1-32: enable and set period. +- `13xx`: setup sweep amount (channel 3 only). +- `17xx`: toggle PCM mode (channel 2 only). diff --git a/papers/doc/7-systems/x1-010.md b/papers/doc/7-systems/x1-010.md new file mode 100644 index 000000000..411afb3e7 --- /dev/null +++ b/papers/doc/7-systems/x1-010.md @@ -0,0 +1,47 @@ +# Seta/Allumer X1-010 + +A sound chip designed by Seta, mainly used in their own arcade hardware from the late 80s to the early 2000s. +It has 2 output channels, but there is no known hardware taking advantage of stereo sound capabilities. +Later hardware paired this with external bankswitching logic, but this isn't emulated yet. +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. + +# waveform types + +This chip supports 2 types of waveforms, needs to be paired to external 8 KB RAM to access these features: + +One is a signed 8 bit mono waveform, operated like other wavetable based sound systems. +These are stored at the lower half of RAM at common case. + +The other one ("Envelope") is a 4 bit stereo waveform, multiplied with the above and calculates final output, each nibble is used for each output channel. +These are stored at the upper half of RAM at common case. + +Both waveforms are 128 bytes (fixed size), freely allocated at each half of RAM except the channel register area: each half can store total 32/31 waveforms at once. +In furnace, you can enable the envelope shape split mode. When it is set, its waveform will be split to the left and right halves for each output. Each max size is 128 bytes, total 256 bytes. + +# effects + +- `10xx`: change wave. +- `11xx`: change envelope shape (also wavetable). +- `17xx`: toggle PCM mode. +- `20xx`: set PCM frequency (1 to FF). +- `22xx`: set envelope mode. + - bit 0 sets whether envelope will affect this channel. + - bit 1 toggles the envelope one-shot mode. when it is set, channel is halted after envelope cycle is finished. + - bit 2 toggles the envelope shape split mode. when it is set, envelope shape will be split to left half and right half. + - bit 3/5 sets whether the right/left shape will mirror the original one. + - bit 4/6 sets whether the right/left output will mirror the original one. +- `23xx`: set envelope period. +- `25xx`: slide envelope period up. +- `26xx`: slide envelope period down. +- `29xy`: enable auto-envelope mode. + - in this mode the envelope period is set to the channel's notes, multiplied by a fraction. + - `x` is the numerator. + - `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. diff --git a/papers/doc/7-systems/ym2151.md b/papers/doc/7-systems/ym2151.md index ff5a789df..69eba0fb3 100644 --- a/papers/doc/7-systems/ym2151.md +++ b/papers/doc/7-systems/ym2151.md @@ -1,6 +1,8 @@ # Yamaha YM2151 -the sound chip powering the Arcade system, available for standalone use if you want to make X68000 music or pair it with a 16-channel Sega PCM when it comes. +the sound chip powering several arcade boards and the Sharp X1/X68000. Eight 4-op FM channels, with overpowered LFO and almost unused noise generator. + +it also was present on several pinball machines and synthesizers of the era, and later surpassed by the YM2414 (OPZ) present in the world-famous TX81Z. # effects diff --git a/papers/doc/7-systems/ym2610.md b/papers/doc/7-systems/ym2610.md index dbe8b6a54..4515fde23 100644 --- a/papers/doc/7-systems/ym2610.md +++ b/papers/doc/7-systems/ym2610.md @@ -2,7 +2,7 @@ originally an arcade board, but SNK shortly adapted it to a rather expensive video game console with the world's biggest cartridges because some people liked the system so much they wanted a home version of it. -its soundchip is a 3-in-1: FM, YM2149 (AY-3-8910 clone) and ADPCM in a single package! +its soundchip is a 4-in-1: 4ch 4-op FM, YM2149 (AY-3-8910 clone) and 2 different format ADPCM in a single package! # effects @@ -56,4 +56,4 @@ its soundchip is a 3-in-1: FM, YM2149 (AY-3-8910 clone) and ADPCM in a single pa - in this mode the envelope period is set to the channel's notes, multiplied by a fraction. - `x` is the numerator. - `y` is the denominator. - - if `x` or `y` are 0 this will disable auto-envelope mode. \ No newline at end of file + - if `x` or `y` are 0 this will disable auto-envelope mode. diff --git a/papers/doc/7-systems/ym2610b.md b/papers/doc/7-systems/ym2610b.md new file mode 100644 index 000000000..e251122cd --- /dev/null +++ b/papers/doc/7-systems/ym2610b.md @@ -0,0 +1,58 @@ +# Taito Arcade/Yamaha YM2610B + +YM2610B is basically YM2610 with 2 extra FM channels used at some 90s Taito arcade hardware. +it is backward compatible with the original chip. + +# effects + +- `10xy`: set LFO parameters. + - `x` toggles the LFO. + - `y` sets its speed. +- `11xx`: set feedback of channel. +- `12xx`: set operator 1 level. +- `13xx`: set operator 2 level. +- `14xx`: set operator 3 level. +- `15xx`: set operator 4 level. +- `16xy`: set multiplier of operator. + - `x` is the operator (1-4). + - `y` is the mutliplier. +- `18xx`: toggle extended channel 2 mode. + - 0 disables it and 1 enables it. + - only in extended channel 2 system. +- `19xx`: set attack of all operators. +- `1Axx`: set attack of operator 1. +- `1Bxx`: set attack of operator 2. +- `1Cxx`: set attack of operator 3. +- `1Dxx`: set attack of operator 4. +- `20xx`: set SSG channel mode. `xx` may be one of the following: + - `00`: square + - `01`: noise + - `02`: square and noise + - `03`: nothing (apparently) + - `04`: envelope and square + - `05`: envelope and noise + - `06`: envelope and square and noise + - `07`: nothing +- `21xx`: set noise frequency. `xx` is a value between 00 and 1F. +- `22xy`: set envelope mode. + - `x` sets the envelope shape, which may be one of the following: + - `0: \___` decay + - `4: /___` attack once + - `8: \\\\` saw + - `9: \___` decay + - `A: \/\/` inverse obelisco + - `B: \¯¯¯` decay once + - `C: ////` inverse saw + - `D: /¯¯¯` attack + - `E: /\/\` obelisco + - `F: /___` attack once + - if `y` is 1 then the envelope will affect this channel. +- `23xx`: set envelope period low byte. +- `24xx`: set envelope period high byte. +- `25xx`: slide envelope period up. +- `26xx`: slide envelope period down. +- `29xy`: enable SSG auto-envelope mode. + - in this mode the envelope period is set to the channel's notes, multiplied by a fraction. + - `x` is the numerator. + - `y` is the denominator. + - if `x` or `y` are 0 this will disable auto-envelope mode. diff --git a/papers/doc/7-systems/ym2612.md b/papers/doc/7-systems/ym2612.md index bfe87cf53..b7dd8a16e 100644 --- a/papers/doc/7-systems/ym2612.md +++ b/papers/doc/7-systems/ym2612.md @@ -1,6 +1,6 @@ # Yamaha YM2612 -one of two chips that powered the Sega Genesis. +one of two chips that powered the Sega Genesis. It is a six-channel, four-operator FM synthesizer. Channel #6 can be turned into 8-bit PCM player. # effects diff --git a/papers/doc/7-systems/ymu759.md b/papers/doc/7-systems/ymu759.md index 1ad80939e..339848321 100644 --- a/papers/doc/7-systems/ymu759.md +++ b/papers/doc/7-systems/ymu759.md @@ -4,7 +4,8 @@ the Yamaha YMU759 is a sound chip designed for feature phones during the early 2 it is also known as MA-2. sadly Yamaha didn't care about these chips too much, and the register specs were completely unavailable, which means the YMU759 is totally unsupported and unemulated besides Yamaha's official emulator for it built into MidRadio. -hence Furnace does not emulate the chip (and doesn't even let you add it to a song). + +Furnace 0.6 loads DefleMask modules written for this system; however, it doesn't support any of its effects and is simulated using the OPL core. # effects diff --git a/papers/format.md b/papers/format.md index 882efa7bd..ce1685c63 100644 --- a/papers/format.md +++ b/papers/format.md @@ -29,6 +29,26 @@ furthermore, an `or reserved` indicates this field is always present, but is res the format versions are: +- 80: Furnace dev80 +- 79: Furnace dev79 +- 78: Furnace dev78 +- 77: Furnace dev77 +- 76: Furnace dev76 +- 75: Furnace dev75/April Fools' 0.6pre0 +- 74: Furnace dev74 +- 73: Furnace dev73 +- 72: Furnace dev72 +- 71: Furnace dev71 +- 70: Furnace dev70 +- 69: Furnace dev69 +- 68: Furnace dev68 +- 67: Furnace dev67 +- 66: Furnace dev66 +- 65: Furnace dev65 +- 64: Furnace dev64 +- 63: Furnace dev63 +- 62: Furnace dev62 +- 61: Furnace dev61 - 60: Furnace dev60 - 59: Furnace dev59 - 58: Furnace dev58 @@ -100,12 +120,17 @@ size | description | - 60 is NTSC | - 50 is PAL 2 | pattern length + | - the limit is 256. 2 | orders length + | - the limit is 256 (>=80) or 127 (<80). 1 | highlight A 1 | highlight B 2 | instrument count + | - the limit is 256. 2 | wavetable count + | - the limit is 256. 2 | sample count + | - the limit is 256. 4 | pattern count 32 | list of sound chips | - possible soundchips: @@ -118,63 +143,68 @@ size | description | - 0x06: NES - 5 channels | - 0x07: C64 (8580) - 3 channels | - 0x08: Arcade (YM2151+SegaPCM) - 13 channels (compound!) - | - 0x09: Neo Geo (YM2610) - 13 channels - | - bit 6 enables alternate mode: - | - 0x42: Genesis extended - 13 channels - | - 0x43: SMS (SN76489) + OPLL (YM2413) - 13 channels (compound!) - | - 0x46: NES + VRC7 - 11 channels (compound!) - | - 0x47: C64 (6581) - 3 channels - | - 0x49: Neo Geo extended - 16 channels - | - bit 7 for non-DefleMask chips: - | - 0x80: AY-3-8910 - 3 channels - | - 0x81: Amiga - 4 channels - | - 0x82: YM2151 alone - 8 channels - | - 0x83: YM2612 alone - 6 channels - | - 0x84: TIA - 2 channels - | - 0x85: VIC-20 - 4 channels - | - 0x86: PET - 1 channel - | - 0x87: SNES - 8 channels - | - 0x88: VRC6 - 3 channels - | - 0x89: OPLL (YM2413) - 9 channels - | - 0x8a: FDS - 1 channel - | - 0x8b: MMC5 - 3 channels - | - 0x8c: Namco 163 - 8 channels - | - 0x8d: OPN (YM2203) - 6 channels - | - 0x8e: PC-98 (YM2608) - 16 channels - | - 0x8f: OPL (YM3526) - 9 channels - | - 0x90: OPL2 (YM3812) - 9 channels - | - 0x91: OPL3 (YMF262) - 18 channels - | - 0x92: MultiPCM - 24 channels - | - 0x93: Intel 8253 (beeper) - 1 channel - | - 0x94: POKEY - 4 channels - | - 0x95: RF5C68 - 8 channels - | - 0x96: WonderSwan - 4 channels - | - 0x97: Philips SAA1099 - 6 channels - | - 0x98: OPZ (YM2414) - 8 channels - | - 0x99: Pokémon Mini - 1 channel - | - 0x9a: AY8930 - 3 channels - | - 0x9b: SegaPCM - 16 channels - | - 0x9c: Virtual Boy - 6 channels - | - 0x9d: VRC7 - 6 channels - | - 0x9e: YM2610B - 16 channels - | - 0x9f: ZX Spectrum (beeper) - 6 channels - | - 0xa0: YM2612 extended - 9 channels - | - 0xa1: Konami SCC - 5 channels - | - 0xa2: OPL drums (YM3526) - 11 channels - | - 0xa3: OPL2 drums (YM3812) - 11 channels - | - 0xa4: OPL3 drums (YMF262) - 20 channels - | - 0xa5: OPL3 4-op (YMF262) - 12 channels - | - 0xa6: OPL3 4-op + drums (YMF262) - 14 channels - | - 0xa7: OPLL drums (YM2413) - 11 channels - | - 0xa8: Atari Lynx - 4 channels - | - 0xe0: QSound - 19 channels + | - 0x09: Neo Geo CD (YM2610) - 13 channels + | - 0x42: Genesis extended - 13 channels + | - 0x43: SMS (SN76489) + OPLL (YM2413) - 13 channels (compound!) + | - 0x46: NES + VRC7 - 11 channels (compound!) + | - 0x47: C64 (6581) - 3 channels + | - 0x49: Neo Geo CD extended - 16 channels + | - 0x80: AY-3-8910 - 3 channels + | - 0x81: Amiga - 4 channels + | - 0x82: YM2151 alone - 8 channels + | - 0x83: YM2612 alone - 6 channels + | - 0x84: TIA - 2 channels + | - 0x85: VIC-20 - 4 channels + | - 0x86: PET - 1 channel + | - 0x87: SNES - 8 channels + | - 0x88: VRC6 - 3 channels + | - 0x89: OPLL (YM2413) - 9 channels + | - 0x8a: FDS - 1 channel + | - 0x8b: MMC5 - 3 channels + | - 0x8c: Namco 163 - 8 channels + | - 0x8d: OPN (YM2203) - 6 channels + | - 0x8e: PC-98 (YM2608) - 16 channels + | - 0x8f: OPL (YM3526) - 9 channels + | - 0x90: OPL2 (YM3812) - 9 channels + | - 0x91: OPL3 (YMF262) - 18 channels + | - 0x92: MultiPCM - 24 channels + | - 0x93: Intel 8253 (beeper) - 1 channel + | - 0x94: POKEY - 4 channels + | - 0x95: RF5C68 - 8 channels + | - 0x96: WonderSwan - 4 channels + | - 0x97: Philips SAA1099 - 6 channels + | - 0x98: OPZ (YM2414) - 8 channels + | - 0x99: Pokémon Mini - 1 channel + | - 0x9a: AY8930 - 3 channels + | - 0x9b: SegaPCM - 16 channels + | - 0x9c: Virtual Boy - 6 channels + | - 0x9d: VRC7 - 6 channels + | - 0x9e: YM2610B - 16 channels + | - 0x9f: ZX Spectrum (beeper) - 6 channels + | - 0xa0: YM2612 extended - 9 channels + | - 0xa1: Konami SCC - 5 channels + | - 0xa2: OPL drums (YM3526) - 11 channels + | - 0xa3: OPL2 drums (YM3812) - 11 channels + | - 0xa4: OPL3 drums (YMF262) - 20 channels + | - 0xa5: Neo Geo (YM2610) - 14 channels + | - 0xa6: Neo Geo extended (YM2610) - 17 channels + | - 0xa7: OPLL drums (YM2413) - 11 channels + | - 0xa8: Atari Lynx - 4 channels + | - 0xa9: SegaPCM (for Deflemask Compatibility) - 5 channels + | - 0xaa: MSM6295 - 4 channels + | - 0xab: MSM6258 - 1 channel + | - 0xac: Commander X16 (VERA) - 17 channels + | - 0xad: Bubble System WSG - 2 channels + | - 0xb0: Seta/Allumer X1-010 - 16 channels + | - 0xde: YM2610B extended - 19 channels + | - 0xe0: QSound - 19 channels | - (compound!) means that the system is composed of two or more chips, | and has to be flattened. 32 | sound chip volumes | - signed char, 64=1.0, 127=~2.0 32 | sound chip panning | - signed char, -128=left, 127=right - 128 | sound chip parameters (TODO) + 128 | sound chip parameters STR | song name STR | song author 4f | A-4 tuning @@ -192,7 +222,12 @@ size | description 1 | wack algorithm macro (>=47) or reserved 1 | broken shortcut slides (>=49) or reserved 1 | ignore duplicate slides (>=50) or reserved - 6 | reserved + 1 | stop portamento on note off (>=62) or reserved + 1 | continuous vibrato (>=62) or reserved + 1 | broken DAC mode (>=64) or reserved + 1 | one tick cut (>=65) or reserved + 1 | instrument change allowed during porta (>=66) or reserved + 1 | reset note base on arpeggio effect stop (0000) (>=69) or reserved 4?? | pointers to instruments 4?? | pointers to wavetables 4?? | pointers to samples @@ -201,6 +236,7 @@ size | description | - a table of bytes | - size=channels*ordLen | - read orders then channels + | - the maximum value of a cell is FF (>=80) or 7F (<80). ??? | effect columns | - size=channels 1?? | channel hide status @@ -214,6 +250,15 @@ size | description STR | song comment 4f | master volume, 1.0f=100% (>=59) | this is 2.0f for modules before 59 + --- | **extended compatibility flags** (>=70) + 1 | broken speed selection + 1 | no slides on first tick (>=71) or reserved + 1 | next row reset arp pos (>=71) or reserved + 1 | ignore jump at end (>=71) or reserved + 1 | buggy portamento after slide (>=72) or reserved + 1 | new ins affects envelope (Game Boy) (>=72) or reserved + 1 | ExtCh channel state is shared (>=78) or reserved + 25 | reserved ``` # instrument @@ -237,7 +282,9 @@ size | description 1 | feedback 1 | fms 1 | ams - 1 | operator count (always 4) + 1 | operator count + | - this is either 2 or 4, and is ignored on non-OPL systems. + | - always read 4 ops regardless of this value. 1 | OPLL preset (>=60) or reserved | - 0: custom | - 1-15: pre-defined patches @@ -426,6 +473,133 @@ size | description 4 | DT macro release 4 | D2R macro release 4 | SSG-EG macro release + --- | **extended op macro headers** × 4 (>=61) + 4 | DAM macro length + 4 | DVB macro length + 4 | EGT macro length + 4 | KSL macro length + 4 | SUS macro length + 4 | VIB macro length + 4 | WS macro length + 4 | KSR macro length + 4 | DAM macro loop + 4 | DVB macro loop + 4 | EGT macro loop + 4 | KSL macro loop + 4 | SUS macro loop + 4 | VIB macro loop + 4 | WS macro loop + 4 | KSR macro loop + 4 | DAM macro release + 4 | DVB macro release + 4 | EGT macro release + 4 | KSL macro release + 4 | SUS macro release + 4 | VIB macro release + 4 | WS macro release + 4 | KSR macro release + 1 | DAM macro open + 1 | DVB macro open + 1 | EGT macro open + 1 | KSL macro open + 1 | SUS macro open + 1 | VIB macro open + 1 | WS macro open + 1 | KSR macro open + --- | **extended op macros** × 4 (>=61) + 1?? | DAM macro + 1?? | DVB macro + 1?? | EGT macro + 1?? | KSL macro + 1?? | SUS macro + 1?? | VIB macro + 1?? | WS macro + 1?? | KSR macro + --- | **OPL drums mode data** (>=63) + 1 | fixed frequency mode + 1 | reserved + 2 | kick frequency + 2 | snare/hi-hat frequency + 2 | tom/top frequency + --- | **Sample instrument extra data** (>=67) + 1 | use note map + | - only read the following two data structures if this is true! + 4?? | note frequency × 120 + | - 480 bytes + 2?? | note sample × 120 + | - 240 bytes + --- | **Namco 163 data** (>=73) + 4 | initial waveform + 1 | wave position + 1 | wave length + 1 | wave mode: + | - bit 1: update on change + | - bit 0: load on playback + 1 | reserved + --- | **even more macros** (>=76) + 4 | left panning macro length + 4 | right panning macro length + 4 | phase reset macro length + 4 | extra 4 macro length + 4 | extra 5 macro length + 4 | extra 6 macro length + 4 | extra 7 macro length + 4 | extra 8 macro length + 4 | left panning macro loop + 4 | right panning macro loop + 4 | phase reset macro loop + 4 | extra 4 macro loop + 4 | extra 5 macro loop + 4 | extra 6 macro loop + 4 | extra 7 macro loop + 4 | extra 8 macro loop + 4 | left panning macro release + 4 | right panning macro release + 4 | phase reset macro release + 4 | extra 4 macro release + 4 | extra 5 macro release + 4 | extra 6 macro release + 4 | extra 7 macro release + 4 | extra 8 macro release + 1 | left panning macro open + 1 | right panning macro open + 1 | phase reset macro open + 1 | extra 4 macro open + 1 | extra 5 macro open + 1 | extra 6 macro open + 1 | extra 7 macro open + 1 | extra 8 macro open + --- | **even more macro data** (>=76) + 4?? | left panning macro + 4?? | right panning macro + 4?? | phase reset macro + 4?? | extra 4 macro + 4?? | extra 5 macro + 4?? | extra 6 macro + 4?? | extra 7 macro + 4?? | extra 8 macro + --- | **FDS instrument data** (>=76) + 4 | modulation speed + 4 | modulation depth + 1 | init modulation table with first wave + 3 | reserved + 32 | modulation table + --- | **OPZ instrument extra data** (>=77) + 1 | fms2 + 1 | ams2 + --- | **wavetable synth data** (>=79) + 4 | first wave + 4 | second wave + 1 | rate divider + 1 | effect + | - bit 7: single or dual effect + 1 | enabled + 1 | global + 1 | speed (+1) + 1 | parameter 1 + 1 | parameter 2 + 1 | parameter 3 + 1 | parameter 4 ``` # wavetable @@ -488,10 +662,29 @@ size | description | - size: rows*(4+effectColumns*2)*2 | - read shorts in this order: | - note + | - 0: empty/invalid + | - 1: C# + | - 2: D + | - 3: D# + | - 4: E + | - 5: F + | - 6: F# + | - 7: G + | - 8: G# + | - 9: A + | - 10: A# + | - 11: B + | - 12: C (of next octave) + | - 100: note off + | - 100: note release + | - 100: macro release | - octave + | - this is an signed char stored in a short. + | - therefore octave value 255 is actually octave -1. | - instrument | - volume | - effect and effect data... + | - for instrument, volume, effect and effect data, a value of -1 means empty. STR | pattern name (>=51) ``` diff --git a/res/Info.plist b/res/Info.plist index d6f1c4a03..d9528b157 100644 --- a/res/Info.plist +++ b/res/Info.plist @@ -15,17 +15,17 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString - 0.5.7 + 0.6pre0 CFBundleName Furnace CFBundlePackageType APPL CFBundleShortVersionString - 0.5.7 + 0.6pre0 CFBundleSignature ???? CFBundleVersion - 0.5.7 + 0.6pre0 NSHumanReadableCopyright NSHighResolutionCapable diff --git a/scripts/Cross-MinGW-x86.cmake b/scripts/Cross-MinGW-x86.cmake new file mode 100644 index 000000000..4049ffdd0 --- /dev/null +++ b/scripts/Cross-MinGW-x86.cmake @@ -0,0 +1,4 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR i686) + +include(${CMAKE_CURRENT_LIST_DIR}/Cross-MinGW.cmake) diff --git a/scripts/Cross-MinGW-x86_64.cmake b/scripts/Cross-MinGW-x86_64.cmake new file mode 100644 index 000000000..5b088cb30 --- /dev/null +++ b/scripts/Cross-MinGW-x86_64.cmake @@ -0,0 +1,4 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +include(${CMAKE_CURRENT_LIST_DIR}/Cross-MinGW.cmake) diff --git a/scripts/Cross-MinGW.cmake b/scripts/Cross-MinGW.cmake new file mode 100644 index 000000000..09871d6bb --- /dev/null +++ b/scripts/Cross-MinGW.cmake @@ -0,0 +1,14 @@ +set(TARGET_PREFIX ${CMAKE_SYSTEM_PROCESSOR}-w64-mingw32) + +set(CMAKE_C_COMPILER ${TARGET_PREFIX}-gcc-posix) +set(CMAKE_CXX_COMPILER ${TARGET_PREFIX}-g++-posix) +set(PKG_CONFIG_EXECUTABLE ${TARGET_PREFIX}-pkg-config) + +set(CMAKE_FIND_ROOT_PATH /usr/${TARGET_PREFIX}) + +# Search host system for programs +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# Search target system for libraries, headers & CMake packages +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/scripts/release-win32.sh b/scripts/release-win32.sh index d2c086d2e..2257a595e 100755 --- a/scripts/release-win32.sh +++ b/scripts/release-win32.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd win32build -i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" -DBUILD_SHARED_LIBS=OFF .. || exit 1 +# TODO: potential Arch-ism? +i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" -DBUILD_SHARED_LIBS=OFF .. || exit 1 make -j8 || exit 1 i686-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win32.zip diff --git a/scripts/release-win64.sh b/scripts/release-win64.sh index 69bc602aa..db580e093 100755 --- a/scripts/release-win64.sh +++ b/scripts/release-win64.sh @@ -1,6 +1,6 @@ #!/bin/bash # make Windows release -# this script shall be run from Linux with MinGW installed! +# this script shall be run from Arch Linux with MinGW installed! if [ ! -e /tmp/furnace ]; then ln -s "$PWD" /tmp/furnace || exit 1 @@ -14,7 +14,8 @@ fi cd winbuild -x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Werror" .. || exit 1 +# TODO: potential Arch-ism? +x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Werror" .. || exit 1 make -j8 || exit 1 x86_64-w64-mingw32-strip -s furnace.exe || exit 1 @@ -30,3 +31,7 @@ cp -r ../../papers papers || exit 1 cp -r ../../demos demos || exit 1 zip -r furnace.zip LICENSE.txt furnace.exe README.txt papers demos + +furName=$(git describe --tags | sed "s/v0/0/") + +mv furnace.zip furnace-"$furName"-win64.zip diff --git a/src/audio/abstract.cpp b/src/audio/abstract.cpp index 7f9404828..45960fea9 100644 --- a/src/audio/abstract.cpp +++ b/src/audio/abstract.cpp @@ -55,4 +55,66 @@ bool TAAudio::init(TAAudioDesc& request, TAAudioDesc& response) { } TAAudio::~TAAudio() { +} + +bool TAMidiIn::gather() { + return false; +} + +bool TAMidiOut::send(const TAMidiMessage& what) { + return false; +} + +bool TAMidiIn::isDeviceOpen() { + return false; +} + +bool TAMidiOut::isDeviceOpen() { + return false; +} + +bool TAMidiIn::openDevice(String name) { + return false; +} + +bool TAMidiOut::openDevice(String name) { + return false; +} + +bool TAMidiIn::closeDevice() { + return false; +} + +bool TAMidiOut::closeDevice() { + return false; +} + +std::vector TAMidiIn::listDevices() { + return std::vector(); +} + +std::vector TAMidiOut::listDevices() { + return std::vector(); +} + +bool TAMidiIn::init() { + return false; +} + +bool TAMidiOut::init() { + return false; +} + +bool TAMidiIn::quit() { + return true; +} + +bool TAMidiOut::quit() { + return true; +} + +TAMidiIn::~TAMidiIn() { +} + +TAMidiOut::~TAMidiOut() { } \ No newline at end of file diff --git a/src/audio/jack.cpp b/src/audio/jack.cpp index 080dad2c5..33a464d51 100644 --- a/src/audio/jack.cpp +++ b/src/audio/jack.cpp @@ -52,6 +52,7 @@ 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; iinit()) { + delete midiIn; + midiIn=NULL; + return false; + } + + if (!midiOut->init()) { + midiIn->quit(); + delete midiOut; + delete midiIn; + midiOut=NULL; + midiIn=NULL; + return false; + } + return true; +#endif +} + +void TAAudio::quitMidi() { + if (midiIn!=NULL) { + midiIn->quit(); + delete midiIn; + midiIn=NULL; + } + if (midiOut!=NULL) { + midiOut->quit(); + delete midiOut; + midiOut=NULL; + } +} \ No newline at end of file diff --git a/src/audio/rtmidi.cpp b/src/audio/rtmidi.cpp index e8f0c60d6..c4a6f8f9d 100644 --- a/src/audio/rtmidi.cpp +++ b/src/audio/rtmidi.cpp @@ -1 +1,235 @@ +/** + * 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 "rtmidi.h" +#include "../ta-log.h" +#include "taAudio.h" + +// --- IN --- + +bool TAMidiInRtMidi::gather() { + std::vector msg; + if (port==NULL) return false; + 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)); + } + queue.push(m); + } + return true; +} + +std::vector TAMidiInRtMidi::listDevices() { + std::vector ret; + logD("listing devices."); + if (port==NULL) return ret; + + try { + unsigned int count=port->getPortCount(); + logD("got port count."); + for (unsigned int i=0; igetPortName(i); + if (name!="") ret.push_back(name); + } + } catch (RtMidiError& e) { + logW("could not get MIDI inputs! %s",e.what()); + } + return ret; +} + +bool TAMidiInRtMidi::isDeviceOpen() { + return isOpen; +} + +bool TAMidiInRtMidi::openDevice(String name) { + if (port==NULL) return false; + if (isOpen) return false; + try { + bool portOpen=false; + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i)==name) { + port->openPort(i); + portOpen=true; + break; + } + } + isOpen=portOpen; + if (!portOpen) logW("could not find MIDI in device..."); + return portOpen; + } catch (RtMidiError& e) { + logW("could not open MIDI in device! %s",e.what()); + return false; + } + return true; +} + +bool TAMidiInRtMidi::closeDevice() { + if (port==NULL) return false; + if (!isOpen) return false; + try { + port->closePort(); + } catch (RtMidiError& e) { + logW("could not close MIDI in device! %s",e.what()); + isOpen=false; // still + return false; + } + isOpen=false; + return true; +} + +bool TAMidiInRtMidi::init() { + if (port!=NULL) return true; + try { + port=new RtMidiIn; + } catch (RtMidiError& e) { + logW("could not initialize RtMidi in! %s",e.what()); + return false; + } + return true; +} + +bool TAMidiInRtMidi::quit() { + if (port!=NULL) { + delete port; + port=NULL; + } + return true; +} + +// --- OUT --- + +bool TAMidiOutRtMidi::send(const TAMidiMessage& what) { + if (!isOpen) return false; + if (what.type<0x80) return false; + size_t len=0; + switch (what.type&0xf0) { + case TA_MIDI_NOTE_OFF: + case TA_MIDI_NOTE_ON: + case TA_MIDI_AFTERTOUCH: + case TA_MIDI_CONTROL: + case TA_MIDI_PITCH_BEND: + len=3; + break; + case TA_MIDI_PROGRAM: + case TA_MIDI_CHANNEL_AFTERTOUCH: + len=2; + break; + } + if (len==0) switch (what.type) { + case TA_MIDI_SYSEX: // currently not supported :< + return false; + break; + case TA_MIDI_MTC_FRAME: + case TA_MIDI_SONG_SELECT: + len=2; + break; + case TA_MIDI_POSITION: + len=3; + break; + default: + len=1; + break; + } + port->sendMessage((const unsigned char*)&what.type,len); + return true; +} + +bool TAMidiOutRtMidi::isDeviceOpen() { + return isOpen; +} + +bool TAMidiOutRtMidi::openDevice(String name) { + if (port==NULL) return false; + if (isOpen) return false; + try { + bool portOpen=false; + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i)==name) { + port->openPort(i); + portOpen=true; + break; + } + } + isOpen=portOpen; + if (!portOpen) logW("could not find MIDI out device..."); + return portOpen; + } catch (RtMidiError& e) { + logW("could not open MIDI out device! %s",e.what()); + return false; + } + return true; +} + +bool TAMidiOutRtMidi::closeDevice() { + if (port==NULL) return false; + if (!isOpen) return false; + try { + port->closePort(); + } catch (RtMidiError& e) { + logW("could not close MIDI out device! %s",e.what()); + isOpen=false; // still + return false; + } + isOpen=false; + return true; +} + +std::vector TAMidiOutRtMidi::listDevices() { + std::vector ret; + if (port==NULL) return ret; + + try { + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i); + if (name!="") ret.push_back(name); + } + } catch (RtMidiError& e) { + logW("could not get MIDI outputs! %s",e.what()); + } + return ret; +} + +bool TAMidiOutRtMidi::init() { + if (port!=NULL) return true; + try { + port=new RtMidiOut; + } catch (RtMidiError& e) { + logW("could not initialize RtMidi out! %s",e.what()); + return false; + } + return true; +} + +bool TAMidiOutRtMidi::quit() { + if (port!=NULL) { + delete port; + port=NULL; + } + return true; +} \ No newline at end of file diff --git a/src/audio/rtmidi.h b/src/audio/rtmidi.h index 5fbba78fe..34ddf73e6 100644 --- a/src/audio/rtmidi.h +++ b/src/audio/rtmidi.h @@ -17,4 +17,37 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "../../extern/rtmidi/RtMidi.h" \ No newline at end of file +#include "../../extern/rtmidi/RtMidi.h" +#include "taAudio.h" + +class TAMidiInRtMidi: public TAMidiIn { + RtMidiIn* port; + bool isOpen; + public: + bool gather(); + bool isDeviceOpen(); + bool openDevice(String name); + bool closeDevice(); + std::vector listDevices(); + bool quit(); + bool init(); + TAMidiInRtMidi(): + port(NULL), + isOpen(false) {} +}; + +class TAMidiOutRtMidi: public TAMidiOut { + RtMidiOut* port; + bool isOpen; + public: + bool send(const TAMidiMessage& what); + bool isDeviceOpen(); + bool openDevice(String name); + bool closeDevice(); + std::vector listDevices(); + bool quit(); + bool init(); + TAMidiOutRtMidi(): + port(NULL), + isOpen(false) {} +}; \ No newline at end of file diff --git a/src/audio/sdl.cpp b/src/audio/sdl.cpp index 9f330fd59..3ef38d4fa 100644 --- a/src/audio/sdl.cpp +++ b/src/audio/sdl.cpp @@ -30,6 +30,7 @@ void taSDLProcess(void* inst, unsigned char* buf, int nframes) { void TAAudioSDL::onProcess(unsigned char* buf, int nframes) { if (audioProcCallback!=NULL) { + if (midiIn!=NULL) midiIn->gather(); audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize); } float* fbuf=(float*)buf; @@ -74,7 +75,7 @@ std::vector TAAudioSDL::listAudioDevices() { std::vector ret; if (!audioSysStarted) { if (SDL_Init(SDL_INIT_AUDIO)<0) { - logE("could not initialize SDL to list audio devices\n"); + logE("could not initialize SDL to list audio devices"); } else { audioSysStarted=true; } @@ -95,12 +96,12 @@ std::vector TAAudioSDL::listAudioDevices() { bool TAAudioSDL::init(TAAudioDesc& request, TAAudioDesc& response) { if (initialized) { - logE("audio already initialized\n"); + logE("audio already initialized"); return false; } if (!audioSysStarted) { if (SDL_Init(SDL_INIT_AUDIO)<0) { - logE("could not initialize SDL\n"); + logE("could not initialize SDL"); return false; } audioSysStarted=true; @@ -118,7 +119,7 @@ bool TAAudioSDL::init(TAAudioDesc& request, TAAudioDesc& response) { ai=SDL_OpenAudioDevice(request.deviceName.empty()?NULL:request.deviceName.c_str(),0,&ac,&ar,SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); if (ai==0) { - logE("could not open audio device: %s\n",SDL_GetError()); + logE("could not open audio device: %s",SDL_GetError()); return false; } diff --git a/src/audio/taAudio.h b/src/audio/taAudio.h index 05348a7f0..c1ef519dd 100644 --- a/src/audio/taAudio.h +++ b/src/audio/taAudio.h @@ -20,6 +20,7 @@ #ifndef _TAAUDIO_H #define _TAAUDIO_H #include "../ta-utils.h" +#include #include struct SampleRateChangeEvent { @@ -89,48 +90,63 @@ enum TAMidiMessageTypes { }; struct TAMidiMessage { + double time; unsigned char type; - union { - struct { - unsigned char note, vol; - } note; - struct { - unsigned char which, val; - } control; - unsigned char patch; - unsigned char pressure; - struct { - unsigned char low, high; - } pitch; - struct { - unsigned int vendor; - } sysEx; - unsigned char timeCode; - struct { - unsigned char low, high; - } position; - unsigned char song; - } data; + unsigned char data[7]; unsigned char* sysExData; size_t sysExLen; void submitSysEx(std::vector data); void done(); + + TAMidiMessage(unsigned char t, unsigned char d0, unsigned char d1): + time(0.0), + type(t), + sysExData(NULL), + sysExLen(0) { + memset(&data,0,sizeof(data)); + data[0]=d0; + data[1]=d1; + } + + TAMidiMessage(): + time(0.0), + type(0), + sysExData(NULL), + sysExLen(0) { + memset(&data,0,sizeof(data)); + } }; class TAMidiIn { public: + std::queue queue; + virtual bool gather(); bool next(TAMidiMessage& where); + virtual bool isDeviceOpen(); + virtual bool openDevice(String name); + virtual bool closeDevice(); + virtual std::vector listDevices(); + virtual bool init(); + virtual bool quit(); + TAMidiIn() { + } + virtual ~TAMidiIn(); }; class TAMidiOut { + std::queue queue; public: - bool send(TAMidiMessage& what); -}; - -class TAMidi { - std::vector in; - std::vector out; + virtual bool send(const TAMidiMessage& what); + virtual bool isDeviceOpen(); + virtual bool openDevice(String name); + virtual bool closeDevice(); + virtual std::vector listDevices(); + virtual bool init(); + virtual bool quit(); + TAMidiOut() { + } + virtual ~TAMidiOut(); }; class TAAudio { @@ -145,7 +161,8 @@ class TAAudio { void (*sampleRateChanged)(SampleRateChangeEvent); void (*bufferSizeChanged)(BufferSizeChangeEvent); public: - TAMidi* midi; + TAMidiIn* midiIn; + TAMidiOut* midiOut; void setSampleRateChangeCallback(void (*callback)(SampleRateChangeEvent)); void setBufferSizeChangeCallback(void (*callback)(BufferSizeChangeEvent)); @@ -155,6 +172,8 @@ class TAAudio { virtual bool quit(); virtual bool setRun(bool run); virtual std::vector listAudioDevices(); + bool initMidi(bool jack); + void quitMidi(); virtual bool init(TAAudioDesc& request, TAAudioDesc& response); TAAudio(): @@ -165,7 +184,9 @@ class TAAudio { outBufs(NULL), audioProcCallback(NULL), sampleRateChanged(NULL), - bufferSizeChanged(NULL) {} + bufferSizeChanged(NULL), + midiIn(NULL), + midiOut(NULL) {} virtual ~TAAudio(); }; diff --git a/src/engine/config.cpp b/src/engine/config.cpp index f7444e3cc..92866a9f9 100644 --- a/src/engine/config.cpp +++ b/src/engine/config.cpp @@ -32,13 +32,13 @@ bool DivEngine::saveConf() { configFile=configPath+String(CONFIG_FILE); FILE* f=ps_fopen(configFile.c_str(),"wb"); if (f==NULL) { - logW("could not write config file! %s\n",strerror(errno)); + logW("could not write config file! %s",strerror(errno)); return false; } for (auto& i: conf) { String toWrite=fmt::sprintf("%s=%s\n",i.first,i.second); if (fwrite(toWrite.c_str(),1,toWrite.size(),f)!=toWrite.size()) { - logW("could not write config file! %s\n",strerror(errno)); + logW("could not write config file! %s",strerror(errno)); fclose(f); return false; } @@ -52,10 +52,10 @@ bool DivEngine::loadConf() { configFile=configPath+String(CONFIG_FILE); FILE* f=ps_fopen(configFile.c_str(),"rb"); if (f==NULL) { - logI("creating default config.\n"); + logI("creating default config."); return saveConf(); } - logI("loading config.\n"); + logI("loading config."); while (!feof(f)) { String key=""; String value=""; diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 300b0033b..37e36a2bf 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -29,6 +29,12 @@ #define addWrite(a,v) regWrites.push_back(DivRegWrite(a,v)); +// HOW TO ADD A NEW COMMAND: +// add it to this enum. then see playback.cpp. +// there is a const char* cmdName[] array, which contains the command +// names as strings for the commands (and other debug stuff). +// +// if you miss it, the program will crash or misbehave at some point. enum DivDispatchCmds { DIV_CMD_NOTE_ON=0, DIV_CMD_NOTE_OFF, @@ -48,7 +54,9 @@ enum DivDispatchCmds { DIV_CMD_SAMPLE_MODE, DIV_CMD_SAMPLE_FREQ, DIV_CMD_SAMPLE_BANK, + DIV_CMD_SAMPLE_POS, + DIV_CMD_FM_HARD_RESET, DIV_CMD_FM_LFO, DIV_CMD_FM_LFO_WAVE, DIV_CMD_FM_TL, @@ -94,15 +102,50 @@ enum DivDispatchCmds { DIV_CMD_AY_NOISE_MASK_AND, DIV_CMD_AY_NOISE_MASK_OR, DIV_CMD_AY_AUTO_ENVELOPE, + DIV_CMD_AY_IO_WRITE, + DIV_CMD_AY_AUTO_PWM, + + DIV_CMD_FDS_MOD_DEPTH, + DIV_CMD_FDS_MOD_HIGH, + DIV_CMD_FDS_MOD_LOW, + DIV_CMD_FDS_MOD_POS, + DIV_CMD_FDS_MOD_WAVE, DIV_CMD_SAA_ENVELOPE, + DIV_CMD_AMIGA_FILTER, + DIV_CMD_AMIGA_AM, + DIV_CMD_AMIGA_PM, + DIV_CMD_LYNX_LFSR_LOAD, DIV_CMD_QSOUND_ECHO_FEEDBACK, DIV_CMD_QSOUND_ECHO_DELAY, DIV_CMD_QSOUND_ECHO_LEVEL, + DIV_CMD_X1_010_ENVELOPE_SHAPE, + DIV_CMD_X1_010_ENVELOPE_ENABLE, + DIV_CMD_X1_010_ENVELOPE_MODE, + DIV_CMD_X1_010_ENVELOPE_PERIOD, + DIV_CMD_X1_010_ENVELOPE_SLIDE, + DIV_CMD_X1_010_AUTO_ENVELOPE, + + DIV_CMD_WS_SWEEP_TIME, + DIV_CMD_WS_SWEEP_AMOUNT, + + DIV_CMD_N163_WAVE_POSITION, + DIV_CMD_N163_WAVE_LENGTH, + DIV_CMD_N163_WAVE_MODE, + DIV_CMD_N163_WAVE_LOAD, + DIV_CMD_N163_WAVE_LOADPOS, + DIV_CMD_N163_WAVE_LOADLEN, + DIV_CMD_N163_WAVE_LOADMODE, + DIV_CMD_N163_CHANNEL_LIMIT, + DIV_CMD_N163_GLOBAL_WAVE_LOAD, + DIV_CMD_N163_GLOBAL_WAVE_LOADPOS, + DIV_CMD_N163_GLOBAL_WAVE_LOADLEN, + DIV_CMD_N163_GLOBAL_WAVE_LOADMODE, + DIV_ALWAYS_SET_VOLUME, DIV_CMD_MAX @@ -277,6 +320,18 @@ class DivDispatch { */ virtual int getPortaFloor(int ch); + /** + * get the required amplification level of this dispatch's output. + * @return the amplification level. + */ + virtual float getPostAmp(); + + /** + * check whether DC offset correction is required. + * @return truth. + */ + virtual bool getDCOffRequired(); + /** * get a description of a dispatch-specific effect. * @param effect the effect. @@ -293,7 +348,7 @@ class DivDispatch { /** * set skip reg writes. */ - void setSkipRegisterWrites(bool value); + virtual void setSkipRegisterWrites(bool value); /** * notify instrument change. @@ -310,6 +365,11 @@ class DivDispatch { */ virtual void notifyInsDeletion(void* ins); + /** + * notify that playback stopped. + */ + virtual void notifyPlaybackStop(); + /** * force-retrigger instruments. */ @@ -362,7 +422,8 @@ class DivDispatch { virtual ~DivDispatch(); }; -#define NOTE_PERIODIC(x) parent->calcBaseFreq(chipClock,CHIP_DIVIDER,x,true) +#define NOTE_PERIODIC(x) round(parent->calcBaseFreq(chipClock,CHIP_DIVIDER,x,true)) +#define NOTE_PERIODIC_NOROUND(x) parent->calcBaseFreq(chipClock,CHIP_DIVIDER,x,true) #define NOTE_FREQUENCY(x) parent->calcBaseFreq(chipClock,CHIP_FREQBASE,x,false) #define COLOR_NTSC (315000000.0/88.0) diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 0ff7b8e19..4f4ecf398 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -28,17 +28,32 @@ #include "platform/nes.h" #include "platform/c64.h" #include "platform/arcade.h" +#include "platform/tx81z.h" #include "platform/ym2610.h" #include "platform/ym2610ext.h" +#include "platform/ym2610b.h" +#include "platform/ym2610bext.h" #include "platform/ay.h" #include "platform/ay8930.h" +#include "platform/opl.h" #include "platform/tia.h" #include "platform/saa.h" #include "platform/amiga.h" +#include "platform/pcspkr.h" #include "platform/segapcm.h" #include "platform/qsound.h" -#include "platform/dummy.h" +#include "platform/vera.h" +#include "platform/x1_010.h" +#include "platform/swan.h" #include "platform/lynx.h" +#include "platform/bubsyswsg.h" +#include "platform/n163.h" +#include "platform/pet.h" +#include "platform/vic20.h" +#include "platform/vrc6.h" +#include "platform/fds.h" +#include "platform/mmc5.h" +#include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -64,6 +79,11 @@ void DivDispatchContainer::flush(size_t count) { } void DivDispatchContainer::fillBuf(size_t runtotal, size_t offset, size_t size) { + if (dcOffCompensation && runtotal>0) { + dcOffCompensation=false; + prevSample[0]=bbIn[0][0]; + if (dispatch->isStereo()) prevSample[1]=bbIn[1][0]; + } if (lowQuality) { for (size_t i=0; igetDCOffRequired()) { + dcOffCompensation=true; + } // run for one cycle to determine DC offset // TODO: SAA1099 doesn't like that /*dispatch->acquire(bbIn[0],bbIn[1],0,1); @@ -125,13 +148,13 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do bb[0]=blip_new(32768); if (bb[0]==NULL) { - logE("not enough memory!\n"); + logE("not enough memory!"); return; } bb[1]=blip_new(32768); if (bb[1]==NULL) { - logE("not enough memory!\n"); + logE("not enough memory!"); return; } @@ -142,6 +165,10 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do bbInLen=32768; switch (sys) { + case DIV_SYSTEM_YMU759: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(759,false); + break; case DIV_SYSTEM_YM2612: dispatch=new DivPlatformGenesis; ((DivPlatformGenesis*)dispatch)->setYMFM(eng->getConfInt("ym2612Core",0)); @@ -182,6 +209,12 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_YM2610_FULL_EXT: dispatch=new DivPlatformYM2610Ext; break; + case DIV_SYSTEM_YM2610B: + dispatch=new DivPlatformYM2610B; + break; + case DIV_SYSTEM_YM2610B_EXT: + dispatch=new DivPlatformYM2610BExt; + break; case DIV_SYSTEM_AMIGA: dispatch=new DivPlatformAmiga; break; @@ -191,6 +224,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_AY8930: dispatch=new DivPlatformAY8930; break; + case DIV_SYSTEM_FDS: + dispatch=new DivPlatformFDS; + break; case DIV_SYSTEM_TIA: dispatch=new DivPlatformTIA; break; @@ -201,13 +237,43 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do ((DivPlatformOPLL*)dispatch)->setVRC7(sys==DIV_SYSTEM_VRC7); ((DivPlatformOPLL*)dispatch)->setProperDrums(sys==DIV_SYSTEM_OPLL_DRUMS); break; + case DIV_SYSTEM_OPL: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(1,false); + break; + case DIV_SYSTEM_OPL_DRUMS: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(1,true); + break; + case DIV_SYSTEM_OPL2: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(2,false); + break; + case DIV_SYSTEM_OPL2_DRUMS: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(2,true); + break; + case DIV_SYSTEM_OPL3: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(3,false); + break; + case DIV_SYSTEM_OPL3_DRUMS: + dispatch=new DivPlatformOPL; + ((DivPlatformOPL*)dispatch)->setOPLType(3,true); + break; + case DIV_SYSTEM_OPZ: + dispatch=new DivPlatformTX81Z; + break; case DIV_SYSTEM_SAA1099: { - int saaCore=eng->getConfInt("saaCore",0); + int saaCore=eng->getConfInt("saaCore",1); if (saaCore<0 || saaCore>2) saaCore=0; dispatch=new DivPlatformSAA1099; ((DivPlatformSAA1099*)dispatch)->setCore((DivSAACores)saaCore); break; } + case DIV_SYSTEM_PCSPKR: + dispatch=new DivPlatformPCSpeaker; + break; case DIV_SYSTEM_LYNX: dispatch=new DivPlatformLynx; break; @@ -218,8 +284,35 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_SEGAPCM_COMPAT: dispatch=new DivPlatformSegaPCM; break; + case DIV_SYSTEM_X1_010: + dispatch=new DivPlatformX1_010; + break; + case DIV_SYSTEM_SWAN: + dispatch=new DivPlatformSwan; + break; + case DIV_SYSTEM_VERA: + dispatch=new DivPlatformVERA; + break; + case DIV_SYSTEM_BUBSYS_WSG: + dispatch=new DivPlatformBubSysWSG; + break; + case DIV_SYSTEM_N163: + dispatch=new DivPlatformN163; + break; + case DIV_SYSTEM_PET: + dispatch=new DivPlatformPET; + break; + case DIV_SYSTEM_VIC20: + dispatch=new DivPlatformVIC20; + break; + case DIV_SYSTEM_VRC6: + dispatch=new DivPlatformVRC6; + break; + case DIV_SYSTEM_MMC5: + dispatch=new DivPlatformMMC5; + break; default: - logW("this system is not supported yet! using dummy platform.\n"); + logW("this system is not supported yet! using dummy platform."); dispatch=new DivPlatformDummy; break; } diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 87fac32dc..14577b40e 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -17,8 +17,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "dataErrors.h" -#include "song.h" #define _USE_MATH_DEFINES #include "engine.h" #include "instrument.h" @@ -55,6 +53,8 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan) { return "03xx: Portamento"; case 0x04: return "04xy: Vibrato (x: speed; y: depth)"; + case 0x07: + return "07xy: Tremolo (x: speed; y: depth)"; case 0x08: return "08xy: Set panning (x: left; y: right)"; case 0x09: @@ -70,7 +70,7 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan) { case 0x0f: return "0Fxx: Set speed 2"; case 0xc0: case 0xc1: case 0xc2: case 0xc3: - return "Cxxx: Set tick rate"; + return "Cxxx: Set tick rate (hz)"; case 0xe0: return "E0xx: Set arp speed"; case 0xe1: @@ -95,10 +95,29 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan) { return "EExx: Send external command"; case 0xef: return "EFxx: Set global tuning (quirky!)"; + case 0xf0: + return "F0xx: Set tick rate (bpm)"; + case 0xf1: + return "F1xx: Single tick note slide up"; + case 0xf2: + return "F2xx: Single tick note slide down"; + case 0xf3: + return "F3xx: Fine volume slide up"; + case 0xf4: + return "F4xx: Fine volume slide down"; + case 0xf8: + return "F8xx: Single tick volume slide up"; + case 0xf9: + return "F9xx: Single tick volume slide down"; + case 0xfa: + return "FAxx: Fast volume slide (0y: down; x0: up)"; case 0xff: return "FFxx: Stop song"; default: - if (chan>=0 && chan=0 && changetEffectName(effect); if (ret!=NULL) return ret; } @@ -126,7 +145,7 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { effectVal=pat[k]->data[j][5+(l<<1)]; if (effectVal<0) effectVal=0; if (pat[k]->data[j][4+(l<<1)]==0x0d) { - if (nextOrder==-1 && iEXPORT_BUFSIZE) { - logE("error: total processed is bigger than export bufsize! %d>%d\n",totalProcessed,EXPORT_BUFSIZE); + logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE); } if (sf_writef_float(sf,outBuf[2],totalProcessed)!=(int)totalProcessed) { - logE("error: failed to write entire buffer!\n"); + logE("error: failed to write entire buffer!"); break; } } @@ -210,7 +229,7 @@ void DivEngine::runExportThread() { delete[] outBuf[2]; if (sf_close(sf)!=0) { - logE("could not close audio file!\n"); + logE("could not close audio file!"); } exporting=false; @@ -220,10 +239,10 @@ void DivEngine::runExportThread() { disCont[i].setQuality(lowQuality); } if (!output->setRun(true)) { - logE("error while activating audio!\n"); + logE("error while activating audio!"); } } - logI("done!\n"); + logI("done!"); break; } case DIV_EXPORT_MODE_MANY_SYS: { @@ -243,10 +262,10 @@ void DivEngine::runExportThread() { for (int i=0; iEXPORT_BUFSIZE) { - logE("error: total processed is bigger than export bufsize! (%d) %d>%d\n",i,totalProcessed,EXPORT_BUFSIZE); + logE("error: total processed is bigger than export bufsize! (%d) %d>%d",i,totalProcessed,EXPORT_BUFSIZE); } if (sf_writef_short(sf[i],sysBuf,totalProcessed)!=(int)totalProcessed) { - logE("error: failed to write entire buffer! (%d)\n",i); + logE("error: failed to write entire buffer! (%d)",i); break; } } @@ -292,7 +311,7 @@ void DivEngine::runExportThread() { for (int i=0; isetRun(true)) { - logE("error while activating audio!\n"); + logE("error while activating audio!"); } } - logI("done!\n"); + logI("done!"); break; } case DIV_EXPORT_MODE_MANY_CHAN: { @@ -319,20 +338,20 @@ void DivEngine::runExportThread() { outBuf[2]=new float[EXPORT_BUFSIZE*2]; int loopCount=remainingLoops; - logI("rendering to files...\n"); + logI("rendering to files..."); for (int i=0; iEXPORT_BUFSIZE) { - logE("error: total processed is bigger than export bufsize! %d>%d\n",totalProcessed,EXPORT_BUFSIZE); + logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE); } if (sf_writef_float(sf,outBuf[2],totalProcessed)!=(int)totalProcessed) { - logE("error: failed to write entire buffer!\n"); + logE("error: failed to write entire buffer!"); break; } } if (sf_close(sf)!=0) { - logE("could not close audio file!\n"); + logE("could not close audio file!"); } } exporting=false; @@ -386,10 +405,10 @@ void DivEngine::runExportThread() { disCont[i].setQuality(lowQuality); } if (!output->setRun(true)) { - logE("error while activating audio!\n"); + logE("error while activating audio!"); } } - logI("done!\n"); + logI("done!"); break; } } @@ -419,25 +438,25 @@ bool DivEngine::haltAudioFile() { } void DivEngine::notifyInsChange(int ins) { - isBusy.lock(); + BUSY_BEGIN; for (int i=0; inotifyInsChange(ins); } - isBusy.unlock(); + BUSY_END; } void DivEngine::notifyWaveChange(int wave) { - isBusy.lock(); + BUSY_BEGIN; for (int i=0; inotifyWaveChange(wave); } - isBusy.unlock(); + BUSY_END; } void DivEngine::renderSamplesP() { - isBusy.lock(); + BUSY_BEGIN; renderSamples(); - isBusy.unlock(); + BUSY_END; } void DivEngine::renderSamples() { @@ -460,12 +479,12 @@ void DivEngine::renderSamples() { memPos=(memPos+0xfffff)&0xf00000; } if (memPos>=16777216) { - logW("out of ADPCM-A memory for sample %d!\n",i); + logW("out of ADPCM-A memory for sample %d!",i); break; } if (memPos+paddedLen>=16777216) { memcpy(adpcmAMem+memPos,s->dataA,16777216-memPos); - logW("out of ADPCM-A memory for sample %d!\n",i); + logW("out of ADPCM-A memory for sample %d!",i); } else { memcpy(adpcmAMem+memPos,s->dataA,paddedLen); } @@ -485,12 +504,12 @@ void DivEngine::renderSamples() { memPos=(memPos+0xfffff)&0xf00000; } if (memPos>=16777216) { - logW("out of ADPCM-B memory for sample %d!\n",i); + logW("out of ADPCM-B memory for sample %d!",i); break; } if (memPos+paddedLen>=16777216) { memcpy(adpcmBMem+memPos,s->dataB,16777216-memPos); - logW("out of ADPCM-B memory for sample %d!\n",i); + logW("out of ADPCM-B memory for sample %d!",i); } else { memcpy(adpcmBMem+memPos,s->dataB,paddedLen); } @@ -501,6 +520,7 @@ void DivEngine::renderSamples() { // step 4: allocate qsound pcm samples if (qsoundMem==NULL) qsoundMem=new unsigned char[16777216]; + memset(qsoundMem,0,16777216); memPos=0; for (int i=0; i=16777216) { - logW("out of QSound PCM memory for sample %d!\n",i); + logW("out of QSound PCM memory for sample %d!",i); break; } if (memPos+length>=16777216) { for (unsigned int i=0; i<16777216-(memPos+length); i++) { qsoundMem[(memPos+i)^0x8000]=s->data8[i]; } - logW("out of QSound PCM memory for sample %d!\n",i); + logW("out of QSound PCM memory for sample %d!",i); } else { for (int i=0; idata8[i]; @@ -530,40 +550,87 @@ void DivEngine::renderSamples() { memPos+=length+16; } qsoundMemLen=memPos+256; + + // step 4: allocate x1-010 pcm samples + if (x1_010Mem==NULL) x1_010Mem=new unsigned char[1048576]; + memset(x1_010Mem,0,1048576); + + memPos=0; + for (int i=0; ilength8+4095)&(~0xfff); + // fit sample bank size to 128KB for Seta 2 external bankswitching logic (not emulated yet!) + if (paddedLen>131072) { + paddedLen=131072; + } + if ((memPos&0xfe0000)!=((memPos+paddedLen)&0xfe0000)) { + memPos=(memPos+0x1ffff)&0xfe0000; + } + if (memPos>=1048576) { + logW("out of X1-010 memory for sample %d!",i); + break; + } + if (memPos+paddedLen>=1048576) { + memcpy(x1_010Mem+memPos,s->data8,1048576-memPos); + logW("out of X1-010 memory for sample %d!",i); + } else { + memcpy(x1_010Mem+memPos,s->data8,paddedLen); + } + s->offX1_010=memPos; + memPos+=paddedLen; + } + x1_010MemLen=memPos+256; } -void DivEngine::createNew() { - DivSystem sys=song.system[0]; +void DivEngine::createNew(const int* description) { quitDispatch(); - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); song.unload(); song=DivSong(); - song.system[0]=sys; + if (description!=NULL) { + if (description[0]!=0) { + int index=0; + for (int i=0; description[i]; i+=4) { + song.system[index]=(DivSystem)description[i]; + song.systemVol[index]=description[i+1]; + song.systemPan[index]=description[i+2]; + song.systemFlags[index]=description[i+3]; + index++; + if (index>=32) break; + } + song.systemLen=index; + } + } recalcChans(); renderSamples(); - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; initDispatch(); - isBusy.lock(); + BUSY_BEGIN; reset(); - isBusy.unlock(); + BUSY_END; } void DivEngine::changeSystem(int index, DivSystem which) { quitDispatch(); - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); song.system[index]=which; + song.systemFlags[index]=0; recalcChans(); - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; initDispatch(); - isBusy.lock(); + BUSY_BEGIN; renderSamples(); reset(); - isBusy.unlock(); + BUSY_END; } bool DivEngine::addSystem(DivSystem which) { if (song.systemLen>32) { - lastError="cannot add more than 32"; + lastError="max number of systems is 32"; return false; } // this was DIV_MAX_CHANS but I am setting it to 63 for now due to an ImGui limitation @@ -572,15 +639,20 @@ bool DivEngine::addSystem(DivSystem which) { return false; } quitDispatch(); - isBusy.lock(); - song.system[song.systemLen++]=which; + BUSY_BEGIN; + saveLock.lock(); + song.system[song.systemLen]=which; + song.systemVol[song.systemLen]=64; + song.systemPan[song.systemLen]=0; + song.systemFlags[song.systemLen++]=0; recalcChans(); - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; initDispatch(); - isBusy.lock(); + BUSY_BEGIN; renderSamples(); reset(); - isBusy.unlock(); + BUSY_END; return true; } @@ -594,34 +666,36 @@ bool DivEngine::removeSystem(int index) { return false; } quitDispatch(); - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); song.system[index]=DIV_SYSTEM_NULL; song.systemLen--; for (int i=index; i=song.systemLen) return; - isBusy.lock(); + BUSY_BEGIN; disCont[sys].dispatch->poke(addr,val); - isBusy.unlock(); + BUSY_END; } void DivEngine::poke(int sys, std::vector& wlist) { if (sys<0 || sys>=song.systemLen) return; - isBusy.lock(); + BUSY_BEGIN; disCont[sys].dispatch->poke(wlist); - isBusy.unlock(); + BUSY_END; } String DivEngine::getLastError() { @@ -680,16 +754,17 @@ void DivEngine::enableCommandStream(bool enable) { } void DivEngine::getCommandStream(std::vector& where) { - isBusy.lock(); + BUSY_BEGIN; where.clear(); for (DivCommand& i: cmdStream) { where.push_back(i); } cmdStream.clear(); - isBusy.unlock(); + BUSY_END; } void DivEngine::playSub(bool preserveDrift, int goalRow) { + std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); for (int i=0; isetSkipRegisterWrites(false); reset(); if (preserveDrift && curOrder==0) return; @@ -713,13 +788,20 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { } speedAB=false; playing=true; + skipping=true; for (int i=0; isetSkipRegisterWrites(true); while (playing && curOrdersetSkipRegisterWrites(false); @@ -739,21 +821,38 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { if (!preserveDrift) { ticks=1; } + skipping=false; cmdStream.clear(); + std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); + logV("playSub() took %dµs",std::chrono::duration_cast(timeEnd-timeStart).count()); } +/* int DivEngine::calcBaseFreq(double clock, double divider, int note, bool period) { double base=(period?(song.tuning*0.0625):song.tuning)*pow(2.0,(float)(note+3)/12.0); return period? round((clock/base)/divider): base*(divider/clock); +}*/ + +double DivEngine::calcBaseFreq(double clock, double divider, int note, bool period) { + double base=(period?(song.tuning*0.0625):song.tuning)*pow(2.0,(float)(note+3)/12.0); + return period? + (clock/base)/divider: + base*(divider/clock); } int DivEngine::calcFreq(int base, int pitch, bool period, int octave) { if (song.linearPitch) { + // global pitch multiplier + int whatTheFuck=(1024+(globalPitch<<6)-(globalPitch<0?globalPitch-6:0)); + if (whatTheFuck<1) whatTheFuck=1; // avoids division by zero but please kill me + pitch+=2048; + if (pitch<0) pitch=0; + if (pitch>4095) pitch=4095; return period? - base*pow(2,-(double)pitch/(12.0*128.0))/(98.0+globalPitch*6.0)*98.0: - (base*pow(2,(double)pitch/(12.0*128.0))*(98+globalPitch*6))/98; + ((base*(reversePitchTable[pitch]))/whatTheFuck): + (((base*(pitchTable[pitch]))>>10)*whatTheFuck)/1024; } return period? base-pitch: @@ -761,7 +860,7 @@ int DivEngine::calcFreq(int base, int pitch, bool period, int octave) { } void DivEngine::play() { - isBusy.lock(); + BUSY_BEGIN_SOFT; sPreview.sample=-1; sPreview.wave=-1; sPreview.pos=0; @@ -774,11 +873,16 @@ void DivEngine::play() { for (int i=0; imidiOut!=NULL) { + int pos=totalTicksR/6; + output->midiOut->send(TAMidiMessage(TA_MIDI_POSITION,(pos>>7)&0x7f,pos&0x7f)); + output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_PLAY,0,0)); + } + BUSY_END; } void DivEngine::playToRow(int row) { - isBusy.lock(); + BUSY_BEGIN_SOFT; sPreview.sample=-1; sPreview.wave=-1; sPreview.pos=0; @@ -787,25 +891,27 @@ void DivEngine::playToRow(int row) { for (int i=0; inotifyPlaybackStop(); + } + if (output) if (output->midiOut!=NULL) { + output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_STOP,0,0)); + for (int i=0; i=0) { + output->midiOut->send(TAMidiMessage(0x80|(i&15),chan[i].curMidiNote,0)); + } + } + } + BUSY_END; } void DivEngine::halt() { - isBusy.lock(); + BUSY_BEGIN; halted=true; - isBusy.unlock(); + BUSY_END; } void DivEngine::resume() { - isBusy.lock(); + BUSY_BEGIN; halted=false; haltOn=DIV_HALT_NONE; - isBusy.unlock(); + BUSY_END; } void DivEngine::haltWhen(DivHaltPositions when) { - isBusy.lock(); + BUSY_BEGIN; halted=false; haltOn=when; - isBusy.unlock(); + BUSY_END; } bool DivEngine::isHalted() { @@ -866,11 +983,13 @@ void DivEngine::reset() { chan[i]=DivChannelState(); if (idispatch(DivCommand(DIV_CMD_GET_VOLMAX,dispatchChanOfChan[i]))<<8)|0xff; chan[i].volume=chan[i].volMax; + if (!song.linearPitch) chan[i].vibratoFine=4; } extValue=0; extValuePresent=0; speed1=song.speed1; speed2=song.speed2; + firstTick=false; nextSpeed=speed1; divider=60; if (song.customTempo) { @@ -890,9 +1009,9 @@ void DivEngine::reset() { } void DivEngine::syncReset() { - isBusy.lock(); + BUSY_BEGIN; reset(); - isBusy.unlock(); + BUSY_END; } const int sampleRates[6]={ @@ -933,8 +1052,14 @@ int DivEngine::getEffectiveSampleRate(int rate) { return 1789773/(1789773/rate); case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: return (31250*MIN(255,(rate*255/31250)))/255; - case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_QSOUND: + return (24038*MIN(65535,(rate*4096/24038)))/4096; + case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610_FULL_EXT: case DIV_SYSTEM_YM2610B: case DIV_SYSTEM_YM2610B_EXT: return 18518; + case DIV_SYSTEM_VERA: + return (48828*MIN(128,(rate*128/48828)))/128; + case DIV_SYSTEM_X1_010: + return (31250*MIN(255,(rate*16/31250)))/16; // TODO: support variable clock case default: break; } @@ -942,11 +1067,11 @@ int DivEngine::getEffectiveSampleRate(int rate) { } void DivEngine::previewSample(int sample, int note) { - isBusy.lock(); + BUSY_BEGIN; if (sample<0 || sample>=(int)song.sample.size()) { sPreview.sample=-1; sPreview.pos=0; - isBusy.unlock(); + BUSY_END; return; } blip_clear(samp_bb); @@ -955,47 +1080,50 @@ void DivEngine::previewSample(int sample, int note) { rate=(song.tuning*pow(2.0,(double)(note+3)/12.0)*((double)song.sample[sample]->centerRate/8363.0)); if (rate<=0) rate=song.sample[sample]->rate; } + if (rate<100) rate=100; blip_set_rates(samp_bb,rate,got.rate); samp_prevSample=0; sPreview.pos=0; sPreview.sample=sample; sPreview.wave=-1; - isBusy.unlock(); + BUSY_END; } void DivEngine::stopSamplePreview() { - isBusy.lock(); + BUSY_BEGIN; sPreview.sample=-1; sPreview.pos=0; - isBusy.unlock(); + BUSY_END; } void DivEngine::previewWave(int wave, int note) { - isBusy.lock(); + BUSY_BEGIN; if (wave<0 || wave>=(int)song.wave.size()) { sPreview.wave=-1; sPreview.pos=0; - isBusy.unlock(); + BUSY_END; return; } if (song.wave[wave]->len<=0) { - isBusy.unlock(); + BUSY_END; return; } blip_clear(samp_bb); - blip_set_rates(samp_bb,song.wave[wave]->len*((song.tuning*0.0625)*pow(2.0,(double)(note+3)/12.0)),got.rate); + double rate=song.wave[wave]->len*((song.tuning*0.0625)*pow(2.0,(double)(note+3)/12.0)); + if (rate<100) rate=100; + blip_set_rates(samp_bb,rate,got.rate); samp_prevSample=0; sPreview.pos=0; sPreview.sample=-1; sPreview.wave=wave; - isBusy.unlock(); + BUSY_END; } void DivEngine::stopWavePreview() { - isBusy.lock(); + BUSY_BEGIN; sPreview.wave=-1; sPreview.pos=0; - isBusy.unlock(); + BUSY_END; } String DivEngine::getConfigPath() { @@ -1022,18 +1150,18 @@ unsigned char DivEngine::getSpeed2() { return speed2; } -int DivEngine::getHz() { +float DivEngine::getHz() { if (song.customTempo) { return song.hz; } else if (song.pal) { - return 60; + return 60.0; } else { - return 50; + return 50.0; } - return 60; + return 60.0; } -int DivEngine::getCurHz() { +float DivEngine::getCurHz() { return divider; } @@ -1050,9 +1178,9 @@ bool DivEngine::getRepeatPattern() { } void DivEngine::setRepeatPattern(bool value) { - isBusy.lock(); + BUSY_BEGIN; repeatPattern=value; - isBusy.unlock(); + BUSY_END; } bool DivEngine::hasExtValue() { @@ -1092,7 +1220,7 @@ void DivEngine::toggleSolo(int chan) { } } } - isBusy.lock(); + BUSY_BEGIN; if (!solo) { for (int i=0; imuteChannel(dispatchChanOfChan[chan],isMuted[chan]); } - isBusy.unlock(); + BUSY_END; } void DivEngine::unmuteAll() { - isBusy.lock(); + BUSY_BEGIN; for (int i=0; imuteChannel(dispatchChanOfChan[i],isMuted[i]); } } - isBusy.unlock(); + BUSY_END; } int DivEngine::addInstrument(int refChan) { - isBusy.lock(); + BUSY_BEGIN; DivInstrument* ins=new DivInstrument; int insCount=(int)song.ins.size(); ins->name=fmt::sprintf("Instrument %d",insCount); ins->type=getPreferInsType(refChan); + saveLock.lock(); song.ins.push_back(ins); song.insLen=insCount+1; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return insCount; } -enum DivInsFormats { - DIV_INSFORMAT_DMP, - DIV_INSFORMAT_TFI, - DIV_INSFORMAT_VGI, - DIV_INSFORMAT_FTI, - DIV_INSFORMAT_BTI -}; - -bool DivEngine::addInstrumentFromFile(const char* path) { - 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; - } - } - - FILE* f=ps_fopen(path,"rb"); - if (f==NULL) { - lastError=strerror(errno); - return false; - } - unsigned char* buf; - ssize_t len; - if (fseek(f,0,SEEK_END)!=0) { - lastError=strerror(errno); - fclose(f); - return false; - } - len=ftell(f); - if (len<0) { - lastError=strerror(errno); - fclose(f); - return false; - } - if (len==0) { - lastError=strerror(errno); - fclose(f); - return false; - } - if (fseek(f,0,SEEK_SET)!=0) { - lastError=strerror(errno); - fclose(f); - return false; - } - buf=new unsigned char[len]; - if (fread(buf,1,len,f)!=(size_t)len) { - logW("did not read entire instrument file buffer!\n"); - lastError="did not read entire instrument file!"; - delete[] buf; - return false; - } - fclose(f); - - SafeReader reader=SafeReader(buf,len); - - unsigned char magic[16]; - bool isFurnaceInstr=false; - try { - reader.read(magic,16); - if (memcmp("-Furnace instr.-",magic,16)==0) { - isFurnaceInstr=true; - } - } catch (EndOfFileException e) { - reader.seek(0,SEEK_SET); - } - - DivInstrument* ins=new DivInstrument; - if (isFurnaceInstr) { - try { - short version=reader.readS(); - reader.readS(); // reserved - - if (version>DIV_ENGINE_VERSION) { - warnings="this instrument is made with a more recent version of Furnace!"; - } - - unsigned int dataPtr=reader.readI(); - reader.seek(dataPtr,SEEK_SET); - - if (ins->readInsData(reader,version)!=DIV_DATA_SUCCESS) { - lastError="invalid instrument header/data!"; - delete ins; - delete[] buf; - return false; - } - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - } else { // read as a different format - const char* ext=strrchr(path,'.'); - DivInsFormats format=DIV_INSFORMAT_DMP; - if (ext!=NULL) { - String extS; - for (; *ext; ext++) { - char i=*ext; - if (i>='A' && i<='Z') { - i+='a'-'A'; - } - extS+=i; - } - if (extS==String(".dmp")) { - format=DIV_INSFORMAT_DMP; - } else if (extS==String(".tfi")) { - format=DIV_INSFORMAT_TFI; - } else if (extS==String(".vgi")) { - format=DIV_INSFORMAT_VGI; - } else if (extS==String(".fti")) { - format=DIV_INSFORMAT_FTI; - } else if (extS==String(".bti")) { - format=DIV_INSFORMAT_BTI; - } - } - switch (format) { - case DIV_INSFORMAT_DMP: { - // this is a ridiculous mess - unsigned char version=0; - unsigned char sys=0; - try { - reader.seek(0,SEEK_SET); - version=reader.readC(); - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - - if (version>11) { - lastError="unknown instrument version!"; - delete ins; - delete[] buf; - return false; - } - - ins->name=stripPath; - - if (version>=11) { // 1.0 - try { - sys=reader.readC(); - - switch (sys) { - case 1: // YMU759 - ins->type=DIV_INS_FM; - break; - case 2: // Genesis - ins->type=DIV_INS_FM; - break; - case 3: // SMS - ins->type=DIV_INS_STD; - break; - case 4: // Game Boy - ins->type=DIV_INS_GB; - break; - case 5: // PC Engine - ins->type=DIV_INS_PCE; - break; - case 6: // NES - ins->type=DIV_INS_STD; - break; - case 7: case 0x17: // C64 - ins->type=DIV_INS_C64; - break; - case 8: // Arcade - ins->type=DIV_INS_FM; - break; - default: - lastError="unknown instrument type!"; - delete ins; - delete[] buf; - return false; - break; - } - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - } - - try { - bool mode=true; - if (version>1) { - mode=reader.readC(); - if (mode==0) { - if (version<11) { - ins->type=DIV_INS_STD; - } - } else { - ins->type=DIV_INS_FM; - } - } else { - ins->type=DIV_INS_FM; - } - - if (mode) { // FM - if (version<10) { - if (version>1) { - ins->fm.ops=reader.readC()?4:2; - } else { - ins->fm.ops=reader.readC()?2:4; - } - } - if (version>1) { // HELP! in which version of the format did we start storing FMS! - ins->fm.fms=reader.readC(); - } - ins->fm.fb=reader.readC(); - ins->fm.alg=reader.readC(); - // DITTO - if (sys!=1) ins->fm.ams=reader.readC(); - - for (int j=0; jfm.ops; j++) { - ins->fm.op[j].mult=reader.readC(); - ins->fm.op[j].tl=reader.readC(); - ins->fm.op[j].ar=reader.readC(); - ins->fm.op[j].dr=reader.readC(); - ins->fm.op[j].sl=reader.readC(); - ins->fm.op[j].rr=reader.readC(); - ins->fm.op[j].am=reader.readC(); - // what the hell how do I tell! - if (sys==1) { // YMU759 - ins->fm.op[j].ws=reader.readC(); - ins->fm.op[j].ksl=reader.readC(); - ins->fm.op[j].vib=reader.readC(); - ins->fm.op[j].egt=reader.readC(); - ins->fm.op[j].sus=reader.readC(); - ins->fm.op[j].ksr=reader.readC(); - ins->fm.op[j].dvb=reader.readC(); - ins->fm.op[j].dam=reader.readC(); - } else { - ins->fm.op[j].rs=reader.readC(); - ins->fm.op[j].dt=reader.readC(); - ins->fm.op[j].dt2=ins->fm.op[j].dt>>4; - ins->fm.op[j].dt&=15; - ins->fm.op[j].d2r=reader.readC(); - ins->fm.op[j].ssgEnv=reader.readC(); - } - } - } else { // STD - if (ins->type!=DIV_INS_GB) { - ins->std.volMacroLen=reader.readC(); - if (version>5) { - for (int i=0; istd.volMacroLen; i++) { - ins->std.volMacro[i]=reader.readI(); - } - } else { - for (int i=0; istd.volMacroLen; i++) { - ins->std.volMacro[i]=reader.readC(); - } - } - if (version<11) for (int i=0; istd.volMacroLen; i++) { - if (ins->std.volMacro[i]>15 && ins->type==DIV_INS_STD) ins->type=DIV_INS_PCE; - } - if (ins->std.volMacroLen>0) { - ins->std.volMacroOpen=true; - ins->std.volMacroLoop=reader.readC(); - } else { - ins->std.volMacroOpen=false; - } - } - - ins->std.arpMacroLen=reader.readC(); - if (version>5) { - for (int i=0; istd.arpMacroLen; i++) { - ins->std.arpMacro[i]=reader.readI(); - } - } else { - for (int i=0; istd.arpMacroLen; i++) { - ins->std.arpMacro[i]=reader.readC(); - } - } - if (ins->std.arpMacroLen>0) { - ins->std.arpMacroOpen=true; - ins->std.arpMacroLoop=reader.readC(); - } else { - ins->std.arpMacroOpen=false; - } - if (version>8) { // TODO: when? - ins->std.arpMacroMode=reader.readC(); - } - - ins->std.dutyMacroLen=reader.readC(); - if (version>5) { - for (int i=0; istd.dutyMacroLen; i++) { - ins->std.dutyMacro[i]=reader.readI(); - } - } else { - for (int i=0; istd.dutyMacroLen; i++) { - ins->std.dutyMacro[i]=reader.readC(); - } - } - if (ins->std.dutyMacroLen>0) { - ins->std.dutyMacroOpen=true; - ins->std.dutyMacroLoop=reader.readC(); - } else { - ins->std.dutyMacroOpen=false; - } - - ins->std.waveMacroLen=reader.readC(); - if (version>5) { - for (int i=0; istd.waveMacroLen; i++) { - ins->std.waveMacro[i]=reader.readI(); - } - } else { - for (int i=0; istd.waveMacroLen; i++) { - ins->std.waveMacro[i]=reader.readC(); - } - } - if (ins->std.waveMacroLen>0) { - ins->std.waveMacroOpen=true; - ins->std.waveMacroLoop=reader.readC(); - } else { - ins->std.waveMacroOpen=false; - } - - if (ins->type==DIV_INS_C64) { - ins->c64.triOn=reader.readC(); - ins->c64.sawOn=reader.readC(); - ins->c64.pulseOn=reader.readC(); - ins->c64.noiseOn=reader.readC(); - - ins->c64.a=reader.readC(); - ins->c64.d=reader.readC(); - ins->c64.s=reader.readC(); - ins->c64.r=reader.readC(); - - ins->c64.duty=(reader.readC()*4095)/100; - - ins->c64.ringMod=reader.readC(); - ins->c64.oscSync=reader.readC(); - ins->c64.toFilter=reader.readC(); - if (version<0x07) { // TODO: UNSURE - ins->c64.volIsCutoff=reader.readI(); - } else { - ins->c64.volIsCutoff=reader.readC(); - } - ins->c64.initFilter=reader.readC(); - - ins->c64.res=reader.readC(); - ins->c64.cut=(reader.readC()*2047)/100; - ins->c64.hp=reader.readC(); - ins->c64.bp=reader.readC(); - ins->c64.lp=reader.readC(); - ins->c64.ch3off=reader.readC(); - } - if (ins->type==DIV_INS_GB) { - ins->gb.envVol=reader.readC(); - ins->gb.envDir=reader.readC(); - ins->gb.envLen=reader.readC(); - ins->gb.soundLen=reader.readC(); - } - } - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - break; - } - case DIV_INSFORMAT_TFI: - try { - reader.seek(0,SEEK_SET); - - ins->type=DIV_INS_FM; - ins->name=stripPath; - - ins->fm.alg=reader.readC(); - ins->fm.fb=reader.readC(); - - for (int i=0; i<4; i++) { - DivInstrumentFM::Operator& op=ins->fm.op[i]; - - op.mult=reader.readC(); - op.dt=reader.readC(); - op.tl=reader.readC(); - op.rs=reader.readC(); - op.ar=reader.readC(); - op.dr=reader.readC(); - op.d2r=reader.readC(); - op.rr=reader.readC(); - op.sl=reader.readC(); - op.ssgEnv=reader.readC(); - } - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - break; - case DIV_INSFORMAT_VGI: - try { - reader.seek(0,SEEK_SET); - - ins->type=DIV_INS_FM; - ins->name=stripPath; - - ins->fm.alg=reader.readC(); - ins->fm.fb=reader.readC(); - unsigned char fmsams=reader.readC(); - ins->fm.fms=fmsams&7; - ins->fm.ams=fmsams>>4; - - for (int i=0; i<4; i++) { - DivInstrumentFM::Operator& op=ins->fm.op[i]; - - op.mult=reader.readC(); - op.dt=reader.readC(); - op.tl=reader.readC(); - op.rs=reader.readC(); - op.ar=reader.readC(); - op.dr=reader.readC(); - if (op.dr&0x80) { - op.am=1; - op.dr&=0x7f; - } - op.d2r=reader.readC(); - op.rr=reader.readC(); - op.sl=reader.readC(); - op.ssgEnv=reader.readC(); - } - } catch (EndOfFileException e) { - lastError="premature end of file"; - logE("premature end of file!\n"); - delete ins; - delete[] buf; - return false; - } - break; - case DIV_INSFORMAT_FTI: - break; - case DIV_INSFORMAT_BTI: - break; - } - - if (reader.tell()=0 && index<(int)song.ins.size()) { for (int i=0; inotifyInsDeletion(song.ins[index]); @@ -1623,7 +1294,7 @@ void DivEngine::delInstrument(int index) { song.ins.erase(song.ins.begin()+index); song.insLen=song.ins.size(); for (int i=0; idata[k][2]>index) { @@ -1633,16 +1304,19 @@ void DivEngine::delInstrument(int index) { } } } - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; } int DivEngine::addWave() { - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); DivWavetable* wave=new DivWavetable; int waveCount=(int)song.wave.size(); song.wave.push_back(wave); song.waveLen=waveCount+1; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return waveCount; } @@ -1672,7 +1346,7 @@ bool DivEngine::addWaveFromFile(const char* path) { } buf=new unsigned char[len]; if (fread(buf,1,len,f)!=(size_t)len) { - logW("did not read entire wavetable file buffer!\n"); + logW("did not read entire wavetable file buffer!"); delete[] buf; return false; } @@ -1687,7 +1361,7 @@ bool DivEngine::addWaveFromFile(const char* path) { if (memcmp("-Furnace waveta-",magic,16)==0) { isFurnaceTable=true; } - } catch (EndOfFileException e) { + } catch (EndOfFileException& e) { reader.seek(0,SEEK_SET); } @@ -1712,22 +1386,22 @@ bool DivEngine::addWaveFromFile(const char* path) { wave->max=(unsigned char)reader.readC(); if (wave->max==255) { // new wavetable format unsigned char waveVersion=reader.readC(); - logI("reading modern .dmw...\n"); - logD("wave version %d\n",waveVersion); + logI("reading modern .dmw..."); + logD("wave version %d",waveVersion); wave->max=reader.readC(); for (int i=0; idata[i]=reader.readI(); } } else if (reader.size()==(size_t)(len+5)) { // read as .dmw - logI("reading .dmw...\n"); + logI("reading .dmw..."); if (len>256) len=256; for (int i=0; idata[i]=(unsigned char)reader.readC(); } } else { // read as binary - logI("reading binary...\n"); + logI("reading binary..."); len=reader.size(); if (len>256) len=256; reader.seek(0,SEEK_SET); @@ -1737,10 +1411,10 @@ bool DivEngine::addWaveFromFile(const char* path) { } wave->len=len; } - } catch (EndOfFileException e) { + } catch (EndOfFileException& e) { // read as binary len=reader.size(); - logI("reading binary for being too small...\n"); + logI("reading binary for being too small..."); if (len>256) len=256; reader.seek(0,SEEK_SET); for (int i=0; ilen=len; } } - } catch (EndOfFileException e) { + } catch (EndOfFileException& e) { delete wave; delete[] buf; return false; } - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); int waveCount=(int)song.wave.size(); song.wave.push_back(wave); song.waveLen=waveCount+1; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } void DivEngine::delWave(int index) { - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); if (index>=0 && index<(int)song.wave.size()) { delete song.wave[index]; song.wave.erase(song.wave.begin()+index); song.waveLen=song.wave.size(); } - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; } int DivEngine::addSample() { - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); DivSample* sample=new DivSample; int sampleCount=(int)song.sample.size(); sample->name=fmt::sprintf("Sample %d",sampleCount); song.sample.push_back(sample); song.sampleLen=sampleCount+1; + saveLock.unlock(); renderSamples(); - isBusy.unlock(); + BUSY_END; return sampleCount; } -bool DivEngine::addSampleFromFile(const char* path) { - isBusy.lock(); +int DivEngine::addSampleFromFile(const char* path) { + BUSY_BEGIN; SF_INFO si; SNDFILE* f=sf_open(path,SFM_READ,&si); if (f==NULL) { - isBusy.unlock(); - return false; + BUSY_END; + int err=sf_error(NULL); + 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)); + } + return -1; } - if (si.frames>1000000) { + if (si.frames>16777215) { + lastError="this sample is too big! max sample size is 16777215."; sf_close(f); - isBusy.unlock(); - return false; + BUSY_END; + return -1; } short* buf=new short[si.channels*si.frames]; if (sf_readf_short(f,buf,si.frames)!=si.frames) { - logW("sample read size mismatch!\n"); + logW("sample read size mismatch!"); } DivSample* sample=new DivSample; int sampleCount=(int)song.sample.size(); @@ -1858,28 +1545,32 @@ bool DivEngine::addSampleFromFile(const char* path) { if (sample->centerRate<4000) sample->centerRate=4000; if (sample->centerRate>64000) sample->centerRate=64000; sf_close(f); + saveLock.lock(); song.sample.push_back(sample); song.sampleLen=sampleCount+1; + saveLock.unlock(); renderSamples(); - isBusy.unlock(); + BUSY_END; return sampleCount; } void DivEngine::delSample(int index) { - isBusy.lock(); + BUSY_BEGIN; + saveLock.lock(); if (index>=0 && index<(int)song.sample.size()) { delete song.sample[index]; song.sample.erase(song.sample.begin()+index); song.sampleLen=song.sample.size(); renderSamples(); } - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; } void DivEngine::addOrder(bool duplicate, bool where) { unsigned char order[DIV_MAX_CHANS]; - if (song.ordersLen>=0x7e) return; - isBusy.lock(); + if (song.ordersLen>=0xff) return; + BUSY_BEGIN_SOFT; if (duplicate) { for (int i=0; icurOrder; j--) { song.orders.ord[i][j]=song.orders.ord[i][j-1]; @@ -1913,33 +1607,34 @@ void DivEngine::addOrder(bool duplicate, bool where) { song.orders.ord[i][curOrder+1]=order[i]; } song.ordersLen++; + saveLock.unlock(); curOrder++; if (playing && !freelance) { playSub(false); } } - isBusy.unlock(); + BUSY_END; } void DivEngine::deepCloneOrder(bool where) { unsigned char order[DIV_MAX_CHANS]; - if (song.ordersLen>=0x7e) return; + if (song.ordersLen>=0xff) return; warnings=""; - isBusy.lock(); + BUSY_BEGIN_SOFT; for (int i=0; idata,oldPat->data,256*32*sizeof(short)); - logD("found at %d\n",j); + logD("found at %d",j); didNotFind=false; break; } @@ -1949,11 +1644,14 @@ void DivEngine::deepCloneOrder(bool where) { } } if (where) { // at the end + saveLock.lock(); for (int i=0; icurOrder; j--) { song.orders.ord[i][j]=song.orders.ord[i][j-1]; @@ -1961,69 +1659,76 @@ void DivEngine::deepCloneOrder(bool where) { song.orders.ord[i][curOrder+1]=order[i]; } song.ordersLen++; + saveLock.unlock(); curOrder++; if (playing && !freelance) { playSub(false); } } - isBusy.unlock(); + BUSY_END; } void DivEngine::deleteOrder() { if (song.ordersLen<=1) return; - isBusy.lock(); + BUSY_BEGIN_SOFT; + saveLock.lock(); for (int i=0; i=song.ordersLen) curOrder=song.ordersLen-1; if (playing && !freelance) { playSub(false); } - isBusy.unlock(); + BUSY_END; } void DivEngine::moveOrderUp() { - isBusy.lock(); + BUSY_BEGIN_SOFT; if (curOrder<1) { - isBusy.unlock(); + BUSY_END; return; } + saveLock.lock(); for (int i=0; i=song.ordersLen-1) { - isBusy.unlock(); + BUSY_END; return; } + saveLock.lock(); for (int i=0; idata[k][2]==one) { @@ -2038,116 +1743,171 @@ void DivEngine::exchangeIns(int one, int two) { bool DivEngine::moveInsUp(int which) { if (which<1 || which>=(int)song.ins.size()) return false; - isBusy.lock(); + BUSY_BEGIN; DivInstrument* prev=song.ins[which]; + saveLock.lock(); song.ins[which]=song.ins[which-1]; song.ins[which-1]=prev; exchangeIns(which,which-1); - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } bool DivEngine::moveWaveUp(int which) { if (which<1 || which>=(int)song.wave.size()) return false; - isBusy.lock(); + BUSY_BEGIN; DivWavetable* prev=song.wave[which]; + saveLock.lock(); song.wave[which]=song.wave[which-1]; song.wave[which-1]=prev; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } bool DivEngine::moveSampleUp(int which) { if (which<1 || which>=(int)song.sample.size()) return false; - isBusy.lock(); + BUSY_BEGIN; DivSample* prev=song.sample[which]; + saveLock.lock(); song.sample[which]=song.sample[which-1]; song.sample[which-1]=prev; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } bool DivEngine::moveInsDown(int which) { if (which<0 || which>=((int)song.ins.size())-1) return false; - isBusy.lock(); + BUSY_BEGIN; DivInstrument* prev=song.ins[which]; + saveLock.lock(); song.ins[which]=song.ins[which+1]; song.ins[which+1]=prev; exchangeIns(which,which+1); - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } bool DivEngine::moveWaveDown(int which) { if (which<0 || which>=((int)song.wave.size())-1) return false; - isBusy.lock(); + BUSY_BEGIN; DivWavetable* prev=song.wave[which]; + saveLock.lock(); song.wave[which]=song.wave[which+1]; song.wave[which+1]=prev; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } bool DivEngine::moveSampleDown(int which) { if (which<0 || which>=((int)song.sample.size())-1) return false; - isBusy.lock(); + BUSY_BEGIN; DivSample* prev=song.sample[which]; + saveLock.lock(); song.sample[which]=song.sample[which+1]; song.sample[which+1]=prev; - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; return true; } void DivEngine::noteOn(int chan, int ins, int note, int vol) { if (chan<0 || chan>=chans) return; - isBusy.lock(); + BUSY_BEGIN; pendingNotes.push(DivNoteEvent(chan,ins,note,vol,true)); if (!playing) { reset(); freelance=true; playing=true; } - isBusy.unlock(); + BUSY_END; } void DivEngine::noteOff(int chan) { if (chan<0 || chan>=chans) return; - isBusy.lock(); + BUSY_BEGIN; pendingNotes.push(DivNoteEvent(chan,-1,-1,-1,false)); if (!playing) { reset(); freelance=true; playing=true; } - isBusy.unlock(); + BUSY_END; +} + +void DivEngine::autoNoteOn(int ch, int ins, int note, int vol) { + //if (ch<0 || ch>=chans) return; + if (midiBaseChan<0) midiBaseChan=0; + if (midiBaseChan>=chans) midiBaseChan=chans-1; + int finalChan=midiBaseChan; + + if (!playing) { + reset(); + freelance=true; + playing=true; + } + + do { + if ((ins==-1 || getPreferInsType(finalChan)==getIns(ins)->type) && chan[finalChan].midiNote==-1) { + chan[finalChan].midiNote=note; + pendingNotes.push(DivNoteEvent(finalChan,ins,note,vol,true)); + break; + } + if (++finalChan>=chans) { + finalChan=0; + } + } while (finalChan!=midiBaseChan); +} + +void DivEngine::autoNoteOff(int ch, int note, int vol) { + if (!playing) { + reset(); + freelance=true; + playing=true; + } + //if (ch<0 || ch>=chans) return; + for (int i=0; i=song.ordersLen) curOrder=0; if (playing && !freelance) { playSub(false); } - isBusy.unlock(); + BUSY_END; } void DivEngine::setSysFlags(int system, unsigned int flags, bool restart) { - isBusy.lock(); + BUSY_BEGIN_SOFT; + saveLock.lock(); song.systemFlags[system]=flags; + saveLock.unlock(); disCont[system].dispatch->setFlags(song.systemFlags[system]); disCont[system].setRates(got.rate); - if (restart) { + if (restart && isPlaying()) { playSub(false); } - isBusy.unlock(); + BUSY_END; } -void DivEngine::setSongRate(int hz, bool pal) { - isBusy.lock(); +void DivEngine::setSongRate(float hz, bool pal) { + BUSY_BEGIN; + saveLock.lock(); song.pal=!pal; song.hz=hz; - song.customTempo=(song.hz!=50 && song.hz!=60); + // what? + song.customTempo=true; divider=60; if (song.customTempo) { divider=song.hz; @@ -2158,7 +1918,8 @@ void DivEngine::setSongRate(int hz, bool pal) { divider=50; } } - isBusy.unlock(); + saveLock.unlock(); + BUSY_END; } void DivEngine::setAudio(DivAudioEngines which) { @@ -2192,7 +1953,7 @@ bool DivEngine::switchMaster() { disCont[i].setQuality(lowQuality); } if (!output->setRun(true)) { - logE("error while activating audio!\n"); + logE("error while activating audio!"); return false; } } else { @@ -2201,6 +1962,39 @@ bool DivEngine::switchMaster() { return true; } +void DivEngine::setMidiBaseChan(int chan) { + if (chan<0 || chan>=chans) chan=0; + midiBaseChan=chan; +} + +void DivEngine::setMidiDirect(bool value) { + midiIsDirect=value; +} + +void DivEngine::setMidiCallback(std::function what) { + midiCallback=what; +} + +void DivEngine::synchronized(const std::function& what) { + BUSY_BEGIN; + what(); + BUSY_END; +} + +void DivEngine::lockSave(const std::function& what) { + saveLock.lock(); + what(); + saveLock.unlock(); +} + +void DivEngine::lockEngine(const std::function& what) { + BUSY_BEGIN; + saveLock.lock(); + what(); + saveLock.unlock(); + BUSY_END; +} + TAAudioDesc& DivEngine::getAudioDescWant() { return want; } @@ -2213,26 +2007,40 @@ std::vector& DivEngine::getAudioDevices() { return audioDevs; } +std::vector& DivEngine::getMidiIns() { + return midiIns; +} + +std::vector& DivEngine::getMidiOuts() { + return midiOuts; +} + void DivEngine::rescanAudioDevices() { audioDevs.clear(); if (output!=NULL) { audioDevs=output->listAudioDevices(); + if (output->midiIn!=NULL) { + midiIns=output->midiIn->listDevices(); + } + if (output->midiOut!=NULL) { + midiOuts=output->midiOut->listDevices(); + } } } void DivEngine::initDispatch() { - isBusy.lock(); + BUSY_BEGIN; for (int i=0; isetCallback(process,this); if (!output->init(want,got)) { - logE("error while initializing audio!\n"); + logE("error while initializing audio!"); delete output; output=NULL; audioEngine=DIV_AUDIO_NULL; return false; } + if (output->initMidi(false)) { + midiIns=output->midiIn->listDevices(); + midiOuts=output->midiOut->listDevices(); + } else { + logW("error while initializing MIDI!"); + } + if (output->midiIn) { + String inName=getConfString("midiInDevice",""); + if (!inName.empty()) { + // try opening device + logI("opening MIDI input."); + if (!output->midiIn->openDevice(inName)) { + logW("could not open MIDI input device!"); + } + } + } + if (output->midiOut) { + String outName=getConfString("midiOutDevice",""); + if (!outName.empty()) { + // try opening device + logI("opening MIDI output."); + if (!output->midiOut->openDevice(outName)) { + logW("could not open MIDI output device!"); + } + } + } + return true; } bool DivEngine::deinitAudioBackend() { if (output!=NULL) { + if (output->midiIn) { + if (output->midiIn->isDeviceOpen()) { + logI("closing MIDI input."); + output->midiIn->closeDevice(); + } + } + if (output->midiOut) { + if (output->midiOut->isDeviceOpen()) { + logI("closing MIDI output."); + output->midiOut->closeDevice(); + } + } + output->quitMidi(); output->quit(); delete output; output=NULL; @@ -2374,7 +2222,7 @@ bool DivEngine::init() { int uid=getuid(); struct passwd* entry=getpwuid(uid); if (entry==NULL) { - logW("unable to determine config directory! (%s)\n",strerror(errno)); + logW("unable to determine config directory! (%s)",strerror(errno)); configPath="."; } else { configPath=entry->pw_dir; @@ -2393,21 +2241,21 @@ bool DivEngine::init() { #endif } #endif - logD("config path: %s\n",configPath.c_str()); + logD("config path: %s",configPath.c_str()); loadConf(); // init the rest of engine bool haveAudio=false; if (!initAudioBackend()) { - logE("no audio output available!\n"); + logE("no audio output available!"); } else { haveAudio=true; } samp_bb=blip_new(32768); if (samp_bb==NULL) { - logE("not enough memory!\n"); + logE("not enough memory!"); return false; } @@ -2421,6 +2269,10 @@ bool DivEngine::init() { for (int i=0; i<64; i++) { vibTable[i]=127*sin(((double)i/64.0)*(2*M_PI)); } + for (int i=0; i<4096; i++) { + reversePitchTable[i]=round(1024.0*pow(2.0,(2048.0-(double)i)/(12.0*128.0))); + pitchTable[i]=round(1024.0*pow(2.0,((double)i-2048.0)/(12.0*128.0))); + } for (int i=0; isetRun(true)) { - logE("error while activating!\n"); + logE("error while activating!"); return false; } } @@ -2448,7 +2300,7 @@ bool DivEngine::init() { bool DivEngine::quit() { deinitAudioBackend(); quitDispatch(); - logI("saving config.\n"); + logI("saving config."); saveConf(); active=false; delete[] oscBuf[0]; diff --git a/src/engine/engine.h b/src/engine/engine.h index f0ef1833a..89f6ff732 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -25,6 +25,7 @@ #include "safeWriter.h" #include "../audio/taAudio.h" #include "blip_buf.h" +#include #include #include #include @@ -37,8 +38,15 @@ warnings+=(String("\n")+x); \ } -#define DIV_VERSION "dev60" -#define DIV_ENGINE_VERSION 60 +#define BUSY_BEGIN softLocked=false; isBusy.lock(); +#define BUSY_BEGIN_SOFT softLocked=true; isBusy.lock(); +#define BUSY_END isBusy.unlock(); softLocked=false; + +#define DIV_VERSION "dev81" +#define DIV_ENGINE_VERSION 81 + +// for imports +#define DIV_VERSION_MOD 0xff01 enum DivStatusView { DIV_STATUS_NOTHING=0, @@ -69,17 +77,22 @@ enum DivHaltPositions { struct DivChannelState { std::vector delayed; - int note, oldNote, pitch, portaSpeed, portaNote; + int note, oldNote, lastIns, pitch, portaSpeed, portaNote; int volume, volSpeed, cut, rowDelay, volMax; int delayOrder, delayRow, retrigSpeed, retrigTick; int vibratoDepth, vibratoRate, vibratoPos, vibratoDir, vibratoFine; int tremoloDepth, tremoloRate, tremoloPos; unsigned char arp, arpStage, arpTicks; - bool doNote, legato, portaStop, keyOn, keyOff, nowYouCanStop, stopOnOff, arpYield, delayLocked, inPorta, scheduledSlideReset, shorthandPorta, noteOnInhibit; + bool doNote, legato, portaStop, keyOn, keyOff, nowYouCanStop, stopOnOff; + bool arpYield, delayLocked, inPorta, scheduledSlideReset, shorthandPorta, noteOnInhibit, resetArp; + + int midiNote, curMidiNote, midiPitch; + bool midiAftertouch; DivChannelState(): note(-1), oldNote(-1), + lastIns(-1), pitch(0), portaSpeed(-1), portaNote(-1), @@ -115,7 +128,12 @@ struct DivChannelState { inPorta(false), scheduledSlideReset(false), shorthandPorta(false), - noteOnInhibit(false) {} + noteOnInhibit(false), + resetArp(false), + midiNote(-1), + curMidiNote(-1), + midiPitch(-1), + midiAftertouch(false) {} }; struct DivNoteEvent { @@ -136,7 +154,7 @@ struct DivDispatchContainer { int temp[2], prevSample[2]; short* bbIn[2]; short* bbOut[2]; - bool lowQuality; + bool lowQuality, dcOffCompensation; void setRates(double gotRate); void setQuality(bool lowQual); @@ -154,7 +172,8 @@ struct DivDispatchContainer { prevSample{0,0}, bbIn{NULL,NULL}, bbOut{NULL,NULL}, - lowQuality(false) {} + lowQuality(false), + dcOffCompensation(false) {} }; class DivEngine { @@ -178,8 +197,16 @@ class DivEngine { bool halted; bool forceMono; bool cmdStreamEnabled; - int ticks, curRow, curOrder, remainingLoops, nextSpeed, divider; - int cycles, clockDrift, stepPlay; + bool softLocked; + bool firstTick; + bool skipping; + bool midiIsDirect; + int softLockCount; + int ticks, curRow, curOrder, remainingLoops, nextSpeed; + double divider; + int cycles; + double clockDrift; + int stepPlay; int changeOrd, changePos, totalSeconds, totalTicks, totalTicksR, totalCmds, lastCmds, cmdsPerSecond, globalPitch; unsigned char extValue; unsigned char speed1, speed2; @@ -191,12 +218,14 @@ class DivEngine { std::map conf; std::queue pendingNotes; bool isMuted[DIV_MAX_CHANS]; - std::mutex isBusy; + std::mutex isBusy, saveLock; String configPath; String configFile; String lastError; String warnings; std::vector audioDevs; + std::vector midiIns; + std::vector midiOuts; std::vector cmdStream; struct SamplePreview { @@ -210,6 +239,9 @@ class DivEngine { } sPreview; short vibTable[64]; + int reversePitchTable[4096]; + int pitchTable[4096]; + int midiBaseChan; blip_buffer_t* samp_bb; size_t samp_bbInLen; @@ -223,8 +255,13 @@ class DivEngine { size_t totalProcessed; - DivSystem systemFromFile(unsigned char val); - unsigned char systemToFile(DivSystem val); + // MIDI stuff + std::function midiCallback=[](const TAMidiMessage&) -> int {return -2;}; + + DivSystem systemFromFileFur(unsigned char val); + unsigned char systemToFileFur(DivSystem val); + DivSystem systemFromFileDMF(unsigned char val); + unsigned char systemToFileDMF(DivSystem val); int dispatchCmd(DivCommand c); void processRow(int i, bool afterDelay); void nextOrder(); @@ -235,12 +272,19 @@ class DivEngine { bool perSystemEffect(int ch, unsigned char effect, unsigned char effectVal); bool perSystemPostEffect(int ch, unsigned char effect, unsigned char effectVal); void recalcChans(); - void renderSamples(); void reset(); void playSub(bool preserveDrift, int goalRow=0); bool loadDMF(unsigned char* file, size_t len); bool loadFur(unsigned char* file, size_t len); + bool loadMod(unsigned char* file, size_t len); + + void loadDMP(SafeReader& reader, std::vector& ret, String& stripPath); + void loadTFI(SafeReader& reader, std::vector& ret, String& stripPath); + void loadVGI(SafeReader& reader, std::vector& ret, String& stripPath); + void loadS3I(SafeReader& reader, std::vector& ret, String& stripPath); + void loadSBI(SafeReader& reader, std::vector& ret, String& stripPath); + void loadOPM(SafeReader& reader, std::vector& ret, String& stripPath); bool initAudioBackend(); bool deinitAudioBackend(); @@ -255,6 +299,7 @@ class DivEngine { bool keyHit[DIV_MAX_CHANS]; float* oscBuf[2]; float oscSize; + int oscReadPos, oscWritePos; void runExportThread(); void nextBuf(float** in, float** out, int inChans, int outChans, unsigned int size); @@ -262,18 +307,19 @@ class DivEngine { DivWavetable* getWave(int index); DivSample* getSample(int index); // start fresh - void createNew(); + void createNew(const int* description); // load a file. bool load(unsigned char* f, size_t length); // save as .dmf. SafeWriter* saveDMF(unsigned char version); // save as .fur. - SafeWriter* saveFur(); + // if notPrimary is true then the song will not be altered + SafeWriter* saveFur(bool notPrimary=false); // build a ROM file (TODO). // specify system to build ROM for. SafeWriter* buildROM(int sys); // dump to VGM. - SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true); + SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171); // export to an audio file bool saveAudio(const char* path, int loops, DivAudioExportModes mode); // wait for audio export to finish @@ -285,8 +331,8 @@ class DivEngine { // notify wavetable change void notifyWaveChange(int wave); - // returns whether a system is VGM compatible - bool isVGMExportable(DivSystem which); + // returns the minimum VGM version which may carry the specified system, or 0 if none. + int minVGMVersion(DivSystem which); // save config bool saveConf(); @@ -309,7 +355,7 @@ class DivEngine { void setConf(String key, String value); // calculate base frequency/period - int calcBaseFreq(double clock, double divider, int note, bool period); + double calcBaseFreq(double clock, double divider, int note, bool period); // calculate frequency/period int calcFreq(int base, int pitch, bool period=false, int octave=0); @@ -426,10 +472,10 @@ class DivEngine { unsigned char getSpeed2(); // get Hz - int getHz(); + float getHz(); // get current Hz - int getCurHz(); + float getCurHz(); // get time int getTotalTicks(); // 1/1000000th of a second @@ -459,8 +505,12 @@ class DivEngine { // add instrument int addInstrument(int refChan=0); - // add instrument from file - bool addInstrumentFromFile(const char* path); + // add instrument from pointer + int addInstrumentPtr(DivInstrument* which); + + // get instrument from file + // if the returned vector is empty then there was an error. + std::vector instrumentFromFile(const char* path); // delete instrument void delInstrument(int index); @@ -478,7 +528,7 @@ class DivEngine { int addSample(); // add sample from file - bool addSampleFromFile(const char* path); + int addSampleFromFile(const char* path); // delete sample void delSample(int index); @@ -514,6 +564,9 @@ class DivEngine { // stop note void noteOff(int chan); + void autoNoteOn(int chan, int ins, int note, int vol=-1); + void autoNoteOff(int chan, int note, int vol=-1); + // go to order void setOrder(unsigned char order); @@ -521,7 +574,7 @@ class DivEngine { void setSysFlags(int system, unsigned int flags, bool restart); // set Hz - void setSongRate(int hz, bool pal); + void setSongRate(float hz, bool pal); // set remaining loops. -1 means loop forever. void setLoops(int loops); @@ -550,6 +603,12 @@ class DivEngine { // get available audio devices std::vector& getAudioDevices(); + // get available MIDI inputs + std::vector& getMidiIns(); + + // get available MIDI inputs + std::vector& getMidiOuts(); + // rescan audio devices void rescanAudioDevices(); @@ -577,6 +636,9 @@ class DivEngine { // get register cheatsheet const char** getRegisterSheet(int sys); + // UNSAFE render samples - only execute when locked + void renderSamples(); + // public render samples void renderSamplesP(); @@ -604,6 +666,25 @@ class DivEngine { // switch master bool switchMaster(); + // set MIDI base channel + void setMidiBaseChan(int chan); + + // set MIDI direct channel map + void setMidiDirect(bool value); + + // set MIDI input callback + // if the specified function returns -2, note feedback will be inhibited. + void setMidiCallback(std::function what); + + // perform secure/sync operation + void synchronized(const std::function& what); + + // perform secure/sync song operation + void lockSave(const std::function& what); + + // perform secure/sync song operation (and lock audio too) + void lockEngine(const std::function& what); + // get audio desc want TAAudioDesc& getAudioDescWant(); @@ -632,6 +713,8 @@ class DivEngine { size_t qsoundAMemLen; unsigned char* dpcmMem; size_t dpcmMemLen; + unsigned char* x1_010Mem; + size_t x1_010MemLen; DivEngine(): output(NULL), @@ -651,6 +734,11 @@ class DivEngine { halted(false), forceMono(false), cmdStreamEnabled(false), + softLocked(false), + firstTick(false), + skipping(false), + midiIsDirect(false), + softLockCount(0), ticks(0), curRow(0), curOrder(0), @@ -674,6 +762,7 @@ class DivEngine { view(DIV_STATUS_NOTHING), haltOn(DIV_HALT_NONE), audioEngine(DIV_AUDIO_NULL), + midiBaseChan(0), samp_bbInLen(0), samp_temp(0), samp_prevSample(0), @@ -685,6 +774,8 @@ class DivEngine { totalProcessed(0), oscBuf{NULL,NULL}, oscSize(1), + oscReadPos(0), + oscWritePos(0), adpcmAMem(NULL), adpcmAMemLen(0), adpcmBMem(NULL), diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 7bb53c408..356e40e7e 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -53,6 +53,10 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { warnings=""; try { DivSong ds; + unsigned char historicColIns[DIV_MAX_CHANS]; + for (int i=0; i0x19) { - logE("this version is not supported by Furnace yet!\n"); + logI("module version %d (0x%.2x)",ds.version,ds.version); + if (ds.version>0x1a) { + logE("this version is not supported by Furnace yet!"); lastError="this version is not supported by Furnace yet"; delete[] file; return false; @@ -83,7 +87,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.system[0]=DIV_SYSTEM_YMU759; } else { sys=reader.readC(); - ds.system[0]=systemFromFile(sys); + ds.system[0]=systemFromFileDMF(sys); } if (ds.system[0]==DIV_SYSTEM_NULL) { logE("invalid system 0x%.2x!",sys); @@ -106,23 +110,23 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.manInfo=reader.readString((unsigned char)reader.readC()); ds.createdDate=reader.readString((unsigned char)reader.readC()); ds.revisionDate=reader.readString((unsigned char)reader.readC()); - logI("%s by %s\n",ds.name.c_str(),ds.author.c_str()); - logI("has YMU-specific data:\n"); - logI("- carrier: %s\n",ds.carrier.c_str()); - logI("- category: %s\n",ds.category.c_str()); - logI("- vendor: %s\n",ds.vendor.c_str()); - logI("- writer: %s\n",ds.writer.c_str()); - logI("- composer: %s\n",ds.composer.c_str()); - logI("- arranger: %s\n",ds.arranger.c_str()); - logI("- copyright: %s\n",ds.copyright.c_str()); - logI("- management group: %s\n",ds.manGroup.c_str()); - logI("- management info: %s\n",ds.manInfo.c_str()); - logI("- created on: %s\n",ds.createdDate.c_str()); - logI("- revision date: %s\n",ds.revisionDate.c_str()); + logI("%s by %s",ds.name.c_str(),ds.author.c_str()); + logI("has YMU-specific data:"); + logI("- carrier: %s",ds.carrier.c_str()); + logI("- category: %s",ds.category.c_str()); + logI("- vendor: %s",ds.vendor.c_str()); + logI("- writer: %s",ds.writer.c_str()); + logI("- composer: %s",ds.composer.c_str()); + logI("- arranger: %s",ds.arranger.c_str()); + logI("- copyright: %s",ds.copyright.c_str()); + logI("- management group: %s",ds.manGroup.c_str()); + logI("- management info: %s",ds.manInfo.c_str()); + logI("- created on: %s",ds.createdDate.c_str()); + logI("- revision date: %s",ds.revisionDate.c_str()); } else { ds.name=reader.readString((unsigned char)reader.readC()); ds.author=reader.readString((unsigned char)reader.readC()); - logI("%s by %s\n",ds.name.c_str(),ds.author.c_str()); + logI("%s by %s",ds.name.c_str(),ds.author.c_str()); } // compatibility flags @@ -140,6 +144,16 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.algMacroBehavior=false; ds.brokenShortcutSlides=false; ds.ignoreDuplicateSlides=true; + ds.brokenDACMode=true; + ds.oneTickCut=false; + ds.newInsTriggersInPorta=true; + ds.arp0Reset=true; + ds.brokenSpeedSel=true; + ds.noSlidesOnFirstTick=false; + ds.rowResetsArpPos=false; + ds.ignoreJumpAtEnd=true; + ds.buggyPortaAfterSlide=true; + ds.gbInsAffectsEnvelope=true; // 1.1 compat flags if (ds.version>24) { @@ -148,11 +162,13 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } // Neo Geo detune - if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT) { + if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT + || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT + || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { ds.tuning=443.23; } - logI("reading module data...\n"); + logI("reading module data..."); if (ds.version>0x0c) { ds.hilightA=reader.readC(); ds.hilightB=reader.readC(); @@ -160,7 +176,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.timeBase=reader.readC(); ds.speed1=reader.readC(); - if (ds.version>0x03) { + if (ds.version>0x05) { ds.speed2=reader.readC(); ds.pal=reader.readC(); ds.hz=(ds.pal)?60:50; @@ -174,7 +190,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { try { ds.hz=std::stoi(hz); } catch (std::exception& e) { - logW("invalid custom Hz!\n"); + logW("invalid custom Hz!"); ds.hz=60; } } @@ -186,6 +202,31 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } ds.ordersLen=(unsigned char)reader.readC(); + if (ds.patLen<0) { + logE("pattern length is negative!"); + lastError="pattern lengrh is negative!"; + delete[] file; + return false; + } + if (ds.patLen>256) { + logE("pattern length is too large!"); + lastError="pattern length is too large!"; + delete[] file; + return false; + } + if (ds.ordersLen<0) { + logE("song length is negative!"); + lastError="song length is negative!"; + delete[] file; + return false; + } + if (ds.ordersLen>127) { + logE("song is too long!"); + lastError="song is too long!"; + delete[] file; + return false; + } + if (ds.version<20 && ds.version>3) { ds.arpLen=reader.readC(); } else { @@ -218,36 +259,51 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } ds.customTempo=true; ds.timeBase=0; - addWarning("Yamaha YMU759 emulation is not currently possible!"); + addWarning("Yamaha YMU759 emulation is incomplete! please migrate your song to the OPL3 system."); } - logI("reading pattern matrix (%d)...\n",ds.ordersLen); + logV("%x",reader.tell()); + + logI("reading pattern matrix (%d * %d = %d)...",ds.ordersLen,getChannelCount(ds.system[0]),ds.ordersLen*getChannelCount(ds.system[0])); for (int i=0; i0x7f) { + logE("order at %d, %d out of range! (%d)",i,j,ds.orders.ord[i][j]); + lastError=fmt::sprintf("order at %d, %d out of range! (%d)",i,j,ds.orders.ord[i][j]); + delete[] file; + return false; + } if (ds.version>0x18) { // 1.1 pattern names ds.pat[i].getPattern(j,true)->name=reader.readString((unsigned char)reader.readC()); } } + if (ds.version>0x03 && ds.version<0x06 && i<16) { + historicColIns[i]=reader.readC(); + } } - if (ds.version>0x03) { + logV("%x",reader.tell()); + + if (ds.version>0x05) { ds.insLen=(unsigned char)reader.readC(); } else { ds.insLen=16; } - logI("reading instruments (%d)...\n",ds.insLen); + logI("reading instruments (%d)...",ds.insLen); for (int i=0; i0x03) { + if (ds.version>0x05) { ins->name=reader.readString((unsigned char)reader.readC()); } - logD("%d name: %s\n",i,ins->name.c_str()); + logD("%d name: %s",i,ins->name.c_str()); if (ds.version<0x0b) { // instruments in ancient versions were all FM or STD. ins->mode=1; } else { - ins->mode=reader.readC(); + unsigned char mode=reader.readC(); + if (mode>1) logW("%d: invalid instrument mode %d!",i,mode); + ins->mode=mode; } ins->type=ins->mode?DIV_INS_FM:DIV_INS_STD; if (ds.system[0]==DIV_SYSTEM_GB) { @@ -256,45 +312,59 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { if (ds.system[0]==DIV_SYSTEM_C64_8580 || ds.system[0]==DIV_SYSTEM_C64_6581) { ins->type=DIV_INS_C64; } - if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT) { + if (ds.system[0]==DIV_SYSTEM_YM2610 || ds.system[0]==DIV_SYSTEM_YM2610_EXT + || ds.system[0]==DIV_SYSTEM_YM2610_FULL || ds.system[0]==DIV_SYSTEM_YM2610_FULL_EXT + || ds.system[0]==DIV_SYSTEM_YM2610B || ds.system[0]==DIV_SYSTEM_YM2610B_EXT) { if (!ins->mode) { ins->type=DIV_INS_AY; } - ins->std.dutyMacroHeight=31; - ins->std.waveMacroHeight=7; } if (ds.system[0]==DIV_SYSTEM_PCE) { ins->type=DIV_INS_PCE; - ins->std.volMacroHeight=31; } if ((ds.system[0]==DIV_SYSTEM_SMS_OPLL || ds.system[0]==DIV_SYSTEM_NES_VRC7) && ins->type==DIV_INS_FM) { ins->type=DIV_INS_OPLL; } + if (ds.system[0]==DIV_SYSTEM_YMU759) { + ins->type=DIV_INS_OPL; + } if (ins->mode) { // FM - ins->fm.alg=reader.readC(); - if (ds.version<0x13) { - reader.readC(); - } - ins->fm.fb=reader.readC(); - if (ds.version<0x13) { - reader.readC(); - } - ins->fm.fms=reader.readC(); - if (ds.version<0x13) { - reader.readC(); - ins->fm.ops=2+reader.readC()*2; - if (ds.system[0]!=DIV_SYSTEM_YMU759) ins->fm.ops=4; + if (ds.version>0x05) { + ins->fm.alg=reader.readC(); + if (ds.version<0x13) { + reader.readC(); + } + ins->fm.fb=reader.readC(); + if (ds.version<0x13) { + reader.readC(); + } + ins->fm.fms=reader.readC(); + if (ds.version<0x13) { + reader.readC(); + ins->fm.ops=2+reader.readC()*2; + if (ds.system[0]!=DIV_SYSTEM_YMU759) ins->fm.ops=4; + } else { + ins->fm.ops=4; + } + ins->fm.ams=reader.readC(); } else { - ins->fm.ops=4; + ins->fm.alg=reader.readC(); + reader.readC(); + ins->fm.fb=reader.readC(); + reader.readC(); // apparently an index of sorts starting from 0x59? + ins->fm.fms=reader.readC(); + reader.readC(); // 0x59+index? + ins->fm.ops=2+reader.readC()*2; } + + logD("ALG %d FB %d FMS %d AMS %d OPS %d",ins->fm.alg,ins->fm.fb,ins->fm.fms,ins->fm.ams,ins->fm.ops); if (ins->fm.ops!=2 && ins->fm.ops!=4) { - logE("invalid op count %d. did we read it wrong?\n",ins->fm.ops); + logE("invalid op count %d. did we read it wrong?",ins->fm.ops); lastError="file is corrupt or unreadable at operators"; delete[] file; return false; } - ins->fm.ams=reader.readC(); for (int j=0; jfm.ops; j++) { ins->fm.op[j].am=reader.readC(); @@ -338,7 +408,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ins->fm.op[j].dt2=reader.readC(); } } - if (ds.version>0x03) { + if (ds.version>0x05) { if (ds.system[0]==DIV_SYSTEM_SMS_OPLL || ds.system[0]==DIV_SYSTEM_NES_VRC7) { ins->fm.op[j].ksr=reader.readC(); ins->fm.op[j].vib=reader.readC(); @@ -352,7 +422,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } } - logD("OP%d: AM %d AR %d DAM %d DR %d DVB %d EGT %d KSL %d MULT %d RR %d SL %d SUS %d TL %d VIB %d WS %d RS %d DT %d D2R %d SSG-EG %d\n",j, + logD("OP%d: AM %d AR %d DAM %d DR %d DVB %d EGT %d KSL %d MULT %d RR %d SL %d SUS %d TL %d VIB %d WS %d RS %d DT %d D2R %d SSG-EG %d",j, ins->fm.op[j].am, ins->fm.op[j].ar, ins->fm.op[j].dam, @@ -375,76 +445,76 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } } else { // STD if (ds.system[0]!=DIV_SYSTEM_GB || ds.version<0x12) { - ins->std.volMacroLen=reader.readC(); - for (int j=0; jstd.volMacroLen; j++) { + ins->std.volMacro.len=reader.readC(); + for (int j=0; jstd.volMacro.len; j++) { if (ds.version<0x0e) { - ins->std.volMacro[j]=reader.readC(); + ins->std.volMacro.val[j]=reader.readC(); } else { - ins->std.volMacro[j]=reader.readI(); + ins->std.volMacro.val[j]=reader.readI(); } } - if (ins->std.volMacroLen>0) { - ins->std.volMacroOpen=true; - ins->std.volMacroLoop=reader.readC(); + if (ins->std.volMacro.len>0) { + ins->std.volMacro.open=true; + ins->std.volMacro.loop=reader.readC(); } else { - ins->std.volMacroOpen=false; + ins->std.volMacro.open=false; } } - ins->std.arpMacroLen=reader.readC(); - for (int j=0; jstd.arpMacroLen; j++) { + ins->std.arpMacro.len=reader.readC(); + for (int j=0; jstd.arpMacro.len; j++) { if (ds.version<0x0e) { - ins->std.arpMacro[j]=reader.readC(); + ins->std.arpMacro.val[j]=reader.readC(); } else { - ins->std.arpMacro[j]=reader.readI(); + ins->std.arpMacro.val[j]=reader.readI(); } } - if (ins->std.arpMacroLen>0) { - ins->std.arpMacroLoop=reader.readC(); - ins->std.arpMacroOpen=true; + if (ins->std.arpMacro.len>0) { + ins->std.arpMacro.loop=reader.readC(); + ins->std.arpMacro.open=true; } else { - ins->std.arpMacroOpen=false; + ins->std.arpMacro.open=false; } if (ds.version>0x0f) { - ins->std.arpMacroMode=reader.readC(); + ins->std.arpMacro.mode=reader.readC(); } - if (!ins->std.arpMacroMode) { - for (int j=0; jstd.arpMacroLen; j++) { - ins->std.arpMacro[j]-=12; + if (!ins->std.arpMacro.mode) { + for (int j=0; jstd.arpMacro.len; j++) { + ins->std.arpMacro.val[j]-=12; } } - ins->std.dutyMacroLen=reader.readC(); - for (int j=0; jstd.dutyMacroLen; j++) { + ins->std.dutyMacro.len=reader.readC(); + for (int j=0; jstd.dutyMacro.len; j++) { if (ds.version<0x0e) { - ins->std.dutyMacro[j]=reader.readC(); + ins->std.dutyMacro.val[j]=reader.readC(); } else { - ins->std.dutyMacro[j]=reader.readI(); + ins->std.dutyMacro.val[j]=reader.readI(); } - if ((ds.system[0]==DIV_SYSTEM_C64_8580 || ds.system[0]==DIV_SYSTEM_C64_6581) && ins->std.dutyMacro[j]>24) { - ins->std.dutyMacro[j]=24; + if ((ds.system[0]==DIV_SYSTEM_C64_8580 || ds.system[0]==DIV_SYSTEM_C64_6581) && ins->std.dutyMacro.val[j]>24) { + ins->std.dutyMacro.val[j]=24; } } - if (ins->std.dutyMacroLen>0) { - ins->std.dutyMacroOpen=true; - ins->std.dutyMacroLoop=reader.readC(); + if (ins->std.dutyMacro.len>0) { + ins->std.dutyMacro.open=true; + ins->std.dutyMacro.loop=reader.readC(); } else { - ins->std.dutyMacroOpen=false; + ins->std.dutyMacro.open=false; } - ins->std.waveMacroLen=reader.readC(); - for (int j=0; jstd.waveMacroLen; j++) { + ins->std.waveMacro.len=reader.readC(); + for (int j=0; jstd.waveMacro.len; j++) { if (ds.version<0x0e) { - ins->std.waveMacro[j]=reader.readC(); + ins->std.waveMacro.val[j]=reader.readC(); } else { - ins->std.waveMacro[j]=reader.readI(); + ins->std.waveMacro.val[j]=reader.readI(); } } - if (ins->std.waveMacroLen>0) { - ins->std.waveMacroOpen=true; - ins->std.waveMacroLoop=reader.readC(); + if (ins->std.waveMacro.len>0) { + ins->std.waveMacro.open=true; + ins->std.waveMacro.loop=reader.readC(); } else { - ins->std.waveMacroOpen=false; + ins->std.waveMacro.open=false; } if (ds.system[0]==DIV_SYSTEM_C64_6581 || ds.system[0]==DIV_SYSTEM_C64_8580) { @@ -483,18 +553,18 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ins->gb.envDir=reader.readC(); ins->gb.envLen=reader.readC(); ins->gb.soundLen=reader.readC(); - ins->std.volMacroOpen=false; + ins->std.volMacro.open=false; - logD("GB data: vol %d dir %d len %d sl %d\n",ins->gb.envVol,ins->gb.envDir,ins->gb.envLen,ins->gb.soundLen); + 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 - if (ins->std.volMacroLen>0) { - ins->gb.envVol=ins->std.volMacro[0]; - if (ins->std.volMacro[0]std.volMacro[1]) { + if (ins->std.volMacro.len>0) { + ins->gb.envVol=ins->std.volMacro.val[0]; + if (ins->std.volMacro.val[0]std.volMacro.val[1]) { ins->gb.envDir=true; } - if (ins->std.volMacro[ins->std.volMacroLen-1]==0) { - ins->gb.soundLen=ins->std.volMacroLen*2; + if (ins->std.volMacro.val[ins->std.volMacro.len-1]==0) { + ins->gb.soundLen=ins->std.volMacro.len*2; } } addWarning("Game Boy volume macros converted to envelopes. may not be perfect!"); @@ -506,32 +576,44 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { if (ds.version>0x0b) { ds.waveLen=(unsigned char)reader.readC(); - logI("reading wavetables (%d)...\n",ds.waveLen); + logI("reading wavetables (%d)...",ds.waveLen); for (int i=0; ilen=(unsigned char)reader.readI(); if (ds.system[0]==DIV_SYSTEM_GB) { wave->max=15; } - if (wave->len>33) { - logE("invalid wave length %d. are we doing something wrong?\n",wave->len); + if (ds.system[0]==DIV_SYSTEM_NES_FDS) { + wave->max=63; + } + if (wave->len>65) { + logE("invalid wave length %d. are we doing something wrong?",wave->len); lastError="file is corrupt or unreadable at wavetables"; delete[] file; return false; } - logD("%d length %d\n",i,wave->len); + logD("%d length %d",i,wave->len); for (int j=0; jlen; j++) { if (ds.version<0x0e) { wave->data[j]=reader.readC(); } else { wave->data[j]=reader.readI(); } + wave->data[j]&=wave->max; + } + // #FDS4Bit + if (ds.system[0]==DIV_SYSTEM_NES_FDS && ds.version<0x1a) { + for (int j=0; jlen; j++) { + wave->data[j]*=4; + } } ds.wave.push_back(wave); } } - logI("reading patterns (%d channels, %d orders)...\n",getChannelCount(ds.system[0]),ds.ordersLen); + logV("%x",reader.tell()); + + logI("reading patterns (%d channels, %d orders)...",getChannelCount(ds.system[0]),ds.ordersLen); for (int i=0; i4 || chan.effectRows<1) { - logE("invalid effect row count %d. are you sure everything is ok?\n",chan.effectRows); + logE("invalid effect row count %d. are you sure everything is ok?",chan.effectRows); lastError="file is corrupt or unreadable at effect rows"; delete[] file; return false; } for (int j=0; jdata[k][0]=reader.readS(); - // octave - pat->data[k][1]=reader.readS(); - if (ds.system[0]==DIV_SYSTEM_SMS && ds.version<0x0e && pat->data[k][1]>0) { - // apparently it was up one octave before - pat->data[k][1]--; - } else if (ds.system[0]==DIV_SYSTEM_GENESIS && ds.version<0x0e && pat->data[k][1]>0 && i>5) { - // ditto - pat->data[k][1]--; - } - if (ds.version<0x12) { - if (ds.system[0]==DIV_SYSTEM_GB && i==3 && pat->data[k][1]>0) { - // back then noise was 2 octaves lower - pat->data[k][1]-=2; + if (ds.version>0x05) { // current pattern format + for (int k=0; kdata[k][0]=reader.readS(); + // octave + pat->data[k][1]=reader.readS(); + if (ds.system[0]==DIV_SYSTEM_SMS && ds.version<0x0e && pat->data[k][1]>0) { + // apparently it was up one octave before + pat->data[k][1]--; + } else if (ds.system[0]==DIV_SYSTEM_GENESIS && ds.version<0x0e && pat->data[k][1]>0 && i>5) { + // ditto + pat->data[k][1]--; } - } - if (pat->data[k][0]==0 && pat->data[k][1]!=0) { - logD("what? %d:%d:%d note %d octave %d\n",i,j,k,pat->data[k][0],pat->data[k][1]); - pat->data[k][0]=12; - pat->data[k][1]--; - } - // volume - pat->data[k][3]=reader.readS(); - if (ds.version<0x0a) { - // back then volume was stored as 00-ff instead of 00-7f/0-f - if (i>5) { - pat->data[k][3]>>=4; - } else { - pat->data[k][3]>>=1; + if (ds.version<0x12) { + if (ds.system[0]==DIV_SYSTEM_GB && i==3 && pat->data[k][1]>0) { + // back then noise was 2 octaves lower + pat->data[k][1]-=2; + } } - } - if (ds.version<0x12) { - if (ds.system[0]==DIV_SYSTEM_GB && i==2 && pat->data[k][3]>0) { - // volume range of GB wave channel was 0-3 rather than 0-F - pat->data[k][3]=(pat->data[k][3]&3)*5; + if (ds.system[0]==DIV_SYSTEM_YMU759 && pat->data[k][0]!=0) { + // apparently YMU759 is stored 2 octaves lower + pat->data[k][1]+=2; } - } - for (int l=0; ldata[k][4+(l<<1)]=reader.readS(); - pat->data[k][5+(l<<1)]=reader.readS(); + if (pat->data[k][0]==0 && pat->data[k][1]!=0) { + logD("what? %d:%d:%d note %d octave %d",i,j,k,pat->data[k][0],pat->data[k][1]); + pat->data[k][0]=12; + pat->data[k][1]--; + } + // volume + pat->data[k][3]=reader.readS(); + if (ds.version<0x0a) { + // back then volume was stored as 00-ff instead of 00-7f/0-f + if (i>5) { + pat->data[k][3]>>=4; + } else { + pat->data[k][3]>>=1; + } + } + if (ds.version<0x12) { + if (ds.system[0]==DIV_SYSTEM_GB && i==2 && pat->data[k][3]>0) { + // volume range of GB wave channel was 0-3 rather than 0-F + pat->data[k][3]=(pat->data[k][3]&3)*5; + } + } + for (int l=0; ldata[k][4+(l<<1)]=reader.readS(); + pat->data[k][5+(l<<1)]=reader.readS(); - if (ds.version<0x14) { - if (pat->data[k][4+(l<<1)]==0xe5 && pat->data[k][5+(l<<1)]!=-1) { - pat->data[k][5+(l<<1)]=128+((pat->data[k][5+(l<<1)]-128)/4); + if (ds.version<0x14) { + if (pat->data[k][4+(l<<1)]==0xe5 && pat->data[k][5+(l<<1)]!=-1) { + pat->data[k][5+(l<<1)]=128+((pat->data[k][5+(l<<1)]-128)/4); + } + } + } + // instrument + pat->data[k][2]=reader.readS(); + + // this is sad + if (ds.system[0]==DIV_SYSTEM_NES_FDS) { + if (i==5 && pat->data[k][2]!=-1) { + if (pat->data[k][2]>=0 && pat->data[k][2]data[k][2]]->type=DIV_INS_FDS; + } } } } - // instrument - pat->data[k][2]=reader.readS(); + } else { // historic pattern format + if (i<16) pat->data[0][2]=historicColIns[i]; + for (int k=0; kdata[k][0]=reader.readC(); + // octave + pat->data[k][1]=reader.readC(); + if (pat->data[k][0]!=0) { + // YMU759 is stored 2 octaves lower + pat->data[k][1]+=2; + } + if (pat->data[k][0]==0 && pat->data[k][1]!=0) { + logD("what? %d:%d:%d note %d octave %d",i,j,k,pat->data[k][0],pat->data[k][1]); + pat->data[k][0]=12; + pat->data[k][1]--; + } + // volume and effect + unsigned char vol=reader.readC(); + unsigned char fx=reader.readC(); + unsigned char fxVal=reader.readC(); + pat->data[k][3]=(vol==0x80)?-1:vol; + // effect + pat->data[k][4]=(fx==0x80)?-1:fx; + pat->data[k][5]=(fxVal==0x80)?-1:fxVal; + // instrument wasn't stored back then + } } } } + int ymuSampleRate=20; + ds.sampleLen=(unsigned char)reader.readC(); - logI("reading samples (%d)...\n",ds.sampleLen); - if (ds.version<0x0b && ds.sampleLen>0) { // TODO what is this for? - reader.readC(); + logI("reading samples (%d)...",ds.sampleLen); + if (ds.version<0x0b && ds.sampleLen>0) { + // it appears this byte stored the YMU759 sample rate + ymuSampleRate=reader.readC(); } for (int i=0; iname=""; } - logD("%d name %s (%d)\n",i,sample->name.c_str(),length); + logD("%d name %s (%d)",i,sample->name.c_str(),length); sample->rate=22050; if (ds.version>=0x0b) { sample->rate=fileToDivRate(reader.readC()); pitch=reader.readC(); vol=reader.readC(); } + if (ds.version<=0x05) { + sample->rate=ymuSampleRate*400; + } if (ds.version>0x15) { sample->depth=reader.readC(); if (sample->depth!=8 && sample->depth!=16) { - logW("%d: sample depth is wrong! (%d)\n",i,sample->depth); + logW("%d: sample depth is wrong! (%d)",i,sample->depth); sample->depth=16; } } else { - sample->depth=16; + if (ds.version>0x05) { + sample->depth=16; + } else { + // it appears samples were stored as ADPCM back then + sample->depth=6; + } } if (length>0) { - if (ds.version<0x0b) { - data=new short[1+(length/2)]; - reader.read(data,length); - length/=2; - } else { - data=new short[length]; - reader.read(data,length*2); - } - - if (pitch!=5) { - logD("%d: scaling from %d...\n",i,pitch); - } - - // render data - if (!sample->init((double)length/samplePitches[pitch])) { - logE("%d: error while initializing sample!\n",i); - } - - unsigned int k=0; - float mult=(float)(vol)/50.0f; - for (double j=0; j=sample->samples) { - break; - } - if (sample->depth==8) { - float next=(float)(data[(unsigned int)j]-0x80)*mult; - sample->data8[k++]=fmin(fmax(next,-128),127); + if (ds.version>0x05) { + if (ds.version<0x0b) { + data=new short[1+(length/2)]; + reader.read(data,length); + length/=2; } else { - float next=(float)data[(unsigned int)j]*mult; - sample->data16[k++]=fmin(fmax(next,-32768),32767); + data=new short[length]; + reader.read(data,length*2); } - } - delete[] data; + if (pitch!=5) { + logD("%d: scaling from %d...",i,pitch); + } + + // render data + if (!sample->init((double)length/samplePitches[pitch])) { + logE("%d: error while initializing sample!",i); + } + + unsigned int k=0; + float mult=(float)(vol)/50.0f; + for (double j=0; j=sample->samples) { + break; + } + if (sample->depth==8) { + float next=(float)(data[(unsigned int)j]-0x80)*mult; + sample->data8[k++]=fmin(fmax(next,-128),127); + } else { + float next=(float)data[(unsigned int)j]*mult; + sample->data16[k++]=fmin(fmax(next,-32768),32767); + } + } + + delete[] data; + } else { + // ADPCM? + // it appears to be a slightly modified version of ADPCM-B! + adpcmData=new unsigned char[length]; + logV("%x",reader.tell()); + reader.read(adpcmData,length); + for (int i=0; i>4); + } + if (!sample->init(length*2)) { + logE("%d: error while initializing sample!",i); + } + + memcpy(sample->dataB,adpcmData,length); + delete[] adpcmData; + } } ds.sample.push_back(sample); } if (reader.tell() patPtr; char magic[5]; memset(magic,0,5); @@ -750,16 +909,16 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { DivSong ds; if (!reader.seek(16,SEEK_SET)) { - logE("premature end of file!\n"); + logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } ds.version=reader.readS(); - logI("module version %d (0x%.2x)\n",ds.version,ds.version); + logI("module version %d (0x%.2x)",ds.version,ds.version); if (ds.version>DIV_ENGINE_VERSION) { - logW("this module was created with a more recent version of Furnace!\n"); + logW("this module was created with a more recent version of Furnace!"); addWarning("this module was created with a more recent version of Furnace!"); } @@ -792,17 +951,49 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<50) { ds.ignoreDuplicateSlides=false; } + if (ds.version<62) { + ds.stopPortaOnNoteOff=true; + } + if (ds.version<64) { + ds.brokenDACMode=false; + } + if (ds.version<65) { + ds.oneTickCut=false; + } + if (ds.version<66) { + ds.newInsTriggersInPorta=false; + } + if (ds.version<69) { + ds.arp0Reset=false; + } + if (ds.version<71) { + ds.noSlidesOnFirstTick=false; + ds.rowResetsArpPos=false; + ds.ignoreJumpAtEnd=true; + } + if (ds.version<72) { + ds.buggyPortaAfterSlide=true; + ds.gbInsAffectsEnvelope=false; + } + if (ds.version<78) { + ds.sharedExtStat=false; + } ds.isDMF=false; reader.readS(); // reserved int infoSeek=reader.readI(); - reader.seek(infoSeek,SEEK_SET); + if (!reader.seek(infoSeek,SEEK_SET)) { + logE("couldn't seek to info header at %d!",infoSeek); + lastError="couldn't seek to info header!"; + delete[] file; + return false; + } // read header reader.read(magic,4); if (strcmp(magic,"INFO")!=0) { - logE("invalid info header!\n"); + logE("invalid info header!"); lastError="invalid info header!"; delete[] file; return false; @@ -815,7 +1006,7 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { ds.arpLen=reader.readC(); ds.hz=reader.readF(); ds.pal=(ds.hz>=53); - if (ds.hz!=50 && ds.hz!=60) ds.customTempo=true; + ds.customTempo=true; ds.patLen=reader.readS(); ds.ordersLen=reader.readS(); @@ -828,15 +1019,74 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { ds.sampleLen=reader.readS(); int numberOfPats=reader.readI(); + if (ds.patLen<0) { + logE("pattern length is negative!"); + lastError="pattern lengrh is negative!"; + delete[] file; + return false; + } + if (ds.patLen>256) { + logE("pattern length is too large!"); + lastError="pattern length is too large!"; + delete[] file; + return false; + } + if (ds.ordersLen<0) { + logE("song length is negative!"); + lastError="song length is negative!"; + delete[] file; + return false; + } + if (ds.ordersLen>256) { + logE("song is too long!"); + lastError="song is too long!"; + delete[] file; + return false; + } + if (ds.insLen<0 || ds.insLen>256) { + logE("invalid instrument count!"); + lastError="invalid instrument count!"; + delete[] file; + return false; + } + if (ds.waveLen<0 || ds.waveLen>256) { + logE("invalid wavetable count!"); + lastError="invalid wavetable count!"; + delete[] file; + return false; + } + if (ds.sampleLen<0 || ds.sampleLen>256) { + logE("invalid sample count!"); + lastError="invalid sample count!"; + delete[] file; + return false; + } + if (numberOfPats<0) { + logE("invalid pattern count!"); + lastError="invalid pattern count!"; + delete[] file; + return false; + } + for (int i=0; i<32; i++) { - ds.system[i]=systemFromFile(reader.readC()); + unsigned char sysID=reader.readC(); + ds.system[i]=systemFromFileFur(sysID); + if (sysID!=0 && systemToFileFur(ds.system[i])==0) { + logE("unrecognized system ID %.2x",ds.system[i]); + lastError=fmt::sprintf("unrecognized system ID %.2x!",ds.system[i]); + delete[] file; + return false; + } if (ds.system[i]!=DIV_SYSTEM_NULL) ds.systemLen=i+1; } int tchans=0; for (int i=0; iDIV_MAX_CHANS) tchans=DIV_MAX_CHANS; + if (tchans>DIV_MAX_CHANS) { + tchans=DIV_MAX_CHANS; + logW("too many channels!"); + } // system volume for (int i=0; i<32; i++) { @@ -892,7 +1142,7 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { ds.name=reader.readString(); ds.author=reader.readString(); - logI("%s by %s\n",ds.name.c_str(),ds.author.c_str()); + logI("%s by %s",ds.name.c_str(),ds.author.c_str()); if (ds.version>=33) { ds.tuning=reader.readF(); @@ -961,7 +1211,33 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } else { reader.readC(); } - for (int i=0; i<6; i++) reader.readC(); + if (ds.version>=62) { + ds.stopPortaOnNoteOff=reader.readC(); + ds.continuousVibrato=reader.readC(); + } else { + reader.readC(); + reader.readC(); + } + if (ds.version>=64) { + ds.brokenDACMode=reader.readC(); + } else { + reader.readC(); + } + if (ds.version>=65) { + ds.oneTickCut=reader.readC(); + } else { + reader.readC(); + } + if (ds.version>=66) { + ds.newInsTriggersInPorta=reader.readC(); + } else { + reader.readC(); + } + if (ds.version>=69) { + ds.arp0Reset=reader.readC(); + } else { + reader.readC(); + } } else { for (int i=0; i<20; i++) reader.readC(); } @@ -972,6 +1248,7 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { reader.read(samplePtr,ds.sampleLen*4); for (int i=0; i8) { + logE("channel %d has zero or too many effect columns! (%d)",i,ds.pat[i].effectRows); + lastError=fmt::sprintf("channel %d has too many effect columns! (%d)",i,ds.pat[i].effectRows); + delete[] file; + return false; + } } if (ds.version>=39) { @@ -1008,13 +1291,51 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { ds.masterVol=2.0f; } + if (ds.version>=70) { + // extended compat flags + ds.brokenSpeedSel=reader.readC(); + if (ds.version>=71) { + ds.noSlidesOnFirstTick=reader.readC(); + ds.rowResetsArpPos=reader.readC(); + ds.ignoreJumpAtEnd=reader.readC(); + } else { + reader.readC(); + reader.readC(); + reader.readC(); + } + if (ds.version>=72) { + ds.buggyPortaAfterSlide=reader.readC(); + ds.gbInsAffectsEnvelope=reader.readC(); + } else { + reader.readC(); + reader.readC(); + } + if (ds.version>=78) { + ds.sharedExtStat=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<25; i++) { + reader.readC(); + } + } + // read instruments for (int i=0; ireadInsData(reader,ds.version)!=DIV_DATA_SUCCESS) { lastError="invalid instrument header/data!"; + ds.unload(); delete ins; delete[] file; return false; @@ -1026,10 +1347,19 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { // read wavetables for (int i=0; ireadWaveData(reader,ds.version)!=DIV_DATA_SUCCESS) { lastError="invalid wavetable header/data!"; + ds.unload(); delete wave; delete[] file; return false; @@ -1043,16 +1373,25 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { int vol=0; int pitch=0; - reader.seek(samplePtr[i],SEEK_SET); + if (!reader.seek(samplePtr[i],SEEK_SET)) { + logE("couldn't seek to sample %d!",i); + lastError=fmt::sprintf("couldn't seek to sample %d!",i); + ds.unload(); + delete[] file; + return false; + } + reader.read(magic,4); if (strcmp(magic,"SMPL")!=0) { - logE("%d: invalid sample header!\n",i); + logE("%d: invalid sample header!",i); lastError="invalid sample header!"; + ds.unload(); delete[] file; return false; } reader.readI(); DivSample* sample=new DivSample; + logD("reading sample %d at %x...",i,samplePtr[i]); sample->name=reader.readString(); sample->samples=reader.readI(); @@ -1090,14 +1429,15 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { reader.read(data,2*length); if (pitch!=5) { - logD("%d: scaling from %d...\n",i,pitch); + logD("%d: scaling from %d...",i,pitch); } // render data if (sample->depth!=8 && sample->depth!=16) { - logW("%d: sample depth is wrong! (%d)\n",i,sample->depth); + logW("%d: sample depth is wrong! (%d)",i,sample->depth); sample->depth=16; } + sample->samples=(double)sample->samples/samplePitches[pitch]; sample->init(sample->samples); unsigned int k=0; @@ -1123,11 +1463,19 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { // read patterns for (int i: patPtr) { - reader.seek(i,SEEK_SET); + if (!reader.seek(i,SEEK_SET)) { + logE("couldn't seek to pattern in %x!",i); + lastError=fmt::sprintf("couldn't seek to pattern in %x!",i); + ds.unload(); + delete[] file; + return false; + } reader.read(magic,4); + logD("reading pattern in %x...",i); if (strcmp(magic,"PATR")!=0) { - logE("%x: invalid pattern header!\n",i); + logE("%x: invalid pattern header!",i); lastError="invalid pattern header!"; + ds.unload(); delete[] file; return false; } @@ -1137,6 +1485,23 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { int index=reader.readS(); reader.readI(); + logD("- %d, %d",chan,index); + + if (chan<0 || chan>=tchans) { + logE("pattern channel out of range!",i); + lastError="pattern channel out of range!"; + ds.unload(); + delete[] file; + return false; + } + if (index<0 || index>255) { + logE("pattern index out of range!",i); + lastError="pattern index out of range!"; + ds.unload(); + delete[] file; + return false; + } + DivPattern* pat=ds.pat[chan].getPattern(index,true); for (int j=0; jdata[j][0]=reader.readS(); @@ -1156,23 +1521,25 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (reader.tell() patPtr; + char magic[4]={0,0,0,0}; + short defaultVols[31]; + int sampLens[31]; + // 0=arp, 1=pslide, 2=vib, 3=trem, 4=vslide + bool fxUsage[DIV_MAX_CHANS][5]; + SafeReader reader=SafeReader(file,len); + warnings=""; + + memset(defaultVols,0,31*sizeof(short)); + memset(sampLens,0,31*sizeof(int)); + memset(fxUsage,0,DIV_MAX_CHANS*5*sizeof(bool)); + + try { + DivSong ds; + ds.tuning=436.0; + ds.version=DIV_VERSION_MOD; + ds.linearPitch=false; + ds.noSlidesOnFirstTick=true; + ds.rowResetsArpPos=true; + ds.ignoreJumpAtEnd=false; + + int insCount=31; + bool bypassLimits=false; + + // check mod magic bytes + if (!reader.seek(1080,SEEK_SET)) { + logD("couldn't seek to 1080"); + throw EndOfFileException(&reader,reader.tell()); + } + reader.read(magic,4); + if (memcmp(magic,"M.K.",4)==0 || memcmp(magic,"M!K!",4)==0 || memcmp(magic,"M&K!",4)==0) { + logD("detected a ProTracker module"); + 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"); + chCount=8; + } else if (memcmp(magic+1,"CHN",3)==0 && magic[0]>='1' && magic[0]<='9') { + logD("detected a FastTracker module"); + chCount=magic[0]-'0'; + } else if (memcmp(magic,"FLT",3)==0 && magic[3]>='1' && magic[3]<='9') { + logD("detected a Fairlight module"); + chCount=magic[3]-'0'; + } else if (memcmp(magic,"TDZ",3)==0 && magic[3]>='1' && magic[3]<='9') { + logD("detected a TakeTracker module"); + 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"); + chCount=((magic[0]-'0')*10)+(magic[1]-'0'); + } else { + // TODO: Soundtracker MOD? + insCount=15; + throw InvalidHeaderException(); + } + + // song name + reader.seek(0,SEEK_SET); + ds.name=reader.readString(20); + + // samples + for (int i=0; idepth=8; + sample->name=reader.readString(22); + int slen=((unsigned short)reader.readS_BE())*2; + sampLens[i]=slen; + if (slen==2) slen=0; + signed char fineTune=reader.readC()&0x0f; + if (fineTune>=8) fineTune-=16; + sample->rate=(int)(pow(2.0,(double)fineTune/96.0)*8363.0); + sample->centerRate=sample->rate; + defaultVols[i]=reader.readC(); + int loopStart=reader.readS_BE()*2; + int loopLen=reader.readS_BE()*2; + int loopEnd=loopStart+loopLen; + // bunch of checks since ProTracker abuses those for one-shot samples + if (loopStart>loopEnd || loopEnd<4 || loopLen<4) { + loopStart=0; + loopLen=0; + } + if(loopLen>=2) { + if (loopEndloopStart=loopStart; + } + sample->init(slen); + ds.sample.push_back(sample); + } + ds.sampleLen=ds.sample.size(); + + // orders + ds.ordersLen=ordCount=reader.readC(); + reader.readC(); // restart position, unused + int patMax=0; + for (int i=0; i<128; i++) { + unsigned char pat=reader.readC(); + if (pat>patMax) patMax=pat; + for (int j=0; jdata[row]; + unsigned char data[4]; + reader.read(&data,4); + // instrument + short ins=(data[0]&0xf0)|(data[2]>>4); + if (ins>0) { + dstrow[2]=ins-1; + dstrow[3]=defaultVols[ins-1]; + } + // note + int period=data[1]+((data[0]&0x0f)<<8); + if (period>0 && period<0x0fff) { + short note=(short)round(log2(3424.0/period)*12); + dstrow[0]=((note-1)%12)+1; + dstrow[1]=(note-1)/12+1; + if (period<114) { + bypassLimits=true; + } + } + // effects are done later + short fxtyp=data[2]&0x0f; + short fxval=data[3]; + dstrow[4]=fxtyp; + dstrow[5]=fxval; + switch (fxtyp) { + case 0: + if (fxval!=0) fxUsage[ch][0]=true; + break; + case 1: case 2: case 3: + fxUsage[ch][1]=true; + break; + case 4: + fxUsage[ch][2]=true; + break; + case 5: + fxUsage[ch][1]=true; + fxUsage[ch][4]=true; + break; + case 6: + fxUsage[ch][2]=true; + fxUsage[ch][4]=true; + break; + case 7: + fxUsage[ch][3]=true; + break; + case 10: + if (fxval!=0) fxUsage[ch][4]=true; + break; + } + } + } + } + + // samples + size_t pos=reader.tell(); + for (int i=0; i<31; i++) { + reader.seek(pos,SEEK_SET); + reader.read(ds.sample[i]->data8,ds.sample[i]->samples); + pos+=sampLens[i]; + } + + // convert effects + for (int ch=0; ch<=chCount; ch++) { + unsigned char fxCols=1; + for (int pat=0; pat<=patMax; pat++) { + auto* data=ds.pat[ch].getPattern(pat,true)->data; + short lastPitchEffect=-1; + short lastEffectState[5]={-1,-1,-1,-1,-1}; + short setEffectState[5]={-1,-1,-1,-1,-1}; + for (int row=0;row<64;row++) { + const short fxUsageTyp[5]={0x00,0x01,0x04,0x07,0xFA}; + short effectState[5]={0,0,0,0,0}; + unsigned char curFxCol=0; + short fxTyp=data[row][4]; + short fxVal=data[row][5]; + auto writeFxCol=[data,row,&curFxCol](short typ, short val) { + data[row][4+curFxCol*2]=typ; + data[row][5+curFxCol*2]=val; + curFxCol++; + }; + writeFxCol(-1,-1); + curFxCol=0; + switch (fxTyp) { + case 0: // arp + effectState[0]=fxVal; + break; + case 5: // vol slide + porta + effectState[4]=fxVal; + fxTyp=3; + fxVal=0; + // fall through + case 1: // note slide up + case 2: // note slide down + case 3: // porta + if (fxTyp==3 && fxVal==0) { + if (setEffectState[1]<0) break; + fxVal=setEffectState[1]; + } + setEffectState[1]=fxVal; + effectState[1]=fxVal; + if ((effectState[1]!=lastEffectState[1]) || + (fxTyp!=lastPitchEffect) || + (effectState[1]!=0 && data[row][0]>0)) { + writeFxCol(fxTyp,fxVal); + } + lastPitchEffect=fxTyp; + lastEffectState[1]=fxVal; + break; + case 6: // vol slide + vibrato + effectState[4]=fxVal; + fxTyp=4; + fxVal=0; + // fall through + case 4: // vibrato + // TODO: handle 0 value? + if (fxVal==0) { + if (setEffectState[2]<0) break; + fxVal=setEffectState[2]; + } + effectState[2]=fxVal; + setEffectState[2]=fxVal; + break; + case 7: // tremolo + if (fxVal==0) { + if (setEffectState[3]<0) break; + fxVal=setEffectState[3]; + } + effectState[3]=fxVal; + setEffectState[3]=fxVal; + break; + case 9: // set offset + writeFxCol(0x90,fxVal); + break; + case 10: // vol slide + effectState[4]=fxVal; + break; + case 11: // jump to pos + writeFxCol(fxTyp,fxVal); + break; + case 12: // set vol + data[row][3]=fxVal; + break; + case 13: // break to row (BCD) + writeFxCol(fxTyp,((fxVal>>4)*10)+(fxVal&15)); + break; + case 15: // set speed + // TODO: somehow handle VBlank tunes + // TODO: klisje is still broken, perhaps because there wasn't tempo set back then? + if (fxVal>0x20) { + writeFxCol(0xf0,fxVal); + } else { + writeFxCol(0x09,fxVal); + writeFxCol(0x0f,fxVal); + } + break; + case 14: // extended + fxTyp=fxVal>>4; + fxVal&=0x0f; + switch (fxTyp) { + case 0: + writeFxCol(0x10,!fxVal); + break; + case 1: // single note slide up + case 2: // single note slide down + writeFxCol(fxTyp-1+0xf1,fxVal); + break; + case 9: // retrigger + writeFxCol(0x0c,fxVal); + break; + case 10: // single vol slide up + case 11: // single vol slide down + writeFxCol(fxTyp-10+0xf8,fxVal); + break; + case 12: // note cut + case 13: // note delay + writeFxCol(fxTyp-12+0xec,fxVal); + break; + } + break; + } + for (int i=0; i<5; i++) { + // pitch slide and volume slide needs to be kept active on new note + // even after target/max is reached + if (fxUsage[ch][i] && (effectState[i]!=lastEffectState[i] || (effectState[i]!=0 && i==4 && data[row][3]>=0))) { + writeFxCol(fxUsageTyp[i],effectState[i]); + } + } + memcpy(lastEffectState,effectState,sizeof(effectState)); + if (curFxCol>fxCols) { + fxCols=curFxCol; + } + } + } + ds.pat[ch].effectRows=fxCols; + } + + ds.pal=false; + ds.hz=50; + ds.customTempo=false; + ds.systemLen=(chCount+3)/4; + for(int i=0; i1 || bypassLimits)?2:0); // PAL + } + for(int i=0; itype=DIV_INS_AMIGA; + ins->amiga.initSample=i; + ins->name=ds.sample[i]->name; + ds.ins.push_back(ins); + } + ds.insLen=ds.ins.size(); + + if (active) quitDispatch(); + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + recalcChans(); + renderSamples(); + saveLock.unlock(); + BUSY_END; + if (active) { + initDispatch(); + syncReset(); + } + success=true; + } catch (EndOfFileException& e) { + //logE("premature end of file!"); + lastError="incomplete file"; + } catch (InvalidHeaderException& e) { + //logE("invalid info header!"); + lastError="invalid info header!"; + } + return success; +} + bool DivEngine::load(unsigned char* f, size_t slen) { unsigned char* file; size_t len; @@ -1191,7 +1932,16 @@ bool DivEngine::load(unsigned char* f, size_t slen) { return false; } if (memcmp(f,DIV_DMF_MAGIC,16)!=0 && memcmp(f,DIV_FUR_MAGIC,16)!=0) { - logD("loading as zlib...\n"); + // try loading as a .mod first before trying to decompress + // TODO: move to a different location? + logD("loading as .mod..."); + if (loadMod(f,slen)) { + delete[] f; + return true; + } + + lastError="not a .mod song"; + logD("loading as zlib..."); // try zlib z_stream zl; memset(&zl,0,sizeof(z_stream)); @@ -1206,9 +1956,9 @@ bool DivEngine::load(unsigned char* f, size_t slen) { nextErr=inflateInit(&zl); if (nextErr!=Z_OK) { if (zl.msg==NULL) { - logE("zlib error: unknown! %d\n",nextErr); + logE("zlib error: unknown! %d",nextErr); } else { - logE("zlib error: %s\n",zl.msg); + logE("zlib error: %s",zl.msg); } inflateEnd(&zl); delete[] f; @@ -1225,10 +1975,10 @@ bool DivEngine::load(unsigned char* f, size_t slen) { nextErr=inflate(&zl,Z_SYNC_FLUSH); if (nextErr!=Z_OK && nextErr!=Z_STREAM_END) { if (zl.msg==NULL) { - logE("zlib error: unknown error! %d\n",nextErr); + logE("zlib error: unknown error! %d",nextErr); lastError="unknown decompression error"; } else { - logE("zlib inflate: %s\n",zl.msg); + logE("zlib inflate: %s",zl.msg); lastError=fmt::sprintf("decompression error: %s",zl.msg); } for (InflateBlock* i: blocks) delete i; @@ -1247,10 +1997,10 @@ bool DivEngine::load(unsigned char* f, size_t slen) { nextErr=inflateEnd(&zl); if (nextErr!=Z_OK) { if (zl.msg==NULL) { - logE("zlib end error: unknown error! %d\n",nextErr); + logE("zlib end error: unknown error! %d",nextErr); lastError="unknown decompression finish error"; } else { - logE("zlib end: %s\n",zl.msg); + logE("zlib end: %s",zl.msg); lastError=fmt::sprintf("decompression finish error: %s",zl.msg); } for (InflateBlock* i: blocks) delete i; @@ -1265,7 +2015,7 @@ bool DivEngine::load(unsigned char* f, size_t slen) { finalSize+=i->blockSize; } if (finalSize<1) { - logE("compressed too small!\n"); + logE("compressed too small!"); lastError="file too small"; for (InflateBlock* i: blocks) delete i; blocks.clear(); @@ -1282,7 +2032,7 @@ bool DivEngine::load(unsigned char* f, size_t slen) { len=finalSize; delete[] f; } else { - logD("loading as uncompressed\n"); + logD("loading as uncompressed"); file=(unsigned char*)f; len=slen; } @@ -1291,13 +2041,14 @@ bool DivEngine::load(unsigned char* f, size_t slen) { } else if (memcmp(file,DIV_FUR_MAGIC,16)==0) { return loadFur(file,len); } - logE("not a valid module!\n"); + logE("not a valid module!"); lastError="not a compatible song"; delete[] file; return false; } -SafeWriter* DivEngine::saveFur() { +SafeWriter* DivEngine::saveFur(bool notPrimary) { + saveLock.lock(); int insPtr[256]; int wavePtr[256]; int samplePtr[256]; @@ -1305,8 +2056,10 @@ SafeWriter* DivEngine::saveFur() { size_t ptrSeek; warnings=""; - song.isDMF=false; - song.version=DIV_ENGINE_VERSION; + if (!notPrimary) { + song.isDMF=false; + song.version=DIV_ENGINE_VERSION; + } SafeWriter* w=new SafeWriter; w->init(); @@ -1362,7 +2115,7 @@ SafeWriter* DivEngine::saveFur() { if (i>=song.systemLen) { w->writeC(0); } else { - w->writeC(systemToFile(song.system[i])); + w->writeC(systemToFileFur(song.system[i])); } } @@ -1400,9 +2153,12 @@ SafeWriter* DivEngine::saveFur() { w->writeC(song.algMacroBehavior); w->writeC(song.brokenShortcutSlides); w->writeC(song.ignoreDuplicateSlides); - for (int i=0; i<6; i++) { - w->writeC(0); - } + w->writeC(song.stopPortaOnNoteOff); + w->writeC(song.continuousVibrato); + w->writeC(song.brokenDACMode); + w->writeC(song.oneTickCut); + w->writeC(song.newInsTriggersInPorta); + w->writeC(song.arp0Reset); ptrSeek=w->tell(); // instrument pointers (we'll seek here later) @@ -1455,6 +2211,18 @@ SafeWriter* DivEngine::saveFur() { w->writeF(song.masterVol); + // extended compat flags + w->writeC(song.brokenSpeedSel); + w->writeC(song.noSlidesOnFirstTick); + w->writeC(song.rowResetsArpPos); + w->writeC(song.ignoreJumpAtEnd); + w->writeC(song.buggyPortaAfterSlide); + w->writeC(song.gbInsAffectsEnvelope); + w->writeC(song.sharedExtStat); + for (int i=0; i<25; i++) { + w->writeC(0); + } + /// INSTRUMENT for (int i=0; iwriteI(i); } + saveLock.unlock(); return w; } SafeWriter* DivEngine::saveDMF(unsigned char version) { // fail if version is not supported - if (version<24 || version>25) { - logE("cannot save in this version!\n"); + if (version<24 || version>26) { + logE("cannot save in this version!"); lastError="invalid version to save in! this is a bug!"; return NULL; } @@ -1547,51 +2316,86 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { bool isFlat=false; if (song.systemLen==2) { if (song.system[0]==DIV_SYSTEM_YM2612 && song.system[1]==DIV_SYSTEM_SMS) { - isFlat=true; + isFlat=true; } if (song.system[0]==DIV_SYSTEM_YM2612_EXT && song.system[1]==DIV_SYSTEM_SMS) { - isFlat=true; + isFlat=true; } if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_SEGAPCM_COMPAT) { isFlat=true; } if (song.system[0]==DIV_SYSTEM_SMS && song.system[1]==DIV_SYSTEM_OPLL) { - isFlat=true; + isFlat=true; } if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_VRC7) { - isFlat=true; + isFlat=true; + } + if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_FDS) { + isFlat=true; } } // fail if more than one system if (!isFlat && song.systemLen!=1) { - logE("cannot save multiple systems in this format!\n"); + logE("cannot save multiple systems in this format!"); lastError="multiple systems not possible on .dmf"; return NULL; } // fail if this is an YMU759 song if (song.system[0]==DIV_SYSTEM_YMU759) { - logE("cannot save YMU759 song!\n"); + logE("cannot save YMU759 song!"); lastError="YMU759 song saving is not supported"; return NULL; } // fail if the system is SMS+OPLL and version<25 if (version<25 && song.system[0]==DIV_SYSTEM_SMS && song.system[1]==DIV_SYSTEM_OPLL) { - logE("Master System FM expansion not supported in 1.0/legacy .dmf!\n"); + logE("Master System FM expansion not supported in 1.0/legacy .dmf!"); lastError="Master System FM expansion not supported in 1.0/legacy .dmf!"; return NULL; } // fail if the system is NES+VRC7 and version<25 if (version<25 && song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_VRC7) { - logE("NES + VRC7 not supported in 1.0/legacy .dmf!\n"); + logE("NES + VRC7 not supported in 1.0/legacy .dmf!"); lastError="NES + VRC7 not supported in 1.0/legacy .dmf!"; return NULL; } + // fail if the system is FDS and version<25 + if (version<25 && song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_FDS) { + logE("FDS not supported in 1.0/legacy .dmf!"); + lastError="FDS not supported in 1.0/legacy .dmf!"; + return NULL; + } // fail if the system is Furnace-exclusive - if (!isFlat && systemToFile(song.system[0])&0x80) { - logE("cannot save Furnace-exclusive system song!\n"); + if (!isFlat && systemToFileDMF(song.system[0])==0) { + logE("cannot save Furnace-exclusive system song!"); lastError="this system is not possible on .dmf"; return NULL; } + // fail if values are out of range + if (song.ordersLen>127) { + logE("maximum .dmf song length is 127!"); + lastError="maximum .dmf song length is 127"; + return NULL; + } + if (song.ins.size()>128) { + logE("maximum number of instruments in .dmf is 128!"); + lastError="maximum number of instruments in .dmf is 128"; + return NULL; + } + if (song.wave.size()>64) { + logE("maximum number of wavetables in .dmf is 64!"); + lastError="maximum number of wavetables in .dmf is 64"; + return NULL; + } + for (int i=0; i0x7f) { + logE("order %d, %d is out of range (0-127)!",song.orders.ord[i][j]); + lastError=fmt::sprintf("order %d, %d is out of range (0-127)",song.orders.ord[i][j]); + return NULL; + } + } + } + saveLock.lock(); warnings=""; song.version=version; song.isDMF=true; @@ -1604,22 +2408,25 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { w->writeC(version); DivSystem sys=DIV_SYSTEM_NULL; if (song.system[0]==DIV_SYSTEM_YM2612 && song.system[1]==DIV_SYSTEM_SMS) { - w->writeC(systemToFile(DIV_SYSTEM_GENESIS)); + w->writeC(systemToFileDMF(DIV_SYSTEM_GENESIS)); sys=DIV_SYSTEM_GENESIS; } else if (song.system[0]==DIV_SYSTEM_YM2612_EXT && song.system[1]==DIV_SYSTEM_SMS) { - w->writeC(systemToFile(DIV_SYSTEM_GENESIS_EXT)); + w->writeC(systemToFileDMF(DIV_SYSTEM_GENESIS_EXT)); sys=DIV_SYSTEM_GENESIS_EXT; } else if (song.system[0]==DIV_SYSTEM_YM2151 && song.system[1]==DIV_SYSTEM_SEGAPCM_COMPAT) { - w->writeC(systemToFile(DIV_SYSTEM_ARCADE)); + w->writeC(systemToFileDMF(DIV_SYSTEM_ARCADE)); sys=DIV_SYSTEM_ARCADE; } else if (song.system[0]==DIV_SYSTEM_SMS && song.system[1]==DIV_SYSTEM_OPLL) { - w->writeC(systemToFile(DIV_SYSTEM_SMS_OPLL)); + w->writeC(systemToFileDMF(DIV_SYSTEM_SMS_OPLL)); sys=DIV_SYSTEM_SMS_OPLL; } else if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_VRC7) { - w->writeC(systemToFile(DIV_SYSTEM_NES_VRC7)); + w->writeC(systemToFileDMF(DIV_SYSTEM_NES_VRC7)); sys=DIV_SYSTEM_NES_VRC7; + } else if (song.system[0]==DIV_SYSTEM_NES && song.system[1]==DIV_SYSTEM_FDS) { + w->writeC(systemToFileDMF(DIV_SYSTEM_NES_FDS)); + sys=DIV_SYSTEM_NES_FDS; } else { - w->writeC(systemToFile(song.system[0])); + w->writeC(systemToFileDMF(song.system[0])); sys=song.system[0]; } @@ -1636,7 +2443,7 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { w->writeC(song.customTempo); char customHz[4]; memset(customHz,0,4); - snprintf(customHz,4,"%d",song.hz); + snprintf(customHz,4,"%d",(int)song.hz); w->write(customHz,3); w->writeI(song.patLen); w->writeC(song.ordersLen); @@ -1717,36 +2524,36 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { } } else { // STD if (sys!=DIV_SYSTEM_GB) { - w->writeC(i->std.volMacroLen); - w->write(i->std.volMacro,4*i->std.volMacroLen); - if (i->std.volMacroLen>0) { - w->writeC(i->std.volMacroLoop); + w->writeC(i->std.volMacro.len); + w->write(i->std.volMacro.val,4*i->std.volMacro.len); + if (i->std.volMacro.len>0) { + w->writeC(i->std.volMacro.loop); } } - w->writeC(i->std.arpMacroLen); - if (i->std.arpMacroMode) { - w->write(i->std.arpMacro,4*i->std.arpMacroLen); + w->writeC(i->std.arpMacro.len); + if (i->std.arpMacro.mode) { + w->write(i->std.arpMacro.val,4*i->std.arpMacro.len); } else { - for (int j=0; jstd.arpMacroLen; j++) { - w->writeI(i->std.arpMacro[j]+12); + for (int j=0; jstd.arpMacro.len; j++) { + w->writeI(i->std.arpMacro.val[j]+12); } } - if (i->std.arpMacroLen>0) { - w->writeC(i->std.arpMacroLoop); + if (i->std.arpMacro.len>0) { + w->writeC(i->std.arpMacro.loop); } - w->writeC(i->std.arpMacroMode); + w->writeC(i->std.arpMacro.mode); - w->writeC(i->std.dutyMacroLen); - w->write(i->std.dutyMacro,4*i->std.dutyMacroLen); - if (i->std.dutyMacroLen>0) { - w->writeC(i->std.dutyMacroLoop); + w->writeC(i->std.dutyMacro.len); + w->write(i->std.dutyMacro.val,4*i->std.dutyMacro.len); + if (i->std.dutyMacro.len>0) { + w->writeC(i->std.dutyMacro.loop); } - w->writeC(i->std.waveMacroLen); - w->write(i->std.waveMacro,4*i->std.waveMacroLen); - if (i->std.waveMacroLen>0) { - w->writeC(i->std.waveMacroLoop); + w->writeC(i->std.waveMacro.len); + w->write(i->std.waveMacro.val,4*i->std.waveMacro.len); + if (i->std.waveMacro.len>0) { + w->writeC(i->std.waveMacro.loop); } if (sys==DIV_SYSTEM_C64_6581 || sys==DIV_SYSTEM_C64_8580) { @@ -1760,7 +2567,7 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { w->writeC(i->c64.s); w->writeC(i->c64.r); - logW("duty and cutoff precision will be lost!\n"); + logW("duty and cutoff precision will be lost!"); w->writeC((i->c64.duty*100)/4095); w->writeC(i->c64.ringMod); @@ -1790,7 +2597,13 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { w->writeC(song.wave.size()); for (DivWavetable* i: song.wave) { w->writeI(i->len); - w->write(i->data,4*i->len); + if (sys==DIV_SYSTEM_NES_FDS && version<26) { + for (int j=0; jlen; j++) { + w->writeI(i->data[j]>>2); + } + } else { + w->write(i->data,4*i->len); + } } for (int i=0; iwriteC(16); w->write(i->data16,i->length16); } - + + saveLock.unlock(); return w; } diff --git a/src/engine/fileOpsIns.cpp b/src/engine/fileOpsIns.cpp new file mode 100644 index 000000000..b88da310e --- /dev/null +++ b/src/engine/fileOpsIns.cpp @@ -0,0 +1,810 @@ +/** + * 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 "engine.h" +#include "../ta-log.h" +#include "../fileutils.h" +#include + +enum DivInsFormats { + DIV_INSFORMAT_DMP, + DIV_INSFORMAT_TFI, + DIV_INSFORMAT_VGI, + DIV_INSFORMAT_FTI, + DIV_INSFORMAT_BTI, + DIV_INSFORMAT_S3I, + DIV_INSFORMAT_SBI, + DIV_INSFORMAT_OPM +}; + +void DivEngine::loadDMP(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins=new DivInstrument; + // this is a ridiculous mess + unsigned char version=0; + unsigned char sys=0; + try { + reader.seek(0,SEEK_SET); + version=reader.readC(); + logD(".dmp version %d",version); + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + if (version>11) { + lastError="unknown instrument version!"; + delete ins; + return; + } + + ins->name=stripPath; + + if (version>=11) { // 1.0 + try { + sys=reader.readC(); + + switch (sys) { + case 1: // YMU759 + ins->type=DIV_INS_FM; + logD("instrument type is YMU759"); + break; + case 2: // Genesis + ins->type=DIV_INS_FM; + logD("instrument type is Genesis"); + break; + case 3: // SMS + ins->type=DIV_INS_STD; + logD("instrument type is SMS"); + break; + case 4: // Game Boy + ins->type=DIV_INS_GB; + logD("instrument type is Game Boy"); + break; + case 5: // PC Engine + ins->type=DIV_INS_PCE; + logD("instrument type is PC Engine"); + break; + case 6: // NES + ins->type=DIV_INS_STD; + logD("instrument type is NES"); + break; + case 7: case 0x17: // C64 + ins->type=DIV_INS_C64; + logD("instrument type is C64"); + break; + case 8: // Arcade + ins->type=DIV_INS_FM; + logD("instrument type is Arcade"); + break; + default: + logD("instrument type is unknown"); + lastError="unknown instrument type!"; + delete ins; + return; + break; + } + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + } + + try { + bool mode=true; + if (version>1) { + mode=reader.readC(); + logD("instrument mode is %d",mode); + if (mode==0) { + if (version<11) { + ins->type=DIV_INS_STD; + } + } else { + ins->type=DIV_INS_FM; + } + } else { + ins->type=DIV_INS_FM; + } + + if (mode) { // FM + logD("reading FM data..."); + if (version<10) { + if (version>1) { + // bullcrap! no way to determine the instrument type other than a vague FM/STD! + if (reader.size()==51) { + reader.readC(); + ins->fm.ops=4; + } else { + ins->fm.ops=reader.readC()?4:2; + } + } else { + ins->fm.ops=reader.readC()?2:4; + } + } else { + ins->fm.ops=4; + } + if (version>1) { // HELP! in which version of the format did we start storing FMS! + ins->fm.fms=reader.readC(); + } + ins->fm.fb=reader.readC(); + ins->fm.alg=reader.readC(); + // DITTO + if (sys!=1) ins->fm.ams=reader.readC(); + + for (int j=0; jfm.ops; j++) { + logD("OP%d is at %d",j,reader.tell()); + ins->fm.op[j].mult=reader.readC(); + ins->fm.op[j].tl=reader.readC(); + ins->fm.op[j].ar=reader.readC(); + ins->fm.op[j].dr=reader.readC(); + ins->fm.op[j].sl=reader.readC(); + ins->fm.op[j].rr=reader.readC(); + ins->fm.op[j].am=reader.readC(); + // what the hell how do I tell! + if (sys==1) { // YMU759 + ins->fm.op[j].ws=reader.readC(); + ins->fm.op[j].ksl=reader.readC(); + ins->fm.op[j].vib=reader.readC(); + ins->fm.op[j].egt=reader.readC(); + ins->fm.op[j].sus=reader.readC(); + ins->fm.op[j].ksr=reader.readC(); + ins->fm.op[j].dvb=reader.readC(); + ins->fm.op[j].dam=reader.readC(); + } else { + ins->fm.op[j].rs=reader.readC(); + ins->fm.op[j].dt=reader.readC(); + ins->fm.op[j].dt2=ins->fm.op[j].dt>>4; + ins->fm.op[j].dt&=15; + ins->fm.op[j].d2r=reader.readC(); + ins->fm.op[j].ssgEnv=reader.readC(); + } + } + } else { // STD + logD("reading STD data..."); + if (ins->type!=DIV_INS_GB) { + ins->std.volMacro.len=reader.readC(); + if (version>5) { + for (int i=0; istd.volMacro.len; i++) { + ins->std.volMacro.val[i]=reader.readI(); + } + } else { + for (int i=0; istd.volMacro.len; i++) { + ins->std.volMacro.val[i]=reader.readC(); + } + } + if (version<11) for (int i=0; istd.volMacro.len; i++) { + if (ins->std.volMacro.val[i]>15 && ins->type==DIV_INS_STD) ins->type=DIV_INS_PCE; + } + if (ins->std.volMacro.len>0) { + ins->std.volMacro.open=true; + ins->std.volMacro.loop=reader.readC(); + } else { + ins->std.volMacro.open=false; + } + } + + ins->std.arpMacro.len=reader.readC(); + if (version>5) { + for (int i=0; istd.arpMacro.len; i++) { + ins->std.arpMacro.val[i]=reader.readI(); + } + } else { + for (int i=0; istd.arpMacro.len; i++) { + ins->std.arpMacro.val[i]=reader.readC(); + } + } + if (ins->std.arpMacro.len>0) { + ins->std.arpMacro.open=true; + ins->std.arpMacro.loop=reader.readC(); + } else { + ins->std.arpMacro.open=false; + } + if (version>8) { // TODO: when? + ins->std.arpMacro.mode=reader.readC(); + } + + ins->std.dutyMacro.len=reader.readC(); + if (version>5) { + for (int i=0; istd.dutyMacro.len; i++) { + ins->std.dutyMacro.val[i]=reader.readI(); + } + } else { + for (int i=0; istd.dutyMacro.len; i++) { + ins->std.dutyMacro.val[i]=reader.readC(); + } + } + if (ins->std.dutyMacro.len>0) { + ins->std.dutyMacro.open=true; + ins->std.dutyMacro.loop=reader.readC(); + } else { + ins->std.dutyMacro.open=false; + } + + ins->std.waveMacro.len=reader.readC(); + if (version>5) { + for (int i=0; istd.waveMacro.len; i++) { + ins->std.waveMacro.val[i]=reader.readI(); + } + } else { + for (int i=0; istd.waveMacro.len; i++) { + ins->std.waveMacro.val[i]=reader.readC(); + } + } + if (ins->std.waveMacro.len>0) { + ins->std.waveMacro.open=true; + ins->std.waveMacro.loop=reader.readC(); + } else { + ins->std.waveMacro.open=false; + } + + if (ins->type==DIV_INS_C64) { + ins->c64.triOn=reader.readC(); + ins->c64.sawOn=reader.readC(); + ins->c64.pulseOn=reader.readC(); + ins->c64.noiseOn=reader.readC(); + + ins->c64.a=reader.readC(); + ins->c64.d=reader.readC(); + ins->c64.s=reader.readC(); + ins->c64.r=reader.readC(); + + ins->c64.duty=(reader.readC()*4095)/100; + + ins->c64.ringMod=reader.readC(); + ins->c64.oscSync=reader.readC(); + ins->c64.toFilter=reader.readC(); + if (version<0x07) { // TODO: UNSURE + ins->c64.volIsCutoff=reader.readI(); + } else { + ins->c64.volIsCutoff=reader.readC(); + } + ins->c64.initFilter=reader.readC(); + + ins->c64.res=reader.readC(); + ins->c64.cut=(reader.readC()*2047)/100; + ins->c64.hp=reader.readC(); + ins->c64.bp=reader.readC(); + ins->c64.lp=reader.readC(); + ins->c64.ch3off=reader.readC(); + } + if (ins->type==DIV_INS_GB) { + ins->gb.envVol=reader.readC(); + ins->gb.envDir=reader.readC(); + ins->gb.envLen=reader.readC(); + ins->gb.soundLen=reader.readC(); + } + } + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + ret.push_back(ins); +} + +void DivEngine::loadTFI(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins=new DivInstrument; + try { + reader.seek(0,SEEK_SET); + + ins->type=DIV_INS_FM; + ins->name=stripPath; + + ins->fm.alg=reader.readC(); + ins->fm.fb=reader.readC(); + + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=ins->fm.op[i]; + + op.mult=reader.readC(); + op.dt=reader.readC(); + op.tl=reader.readC(); + op.rs=reader.readC(); + op.ar=reader.readC(); + op.dr=reader.readC(); + op.d2r=reader.readC(); + op.rr=reader.readC(); + op.sl=reader.readC(); + op.ssgEnv=reader.readC(); + } + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + ret.push_back(ins); +} + +void DivEngine::loadVGI(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins=new DivInstrument; + try { + reader.seek(0,SEEK_SET); + + ins->type=DIV_INS_FM; + ins->name=stripPath; + + ins->fm.alg=reader.readC(); + ins->fm.fb=reader.readC(); + unsigned char fmsams=reader.readC(); + ins->fm.fms=fmsams&7; + ins->fm.ams=fmsams>>4; + + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=ins->fm.op[i]; + + op.mult=reader.readC(); + op.dt=reader.readC(); + op.tl=reader.readC(); + op.rs=reader.readC(); + op.ar=reader.readC(); + op.dr=reader.readC(); + if (op.dr&0x80) { + op.am=1; + op.dr&=0x7f; + } + op.d2r=reader.readC(); + op.rr=reader.readC(); + op.sl=reader.readC(); + op.ssgEnv=reader.readC(); + } + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + ret.push_back(ins); +} + +void DivEngine::loadS3I(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins=new DivInstrument; + try { + reader.seek(0, SEEK_SET); + + uint8_t s3i_type = reader.readC(); + + if (s3i_type >= 2) { + ins->type = DIV_INS_OPL; + // skip internal filename - we'll use the long name description + reader.seek(12, SEEK_CUR); + + // skip reserved bytes + reader.seek(3, SEEK_CUR); + + // 12-byte opl value + uint8_t s3i_Mcharacteristics = reader.readC(); + uint8_t s3i_Ccharacteristics = reader.readC(); + uint8_t s3i_Mscaling_output = reader.readC(); + uint8_t s3i_Cscaling_output = reader.readC(); + uint8_t s3i_Meg_AD = reader.readC(); + uint8_t s3i_Ceg_AD = reader.readC(); + uint8_t s3i_Meg_SR = reader.readC(); + uint8_t s3i_Ceg_SR = reader.readC(); + uint8_t s3i_Mwave = reader.readC(); + uint8_t s3i_Cwave = reader.readC(); + uint8_t s3i_FeedConnect = reader.readC(); + + DivInstrumentFM::Operator& opM = ins->fm.op[0]; + DivInstrumentFM::Operator& opC = ins->fm.op[1]; + ins->fm.ops = 2; + opM.mult = s3i_Mcharacteristics & 0xF; + opM.ksr = ((s3i_Mcharacteristics >> 4) & 0x1); + opM.sus = ((s3i_Mcharacteristics >> 5) & 0x1); + opM.vib = ((s3i_Mcharacteristics >> 6) & 0x1); + opM.am = ((s3i_Mcharacteristics >> 7) & 0x1); + opM.tl = s3i_Mscaling_output & 0x3F; + opM.ksl = ((s3i_Mscaling_output >> 6) & 0x3); + opM.ar = ((s3i_Meg_AD >> 4) & 0xF); + opM.dr = (s3i_Meg_AD & 0xF); + opM.rr = (s3i_Meg_SR & 0xF); + opM.sl = ((s3i_Meg_SR >> 4) & 0xF); + opM.ws = s3i_Mwave; + + ins->fm.alg = (s3i_FeedConnect & 0x1); + ins->fm.fb = ((s3i_FeedConnect >> 1) & 0x7); + + opC.mult = s3i_Ccharacteristics & 0xF; + opC.ksr = ((s3i_Ccharacteristics >> 4) & 0x1); + opC.sus = ((s3i_Ccharacteristics >> 5) & 0x1); + opC.vib = ((s3i_Ccharacteristics >> 6) & 0x1); + opC.am = ((s3i_Ccharacteristics >> 7) & 0x1); + opC.tl = s3i_Cscaling_output & 0x3F; + opC.ksl = ((s3i_Cscaling_output >> 6) & 0x3); + opC.ar = ((s3i_Ceg_AD >> 4) & 0xF); + opC.dr = (s3i_Ceg_AD & 0xF); + opC.rr = (s3i_Ceg_SR & 0xF); + opC.sl = ((s3i_Ceg_SR >> 4) & 0xF); + opC.ws = s3i_Cwave; + + // Skip more stuff we don't need + reader.seek(21, SEEK_CUR); + } else { + logE("S3I PCM samples currently not supported."); + } + ins->name = reader.readString(28); + int s3i_signature = reader.readI(); + + if (s3i_signature != 0x49524353) { + logW("S3I signature invalid."); + }; + } catch (EndOfFileException& e) { + lastError = "premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + ret.push_back(ins); +} + +void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins=new DivInstrument; + try { + reader.seek(0, SEEK_SET); + ins->type = DIV_INS_OPL; + + int sbi_header = reader.readI(); + // SBI header determines format + bool is_2op = (sbi_header == 0x1A494253); // SBI\x1A + bool is_4op = (sbi_header == 0x1A504F34); // 4OP\x1A + bool is_6op = (sbi_header == 0x1A504F36); // 6OP\x1A - Freq Monster 801-specific + + // 32-byte null terminated instrument name + ins->name = reader.readString(32); + + // 2op SBI + uint8_t sbi_Mcharacteristics = reader.readC(); + uint8_t sbi_Ccharacteristics = reader.readC(); + uint8_t sbi_Mscaling_output = reader.readC(); + uint8_t sbi_Cscaling_output = reader.readC(); + uint8_t sbi_Meg_AD = reader.readC(); + uint8_t sbi_Ceg_AD = reader.readC(); + uint8_t sbi_Meg_SR = reader.readC(); + uint8_t sbi_Ceg_SR = reader.readC(); + uint8_t sbi_Mwave = reader.readC(); + uint8_t sbi_Cwave = reader.readC(); + uint8_t sbi_FeedConnect = reader.readC(); + + // 4op SBI + uint8_t sbi_M4characteristics; + uint8_t sbi_C4characteristics; + uint8_t sbi_M4scaling_output; + uint8_t sbi_C4scaling_output; + uint8_t sbi_M4eg_AD; + uint8_t sbi_C4eg_AD; + uint8_t sbi_M4eg_SR; + uint8_t sbi_C4eg_SR; + uint8_t sbi_M4wave; + uint8_t sbi_C4wave; + uint8_t sbi_4opConnect; + + if (is_2op) { + DivInstrumentFM::Operator& opM = ins->fm.op[0]; + DivInstrumentFM::Operator& opC = ins->fm.op[1]; + ins->fm.ops = 2; + opM.mult = sbi_Mcharacteristics & 0xF; + opM.ksr = ((sbi_Mcharacteristics >> 4) & 0x1); + opM.sus = ((sbi_Mcharacteristics >> 5) & 0x1); + opM.vib = ((sbi_Mcharacteristics >> 6) & 0x1); + opM.am = ((sbi_Mcharacteristics >> 7) & 0x1); + opM.tl = sbi_Mscaling_output & 0x3F; + opM.ksl = ((sbi_Mscaling_output >> 6) & 0x3); + opM.ar = ((sbi_Meg_AD >> 4) & 0xF); + opM.dr = (sbi_Meg_AD & 0xF); + opM.rr = (sbi_Meg_SR & 0xF); + opM.sl = ((sbi_Meg_SR >> 4) & 0xF); + opM.ws = sbi_Mwave; + + ins->fm.alg = (sbi_FeedConnect & 0x1); + ins->fm.fb = ((sbi_FeedConnect >> 1) & 0x7); + + opC.mult = sbi_Ccharacteristics & 0xF; + opC.ksr = ((sbi_Ccharacteristics >> 4) & 0x1); + opC.sus = ((sbi_Ccharacteristics >> 5) & 0x1); + opC.vib = ((sbi_Ccharacteristics >> 6) & 0x1); + opC.am = ((sbi_Ccharacteristics >> 7) & 0x1); + opC.tl = sbi_Cscaling_output & 0x3F; + opC.ksl = ((sbi_Cscaling_output >> 6) & 0x3); + opC.ar = ((sbi_Ceg_AD >> 4) & 0xF); + opC.dr = (sbi_Ceg_AD & 0xF); + opC.rr = (sbi_Ceg_SR & 0xF); + opC.sl = ((sbi_Ceg_SR >> 4) & 0xF); + opC.ws = sbi_Cwave; + + // Ignore rest of file - rest is 'reserved padding'. + reader.seek(0, SEEK_END); + } + + if (is_4op || is_6op) { + // Operator placement is different so need to place in correct registers. + // Note: 6op is an unofficial extension of 4op SBIs by Darron Broad (Freq Monster 801). + // We'll only use the 4op portion here for pure OPL3. + DivInstrumentFM::Operator& opM = ins->fm.op[0]; + DivInstrumentFM::Operator& opC = ins->fm.op[2]; + DivInstrumentFM::Operator& opM4 = ins->fm.op[1]; + DivInstrumentFM::Operator& opC4 = ins->fm.op[3]; + ins->fm.ops = 4; + + sbi_M4characteristics = reader.readC(); + sbi_C4characteristics = reader.readC(); + sbi_M4scaling_output = reader.readC(); + sbi_C4scaling_output = reader.readC(); + sbi_M4eg_AD = reader.readC(); + sbi_C4eg_AD = reader.readC(); + sbi_M4eg_SR = reader.readC(); + sbi_C4eg_SR = reader.readC(); + sbi_M4wave = reader.readC(); + sbi_C4wave = reader.readC(); + sbi_4opConnect = reader.readC(); + + ins->fm.alg = (sbi_FeedConnect & 0x1) | ((sbi_4opConnect & 0x1) << 1); + ins->fm.fb = ((sbi_FeedConnect >> 1) & 0x7); + + opM.mult = sbi_Mcharacteristics & 0xF; + opM.ksr = ((sbi_Mcharacteristics >> 4) & 0x1); + opM.sus = ((sbi_Mcharacteristics >> 5) & 0x1); + opM.vib = ((sbi_Mcharacteristics >> 6) & 0x1); + opM.am = ((sbi_Mcharacteristics >> 7) & 0x1); + opM.tl = sbi_Mscaling_output & 0x3F; + opM.ksl = ((sbi_Mscaling_output >> 6) & 0x3); + opM.ar = ((sbi_Meg_AD >> 4) & 0xF); + opM.dr = (sbi_Meg_AD & 0xF); + opM.rr = (sbi_Meg_SR & 0xF); + opM.sl = ((sbi_Meg_SR >> 4) & 0xF); + opM.ws = sbi_Mwave; + + opC.mult = sbi_Ccharacteristics & 0xF; + opC.ksr = ((sbi_Ccharacteristics >> 4) & 0x1); + opC.sus = ((sbi_Ccharacteristics >> 5) & 0x1); + opC.vib = ((sbi_Ccharacteristics >> 6) & 0x1); + opC.am = ((sbi_Ccharacteristics >> 7) & 0x1); + opC.tl = sbi_Cscaling_output & 0x3F; + opC.ksl = ((sbi_Cscaling_output >> 6) & 0x3); + opC.ar = ((sbi_Ceg_AD >> 4) & 0xF); + opC.dr = (sbi_Ceg_AD & 0xF); + opC.rr = (sbi_Ceg_SR & 0xF); + opC.sl = ((sbi_Ceg_SR >> 4) & 0xF); + opC.ws = sbi_Cwave; + + opM4.mult = sbi_M4characteristics & 0xF; + opM4.ksr = ((sbi_M4characteristics >> 4) & 0x1); + opM4.sus = ((sbi_M4characteristics >> 5) & 0x1); + opM4.vib = ((sbi_M4characteristics >> 6) & 0x1); + opM4.am = ((sbi_M4characteristics >> 7) & 0x1); + opM4.tl = sbi_M4scaling_output & 0x3F; + opM4.ksl = ((sbi_M4scaling_output >> 6) & 0x3); + opM4.ar = ((sbi_M4eg_AD >> 4) & 0xF); + opM4.dr = (sbi_M4eg_AD & 0xF); + opM4.rr = (sbi_M4eg_SR & 0xF); + opM4.sl = ((sbi_M4eg_SR >> 4) & 0xF); + opM4.ws = sbi_M4wave; + + opC4.mult = sbi_C4characteristics & 0xF; + opC4.ksr = ((sbi_C4characteristics >> 4) & 0x1); + opC4.sus = ((sbi_C4characteristics >> 5) & 0x1); + opC4.vib = ((sbi_C4characteristics >> 6) & 0x1); + opC4.am = ((sbi_C4characteristics >> 7) & 0x1); + opC4.tl = sbi_C4scaling_output & 0x3F; + opC4.ksl = ((sbi_C4scaling_output >> 6) & 0x3); + opC4.ar = ((sbi_C4eg_AD >> 4) & 0xF); + opC4.dr = (sbi_C4eg_AD & 0xF); + opC4.rr = (sbi_C4eg_SR & 0xF); + opC4.sl = ((sbi_C4eg_SR >> 4) & 0xF); + opC4.ws = sbi_C4wave; + + // Ignore rest of file once we've read in all we need. + // Note: Freq Monster 801 adds a ton of other additional fields irrelevant to chip registers. + reader.seek(0, SEEK_END); + } + + } catch (EndOfFileException& e) { + lastError = "premature end of file"; + logE("premature end of file!"); + delete ins; + return; + } + + ret.push_back(ins); +} + +void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, String& stripPath) { + DivInstrument* ins[128]; + memset(ins,0,128*sizeof(void*)); + + try { + String line; + + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + return; + } +} + +std::vector DivEngine::instrumentFromFile(const char* path) { + std::vector ret; + 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; + } + } + + FILE* f=ps_fopen(path,"rb"); + if (f==NULL) { + lastError=strerror(errno); + return ret; + } + unsigned char* buf; + ssize_t len; + if (fseek(f,0,SEEK_END)!=0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + len=ftell(f); + if (len<0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + if (len==0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + if (fseek(f,0,SEEK_SET)!=0) { + lastError=strerror(errno); + fclose(f); + return ret; + } + buf=new unsigned char[len]; + if (fread(buf,1,len,f)!=(size_t)len) { + logW("did not read entire instrument file buffer!"); + lastError="did not read entire instrument file!"; + delete[] buf; + return ret; + } + fclose(f); + + SafeReader reader=SafeReader(buf,len); + + unsigned char magic[16]; + bool isFurnaceInstr=false; + try { + reader.read(magic,16); + if (memcmp("-Furnace instr.-",magic,16)==0) { + isFurnaceInstr=true; + } + } catch (EndOfFileException& e) { + reader.seek(0,SEEK_SET); + } + + if (isFurnaceInstr) { + DivInstrument* ins=new DivInstrument; + try { + short version=reader.readS(); + reader.readS(); // reserved + + if (version>DIV_ENGINE_VERSION) { + warnings="this instrument is made with a more recent version of Furnace!"; + } + + unsigned int dataPtr=reader.readI(); + reader.seek(dataPtr,SEEK_SET); + + if (ins->readInsData(reader,version)!=DIV_DATA_SUCCESS) { + lastError="invalid instrument header/data!"; + delete ins; + delete[] buf; + return ret; + } else { + ret.push_back(ins); + } + } catch (EndOfFileException& e) { + lastError="premature end of file"; + logE("premature end of file!"); + delete ins; + delete[] buf; + return ret; + } + } else { // read as a different format + const char* ext=strrchr(path,'.'); + DivInsFormats format=DIV_INSFORMAT_DMP; + if (ext!=NULL) { + String extS; + for (; *ext; ext++) { + char i=*ext; + if (i>='A' && i<='Z') { + i+='a'-'A'; + } + extS+=i; + } + if (extS==String(".dmp")) { + format=DIV_INSFORMAT_DMP; + } else if (extS==String(".tfi")) { + format=DIV_INSFORMAT_TFI; + } else if (extS==String(".vgi")) { + format=DIV_INSFORMAT_VGI; + } else if (extS==String(".fti")) { + format=DIV_INSFORMAT_FTI; + } else if (extS==String(".bti")) { + format=DIV_INSFORMAT_BTI; + } else if (extS==String(".s3i")) { + format=DIV_INSFORMAT_S3I; + } else if (extS==String(".sbi")) { + format=DIV_INSFORMAT_SBI; + } else if (extS==String(".opm")) { + format=DIV_INSFORMAT_OPM; + } + } + + switch (format) { + case DIV_INSFORMAT_DMP: { + loadDMP(reader,ret,stripPath); + break; + } + case DIV_INSFORMAT_TFI: + loadTFI(reader,ret,stripPath); + break; + case DIV_INSFORMAT_VGI: + loadVGI(reader,ret,stripPath); + break; + case DIV_INSFORMAT_FTI: // TODO + break; + case DIV_INSFORMAT_BTI: // TODO + break; + case DIV_INSFORMAT_OPM: // TODO + break; + case DIV_INSFORMAT_S3I: + loadS3I(reader,ret,stripPath); + break; + case DIV_INSFORMAT_SBI: + loadSBI(reader,ret,stripPath); + break; + } + + if (reader.tell() +#include "filter.h" +#include "../ta-log.h" + +float* DivFilterTables::cubicTable=NULL; +float* DivFilterTables::sincTable=NULL; +float* DivFilterTables::sincIntegralTable=NULL; + +// portions from Schism Tracker (scripts/lutgen.c) +// licensed under same license as this program. +float* DivFilterTables::getCubicTable() { + if (cubicTable==NULL) { + logD("initializing cubic spline table."); + cubicTable=new float[4096]; + + for (int i=0; i<1024; i++) { + float x=(float)i/1024.0; + cubicTable[(i<<2)]=-0.5*pow(x,3)+1.0*pow(x,2)-0.5*x; + cubicTable[1+(i<<2)]=1.5*pow(x,3)-2.5*pow(x,2)+1.0; + cubicTable[2+(i<<2)]=-1.5*pow(x,3)+2.0*pow(x,2)+0.5*x; + cubicTable[3+(i<<2)]=0.5*pow(x,3)-0.5*pow(x,2); + } + } + return cubicTable; +} + +float* DivFilterTables:: getSincTable() { + if (sincTable==NULL) { + logD("initializing sinc table."); + sincTable=new float[65536]; + + sincTable[0]=1.0f; + for (int i=1; i<65536; i++) { + int mapped=((i&8191)<<3)|(i>>13); + double x=(double)i*M_PI/8192.0; + sincTable[mapped]=sin(x)/x; + } + + for (int i=0; i<65536; i++) { + int mapped=((i&8191)<<3)|(i>>13); + sincTable[mapped]*=pow(cos(M_PI*(double)i/131072.0),2.0); + } + } + return sincTable; +} + +float* DivFilterTables::getSincIntegralTable() { + if (sincIntegralTable==NULL) { + logD("initializing sinc integral table."); + sincIntegralTable=new float[65536]; + + sincIntegralTable[0]=-0.5f; + for (int i=1; i<65536; i++) { + int mapped=((i&8191)<<3)|(i>>13); + int mappedPrev=(((i-1)&8191)<<3)|((i-1)>>13); + double x=(double)i*M_PI/8192.0; + double sinc=sin(x)/x; + sincIntegralTable[mapped]=sincIntegralTable[mappedPrev]+(sinc/8192.0); + } + + for (int i=0; i<65536; i++) { + int mapped=((i&8191)<<3)|(i>>13); + sincIntegralTable[mapped]*=pow(cos(M_PI*(double)i/131072.0),2.0); + } + } + return sincIntegralTable; +} diff --git a/src/engine/filter.h b/src/engine/filter.h new file mode 100644 index 000000000..974a448e3 --- /dev/null +++ b/src/engine/filter.h @@ -0,0 +1,43 @@ +/** + * 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. + */ + +class DivFilterTables { + public: + static float* cubicTable; + static float* sincTable; + static float* sincIntegralTable; + + /** + * get a 1024x4 cubic spline table. + * @return the table. + */ + static float* getCubicTable(); + + /** + * get a 8192x8 one-side sine-windowed sinc table. + * @return the table. + */ + static float* getSincTable(); + + /** + * get a 8192x8 one-side sine-windowed sinc integral table. + * @return the table. + */ + static float* getSincIntegralTable(); +}; \ No newline at end of file diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index fa5564c93..68896a45e 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -39,7 +39,7 @@ void DivInstrument::putInsData(SafeWriter* w) { w->writeC(fm.fb); w->writeC(fm.fms); w->writeC(fm.ams); - w->writeC(4); // operator count; always 4 + w->writeC(fm.ops); w->writeC(fm.opllPreset); w->writeC(0); // reserved w->writeC(0); @@ -111,204 +111,382 @@ void DivInstrument::putInsData(SafeWriter* w) { } // standard - w->writeI(std.volMacroLen); - w->writeI(std.arpMacroLen); - w->writeI(std.dutyMacroLen); - w->writeI(std.waveMacroLen); - w->writeI(std.pitchMacroLen); - w->writeI(std.ex1MacroLen); - w->writeI(std.ex2MacroLen); - w->writeI(std.ex3MacroLen); - w->writeI(std.volMacroLoop); - w->writeI(std.arpMacroLoop); - w->writeI(std.dutyMacroLoop); - w->writeI(std.waveMacroLoop); - w->writeI(std.pitchMacroLoop); - w->writeI(std.ex1MacroLoop); - w->writeI(std.ex2MacroLoop); - w->writeI(std.ex3MacroLoop); - w->writeC(std.arpMacroMode); + w->writeI(std.volMacro.len); + w->writeI(std.arpMacro.len); + w->writeI(std.dutyMacro.len); + w->writeI(std.waveMacro.len); + w->writeI(std.pitchMacro.len); + w->writeI(std.ex1Macro.len); + w->writeI(std.ex2Macro.len); + w->writeI(std.ex3Macro.len); + w->writeI(std.volMacro.loop); + w->writeI(std.arpMacro.loop); + w->writeI(std.dutyMacro.loop); + w->writeI(std.waveMacro.loop); + w->writeI(std.pitchMacro.loop); + w->writeI(std.ex1Macro.loop); + w->writeI(std.ex2Macro.loop); + w->writeI(std.ex3Macro.loop); + w->writeC(std.arpMacro.mode); w->writeC(0); // reserved w->writeC(0); w->writeC(0); - for (int j=0; jwriteI(std.volMacro[j]); + for (int j=0; jwriteI(std.volMacro.val[j]); } - for (int j=0; jwriteI(std.arpMacro[j]); + for (int j=0; jwriteI(std.arpMacro.val[j]); } - for (int j=0; jwriteI(std.dutyMacro[j]); + for (int j=0; jwriteI(std.dutyMacro.val[j]); } - for (int j=0; jwriteI(std.waveMacro[j]); + for (int j=0; jwriteI(std.waveMacro.val[j]); } - for (int j=0; jwriteI(std.pitchMacro[j]); + for (int j=0; jwriteI(std.pitchMacro.val[j]); } - for (int j=0; jwriteI(std.ex1Macro[j]); + for (int j=0; jwriteI(std.ex1Macro.val[j]); } - for (int j=0; jwriteI(std.ex2Macro[j]); + for (int j=0; jwriteI(std.ex2Macro.val[j]); } - for (int j=0; jwriteI(std.ex3Macro[j]); + for (int j=0; jwriteI(std.ex3Macro.val[j]); } // FM macros and open status - w->writeI(std.algMacroLen); - w->writeI(std.fbMacroLen); - w->writeI(std.fmsMacroLen); - w->writeI(std.amsMacroLen); - w->writeI(std.algMacroLoop); - w->writeI(std.fbMacroLoop); - w->writeI(std.fmsMacroLoop); - w->writeI(std.amsMacroLoop); + w->writeI(std.algMacro.len); + w->writeI(std.fbMacro.len); + w->writeI(std.fmsMacro.len); + w->writeI(std.amsMacro.len); + w->writeI(std.algMacro.loop); + w->writeI(std.fbMacro.loop); + w->writeI(std.fmsMacro.loop); + w->writeI(std.amsMacro.loop); - w->writeC(std.volMacroOpen); - w->writeC(std.arpMacroOpen); - w->writeC(std.dutyMacroOpen); - w->writeC(std.waveMacroOpen); - w->writeC(std.pitchMacroOpen); - w->writeC(std.ex1MacroOpen); - w->writeC(std.ex2MacroOpen); - w->writeC(std.ex3MacroOpen); - w->writeC(std.algMacroOpen); - w->writeC(std.fbMacroOpen); - w->writeC(std.fmsMacroOpen); - w->writeC(std.amsMacroOpen); + w->writeC(std.volMacro.open); + w->writeC(std.arpMacro.open); + w->writeC(std.dutyMacro.open); + w->writeC(std.waveMacro.open); + w->writeC(std.pitchMacro.open); + w->writeC(std.ex1Macro.open); + w->writeC(std.ex2Macro.open); + w->writeC(std.ex3Macro.open); + w->writeC(std.algMacro.open); + w->writeC(std.fbMacro.open); + w->writeC(std.fmsMacro.open); + w->writeC(std.amsMacro.open); - for (int j=0; jwriteI(std.algMacro[j]); + for (int j=0; jwriteI(std.algMacro.val[j]); } - for (int j=0; jwriteI(std.fbMacro[j]); + for (int j=0; jwriteI(std.fbMacro.val[j]); } - for (int j=0; jwriteI(std.fmsMacro[j]); + for (int j=0; jwriteI(std.fmsMacro.val[j]); } - for (int j=0; jwriteI(std.amsMacro[j]); + for (int j=0; jwriteI(std.amsMacro.val[j]); } for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - w->writeI(op.amMacroLen); - w->writeI(op.arMacroLen); - w->writeI(op.drMacroLen); - w->writeI(op.multMacroLen); - w->writeI(op.rrMacroLen); - w->writeI(op.slMacroLen); - w->writeI(op.tlMacroLen); - w->writeI(op.dt2MacroLen); - w->writeI(op.rsMacroLen); - w->writeI(op.dtMacroLen); - w->writeI(op.d2rMacroLen); - w->writeI(op.ssgMacroLen); - w->writeI(op.amMacroLoop); - w->writeI(op.arMacroLoop); - w->writeI(op.drMacroLoop); - w->writeI(op.multMacroLoop); - w->writeI(op.rrMacroLoop); - w->writeI(op.slMacroLoop); - w->writeI(op.tlMacroLoop); - w->writeI(op.dt2MacroLoop); - w->writeI(op.rsMacroLoop); - w->writeI(op.dtMacroLoop); - w->writeI(op.d2rMacroLoop); - w->writeI(op.ssgMacroLoop); - w->writeC(op.amMacroOpen); - w->writeC(op.arMacroOpen); - w->writeC(op.drMacroOpen); - w->writeC(op.multMacroOpen); - w->writeC(op.rrMacroOpen); - w->writeC(op.slMacroOpen); - w->writeC(op.tlMacroOpen); - w->writeC(op.dt2MacroOpen); - w->writeC(op.rsMacroOpen); - w->writeC(op.dtMacroOpen); - w->writeC(op.d2rMacroOpen); - w->writeC(op.ssgMacroOpen); + w->writeI(op.amMacro.len); + w->writeI(op.arMacro.len); + w->writeI(op.drMacro.len); + w->writeI(op.multMacro.len); + w->writeI(op.rrMacro.len); + w->writeI(op.slMacro.len); + w->writeI(op.tlMacro.len); + w->writeI(op.dt2Macro.len); + w->writeI(op.rsMacro.len); + w->writeI(op.dtMacro.len); + w->writeI(op.d2rMacro.len); + w->writeI(op.ssgMacro.len); + w->writeI(op.amMacro.loop); + w->writeI(op.arMacro.loop); + w->writeI(op.drMacro.loop); + w->writeI(op.multMacro.loop); + w->writeI(op.rrMacro.loop); + w->writeI(op.slMacro.loop); + w->writeI(op.tlMacro.loop); + w->writeI(op.dt2Macro.loop); + w->writeI(op.rsMacro.loop); + w->writeI(op.dtMacro.loop); + w->writeI(op.d2rMacro.loop); + w->writeI(op.ssgMacro.loop); + w->writeC(op.amMacro.open); + w->writeC(op.arMacro.open); + w->writeC(op.drMacro.open); + w->writeC(op.multMacro.open); + w->writeC(op.rrMacro.open); + w->writeC(op.slMacro.open); + w->writeC(op.tlMacro.open); + w->writeC(op.dt2Macro.open); + w->writeC(op.rsMacro.open); + w->writeC(op.dtMacro.open); + w->writeC(op.d2rMacro.open); + w->writeC(op.ssgMacro.open); } for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - for (int j=0; jwriteC(op.amMacro[j]); + for (int j=0; jwriteC(op.amMacro.val[j]); } - for (int j=0; jwriteC(op.arMacro[j]); + for (int j=0; jwriteC(op.arMacro.val[j]); } - for (int j=0; jwriteC(op.drMacro[j]); + for (int j=0; jwriteC(op.drMacro.val[j]); } - for (int j=0; jwriteC(op.multMacro[j]); + for (int j=0; jwriteC(op.multMacro.val[j]); } - for (int j=0; jwriteC(op.rrMacro[j]); + for (int j=0; jwriteC(op.rrMacro.val[j]); } - for (int j=0; jwriteC(op.slMacro[j]); + for (int j=0; jwriteC(op.slMacro.val[j]); } - for (int j=0; jwriteC(op.tlMacro[j]); + for (int j=0; jwriteC(op.tlMacro.val[j]); } - for (int j=0; jwriteC(op.dt2Macro[j]); + for (int j=0; jwriteC(op.dt2Macro.val[j]); } - for (int j=0; jwriteC(op.rsMacro[j]); + for (int j=0; jwriteC(op.rsMacro.val[j]); } - for (int j=0; jwriteC(op.dtMacro[j]); + for (int j=0; jwriteC(op.dtMacro.val[j]); } - for (int j=0; jwriteC(op.d2rMacro[j]); + for (int j=0; jwriteC(op.d2rMacro.val[j]); } - for (int j=0; jwriteC(op.ssgMacro[j]); + for (int j=0; jwriteC(op.ssgMacro.val[j]); } } // release points - w->writeI(std.volMacroRel); - w->writeI(std.arpMacroRel); - w->writeI(std.dutyMacroRel); - w->writeI(std.waveMacroRel); - w->writeI(std.pitchMacroRel); - w->writeI(std.ex1MacroRel); - w->writeI(std.ex2MacroRel); - w->writeI(std.ex3MacroRel); - w->writeI(std.algMacroRel); - w->writeI(std.fbMacroRel); - w->writeI(std.fmsMacroRel); - w->writeI(std.amsMacroRel); + w->writeI(std.volMacro.rel); + w->writeI(std.arpMacro.rel); + w->writeI(std.dutyMacro.rel); + w->writeI(std.waveMacro.rel); + w->writeI(std.pitchMacro.rel); + w->writeI(std.ex1Macro.rel); + w->writeI(std.ex2Macro.rel); + w->writeI(std.ex3Macro.rel); + w->writeI(std.algMacro.rel); + w->writeI(std.fbMacro.rel); + w->writeI(std.fmsMacro.rel); + w->writeI(std.amsMacro.rel); for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - w->writeI(op.amMacroRel); - w->writeI(op.arMacroRel); - w->writeI(op.drMacroRel); - w->writeI(op.multMacroRel); - w->writeI(op.rrMacroRel); - w->writeI(op.slMacroRel); - w->writeI(op.tlMacroRel); - w->writeI(op.dt2MacroRel); - w->writeI(op.rsMacroRel); - w->writeI(op.dtMacroRel); - w->writeI(op.d2rMacroRel); - w->writeI(op.ssgMacroRel); + w->writeI(op.amMacro.rel); + w->writeI(op.arMacro.rel); + w->writeI(op.drMacro.rel); + w->writeI(op.multMacro.rel); + w->writeI(op.rrMacro.rel); + w->writeI(op.slMacro.rel); + w->writeI(op.tlMacro.rel); + w->writeI(op.dt2Macro.rel); + w->writeI(op.rsMacro.rel); + w->writeI(op.dtMacro.rel); + w->writeI(op.d2rMacro.rel); + w->writeI(op.ssgMacro.rel); } + + // extended op macros + for (int i=0; i<4; i++) { + DivInstrumentSTD::OpMacro& op=std.opMacros[i]; + + w->writeI(op.damMacro.len); + w->writeI(op.dvbMacro.len); + w->writeI(op.egtMacro.len); + w->writeI(op.kslMacro.len); + w->writeI(op.susMacro.len); + w->writeI(op.vibMacro.len); + w->writeI(op.wsMacro.len); + w->writeI(op.ksrMacro.len); + + w->writeI(op.damMacro.loop); + w->writeI(op.dvbMacro.loop); + w->writeI(op.egtMacro.loop); + w->writeI(op.kslMacro.loop); + w->writeI(op.susMacro.loop); + w->writeI(op.vibMacro.loop); + w->writeI(op.wsMacro.loop); + w->writeI(op.ksrMacro.loop); + + w->writeI(op.damMacro.rel); + w->writeI(op.dvbMacro.rel); + w->writeI(op.egtMacro.rel); + w->writeI(op.kslMacro.rel); + w->writeI(op.susMacro.rel); + w->writeI(op.vibMacro.rel); + w->writeI(op.wsMacro.rel); + w->writeI(op.ksrMacro.rel); + + w->writeC(op.damMacro.open); + w->writeC(op.dvbMacro.open); + w->writeC(op.egtMacro.open); + w->writeC(op.kslMacro.open); + w->writeC(op.susMacro.open); + w->writeC(op.vibMacro.open); + w->writeC(op.wsMacro.open); + w->writeC(op.ksrMacro.open); + } + + for (int i=0; i<4; i++) { + DivInstrumentSTD::OpMacro& op=std.opMacros[i]; + for (int j=0; jwriteC(op.damMacro.val[j]); + } + for (int j=0; jwriteC(op.dvbMacro.val[j]); + } + for (int j=0; jwriteC(op.egtMacro.val[j]); + } + for (int j=0; jwriteC(op.kslMacro.val[j]); + } + for (int j=0; jwriteC(op.susMacro.val[j]); + } + for (int j=0; jwriteC(op.vibMacro.val[j]); + } + for (int j=0; jwriteC(op.wsMacro.val[j]); + } + for (int j=0; jwriteC(op.ksrMacro.val[j]); + } + } + + // OPL drum data + w->writeC(fm.fixedDrums); + w->writeC(0); // reserved + w->writeS(fm.kickFreq); + w->writeS(fm.snareHatFreq); + w->writeS(fm.tomTopFreq); + + // sample map + w->writeC(amiga.useNoteMap); + if (amiga.useNoteMap) { + w->write(amiga.noteFreq,120*sizeof(unsigned int)); + w->write(amiga.noteMap,120*sizeof(short)); + } + + // N163 + w->writeI(n163.wave); + w->writeC(n163.wavePos); + w->writeC(n163.waveLen); + w->writeC(n163.waveMode); + w->writeC(0); // reserved + + // more macros + w->writeI(std.panLMacro.len); + w->writeI(std.panRMacro.len); + w->writeI(std.phaseResetMacro.len); + w->writeI(std.ex4Macro.len); + w->writeI(std.ex5Macro.len); + w->writeI(std.ex6Macro.len); + w->writeI(std.ex7Macro.len); + w->writeI(std.ex8Macro.len); + + w->writeI(std.panLMacro.loop); + w->writeI(std.panRMacro.loop); + w->writeI(std.phaseResetMacro.loop); + w->writeI(std.ex4Macro.loop); + w->writeI(std.ex5Macro.loop); + w->writeI(std.ex6Macro.loop); + w->writeI(std.ex7Macro.loop); + w->writeI(std.ex8Macro.loop); + + w->writeI(std.panLMacro.rel); + w->writeI(std.panRMacro.rel); + w->writeI(std.phaseResetMacro.rel); + w->writeI(std.ex4Macro.rel); + w->writeI(std.ex5Macro.rel); + w->writeI(std.ex6Macro.rel); + w->writeI(std.ex7Macro.rel); + w->writeI(std.ex8Macro.rel); + + w->writeC(std.panLMacro.open); + w->writeC(std.panRMacro.open); + w->writeC(std.phaseResetMacro.open); + w->writeC(std.ex4Macro.open); + w->writeC(std.ex5Macro.open); + w->writeC(std.ex6Macro.open); + w->writeC(std.ex7Macro.open); + w->writeC(std.ex8Macro.open); + + for (int j=0; jwriteI(std.panLMacro.val[j]); + } + for (int j=0; jwriteI(std.panRMacro.val[j]); + } + for (int j=0; jwriteI(std.phaseResetMacro.val[j]); + } + for (int j=0; jwriteI(std.ex4Macro.val[j]); + } + for (int j=0; jwriteI(std.ex5Macro.val[j]); + } + for (int j=0; jwriteI(std.ex6Macro.val[j]); + } + for (int j=0; jwriteI(std.ex7Macro.val[j]); + } + for (int j=0; jwriteI(std.ex8Macro.val[j]); + } + + // FDS + w->writeI(fds.modSpeed); + w->writeI(fds.modDepth); + w->writeC(fds.initModTableWithFirstWave); + w->writeC(0); // reserved + w->writeC(0); + w->writeC(0); + w->write(fds.modTable,32); + + // OPZ + w->writeC(fm.fms2); + w->writeC(fm.ams2); + + // wave synth + w->writeI(ws.wave1); + w->writeI(ws.wave2); + w->writeC(ws.rateDivider); + w->writeC(ws.effect); + w->writeC(ws.enabled); + w->writeC(ws.global); + w->writeC(ws.speed); + w->writeC(ws.param1); + w->writeC(ws.param2); + w->writeC(ws.param3); + w->writeC(ws.param4); } DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { char magic[4]; reader.read(magic,4); if (memcmp(magic,"INST",4)!=0) { - logE("invalid instrument header!\n"); + logE("invalid instrument header!"); return DIV_DATA_INVALID_HEADER; } reader.readI(); @@ -397,53 +575,51 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { for (int k=0; k<14; k++) reader.readC(); // standard - std.volMacroLen=reader.readI(); - std.arpMacroLen=reader.readI(); - std.dutyMacroLen=reader.readI(); - std.waveMacroLen=reader.readI(); + std.volMacro.len=reader.readI(); + std.arpMacro.len=reader.readI(); + std.dutyMacro.len=reader.readI(); + std.waveMacro.len=reader.readI(); if (version>=17) { - std.pitchMacroLen=reader.readI(); - std.ex1MacroLen=reader.readI(); - std.ex2MacroLen=reader.readI(); - std.ex3MacroLen=reader.readI(); + std.pitchMacro.len=reader.readI(); + std.ex1Macro.len=reader.readI(); + std.ex2Macro.len=reader.readI(); + std.ex3Macro.len=reader.readI(); } - std.volMacroLoop=reader.readI(); - std.arpMacroLoop=reader.readI(); - std.dutyMacroLoop=reader.readI(); - std.waveMacroLoop=reader.readI(); + std.volMacro.loop=reader.readI(); + std.arpMacro.loop=reader.readI(); + std.dutyMacro.loop=reader.readI(); + std.waveMacro.loop=reader.readI(); if (version>=17) { - std.pitchMacroLoop=reader.readI(); - std.ex1MacroLoop=reader.readI(); - std.ex2MacroLoop=reader.readI(); - std.ex3MacroLoop=reader.readI(); + std.pitchMacro.loop=reader.readI(); + std.ex1Macro.loop=reader.readI(); + std.ex2Macro.loop=reader.readI(); + std.ex3Macro.loop=reader.readI(); } - std.arpMacroMode=reader.readC(); - std.volMacroHeight=reader.readC(); - std.dutyMacroHeight=reader.readC(); - std.waveMacroHeight=reader.readC(); - if (std.volMacroHeight==0) std.volMacroHeight=15; - if (std.dutyMacroHeight==0) std.dutyMacroHeight=3; - if (std.waveMacroHeight==0) std.waveMacroHeight=63; - reader.read(std.volMacro,4*std.volMacroLen); - reader.read(std.arpMacro,4*std.arpMacroLen); - reader.read(std.dutyMacro,4*std.dutyMacroLen); - reader.read(std.waveMacro,4*std.waveMacroLen); + std.arpMacro.mode=reader.readC(); + // these 3 were macro heights before but they are not used anymore + int oldVolHeight=reader.readC(); + int oldDutyHeight=reader.readC(); + reader.readC(); // oldWaveHeight + reader.read(std.volMacro.val,4*std.volMacro.len); + reader.read(std.arpMacro.val,4*std.arpMacro.len); + reader.read(std.dutyMacro.val,4*std.dutyMacro.len); + reader.read(std.waveMacro.val,4*std.waveMacro.len); if (version<31) { - if (!std.arpMacroMode) for (int j=0; j=17) { - reader.read(std.pitchMacro,4*std.pitchMacroLen); - reader.read(std.ex1Macro,4*std.ex1MacroLen); - reader.read(std.ex2Macro,4*std.ex2MacroLen); - reader.read(std.ex3Macro,4*std.ex3MacroLen); + reader.read(std.pitchMacro.val,4*std.pitchMacro.len); + reader.read(std.ex1Macro.val,4*std.ex1Macro.len); + reader.read(std.ex2Macro.val,4*std.ex2Macro.len); + reader.read(std.ex3Macro.val,4*std.ex3Macro.len); } else { if (type==DIV_INS_STD) { - if (std.volMacroHeight==31) { + if (oldVolHeight==31) { type=DIV_INS_PCE; } - if (std.dutyMacroHeight==31) { + if (oldDutyHeight==31) { type=DIV_INS_AY; } } @@ -451,125 +627,300 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { // FM macros if (version>=29) { - std.algMacroLen=reader.readI(); - std.fbMacroLen=reader.readI(); - std.fmsMacroLen=reader.readI(); - std.amsMacroLen=reader.readI(); - std.algMacroLoop=reader.readI(); - std.fbMacroLoop=reader.readI(); - std.fmsMacroLoop=reader.readI(); - std.amsMacroLoop=reader.readI(); - std.volMacroOpen=reader.readC(); - std.arpMacroOpen=reader.readC(); - std.dutyMacroOpen=reader.readC(); - std.waveMacroOpen=reader.readC(); - std.pitchMacroOpen=reader.readC(); - std.ex1MacroOpen=reader.readC(); - std.ex2MacroOpen=reader.readC(); - std.ex3MacroOpen=reader.readC(); - std.algMacroOpen=reader.readC(); - std.fbMacroOpen=reader.readC(); - std.fmsMacroOpen=reader.readC(); - std.amsMacroOpen=reader.readC(); + std.algMacro.len=reader.readI(); + std.fbMacro.len=reader.readI(); + std.fmsMacro.len=reader.readI(); + std.amsMacro.len=reader.readI(); + std.algMacro.loop=reader.readI(); + std.fbMacro.loop=reader.readI(); + std.fmsMacro.loop=reader.readI(); + std.amsMacro.loop=reader.readI(); + std.volMacro.open=reader.readC(); + std.arpMacro.open=reader.readC(); + std.dutyMacro.open=reader.readC(); + std.waveMacro.open=reader.readC(); + std.pitchMacro.open=reader.readC(); + std.ex1Macro.open=reader.readC(); + std.ex2Macro.open=reader.readC(); + std.ex3Macro.open=reader.readC(); + std.algMacro.open=reader.readC(); + std.fbMacro.open=reader.readC(); + std.fmsMacro.open=reader.readC(); + std.amsMacro.open=reader.readC(); - reader.read(std.algMacro,4*std.algMacroLen); - reader.read(std.fbMacro,4*std.fbMacroLen); - reader.read(std.fmsMacro,4*std.fmsMacroLen); - reader.read(std.amsMacro,4*std.amsMacroLen); + reader.read(std.algMacro.val,4*std.algMacro.len); + reader.read(std.fbMacro.val,4*std.fbMacro.len); + reader.read(std.fmsMacro.val,4*std.fmsMacro.len); + reader.read(std.amsMacro.val,4*std.amsMacro.len); for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - op.amMacroLen=reader.readI(); - op.arMacroLen=reader.readI(); - op.drMacroLen=reader.readI(); - op.multMacroLen=reader.readI(); - op.rrMacroLen=reader.readI(); - op.slMacroLen=reader.readI(); - op.tlMacroLen=reader.readI(); - op.dt2MacroLen=reader.readI(); - op.rsMacroLen=reader.readI(); - op.dtMacroLen=reader.readI(); - op.d2rMacroLen=reader.readI(); - op.ssgMacroLen=reader.readI(); + op.amMacro.len=reader.readI(); + op.arMacro.len=reader.readI(); + op.drMacro.len=reader.readI(); + op.multMacro.len=reader.readI(); + op.rrMacro.len=reader.readI(); + op.slMacro.len=reader.readI(); + op.tlMacro.len=reader.readI(); + op.dt2Macro.len=reader.readI(); + op.rsMacro.len=reader.readI(); + op.dtMacro.len=reader.readI(); + op.d2rMacro.len=reader.readI(); + op.ssgMacro.len=reader.readI(); - op.amMacroLoop=reader.readI(); - op.arMacroLoop=reader.readI(); - op.drMacroLoop=reader.readI(); - op.multMacroLoop=reader.readI(); - op.rrMacroLoop=reader.readI(); - op.slMacroLoop=reader.readI(); - op.tlMacroLoop=reader.readI(); - op.dt2MacroLoop=reader.readI(); - op.rsMacroLoop=reader.readI(); - op.dtMacroLoop=reader.readI(); - op.d2rMacroLoop=reader.readI(); - op.ssgMacroLoop=reader.readI(); + op.amMacro.loop=reader.readI(); + op.arMacro.loop=reader.readI(); + op.drMacro.loop=reader.readI(); + op.multMacro.loop=reader.readI(); + op.rrMacro.loop=reader.readI(); + op.slMacro.loop=reader.readI(); + op.tlMacro.loop=reader.readI(); + op.dt2Macro.loop=reader.readI(); + op.rsMacro.loop=reader.readI(); + op.dtMacro.loop=reader.readI(); + op.d2rMacro.loop=reader.readI(); + op.ssgMacro.loop=reader.readI(); - op.amMacroOpen=reader.readC(); - op.arMacroOpen=reader.readC(); - op.drMacroOpen=reader.readC(); - op.multMacroOpen=reader.readC(); - op.rrMacroOpen=reader.readC(); - op.slMacroOpen=reader.readC(); - op.tlMacroOpen=reader.readC(); - op.dt2MacroOpen=reader.readC(); - op.rsMacroOpen=reader.readC(); - op.dtMacroOpen=reader.readC(); - op.d2rMacroOpen=reader.readC(); - op.ssgMacroOpen=reader.readC(); + op.amMacro.open=reader.readC(); + op.arMacro.open=reader.readC(); + op.drMacro.open=reader.readC(); + op.multMacro.open=reader.readC(); + op.rrMacro.open=reader.readC(); + op.slMacro.open=reader.readC(); + op.tlMacro.open=reader.readC(); + op.dt2Macro.open=reader.readC(); + op.rsMacro.open=reader.readC(); + op.dtMacro.open=reader.readC(); + op.d2rMacro.open=reader.readC(); + op.ssgMacro.open=reader.readC(); } for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - reader.read(op.amMacro,op.amMacroLen); - reader.read(op.arMacro,op.arMacroLen); - reader.read(op.drMacro,op.drMacroLen); - reader.read(op.multMacro,op.multMacroLen); - reader.read(op.rrMacro,op.rrMacroLen); - reader.read(op.slMacro,op.slMacroLen); - reader.read(op.tlMacro,op.tlMacroLen); - reader.read(op.dt2Macro,op.dt2MacroLen); - reader.read(op.rsMacro,op.rsMacroLen); - reader.read(op.dtMacro,op.dtMacroLen); - reader.read(op.d2rMacro,op.d2rMacroLen); - reader.read(op.ssgMacro,op.ssgMacroLen); + reader.read(op.amMacro.val,op.amMacro.len); + reader.read(op.arMacro.val,op.arMacro.len); + reader.read(op.drMacro.val,op.drMacro.len); + reader.read(op.multMacro.val,op.multMacro.len); + reader.read(op.rrMacro.val,op.rrMacro.len); + reader.read(op.slMacro.val,op.slMacro.len); + reader.read(op.tlMacro.val,op.tlMacro.len); + reader.read(op.dt2Macro.val,op.dt2Macro.len); + reader.read(op.rsMacro.val,op.rsMacro.len); + reader.read(op.dtMacro.val,op.dtMacro.len); + reader.read(op.d2rMacro.val,op.d2rMacro.len); + reader.read(op.ssgMacro.val,op.ssgMacro.len); } } // release points if (version>=44) { - std.volMacroRel=reader.readI(); - std.arpMacroRel=reader.readI(); - std.dutyMacroRel=reader.readI(); - std.waveMacroRel=reader.readI(); - std.pitchMacroRel=reader.readI(); - std.ex1MacroRel=reader.readI(); - std.ex2MacroRel=reader.readI(); - std.ex3MacroRel=reader.readI(); - std.algMacroRel=reader.readI(); - std.fbMacroRel=reader.readI(); - std.fmsMacroRel=reader.readI(); - std.amsMacroRel=reader.readI(); + std.volMacro.rel=reader.readI(); + std.arpMacro.rel=reader.readI(); + std.dutyMacro.rel=reader.readI(); + std.waveMacro.rel=reader.readI(); + std.pitchMacro.rel=reader.readI(); + std.ex1Macro.rel=reader.readI(); + std.ex2Macro.rel=reader.readI(); + std.ex3Macro.rel=reader.readI(); + std.algMacro.rel=reader.readI(); + std.fbMacro.rel=reader.readI(); + std.fmsMacro.rel=reader.readI(); + std.amsMacro.rel=reader.readI(); for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& op=std.opMacros[i]; - op.amMacroRel=reader.readI(); - op.arMacroRel=reader.readI(); - op.drMacroRel=reader.readI(); - op.multMacroRel=reader.readI(); - op.rrMacroRel=reader.readI(); - op.slMacroRel=reader.readI(); - op.tlMacroRel=reader.readI(); - op.dt2MacroRel=reader.readI(); - op.rsMacroRel=reader.readI(); - op.dtMacroRel=reader.readI(); - op.d2rMacroRel=reader.readI(); - op.ssgMacroRel=reader.readI(); + op.amMacro.rel=reader.readI(); + op.arMacro.rel=reader.readI(); + op.drMacro.rel=reader.readI(); + op.multMacro.rel=reader.readI(); + op.rrMacro.rel=reader.readI(); + op.slMacro.rel=reader.readI(); + op.tlMacro.rel=reader.readI(); + op.dt2Macro.rel=reader.readI(); + op.rsMacro.rel=reader.readI(); + op.dtMacro.rel=reader.readI(); + op.d2rMacro.rel=reader.readI(); + op.ssgMacro.rel=reader.readI(); } } + // extended op macros + if (version>=61) { + for (int i=0; i<4; i++) { + DivInstrumentSTD::OpMacro& op=std.opMacros[i]; + + op.damMacro.len=reader.readI(); + op.dvbMacro.len=reader.readI(); + op.egtMacro.len=reader.readI(); + op.kslMacro.len=reader.readI(); + op.susMacro.len=reader.readI(); + op.vibMacro.len=reader.readI(); + op.wsMacro.len=reader.readI(); + op.ksrMacro.len=reader.readI(); + + op.damMacro.loop=reader.readI(); + op.dvbMacro.loop=reader.readI(); + op.egtMacro.loop=reader.readI(); + op.kslMacro.loop=reader.readI(); + op.susMacro.loop=reader.readI(); + op.vibMacro.loop=reader.readI(); + op.wsMacro.loop=reader.readI(); + op.ksrMacro.loop=reader.readI(); + + op.damMacro.rel=reader.readI(); + op.dvbMacro.rel=reader.readI(); + op.egtMacro.rel=reader.readI(); + op.kslMacro.rel=reader.readI(); + op.susMacro.rel=reader.readI(); + op.vibMacro.rel=reader.readI(); + op.wsMacro.rel=reader.readI(); + op.ksrMacro.rel=reader.readI(); + + op.damMacro.open=reader.readC(); + op.dvbMacro.open=reader.readC(); + op.egtMacro.open=reader.readC(); + op.kslMacro.open=reader.readC(); + op.susMacro.open=reader.readC(); + op.vibMacro.open=reader.readC(); + op.wsMacro.open=reader.readC(); + op.ksrMacro.open=reader.readC(); + } + + for (int i=0; i<4; i++) { + DivInstrumentSTD::OpMacro& op=std.opMacros[i]; + reader.read(op.damMacro.val,op.damMacro.len); + reader.read(op.dvbMacro.val,op.dvbMacro.len); + reader.read(op.egtMacro.val,op.egtMacro.len); + reader.read(op.kslMacro.val,op.kslMacro.len); + reader.read(op.susMacro.val,op.susMacro.len); + reader.read(op.vibMacro.val,op.vibMacro.len); + reader.read(op.wsMacro.val,op.wsMacro.len); + reader.read(op.ksrMacro.val,op.ksrMacro.len); + } + } + + // OPL drum data + if (version>=63) { + fm.fixedDrums=reader.readC(); + reader.readC(); // reserved + fm.kickFreq=reader.readS(); + fm.snareHatFreq=reader.readS(); + fm.tomTopFreq=reader.readS(); + } + + // clear noise macro if PCE instrument and version<63 + if (version<63 && type==DIV_INS_PCE) { + std.dutyMacro.len=0; + std.dutyMacro.loop=-1; + std.dutyMacro.rel=-1; + } + + // clear wave macro if OPLL instrument and version<70 + if (version<70 && type==DIV_INS_OPLL) { + std.waveMacro.len=0; + std.waveMacro.loop=-1; + std.waveMacro.rel=-1; + } + + // sample map + 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)); + } + } + + // N163 + if (version>=73) { + n163.wave=reader.readI(); + n163.wavePos=(unsigned char)reader.readC(); + n163.waveLen=(unsigned char)reader.readC(); + n163.waveMode=(unsigned char)reader.readC(); + reader.readC(); // reserved + } + + // more macros + if (version>=76) { + std.panLMacro.len=reader.readI(); + std.panRMacro.len=reader.readI(); + std.phaseResetMacro.len=reader.readI(); + std.ex4Macro.len=reader.readI(); + std.ex5Macro.len=reader.readI(); + std.ex6Macro.len=reader.readI(); + std.ex7Macro.len=reader.readI(); + std.ex8Macro.len=reader.readI(); + + std.panLMacro.loop=reader.readI(); + std.panRMacro.loop=reader.readI(); + std.phaseResetMacro.loop=reader.readI(); + std.ex4Macro.loop=reader.readI(); + std.ex5Macro.loop=reader.readI(); + std.ex6Macro.loop=reader.readI(); + std.ex7Macro.loop=reader.readI(); + std.ex8Macro.loop=reader.readI(); + + std.panLMacro.rel=reader.readI(); + std.panRMacro.rel=reader.readI(); + std.phaseResetMacro.rel=reader.readI(); + std.ex4Macro.rel=reader.readI(); + std.ex5Macro.rel=reader.readI(); + std.ex6Macro.rel=reader.readI(); + std.ex7Macro.rel=reader.readI(); + std.ex8Macro.rel=reader.readI(); + + std.panLMacro.open=reader.readC(); + std.panRMacro.open=reader.readC(); + std.phaseResetMacro.open=reader.readC(); + std.ex4Macro.open=reader.readC(); + std.ex5Macro.open=reader.readC(); + std.ex6Macro.open=reader.readC(); + std.ex7Macro.open=reader.readC(); + std.ex8Macro.open=reader.readC(); + + reader.read(std.panLMacro.val,4*std.panLMacro.len); + reader.read(std.panRMacro.val,4*std.panRMacro.len); + reader.read(std.phaseResetMacro.val,4*std.phaseResetMacro.len); + reader.read(std.ex4Macro.val,4*std.ex4Macro.len); + reader.read(std.ex5Macro.val,4*std.ex5Macro.len); + reader.read(std.ex6Macro.val,4*std.ex6Macro.len); + reader.read(std.ex7Macro.val,4*std.ex7Macro.len); + reader.read(std.ex8Macro.val,4*std.ex8Macro.len); + } + + // FDS + if (version>=76) { + fds.modSpeed=reader.readI(); + fds.modDepth=reader.readI(); + fds.initModTableWithFirstWave=reader.readC(); + reader.readC(); // reserved + reader.readC(); + reader.readC(); + reader.read(fds.modTable,32); + } + + // OPZ + if (version>=77) { + fm.fms2=reader.readC(); + fm.ams2=reader.readC(); + } + + // wave synth + if (version>=79) { + ws.wave1=reader.readI(); + ws.wave2=reader.readI(); + ws.rateDivider=reader.readC(); + ws.effect=reader.readC(); + ws.enabled=reader.readC(); + ws.global=reader.readC(); + ws.speed=reader.readC(); + ws.param1=reader.readC(); + ws.param2=reader.readC(); + ws.param3=reader.readC(); + ws.param4=reader.readC(); + } return DIV_DATA_SUCCESS; } @@ -598,12 +949,12 @@ bool DivInstrument::save(const char* path) { FILE* outFile=ps_fopen(path,"wb"); if (outFile==NULL) { - logE("could not save instrument: %s!\n",strerror(errno)); + 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!\n"); + logW("did not write entire instrument!"); } fclose(outFile); w->finish(); diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 366bd87d6..605241c71 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -23,7 +23,10 @@ #include "dataErrors.h" #include "../ta-utils.h" -enum DivInstrumentType { +// NOTICE! +// before adding new instrument types to this struct, please ask me first. +// absolutely zero support granted to conflicting formats. +enum DivInstrumentType: unsigned short { DIV_INS_STD=0, DIV_INS_FM=1, DIV_INS_GB=2, @@ -48,14 +51,37 @@ enum DivInstrumentType { DIV_INS_BEEPER=21, DIV_INS_SWAN=22, DIV_INS_MIKEY=23, + DIV_INS_VERA=24, + DIV_INS_X1_010=25, + DIV_INS_VRC6_SAW=26, + DIV_INS_MAX, }; +// FM operator structure: +// - OPN: +// - AM, AR, DR, MULT, RR, SL, TL, RS, DT, D2R, SSG-EG +// - OPM: +// - AM, AR, DR, MULT, RR, SL, TL, DT2, RS, DT, D2R +// - OPLL: +// - AM, AR, DR, MULT, RR, SL, TL, SSG-EG&8 = EG-S +// - KSL, VIB, KSR +// - OPL: +// - AM, AR, DR, MULT, RR, SL, TL, SSG-EG&8 = EG-S +// - KSL, VIB, WS (OPL2/3), KSR +// - OPZ: +// - AM, AR, DR, MULT (CRS), RR, SL, TL, DT2, RS, DT, D2R +// - WS, DVB = MULT (FINE), DAM = REV, KSL = EGShift, EGT = Fixed + struct DivInstrumentFM { - unsigned char alg, fb, fms, ams, ops, opllPreset; + unsigned char alg, fb, fms, ams, fms2, ams2, ops, opllPreset; + bool fixedDrums; + unsigned short kickFreq, snareHatFreq, tomTopFreq; struct Operator { + bool enable; unsigned char am, ar, dr, mult, rr, sl, tl, dt2, rs, dt, d2r, ssgEnv; - unsigned char dam, dvb, egt, ksl, sus, vib, ws, ksr; // YMU759/OPL + unsigned char dam, dvb, egt, ksl, sus, vib, ws, ksr; // YMU759/OPL/OPZ Operator(): + enable(true), am(0), ar(0), dr(0), @@ -82,8 +108,14 @@ struct DivInstrumentFM { fb(0), fms(0), ams(0), - ops(4), - opllPreset(0) { + fms2(0), + ams2(0), + ops(2), + opllPreset(0), + fixedDrums(false), + kickFreq(0x520), + snareHatFreq(0x550), + tomTopFreq(0x1c0) { // default instrument fb=4; op[0].tl=42; @@ -120,152 +152,98 @@ struct DivInstrumentFM { } }; +// this is getting out of hand +struct DivInstrumentMacro { + String name; + int val[256]; + unsigned int mode; + bool open; + unsigned char len; + signed char loop; + signed char rel; + DivInstrumentMacro(String n, bool initOpen=false): + name(n), + mode(0), + open(initOpen), + len(0), + loop(-1), + rel(-1) { + memset(val,0,256*sizeof(int)); + } +}; + struct DivInstrumentSTD { - int volMacro[256]; - int arpMacro[256]; - int dutyMacro[256]; - int waveMacro[256]; - int pitchMacro[256]; - int ex1Macro[256]; - int ex2Macro[256]; - int ex3Macro[256]; - int algMacro[256]; - int fbMacro[256]; - int fmsMacro[256]; - int amsMacro[256]; - bool arpMacroMode; - unsigned char volMacroHeight, dutyMacroHeight, waveMacroHeight; - bool volMacroOpen, arpMacroOpen, dutyMacroOpen, waveMacroOpen; - bool pitchMacroOpen, ex1MacroOpen, ex2MacroOpen, ex3MacroOpen; - bool algMacroOpen, fbMacroOpen, fmsMacroOpen, amsMacroOpen; - unsigned char volMacroLen, arpMacroLen, dutyMacroLen, waveMacroLen; - unsigned char pitchMacroLen, ex1MacroLen, ex2MacroLen, ex3MacroLen; - unsigned char algMacroLen, fbMacroLen, fmsMacroLen, amsMacroLen; - signed char volMacroLoop, arpMacroLoop, dutyMacroLoop, waveMacroLoop; - signed char pitchMacroLoop, ex1MacroLoop, ex2MacroLoop, ex3MacroLoop; - signed char algMacroLoop, fbMacroLoop, fmsMacroLoop, amsMacroLoop; - signed char volMacroRel, arpMacroRel, dutyMacroRel, waveMacroRel; - signed char pitchMacroRel, ex1MacroRel, ex2MacroRel, ex3MacroRel; - signed char algMacroRel, fbMacroRel, fmsMacroRel, amsMacroRel; + DivInstrumentMacro volMacro; + DivInstrumentMacro arpMacro; + DivInstrumentMacro dutyMacro; + DivInstrumentMacro waveMacro; + DivInstrumentMacro pitchMacro; + DivInstrumentMacro ex1Macro; + DivInstrumentMacro ex2Macro; + DivInstrumentMacro ex3Macro; + DivInstrumentMacro algMacro; + DivInstrumentMacro fbMacro; + DivInstrumentMacro fmsMacro; + DivInstrumentMacro amsMacro; + DivInstrumentMacro panLMacro; + DivInstrumentMacro panRMacro; + DivInstrumentMacro phaseResetMacro; + DivInstrumentMacro ex4Macro; + DivInstrumentMacro ex5Macro; + DivInstrumentMacro ex6Macro; + DivInstrumentMacro ex7Macro; + DivInstrumentMacro ex8Macro; + struct OpMacro { // ar, dr, mult, rr, sl, tl, dt2, rs, dt, d2r, ssgEnv; - unsigned char amMacro[256]; - unsigned char arMacro[256]; - unsigned char drMacro[256]; - unsigned char multMacro[256]; - unsigned char rrMacro[256]; - unsigned char slMacro[256]; - unsigned char tlMacro[256]; - unsigned char dt2Macro[256]; - unsigned char rsMacro[256]; - unsigned char dtMacro[256]; - unsigned char d2rMacro[256]; - unsigned char ssgMacro[256]; - bool amMacroOpen, arMacroOpen, drMacroOpen, multMacroOpen; - bool rrMacroOpen, slMacroOpen, tlMacroOpen, dt2MacroOpen; - bool rsMacroOpen, dtMacroOpen, d2rMacroOpen, ssgMacroOpen; - unsigned char amMacroLen, arMacroLen, drMacroLen, multMacroLen; - unsigned char rrMacroLen, slMacroLen, tlMacroLen, dt2MacroLen; - unsigned char rsMacroLen, dtMacroLen, d2rMacroLen, ssgMacroLen; - signed char amMacroLoop, arMacroLoop, drMacroLoop, multMacroLoop; - signed char rrMacroLoop, slMacroLoop, tlMacroLoop, dt2MacroLoop; - signed char rsMacroLoop, dtMacroLoop, d2rMacroLoop, ssgMacroLoop; - signed char amMacroRel, arMacroRel, drMacroRel, multMacroRel; - signed char rrMacroRel, slMacroRel, tlMacroRel, dt2MacroRel; - signed char rsMacroRel, dtMacroRel, d2rMacroRel, ssgMacroRel; + DivInstrumentMacro amMacro; + DivInstrumentMacro arMacro; + DivInstrumentMacro drMacro; + DivInstrumentMacro multMacro; + DivInstrumentMacro rrMacro; + DivInstrumentMacro slMacro; + DivInstrumentMacro tlMacro; + DivInstrumentMacro dt2Macro; + DivInstrumentMacro rsMacro; + DivInstrumentMacro dtMacro; + DivInstrumentMacro d2rMacro; + DivInstrumentMacro ssgMacro; + DivInstrumentMacro damMacro; + DivInstrumentMacro dvbMacro; + DivInstrumentMacro egtMacro; + DivInstrumentMacro kslMacro; + DivInstrumentMacro susMacro; + DivInstrumentMacro vibMacro; + DivInstrumentMacro wsMacro; + DivInstrumentMacro ksrMacro; OpMacro(): - amMacroOpen(false), arMacroOpen(false), drMacroOpen(false), multMacroOpen(false), - rrMacroOpen(false), slMacroOpen(false), tlMacroOpen(true), dt2MacroOpen(false), - rsMacroOpen(false), dtMacroOpen(false), d2rMacroOpen(false), ssgMacroOpen(false), - amMacroLen(0), arMacroLen(0), drMacroLen(0), multMacroLen(0), - rrMacroLen(0), slMacroLen(0), tlMacroLen(0), dt2MacroLen(0), - rsMacroLen(0), dtMacroLen(0), d2rMacroLen(0), ssgMacroLen(0), - amMacroLoop(-1), arMacroLoop(-1), drMacroLoop(-1), multMacroLoop(-1), - rrMacroLoop(-1), slMacroLoop(-1), tlMacroLoop(-1), dt2MacroLoop(-1), - rsMacroLoop(-1), dtMacroLoop(-1), d2rMacroLoop(-1), ssgMacroLoop(-1), - amMacroRel(-1), arMacroRel(-1), drMacroRel(-1), multMacroRel(-1), - rrMacroRel(-1), slMacroRel(-1), tlMacroRel(-1), dt2MacroRel(-1), - rsMacroRel(-1), dtMacroRel(-1), d2rMacroRel(-1), ssgMacroRel(-1) { - memset(amMacro,0,256); - memset(arMacro,0,256); - memset(drMacro,0,256); - memset(multMacro,0,256); - memset(rrMacro,0,256); - memset(slMacro,0,256); - memset(tlMacro,0,256); - memset(dt2Macro,0,256); - memset(rsMacro,0,256); - memset(dtMacro,0,256); - memset(d2rMacro,0,256); - memset(ssgMacro,0,256); - } + amMacro("am"), arMacro("ar"), drMacro("dr"), multMacro("mult"), + rrMacro("rr"), slMacro("sl"), tlMacro("tl",true), dt2Macro("dt2"), + rsMacro("rs"), dtMacro("dt"), d2rMacro("d2r"), ssgMacro("ssg"), + damMacro("dam"), dvbMacro("dvb"), egtMacro("egt"), kslMacro("ksl"), + susMacro("sus"), vibMacro("vib"), wsMacro("ws"), ksrMacro("ksr") {} } opMacros[4]; DivInstrumentSTD(): - arpMacroMode(false), - volMacroHeight(15), - dutyMacroHeight(3), - waveMacroHeight(63), - volMacroOpen(true), - arpMacroOpen(false), - dutyMacroOpen(false), - waveMacroOpen(false), - pitchMacroOpen(false), - ex1MacroOpen(false), - ex2MacroOpen(false), - ex3MacroOpen(false), - algMacroOpen(false), - fbMacroOpen(false), - fmsMacroOpen(false), - amsMacroOpen(false), - volMacroLen(0), - arpMacroLen(0), - dutyMacroLen(0), - waveMacroLen(0), - pitchMacroLen(0), - ex1MacroLen(0), - ex2MacroLen(0), - ex3MacroLen(0), - algMacroLen(0), - fbMacroLen(0), - fmsMacroLen(0), - amsMacroLen(0), - volMacroLoop(-1), - arpMacroLoop(-1), - dutyMacroLoop(-1), - waveMacroLoop(-1), - pitchMacroLoop(-1), - ex1MacroLoop(-1), - ex2MacroLoop(-1), - ex3MacroLoop(-1), - algMacroLoop(-1), - fbMacroLoop(-1), - fmsMacroLoop(-1), - amsMacroLoop(-1), - volMacroRel(-1), - arpMacroRel(-1), - dutyMacroRel(-1), - waveMacroRel(-1), - pitchMacroRel(-1), - ex1MacroRel(-1), - ex2MacroRel(-1), - ex3MacroRel(-1), - algMacroRel(-1), - fbMacroRel(-1), - fmsMacroRel(-1), - amsMacroRel(-1) { - memset(volMacro,0,256*sizeof(int)); - memset(arpMacro,0,256*sizeof(int)); - memset(dutyMacro,0,256*sizeof(int)); - memset(waveMacro,0,256*sizeof(int)); - memset(pitchMacro,0,256*sizeof(int)); - memset(ex1Macro,0,256*sizeof(int)); - memset(ex2Macro,0,256*sizeof(int)); - memset(ex3Macro,0,256*sizeof(int)); - memset(algMacro,0,256*sizeof(int)); - memset(fbMacro,0,256*sizeof(int)); - memset(fmsMacro,0,256*sizeof(int)); - memset(amsMacro,0,256*sizeof(int)); - } + volMacro("vol",true), + arpMacro("arp"), + dutyMacro("duty"), + waveMacro("wave"), + pitchMacro("pitch"), + ex1Macro("ex1"), + ex2Macro("ex2"), + ex3Macro("ex3"), + algMacro("alg"), + fbMacro("fb"), + fmsMacro("fms"), + amsMacro("ams"), + panLMacro("panL"), + panRMacro("panR"), + phaseResetMacro("phaseReset"), + ex4Macro("ex4"), + ex5Macro("ex5"), + ex6Macro("ex6"), + ex7Macro("ex7"), + ex8Macro("ex8") {} }; struct DivInstrumentGB { @@ -314,9 +292,84 @@ struct DivInstrumentC64 { struct DivInstrumentAmiga { short initSample; + bool useNoteMap; + int noteFreq[120]; + short noteMap[120]; DivInstrumentAmiga(): - initSample(0) {} + initSample(0), + useNoteMap(false) { + memset(noteMap,-1,120*sizeof(short)); + memset(noteFreq,0,120*sizeof(int)); + } +}; + +struct DivInstrumentN163 { + int wave, wavePos, waveLen; + unsigned char waveMode; + + DivInstrumentN163(): + wave(-1), + wavePos(0), + waveLen(32), + waveMode(3) {} +}; + +struct DivInstrumentFDS { + signed char modTable[32]; + int modSpeed, modDepth; + // this is here for compatibility. + bool initModTableWithFirstWave; + DivInstrumentFDS(): + modSpeed(0), + modDepth(0), + initModTableWithFirstWave(false) { + memset(modTable,0,32); + } +}; + +enum DivWaveSynthEffects { + DIV_WS_NONE=0, + // one waveform effects + DIV_WS_INVERT, + DIV_WS_ADD, + DIV_WS_SUBTRACT, + DIV_WS_AVERAGE, + DIV_WS_PHASE, + + DIV_WS_SINGLE_MAX, + + // two waveform effects + DIV_WS_NONE_DUAL=128, + DIV_WS_WIPE, + DIV_WS_FADE, + DIV_WS_PING_PONG, + DIV_WS_OVERLAY, + DIV_WS_NEGATIVE_OVERLAY, + DIV_WS_PHASE_DUAL, + + DIV_WS_DUAL_MAX +}; + +struct DivInstrumentWaveSynth { + int wave1, wave2; + unsigned char rateDivider; + unsigned char effect; + bool oneShot, enabled, global; + unsigned char speed, param1, param2, param3, param4; + DivInstrumentWaveSynth(): + wave1(0), + wave2(0), + rateDivider(1), + effect(DIV_WS_NONE), + oneShot(false), + enabled(false), + global(false), + speed(0), + param1(0), + param2(0), + param3(0), + param4(0) {} }; struct DivInstrument { @@ -328,14 +381,34 @@ struct DivInstrument { DivInstrumentGB gb; DivInstrumentC64 c64; DivInstrumentAmiga amiga; + DivInstrumentN163 n163; + DivInstrumentFDS fds; + DivInstrumentWaveSynth ws; + /** + * save the instrument to a SafeWriter. + * @param w the SafeWriter in question. + */ void putInsData(SafeWriter* w); + + /** + * read instrument data in .fui format. + * @param reader the reader. + * @param version the format version. + * @return a DivDataErrors. + */ DivDataErrors readInsData(SafeReader& reader, short version); + + /** + * save this instrument to a file. + * @param path file path. + * @return whether it was successful. + */ bool save(const char* path); DivInstrument(): name(""), mode(false), - type(DIV_INS_STD) { + type(DIV_INS_FM) { } }; #endif diff --git a/src/engine/macroInt.cpp b/src/engine/macroInt.cpp index 8ae31411b..21ca93a1a 100644 --- a/src/engine/macroInt.cpp +++ b/src/engine/macroInt.cpp @@ -20,65 +20,41 @@ #include "macroInt.h" #include "instrument.h" -#define doMacro(finished,had,has,val,pos,source,sourceLen,sourceLoop,sourceRel) \ - if (finished) finished=false; \ - if (had!=has) { \ - finished=true; \ - } \ - had=has; \ - if (has) { \ - val=source[pos++]; \ - if (sourceRel>=0 && pos>sourceRel && !released) { \ - if (sourceLoop=0 && sourceLoop=sourceLen) { \ - if (sourceLoop=0 && (sourceLoop>=sourceRel || sourceRel>=sourceLen)) { \ - pos=sourceLoop; \ - } else { \ - has=false; \ - } \ - } \ +void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released) { + if (finished) { + finished=false; } + if (had!=has) { + finished=true; + } + had=has; + if (has) { + val=source.val[pos++]; + if (source.rel>=0 && pos>source.rel && !released) { + if (source.loop=0 && source.loop=source.len) { + if (source.loop=0 && (source.loop>=source.rel || source.rel>=source.len)) { + pos=source.loop; + } else { + has=false; + } + } + } +} void DivMacroInt::next() { if (ins==NULL) return; - - doMacro(finishedVol,hadVol,hasVol,vol,volPos,ins->std.volMacro,ins->std.volMacroLen,ins->std.volMacroLoop,ins->std.volMacroRel); - doMacro(finishedArp,hadArp,hasArp,arp,arpPos,ins->std.arpMacro,ins->std.arpMacroLen,ins->std.arpMacroLoop,ins->std.arpMacroRel); - doMacro(finishedDuty,hadDuty,hasDuty,duty,dutyPos,ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel); - doMacro(finishedWave,hadWave,hasWave,wave,wavePos,ins->std.waveMacro,ins->std.waveMacroLen,ins->std.waveMacroLoop,ins->std.waveMacroRel); - - doMacro(finishedPitch,hadPitch,hasPitch,pitch,pitchPos,ins->std.pitchMacro,ins->std.pitchMacroLen,ins->std.pitchMacroLoop,ins->std.pitchMacroRel); - doMacro(finishedEx1,hadEx1,hasEx1,ex1,ex1Pos,ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel); - doMacro(finishedEx2,hadEx2,hasEx2,ex2,ex2Pos,ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel); - doMacro(finishedEx3,hadEx3,hasEx3,ex3,ex3Pos,ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel); - - doMacro(finishedAlg,hadAlg,hasAlg,alg,algPos,ins->std.algMacro,ins->std.algMacroLen,ins->std.algMacroLoop,ins->std.algMacroRel); - doMacro(finishedFb,hadFb,hasFb,fb,fbPos,ins->std.fbMacro,ins->std.fbMacroLen,ins->std.fbMacroLoop,ins->std.fbMacroRel); - doMacro(finishedFms,hadFms,hasFms,fms,fmsPos,ins->std.fmsMacro,ins->std.fmsMacroLen,ins->std.fmsMacroLoop,ins->std.fmsMacroRel); - doMacro(finishedAms,hadAms,hasAms,ams,amsPos,ins->std.amsMacro,ins->std.amsMacroLen,ins->std.amsMacroLoop,ins->std.amsMacroRel); - - for (int i=0; i<4; i++) { - DivInstrumentSTD::OpMacro& m=ins->std.opMacros[i]; - IntOp& o=op[i]; - doMacro(o.finishedAm,o.hadAm,o.hasAm,o.am,o.amPos,m.amMacro,m.amMacroLen,m.amMacroLoop,m.amMacroRel); - doMacro(o.finishedAr,o.hadAr,o.hasAr,o.ar,o.arPos,m.arMacro,m.arMacroLen,m.arMacroLoop,m.arMacroRel); - doMacro(o.finishedDr,o.hadDr,o.hasDr,o.dr,o.drPos,m.drMacro,m.drMacroLen,m.drMacroLoop,m.drMacroRel); - doMacro(o.finishedMult,o.hadMult,o.hasMult,o.mult,o.multPos,m.multMacro,m.multMacroLen,m.multMacroLoop,m.multMacroRel); - - doMacro(o.finishedRr,o.hadRr,o.hasRr,o.rr,o.rrPos,m.rrMacro,m.rrMacroLen,m.rrMacroLoop,m.rrMacroRel); - doMacro(o.finishedSl,o.hadSl,o.hasSl,o.sl,o.slPos,m.slMacro,m.slMacroLen,m.slMacroLoop,m.slMacroRel); - doMacro(o.finishedTl,o.hadTl,o.hasTl,o.tl,o.tlPos,m.tlMacro,m.tlMacroLen,m.tlMacroLoop,m.tlMacroRel); - doMacro(o.finishedDt2,o.hadDt2,o.hasDt2,o.dt2,o.dt2Pos,m.dt2Macro,m.dt2MacroLen,m.dt2MacroLoop,m.dt2MacroRel); - - doMacro(o.finishedRs,o.hadRs,o.hasRs,o.rs,o.rsPos,m.rsMacro,m.rsMacroLen,m.rsMacroLoop,m.rsMacroRel); - doMacro(o.finishedDt,o.hadDt,o.hasDt,o.dt,o.dtPos,m.dtMacro,m.dtMacroLen,m.dtMacroLoop,m.dtMacroRel); - doMacro(o.finishedD2r,o.hadD2r,o.hasD2r,o.d2r,o.d2rPos,m.d2rMacro,m.d2rMacroLen,m.d2rMacroLoop,m.d2rMacroRel); - doMacro(o.finishedSsg,o.hadSsg,o.hasSsg,o.ssg,o.ssgPos,m.ssgMacro,m.ssgMacroLen,m.ssgMacroLoop,m.ssgMacroRel); + // run macros + // TODO: potentially get rid of list to avoid allocations + for (size_t i=0; idoMacro(*macroSource[i],released); + } } } @@ -86,205 +62,159 @@ void DivMacroInt::release() { released=true; } +#define ADD_MACRO(m,s) \ + macroList[macroListLen]=&m; \ + macroSource[macroListLen++]=&s; + void DivMacroInt::init(DivInstrument* which) { ins=which; - volPos=0; - arpPos=0; - dutyPos=0; - wavePos=0; - pitchPos=0; - ex1Pos=0; - ex2Pos=0; - ex3Pos=0; - algPos=0; - fbPos=0; - fmsPos=0; - amsPos=0; + // initialize + for (size_t i=0; iinit(); + } + macroListLen=0; released=false; - hasVol=false; - hasArp=false; - hasDuty=false; - hasWave=false; - hasPitch=false; - hasEx1=false; - hasEx2=false; - hasEx3=false; - hasAlg=false; - hasFb=false; - hasFms=false; - hasAms=false; - - hadVol=false; - hadArp=false; - hadDuty=false; - hadWave=false; - hadPitch=false; - hadEx1=false; - hadEx2=false; - hadEx3=false; - hadAlg=false; - hadFb=false; - hadFms=false; - hadAms=false; - - willVol=false; - willArp=false; - willDuty=false; - willWave=false; - willPitch=false; - willEx1=false; - willEx2=false; - willEx3=false; - willAlg=false; - willFb=false; - willFms=false; - willAms=false; - - op[0]=IntOp(); - op[1]=IntOp(); - op[2]=IntOp(); - op[3]=IntOp(); - - arpMode=false; - if (ins==NULL) return; - if (ins->std.volMacroLen>0) { - hadVol=true; - hasVol=true; - willVol=true; + // prepare common macro + if (ins->std.volMacro.len>0) { + ADD_MACRO(vol,ins->std.volMacro); } - if (ins->std.arpMacroLen>0) { - hadArp=true; - hasArp=true; - willArp=true; + if (ins->std.arpMacro.len>0) { + ADD_MACRO(arp,ins->std.arpMacro); } - if (ins->std.dutyMacroLen>0) { - hadDuty=true; - hasDuty=true; - willDuty=true; + if (ins->std.dutyMacro.len>0) { + ADD_MACRO(duty,ins->std.dutyMacro); } - if (ins->std.waveMacroLen>0) { - hadWave=true; - hasWave=true; - willWave=true; + if (ins->std.waveMacro.len>0) { + ADD_MACRO(wave,ins->std.waveMacro); } - if (ins->std.pitchMacroLen>0) { - hadPitch=true; - hasPitch=true; - willPitch=true; + if (ins->std.pitchMacro.len>0) { + ADD_MACRO(pitch,ins->std.pitchMacro); } - if (ins->std.ex1MacroLen>0) { - hadEx1=true; - hasEx1=true; - willEx1=true; + if (ins->std.ex1Macro.len>0) { + ADD_MACRO(ex1,ins->std.ex1Macro); } - if (ins->std.ex2MacroLen>0) { - hadEx2=true; - hasEx2=true; - willEx2=true; + if (ins->std.ex2Macro.len>0) { + ADD_MACRO(ex2,ins->std.ex2Macro); } - if (ins->std.ex3MacroLen>0) { - hadEx3=true; - hasEx3=true; - willEx3=true; + if (ins->std.ex3Macro.len>0) { + ADD_MACRO(ex3,ins->std.ex3Macro); } - if (ins->std.algMacroLen>0) { - hadAlg=true; - hasAlg=true; - willAlg=true; + if (ins->std.algMacro.len>0) { + ADD_MACRO(alg,ins->std.algMacro); } - if (ins->std.fbMacroLen>0) { - hadFb=true; - hasFb=true; - willFb=true; + if (ins->std.fbMacro.len>0) { + ADD_MACRO(fb,ins->std.fbMacro); } - if (ins->std.fmsMacroLen>0) { - hadFms=true; - hasFms=true; - willFms=true; + if (ins->std.fmsMacro.len>0) { + ADD_MACRO(fms,ins->std.fmsMacro); } - if (ins->std.amsMacroLen>0) { - hadAms=true; - hasAms=true; - willAms=true; + if (ins->std.amsMacro.len>0) { + ADD_MACRO(ams,ins->std.amsMacro); } - if (ins->std.arpMacroMode) { - arpMode=true; + if (ins->std.panLMacro.len>0) { + ADD_MACRO(panL,ins->std.panLMacro); + } + if (ins->std.panRMacro.len>0) { + ADD_MACRO(panR,ins->std.panRMacro); + } + if (ins->std.phaseResetMacro.len>0) { + ADD_MACRO(phaseReset,ins->std.phaseResetMacro); + } + if (ins->std.ex4Macro.len>0) { + ADD_MACRO(ex4,ins->std.ex4Macro); + } + if (ins->std.ex5Macro.len>0) { + ADD_MACRO(ex5,ins->std.ex5Macro); + } + if (ins->std.ex6Macro.len>0) { + ADD_MACRO(ex6,ins->std.ex6Macro); + } + if (ins->std.ex7Macro.len>0) { + ADD_MACRO(ex7,ins->std.ex7Macro); + } + if (ins->std.ex8Macro.len>0) { + ADD_MACRO(ex8,ins->std.ex8Macro); } + // prepare FM operator macros for (int i=0; i<4; i++) { DivInstrumentSTD::OpMacro& m=ins->std.opMacros[i]; IntOp& o=op[i]; + if (m.amMacro.len>0) { + ADD_MACRO(o.am,m.amMacro); + } + if (m.arMacro.len>0) { + ADD_MACRO(o.ar,m.arMacro); + } + if (m.drMacro.len>0) { + ADD_MACRO(o.dr,m.drMacro); + } + if (m.multMacro.len>0) { + ADD_MACRO(o.mult,m.multMacro); + } + if (m.rrMacro.len>0) { + ADD_MACRO(o.rr,m.rrMacro); + } + if (m.slMacro.len>0) { + ADD_MACRO(o.sl,m.slMacro); + } + if (m.tlMacro.len>0) { + ADD_MACRO(o.tl,m.tlMacro); + } + if (m.dt2Macro.len>0) { + ADD_MACRO(o.dt2,m.dt2Macro); + } + if (m.rsMacro.len>0) { + ADD_MACRO(o.rs,m.rsMacro); + } + if (m.dtMacro.len>0) { + ADD_MACRO(o.dt,m.dtMacro); + } + if (m.d2rMacro.len>0) { + ADD_MACRO(o.d2r,m.d2rMacro); + } + if (m.ssgMacro.len>0) { + ADD_MACRO(o.ssg,m.ssgMacro); + } - if (m.amMacroLen>0) { - o.hadAm=true; - o.hasAm=true; - o.willAm=true; + if (m.damMacro.len>0) { + ADD_MACRO(o.dam,m.damMacro); } - if (m.arMacroLen>0) { - o.hadAr=true; - o.hasAr=true; - o.willAr=true; + if (m.dvbMacro.len>0) { + ADD_MACRO(o.dvb,m.dvbMacro); } - if (m.drMacroLen>0) { - o.hadDr=true; - o.hasDr=true; - o.willDr=true; + if (m.egtMacro.len>0) { + ADD_MACRO(o.egt,m.egtMacro); } - if (m.multMacroLen>0) { - o.hadMult=true; - o.hasMult=true; - o.willMult=true; + if (m.kslMacro.len>0) { + ADD_MACRO(o.ksl,m.kslMacro); } - if (m.rrMacroLen>0) { - o.hadRr=true; - o.hasRr=true; - o.willRr=true; + if (m.susMacro.len>0) { + ADD_MACRO(o.sus,m.susMacro); } - if (m.slMacroLen>0) { - o.hadSl=true; - o.hasSl=true; - o.willSl=true; + if (m.vibMacro.len>0) { + ADD_MACRO(o.vib,m.vibMacro); } - if (m.tlMacroLen>0) { - o.hadTl=true; - o.hasTl=true; - o.willTl=true; + if (m.wsMacro.len>0) { + ADD_MACRO(o.ws,m.wsMacro); } - if (m.dt2MacroLen>0) { - o.hadDt2=true; - o.hasDt2=true; - o.willDt2=true; - } - if (m.rsMacroLen>0) { - o.hadRs=true; - o.hasRs=true; - o.willRs=true; - } - if (m.dtMacroLen>0) { - o.hadDt=true; - o.hasDt=true; - o.willDt=true; - } - if (m.d2rMacroLen>0) { - o.hadD2r=true; - o.hasD2r=true; - o.willD2r=true; - } - if (m.ssgMacroLen>0) { - o.hadSsg=true; - o.hasSsg=true; - o.willSsg=true; + if (m.ksrMacro.len>0) { + ADD_MACRO(o.ksr,m.ksrMacro); } } + + for (size_t i=0; iprepare(*macroSource[i]); + } } void DivMacroInt::notifyInsDeletion(DivInstrument* which) { if (ins==which) { init(NULL); } -} \ No newline at end of file +} diff --git a/src/engine/macroInt.h b/src/engine/macroInt.h index 60221d063..3c26eb659 100644 --- a/src/engine/macroInt.h +++ b/src/engine/macroInt.h @@ -22,164 +22,123 @@ #include "instrument.h" +struct DivMacroStruct { + int pos; + int val; + bool has, had, finished, will; + unsigned int mode; + void doMacro(DivInstrumentMacro& source, bool released); + void init() { + pos=mode=0; + has=had=will=false; + } + void prepare(DivInstrumentMacro& source) { + has=had=will=true; + mode=source.mode; + } + DivMacroStruct(): + pos(0), + val(0), + has(false), + had(false), + finished(false), + will(false), + mode(0) {} +}; + class DivMacroInt { DivInstrument* ins; - int volPos, arpPos, dutyPos, wavePos, pitchPos, ex1Pos, ex2Pos, ex3Pos; - int algPos, fbPos, fmsPos, amsPos; + DivMacroStruct* macroList[128]; + DivInstrumentMacro* macroSource[128]; + size_t macroListLen; bool released; public: - int vol; - int arp; - int duty, wave, pitch, ex1, ex2, ex3; - int alg, fb, fms, ams; - bool hasVol, hasArp, hasDuty, hasWave, hasPitch, hasEx1, hasEx2, hasEx3, hasAlg, hasFb, hasFms, hasAms; - bool hadVol, hadArp, hadDuty, hadWave, hadPitch, hadEx1, hadEx2, hadEx3, hadAlg, hadFb, hadFms, hadAms; - bool finishedVol, finishedArp, finishedDuty, finishedWave, finishedPitch, finishedEx1, finishedEx2, finishedEx3; - bool finishedAlg, finishedFb, finishedFms, finishedAms; - bool willVol, willArp, willDuty, willWave, willPitch, willEx1, willEx2, willEx3, willAlg, willFb, willFms, willAms; - bool arpMode; + // common macro + DivMacroStruct vol; + DivMacroStruct arp; + DivMacroStruct duty, wave, pitch, ex1, ex2, ex3; + DivMacroStruct alg, fb, fms, ams; + DivMacroStruct panL, panR, phaseReset, ex4, ex5, ex6, ex7, ex8; + + // FM operator macro struct IntOp { - int amPos, arPos, drPos, multPos; - int rrPos, slPos, tlPos, dt2Pos; - int rsPos, dtPos, d2rPos, ssgPos; - - int am, ar, dr, mult; - int rr, sl, tl, dt2; - int rs, dt, d2r, ssg; - - bool hasAm, hasAr, hasDr, hasMult; - bool hasRr, hasSl, hasTl, hasDt2; - bool hasRs, hasDt, hasD2r, hasSsg; - - bool hadAm, hadAr, hadDr, hadMult; - bool hadRr, hadSl, hadTl, hadDt2; - bool hadRs, hadDt, hadD2r, hadSsg; - - bool finishedAm, finishedAr, finishedDr, finishedMult; - bool finishedRr, finishedSl, finishedTl, finishedDt2; - bool finishedRs, finishedDt, finishedD2r, finishedSsg; - - bool willAm, willAr, willDr, willMult; - bool willRr, willSl, willTl, willDt2; - bool willRs, willDt, willD2r, willSsg; + DivMacroStruct am, ar, dr, mult; + DivMacroStruct rr, sl, tl, dt2; + DivMacroStruct rs, dt, d2r, ssg; + DivMacroStruct dam, dvb, egt, ksl; + DivMacroStruct sus, vib, ws, ksr; IntOp(): - amPos(0), - arPos(0), - drPos(0), - multPos(0), - rrPos(0), - slPos(0), - tlPos(0), - dt2Pos(0), - rsPos(0), - dtPos(0), - d2rPos(0), - ssgPos(0), - am(0), - ar(0), - dr(0), - mult(0), - rr(0), - sl(0), - tl(0), - dt2(0), - rs(0), - dt(0), - d2r(0), - ssg(0), - hasAm(false), hasAr(false), hasDr(false), hasMult(false), - hasRr(false), hasSl(false), hasTl(false), hasDt2(false), - hasRs(false), hasDt(false), hasD2r(false), hasSsg(false), - hadAm(false), hadAr(false), hadDr(false), hadMult(false), - hadRr(false), hadSl(false), hadTl(false), hadDt2(false), - hadRs(false), hadDt(false), hadD2r(false), hadSsg(false), - finishedAm(false), finishedAr(false), finishedDr(false), finishedMult(false), - finishedRr(false), finishedSl(false), finishedTl(false), finishedDt2(false), - finishedRs(false), finishedDt(false), finishedD2r(false), finishedSsg(false), - willAm(false), willAr(false), willDr(false), willMult(false), - willRr(false), willSl(false), willTl(false), willDt2(false), - willRs(false), willDt(false), willD2r(false), willSsg(false) {} + am(), + ar(), + dr(), + mult(), + rr(), + sl(), + tl(), + dt2(), + rs(), + dt(), + d2r(), + ssg(), + dam(), + dvb(), + egt(), + ksl(), + sus(), + vib(), + ws(), + ksr() {} } op[4]; + + /** + * trigger macro release. + */ void release(); + + /** + * trigger next macro tick. + */ void next(); + + /** + * initialize the macro interpreter. + * @param which an instrument, or NULL. + */ void init(DivInstrument* which); + + /** + * notify this macro interpreter that an instrument has been deleted. + * @param which the instrument in question. + */ void notifyInsDeletion(DivInstrument* which); + DivMacroInt(): ins(NULL), - volPos(0), - arpPos(0), - dutyPos(0), - wavePos(0), - pitchPos(0), - ex1Pos(0), - ex2Pos(0), - ex3Pos(0), - algPos(0), - fbPos(0), - fmsPos(0), - amsPos(0), + macroListLen(0), released(false), - vol(0), - arp(0), - duty(0), - wave(0), - pitch(0), - ex1(0), - ex2(0), - ex3(0), - alg(0), - fb(0), - fms(0), - ams(0), - hasVol(false), - hasArp(false), - hasDuty(false), - hasWave(false), - hasPitch(false), - hasEx1(false), - hasEx2(false), - hasEx3(false), - hasAlg(false), - hasFb(false), - hasFms(false), - hasAms(false), - hadVol(false), - hadArp(false), - hadDuty(false), - hadWave(false), - hadPitch(false), - hadEx1(false), - hadEx2(false), - hadEx3(false), - hadAlg(false), - hadFb(false), - hadFms(false), - hadAms(false), - finishedVol(false), - finishedArp(false), - finishedDuty(false), - finishedWave(false), - finishedPitch(false), - finishedEx1(false), - finishedEx2(false), - finishedEx3(false), - finishedAlg(false), - finishedFb(false), - finishedFms(false), - finishedAms(false), - willVol(false), - willArp(false), - willDuty(false), - willWave(false), - willPitch(false), - willEx1(false), - willEx2(false), - willEx3(false), - willAlg(false), - willFb(false), - willFms(false), - willAms(false), - arpMode(false) {} + vol(), + arp(), + duty(), + wave(), + pitch(), + ex1(), + ex2(), + ex3(), + alg(), + fb(), + fms(), + ams(), + panL(), + panR(), + phaseReset(), + ex4(), + ex5(), + ex6(), + ex7(), + ex8() { + memset(macroList,0,128*sizeof(void*)); + memset(macroSource,0,128*sizeof(void*)); + } }; #endif diff --git a/src/engine/orders.h b/src/engine/orders.h index d10dbe4c4..c57d1aab7 100644 --- a/src/engine/orders.h +++ b/src/engine/orders.h @@ -21,10 +21,10 @@ #define _ORDERS_H struct DivOrders { - unsigned char ord[DIV_MAX_CHANS][128]; + unsigned char ord[DIV_MAX_CHANS][256]; DivOrders() { - memset(ord,0,DIV_MAX_CHANS*128); + memset(ord,0,DIV_MAX_CHANS*256); } }; diff --git a/src/engine/pattern.cpp b/src/engine/pattern.cpp index 91a4ecd47..8241255b3 100644 --- a/src/engine/pattern.cpp +++ b/src/engine/pattern.cpp @@ -41,7 +41,7 @@ DivPattern* DivChannelData::getPattern(int index, bool create) { } void DivChannelData::wipePatterns() { - for (int i=0; i<128; i++) { + for (int i=0; i<256; i++) { if (data[i]!=NULL) { delete data[i]; data[i]=NULL; @@ -131,5 +131,5 @@ SafeReader* DivPattern::compile(int len, int fxRows) { DivChannelData::DivChannelData(): effectRows(1) { - memset(data,0,128*sizeof(void*)); + memset(data,0,256*sizeof(void*)); } diff --git a/src/engine/pattern.h b/src/engine/pattern.h index 749431b28..fd0b0d076 100644 --- a/src/engine/pattern.h +++ b/src/engine/pattern.h @@ -22,7 +22,19 @@ struct DivPattern { String name; short data[256][32]; + + /** + * copy this pattern to another. + * @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(); }; @@ -36,8 +48,20 @@ struct DivChannelData { // 2: instrument // 3: volume // 4-5+: effect/effect value - DivPattern* data[128]; + // do NOT access directly unless you know what you're doing! + DivPattern* data[256]; + + /** + * get a pattern from this channel, or the empty pattern if not initialized. + * @param index the pattern ID. + * @param create whether to initialize a new pattern if not init'ed. always use true if you're going to modify it! + * @return a DivPattern. + */ DivPattern* getPattern(int index, bool create); + + /** + * destroy all patterns on this DivChannelData. + */ void wipePatterns(); DivChannelData(); }; diff --git a/src/engine/platform/abstract.cpp b/src/engine/platform/abstract.cpp index f61bc5be0..b860f6808 100644 --- a/src/engine/platform/abstract.cpp +++ b/src/engine/platform/abstract.cpp @@ -74,6 +74,14 @@ int DivDispatch::getPortaFloor(int ch) { return 0x00; } +float DivDispatch::getPostAmp() { + return 1.0f; +} + +bool DivDispatch::getDCOffRequired() { + return false; +} + const char* DivDispatch::getEffectName(unsigned char effect) { return NULL; } @@ -97,6 +105,10 @@ void DivDispatch::notifyInsDeletion(void* ins) { } +void DivDispatch::notifyPlaybackStop() { + +} + void DivDispatch::forceIns() { } diff --git a/src/engine/platform/amiga.cpp b/src/engine/platform/amiga.cpp index 3ee2fd39b..458c9329d 100644 --- a/src/engine/platform/amiga.cpp +++ b/src/engine/platform/amiga.cpp @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#define _USE_MATH_DEFINES #include "amiga.h" #include "../engine.h" #include @@ -63,10 +64,26 @@ const char** DivPlatformAmiga::getRegisterSheet() { return regCheatSheetAmiga; } +const char* DivPlatformAmiga::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Toggle filter (0 disables; 1 enables)"; + break; + case 0x11: + return "11xx: Toggle AM with next channel"; + break; + case 0x12: + return "12xx: Toggle period modulation with next channel"; + break; + } + return NULL; +} + void DivPlatformAmiga::acquire(short* bufL, short* bufR, size_t start, size_t len) { + static int outL, outR; for (size_t h=start; h=0 && chan[i].samplesong.sampleLen) { chan[i].audSub-=AMIGA_DIVIDER; @@ -74,8 +91,16 @@ void DivPlatformAmiga::acquire(short* bufL, short* bufR, size_t start, size_t le DivSample* s=parent->getSample(chan[i].sample); if (s->samples>0) { chan[i].audDat=s->data8[chan[i].audPos++]; + if (i<3 && chan[i].useV) { + chan[i+1].outVol=(unsigned char)chan[i].audDat^0x80; + if (chan[i+1].outVol>64) chan[i+1].outVol=64; + } + if (i<3 && chan[i].useP) { + chan[i+1].freq=(unsigned char)chan[i].audDat^0x80; + if (chan[i+1].freq=s->samples || chan[i].audPos>=131071) { - if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { chan[i].audPos=s->loopStart; } else { chan[i].sample=-1; @@ -84,7 +109,7 @@ void DivPlatformAmiga::acquire(short* bufL, short* bufR, size_t start, size_t le } else { chan[i].sample=-1; } - if (chan[i].freq<124) { + /*if (chan[i].freq<124) { if (++chan[i].busClock>=512) { unsigned int rAmount=(124-chan[i].freq)*2; if (chan[i].audPos>=rAmount) { @@ -92,28 +117,38 @@ void DivPlatformAmiga::acquire(short* bufL, short* bufR, size_t start, size_t le } chan[i].busClock=0; } + }*/ + if (bypassLimits) { + chan[i].audSub+=MAX(AMIGA_DIVIDER,chan[i].freq); + } else { + chan[i].audSub+=MAX(114,chan[i].freq); } - chan[i].audSub+=MAX(114,chan[i].freq); } } if (!isMuted[i]) { if (i==0 || i==3) { - bufL[h]+=((chan[i].audDat*chan[i].outVol)*sep1)>>7; - bufR[h]+=((chan[i].audDat*chan[i].outVol)*sep2)>>7; + outL+=((chan[i].audDat*chan[i].outVol)*sep1)>>7; + outR+=((chan[i].audDat*chan[i].outVol)*sep2)>>7; } else { - bufL[h]+=((chan[i].audDat*chan[i].outVol)*sep2)>>7; - bufR[h]+=((chan[i].audDat*chan[i].outVol)*sep1)>>7; + outL+=((chan[i].audDat*chan[i].outVol)*sep2)>>7; + outR+=((chan[i].audDat*chan[i].outVol)*sep1)>>7; } } } + filter[0][0]+=(filtConst*(outL-filter[0][0]))>>12; + filter[0][1]+=(filtConst*(filter[0][0]-filter[0][1]))>>12; + filter[1][0]+=(filtConst*(outR-filter[1][0]))>>12; + filter[1][1]+=(filtConst*(filter[1][0]-filter[1][1]))>>12; + bufL[h]=filter[0][1]; + bufR[h]=filter[1][1]; } } void DivPlatformAmiga::tick() { for (int i=0; i<4; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol%65)*MIN(64,chan[i].std.vol))>>6; + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol%65)*MIN(64,chan[i].std.vol.val))>>6; } double off=1.0; if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { @@ -124,24 +159,24 @@ void DivPlatformAmiga::tick() { off=8363.0/(double)s->centerRate; } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=round(off*NOTE_PERIODIC_NOROUND(chan[i].std.arp.val)); } else { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=round(off*NOTE_PERIODIC_NOROUND(chan[i].note+chan[i].std.arp.val)); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].note); + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=round(off*NOTE_PERIODIC_NOROUND(chan[i].note)); chan[i].freqChanged=true; } } - if (chan[i].std.hadWave) { - if (chan[i].wave!=chan[i].std.wave) { - chan[i].wave=chan[i].std.wave; + if (chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val) { + chan[i].wave=chan[i].std.wave.val; if (!chan[i].keyOff) chan[i].keyOn=true; } } @@ -179,12 +214,16 @@ int DivPlatformAmiga::dispatch(DivCommand c) { } } if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value); + chan[c.chan].baseFreq=round(off*NOTE_PERIODIC_NOROUND(c.value)); } if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { chan[c.chan].sample=-1; } - chan[c.chan].audPos=0; + if (chan[c.chan].setPos) { + chan[c.chan].setPos=false; + } else { + chan[c.chan].audPos=0; + } chan[c.chan].audSub=0; if (c.value!=DIV_NOTE_NULL) { chan[c.chan].freqChanged=true; @@ -213,13 +252,13 @@ int DivPlatformAmiga::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } } break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { + if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; @@ -233,7 +272,18 @@ int DivPlatformAmiga::dispatch(DivCommand c) { chan[c.chan].keyOn=true; break; case DIV_CMD_NOTE_PORTA: { - int destFreq=NOTE_PERIODIC(c.value2); + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + chan[c.chan].sample=ins->amiga.initSample; + double off=1.0; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + int destFreq=round(off*NOTE_PERIODIC_NOROUND(c.value2)); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -265,7 +315,7 @@ int DivPlatformAmiga::dispatch(DivCommand c) { off=8363.0/(double)s->centerRate; } } - chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp-12):(0))); + chan[c.chan].baseFreq=round(off*NOTE_PERIODIC_NOROUND(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val-12):(0)))); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; break; @@ -276,6 +326,20 @@ int DivPlatformAmiga::dispatch(DivCommand c) { } chan[c.chan].inPorta=c.value; break; + case DIV_CMD_SAMPLE_POS: + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + break; + case DIV_CMD_AMIGA_FILTER: + filterOn=c.value; + filtConst=filterOn?filtConstOn:filtConstOff; + break; + case DIV_CMD_AMIGA_AM: + chan[c.chan].useV=c.value; + break; + case DIV_CMD_AMIGA_PM: + chan[c.chan].useP=c.value; + break; case DIV_CMD_GET_VOLMAX: return 64; break; @@ -309,7 +373,11 @@ void* DivPlatformAmiga::getChanState(int ch) { void DivPlatformAmiga::reset() { for (int i=0; i<4; i++) { chan[i]=DivPlatformAmiga::Channel(); + filter[0][i]=0; + filter[1][i]=0; } + filterOn=false; + filtConst=filterOn?filtConstOn:filtConstOff; } bool DivPlatformAmiga::isStereo() { @@ -347,6 +415,15 @@ void DivPlatformAmiga::setFlags(unsigned int flags) { rate=chipClock/AMIGA_DIVIDER; sep1=((flags>>8)&127)+127; sep2=127-((flags>>8)&127); + amigaModel=flags&2; + bypassLimits=flags&4; + if (amigaModel) { + filtConstOff=4000; + filtConstOn=sin(M_PI*8000.0/(double)rate)*4096.0; + } else { + filtConstOff=sin(M_PI*16000.0/(double)rate)*4096.0; + filtConstOn=sin(M_PI*5500.0/(double)rate)*4096.0; + } } int DivPlatformAmiga::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { diff --git a/src/engine/platform/amiga.h b/src/engine/platform/amiga.h index 19fe7fb2f..b5f13701d 100644 --- a/src/engine/platform/amiga.h +++ b/src/engine/platform/amiga.h @@ -36,7 +36,7 @@ class DivPlatformAmiga: public DivDispatch { unsigned char ins; int busClock; int note; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos, useV, useP; signed char vol, outVol; DivMacroInt std; Channel(): @@ -60,11 +60,21 @@ class DivPlatformAmiga: public DivDispatch { keyOff(false), inPorta(false), useWave(false), + setPos(false), + useV(false), + useP(false), vol(64), outVol(64) {} }; Channel chan[4]; bool isMuted[4]; + bool bypassLimits; + bool amigaModel; + bool filterOn; + + int filter[2][4]; + int filtConst; + int filtConstOff, filtConstOn; int sep1, sep2; @@ -85,6 +95,7 @@ class DivPlatformAmiga: public DivDispatch { void notifyWaveChange(int wave); void notifyInsDeletion(void* ins); const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); }; diff --git a/src/engine/platform/arcade.cpp b/src/engine/platform/arcade.cpp index 15a255178..55172394c 100644 --- a/src/engine/platform/arcade.cpp +++ b/src/engine/platform/arcade.cpp @@ -42,7 +42,7 @@ static bool isOutput[8][4]={ {true ,true ,true ,true}, }; static unsigned char dtTable[8]={ - 7,6,5,0,1,2,3,0 + 7,6,5,0,1,2,3,4 }; static int orderedOps[4]={ @@ -52,6 +52,8 @@ static int orderedOps[4]={ #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} #define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define NOTE_LINEAR(x) (((x)<<6)+baseFreqOff+log2(parent->song.tuning/440.0)*12.0*64.0) + const char* regCheatSheetOPM[]={ "Test", "00", "NoteCtl", "08", @@ -130,6 +132,9 @@ const char* DivPlatformArcade::getEffectName(unsigned char effect) { case 0x1f: return "1Fxx: Set PM depth (0 to 7F)"; break; + case 0x30: + return "30xx: Toggle hard envelope reset on new notes"; + break; } return NULL; } @@ -206,11 +211,11 @@ void DivPlatformArcade::acquire(short* bufL, short* bufR, size_t start, size_t l } } -unsigned char noteMap[12]={ +static unsigned char noteMap[12]={ 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14 }; -int hScale(int note) { +inline int hScale(int note) { return ((note/12)<<4)+(noteMap[note%12]); } @@ -218,8 +223,8 @@ void DivPlatformArcade::tick() { for (int i=0; i<8; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -231,50 +236,50 @@ void DivPlatformArcade::tick() { } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=(chan[i].std.arp<<6)+baseFreqOff; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_LINEAR(chan[i].std.arp.val); } else { - chan[i].baseFreq=((chan[i].note+(signed char)chan[i].std.arp)<<6)+baseFreqOff; + chan[i].baseFreq=NOTE_LINEAR(chan[i].note+(signed char)chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { - chan[i].baseFreq=(chan[i].note<<6)+baseFreqOff; + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_LINEAR(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - if (chan[i].std.duty>0) { - rWrite(0x0f,0x80|(0x20-chan[i].std.duty)); + if (chan[i].std.duty.had) { + if (chan[i].std.duty.val>0) { + rWrite(0x0f,0x80|(0x20-chan[i].std.duty.val)); } else { rWrite(0x0f,0); } } - if (chan[i].std.hadWave) { - rWrite(0x1b,chan[i].std.wave&3); + if (chan[i].std.wave.had) { + rWrite(0x1b,chan[i].std.wave.val&3); } - if (chan[i].std.hadEx1) { - amDepth=chan[i].std.ex1; + if (chan[i].std.ex1.had) { + amDepth=chan[i].std.ex1.val; immWrite(0x19,amDepth); } - if (chan[i].std.hadEx2) { - pmDepth=chan[i].std.ex2; + if (chan[i].std.ex2.had) { + pmDepth=chan[i].std.ex2.val; immWrite(0x19,0x80|pmDepth); } - if (chan[i].std.hadEx3) { - immWrite(0x18,chan[i].std.ex3); + if (chan[i].std.ex3.had) { + immWrite(0x18,chan[i].std.ex3.val); } - if (chan[i].std.hadAlg) { - chan[i].state.alg=chan[i].std.alg; + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; if (isMuted[i]) { rWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); } else { @@ -294,77 +299,94 @@ void DivPlatformArcade::tick() { } } } - if (chan[i].std.hadFb) { - chan[i].state.fb=chan[i].std.fb; + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; if (isMuted[i]) { rWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); } else { rWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|((chan[i].chVolL&1)<<6)|((chan[i].chVolR&1)<<7)); } } - if (chan[i].std.hadFms) { - chan[i].state.fms=chan[i].std.fms; + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); } - if (chan[i].std.hadAms) { - chan[i].state.ams=chan[i].std.ams; + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; DivMacroInt::IntOp& m=chan[i].std.op[j]; - if (m.hadAm) { - op.am=m.am; + if (m.am.had) { + op.am=m.am.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadAr) { - op.ar=m.ar; + if (m.ar.had) { + op.ar=m.ar.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDr) { - op.dr=m.dr; + if (m.dr.had) { + op.dr=m.dr.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadMult) { - op.mult=m.mult; + if (m.mult.had) { + op.mult=m.mult.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadRr) { - op.rr=m.rr; + if (m.rr.had) { + op.rr=m.rr.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadSl) { - op.sl=m.sl; + if (m.sl.had) { + op.sl=m.sl.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadTl) { - op.tl=127-m.tl; + if (m.tl.had) { + op.tl=127-m.tl.val; if (isOutput[chan[i].state.alg][j]) { rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); } else { rWrite(baseAddr+ADDR_TL,op.tl); } } - if (m.hadRs) { - op.rs=m.rs; + if (m.rs.had) { + op.rs=m.rs.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDt) { - op.dt=m.dt; + if (m.dt.had) { + op.dt=m.dt.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadD2r) { - op.d2r=m.d2r; + if (m.d2r.had) { + op.d2r=m.d2r.val; rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); } - if (m.hadDt2) { - op.dt2=m.dt2; + if (m.dt2.had) { + op.dt2=m.dt2.val; rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); } } if (chan[i].keyOn || chan[i].keyOff) { + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + immWrite(baseAddr+ADDR_SL_RR,0x0f); + immWrite(baseAddr+ADDR_TL,0x7f); + oldWrites[baseAddr+ADDR_SL_RR]=-1; + oldWrites[baseAddr+ADDR_TL]=-1; + } + } immWrite(0x08,i); + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + for (int k=0; k<9; k++) { + immWrite(baseAddr+ADDR_SL_RR,0x0f); + } + } + } chan[i].keyOff=false; } } @@ -411,7 +433,7 @@ int DivPlatformArcade::dispatch(DivCommand c) { } chan[c.chan].std.init(ins); - if (!chan[c.chan].std.willVol) { + if (!chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } @@ -446,7 +468,7 @@ int DivPlatformArcade::dispatch(DivCommand c) { chan[c.chan].insChanged=false; if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=(c.value<<6)+baseFreqOff; + chan[c.chan].baseFreq=NOTE_LINEAR(c.value); chan[c.chan].note=c.value; chan[c.chan].freqChanged=true; } @@ -470,7 +492,7 @@ int DivPlatformArcade::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } for (int i=0; i<4; i++) { @@ -495,8 +517,8 @@ int DivPlatformArcade::dispatch(DivCommand c) { chan[c.chan].ins=c.value; break; case DIV_CMD_PANNING: { - chan[c.chan].chVolL=((c.value>>4)==1); - chan[c.chan].chVolR=((c.value&15)==1); + chan[c.chan].chVolL=((c.value>>4)>0); + chan[c.chan].chVolR=((c.value&15)>0); if (isMuted[c.chan]) { rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); } else { @@ -510,7 +532,7 @@ int DivPlatformArcade::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { - int destFreq=(c.value2<<6)+baseFreqOff; + int destFreq=NOTE_LINEAR(c.value2); int newFreq; bool return2=false; if (destFreq>chan[c.chan].baseFreq) { @@ -535,7 +557,7 @@ int DivPlatformArcade::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: { - chan[c.chan].baseFreq=(c.value<<6)+baseFreqOff; + chan[c.chan].baseFreq=NOTE_LINEAR(c.value); chan[c.chan].freqChanged=true; break; } @@ -600,6 +622,9 @@ int DivPlatformArcade::dispatch(DivCommand c) { immWrite(0x19,0x80|pmDepth); break; } + case DIV_CMD_FM_HARD_RESET: + chan[c.chan].hardReset=c.value; + break; case DIV_CMD_STD_NOISE_FREQ: { if (c.chan!=7) break; if (c.value) { diff --git a/src/engine/platform/arcade.h b/src/engine/platform/arcade.h index a305eefdb..a6ec82c94 100644 --- a/src/engine/platform/arcade.h +++ b/src/engine/platform/arcade.h @@ -39,10 +39,30 @@ class DivPlatformArcade: public DivDispatch { int freq, baseFreq, pitch, note; unsigned char ins; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, portaPause, furnacePCM; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, portaPause, furnacePCM, hardReset; int vol, outVol; unsigned char chVolL, chVolR; - Channel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), note(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), inPorta(false), portaPause(false), furnacePCM(false), vol(0), outVol(0), chVolL(127), chVolR(127) {} + Channel(): + freqH(0), + freqL(0), + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + portaPause(false), + furnacePCM(false), + hardReset(false), + vol(0), + outVol(0), + chVolL(127), + chVolR(127) {} }; Channel chan[8]; struct QueuedWrite { diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 2bcada1e8..727a14408 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -19,12 +19,13 @@ #include "ay.h" #include "../engine.h" +#include "../../ta-log.h" #include "sound/ay8910.h" #include #include #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(regRemap(a),v); if (dumpWrites) {addWrite(regRemap(a),v);} } #define CHIP_DIVIDER 8 @@ -48,8 +49,28 @@ const char* regCheatSheetAY[]={ NULL }; +const char* regCheatSheetAY8914[]={ + "FreqL_A", "0", + "FreqL_B", "1", + "FreqL_C", "2", + "FreqL_Env", "3", + "FreqH_A", "4", + "FreqH_B", "5", + "FreqH_C", "6", + "FreqH_Env", "7", + "Enable", "8", + "FreqNoise", "9", + "Control_Env", "A", + "Volume_A", "B", + "Volume_B", "C", + "Volume_C", "D", + "PortA", "E", + "PortB", "F", + NULL +}; + const char** DivPlatformAY8910::getRegisterSheet() { - return regCheatSheetAY; + return intellivision?regCheatSheetAY8914:regCheatSheetAY; } const char* DivPlatformAY8910::getEffectName(unsigned char effect) { @@ -78,6 +99,12 @@ const char* DivPlatformAY8910::getEffectName(unsigned char effect) { case 0x29: return "29xy: Set auto-envelope (x: numerator; y: denominator)"; break; + case 0x2e: + return "2Exx: Write to I/O port A"; + break; + case 0x2f: + return "2Fxx: Write to I/O port B"; + break; } return NULL; } @@ -92,8 +119,13 @@ void DivPlatformAY8910::acquire(short* bufL, short* bufR, size_t start, size_t l } while (!writes.empty()) { QueuedWrite w=writes.front(); - ay->address_w(w.addr); - ay->data_w(w.val); + if (intellivision) { + ay8914_device* ay8914=(ay8914_device*)ay; + ay8914->write(w.addr,w.val); + } else { + ay->address_w(w.addr); + ay->data_w(w.val); + } regPool[w.addr&0x0f]=w.val; writes.pop(); } @@ -116,58 +148,86 @@ void DivPlatformAY8910::acquire(short* bufL, short* bufR, size_t start, size_t l } } +void DivPlatformAY8910::updateOutSel(bool immediate) { + if (immediate) { + immWrite(0x07, + ~((chan[0].psgMode&1)| + ((chan[1].psgMode&1)<<1)| + ((chan[2].psgMode&1)<<2)| + ((chan[0].psgMode&2)<<2)| + ((chan[1].psgMode&2)<<3)| + ((chan[2].psgMode&2)<<4)| + ((!ioPortA)<<6)| + ((!ioPortB)<<7))); + } else { + rWrite(0x07, + ~((chan[0].psgMode&1)| + ((chan[1].psgMode&1)<<1)| + ((chan[2].psgMode&1)<<2)| + ((chan[0].psgMode&2)<<2)| + ((chan[1].psgMode&2)<<3)| + ((chan[2].psgMode&2)<<4)| + ((!ioPortA)<<6)| + ((!ioPortB)<<7))); + } +} + void DivPlatformAY8910::tick() { // PSG for (int i=0; i<3; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=MIN(15,chan[i].std.vol)-(15-(chan[i].vol&15)); + if (chan[i].std.vol.had) { + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - rWrite(0x06,31-chan[i].std.duty); + if (chan[i].std.duty.had) { + rWrite(0x06,31-chan[i].std.duty.val); } - if (chan[i].std.hadWave) { - chan[i].psgMode=(chan[i].std.wave+1)&7; + if (chan[i].std.wave.had) { + chan[i].psgMode=(chan[i].std.wave.val+1)&7; if (isMuted[i]) { rWrite(0x08+i,0); + } else if (intellivision && (chan[i].psgMode&4)) { + rWrite(0x08+i,(chan[i].outVol&0xc)<<2); } else { rWrite(0x08+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); } } - if (chan[i].std.hadEx2) { - ayEnvMode=chan[i].std.ex2; + if (chan[i].std.ex2.had) { + ayEnvMode=chan[i].std.ex2.val; rWrite(0x0d,ayEnvMode); } - if (chan[i].std.hadEx3) { - chan[i].autoEnvNum=chan[i].std.ex3; + if (chan[i].std.ex3.had) { + chan[i].autoEnvNum=chan[i].std.ex3.val; chan[i].freqChanged=true; - if (!chan[i].std.willAlg) chan[i].autoEnvDen=1; + if (!chan[i].std.alg.will) chan[i].autoEnvDen=1; } - if (chan[i].std.hadAlg) { - chan[i].autoEnvDen=chan[i].std.alg; + if (chan[i].std.alg.had) { + chan[i].autoEnvDen=chan[i].std.alg.val; chan[i].freqChanged=true; - if (!chan[i].std.willEx3) chan[i].autoEnvNum=1; + if (!chan[i].std.ex3.will) chan[i].autoEnvNum=1; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); @@ -192,13 +252,7 @@ void DivPlatformAY8910::tick() { } } - rWrite(0x07, - ~((chan[0].psgMode&1)| - ((chan[1].psgMode&1)<<1)| - ((chan[2].psgMode&1)<<2)| - ((chan[0].psgMode&2)<<2)| - ((chan[1].psgMode&2)<<3)| - ((chan[2].psgMode&2)<<4))); + updateOutSel(); if (ayEnvSlide!=0) { ayEnvSlideLow+=ayEnvSlide; @@ -242,6 +296,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { chan[c.chan].std.init(ins); if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -258,13 +314,19 @@ int DivPlatformAY8910::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else { - if (chan[c.chan].active) rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + if (chan[c.chan].active) { + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); + } + } } break; } @@ -317,7 +379,11 @@ int DivPlatformAY8910::dispatch(DivCommand c) { if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); } else if (chan[c.chan].active) { - rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].outVol&0xc)<<2); + } else { + rWrite(0x08+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); + } } } break; @@ -334,6 +400,8 @@ int DivPlatformAY8910::dispatch(DivCommand c) { } if (isMuted[c.chan]) { rWrite(0x08+c.chan,0); + } else if (intellivision && (chan[c.chan].psgMode&4)) { + rWrite(0x08+c.chan,(chan[c.chan].vol&0xc)<<2); } else { rWrite(0x08+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); } @@ -358,6 +426,20 @@ int DivPlatformAY8910::dispatch(DivCommand c) { chan[c.chan].autoEnvDen=c.value&15; chan[c.chan].freqChanged=true; break; + case DIV_CMD_AY_IO_WRITE: + if (c.value==255) break; + if (c.value) { // port B + ioPortB=true; + portBVal=c.value2; + logI("AY I/O port B write: %x",portBVal); + } else { // port A + ioPortA=true; + portAVal=c.value2; + logI("AY I/O port A write: %x",portAVal); + } + updateOutSel(true); + immWrite(14+(c.value?1:0),(c.value?portBVal:portAVal)); + break; case DIV_ALWAYS_SET_VOLUME: return 0; break; @@ -383,6 +465,8 @@ void DivPlatformAY8910::muteChannel(int ch, bool mute) { isMuted[ch]=mute; if (isMuted[ch]) { rWrite(0x08+ch,0); + } else if (intellivision && (chan[ch].psgMode&4)) { + rWrite(0x08+ch,(chan[ch].vol&0xc)<<2); } else { rWrite(0x08+ch,(chan[ch].outVol&15)|((chan[ch].psgMode&4)<<2)); } @@ -409,6 +493,14 @@ int DivPlatformAY8910::getRegisterPoolSize() { return 16; } +void DivPlatformAY8910::flushWrites() { + while (!writes.empty()) writes.pop(); +} + +bool DivPlatformAY8910::getDCOffRequired() { + return true; +} + void DivPlatformAY8910::reset() { while (!writes.empty()) writes.pop(); ay->device_reset(); @@ -441,6 +533,11 @@ void DivPlatformAY8910::reset() { delay=0; extMode=false; + + ioPortA=false; + ioPortB=false; + portAVal=0; + portBVal=0; } bool DivPlatformAY8910::isStereo() { @@ -491,6 +588,12 @@ void DivPlatformAY8910::setFlags(unsigned int flags) { case 8: chipClock=COLOR_PAL*3.0/16.0; break; + case 9: + chipClock=COLOR_PAL/4.0; + break; + case 10: + chipClock=2097152; + break; default: chipClock=COLOR_NTSC/2.0; break; @@ -502,14 +605,22 @@ void DivPlatformAY8910::setFlags(unsigned int flags) { case 1: ay=new ym2149_device(rate); sunsoft=false; + intellivision=false; break; case 2: ay=new sunsoft_5b_sound_device(rate); sunsoft=true; + intellivision=false; + break; + case 3: + ay=new ay8914_device(rate); + sunsoft=false; + intellivision=true; break; default: ay=new ay8910_device(rate); sunsoft=false; + intellivision=false; break; } ay->device_start(); diff --git a/src/engine/platform/ay.h b/src/engine/platform/ay.h index c22929900..b1a3ea127 100644 --- a/src/engine/platform/ay.h +++ b/src/engine/platform/ay.h @@ -26,6 +26,10 @@ class DivPlatformAY8910: public DivDispatch { protected: + const unsigned char AY8914RegRemap[16]={ + 0,4,1,5,2,6,9,8,11,12,13,3,7,10,14,15 + }; + inline unsigned char regRemap(unsigned char reg) { return intellivision?AY8914RegRemap[reg&0x0f]:reg&0x0f; } struct Channel { unsigned char freqH, freqL; int freq, baseFreq, note, pitch; @@ -60,7 +64,9 @@ class DivPlatformAY8910: public DivDispatch { int delay; bool extMode; - bool stereo, sunsoft; + bool stereo, sunsoft, intellivision; + bool ioPortA, ioPortB; + unsigned char portAVal, portBVal; short oldWrites[16]; short pendingWrites[16]; @@ -71,6 +77,8 @@ class DivPlatformAY8910: public DivDispatch { short* ayBuf[3]; size_t ayBufLen; + void updateOutSel(bool immediate=false); + friend void putDispatchChan(void*,int,int); public: @@ -79,6 +87,7 @@ class DivPlatformAY8910: public DivDispatch { void* getChanState(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); + void flushWrites(); void reset(); void forceIns(); void tick(); @@ -86,6 +95,7 @@ class DivPlatformAY8910: public DivDispatch { void setFlags(unsigned int flags); bool isStereo(); bool keyOffAffectsArp(int ch); + bool getDCOffRequired(); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); void poke(std::vector& wlist); diff --git a/src/engine/platform/ay8930.cpp b/src/engine/platform/ay8930.cpp index 3c56461bb..e9af64fc7 100644 --- a/src/engine/platform/ay8930.cpp +++ b/src/engine/platform/ay8930.cpp @@ -19,6 +19,7 @@ #include "ay8930.h" #include "../engine.h" +#include "../../ta-log.h" #include "sound/ay8910.h" #include #include @@ -99,6 +100,15 @@ const char* DivPlatformAY8930::getEffectName(unsigned char effect) { case 0x29: return "29xy: Set auto-envelope (x: numerator; y: denominator)"; break; + case 0x2d: + return "2Dxx: NOT TO BE EMPLOYED BY THE COMPOSER"; + break; + case 0x2e: + return "2Exx: Write to I/O port A"; + break; + case 0x2f: + return "2Fxx: Write to I/O port B"; + break; } return NULL; } @@ -113,7 +123,7 @@ void DivPlatformAY8930::acquire(short* bufL, short* bufR, size_t start, size_t l } while (!writes.empty()) { QueuedWrite w=writes.front(); - if (bank!=(w.addr>>4)) { + if ((int)bank!=(w.addr>>4)) { bank=w.addr>>4; ay->address_w(0x0d); ay->data_w(0xa0|(bank<<4)|ayEnvMode[0]); @@ -141,6 +151,30 @@ void DivPlatformAY8930::acquire(short* bufL, short* bufR, size_t start, size_t l } } +void DivPlatformAY8930::updateOutSel(bool immediate) { + if (immediate) { + immWrite(0x07, + ~((chan[0].psgMode&1)| + ((chan[1].psgMode&1)<<1)| + ((chan[2].psgMode&1)<<2)| + ((chan[0].psgMode&2)<<2)| + ((chan[1].psgMode&2)<<3)| + ((chan[2].psgMode&2)<<4)| + ((!ioPortA)<<6)| + ((!ioPortB)<<7))); + } else { + rWrite(0x07, + ~((chan[0].psgMode&1)| + ((chan[1].psgMode&1)<<1)| + ((chan[2].psgMode&1)<<2)| + ((chan[0].psgMode&2)<<2)| + ((chan[1].psgMode&2)<<3)| + ((chan[2].psgMode&2)<<4)| + ((!ioPortA)<<6)| + ((!ioPortB)<<7))); + } +} + const unsigned char regPeriodL[3]={ 0x0b, 0x10, 0x12 }; @@ -157,8 +191,8 @@ void DivPlatformAY8930::tick() { // PSG for (int i=0; i<3; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=MIN(31,chan[i].std.vol)-(31-(chan[i].vol&31)); + if (chan[i].std.vol.had) { + chan[i].outVol=MIN(31,chan[i].std.vol.val)-(31-(chan[i].vol&31)); if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(0x08+i,0); @@ -166,55 +200,55 @@ void DivPlatformAY8930::tick() { rWrite(0x08+i,(chan[i].outVol&31)|((chan[i].psgMode&4)<<3)); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - rWrite(0x06,chan[i].std.duty); + if (chan[i].std.duty.had) { + rWrite(0x06,chan[i].std.duty.val); } - if (chan[i].std.hadWave) { - chan[i].psgMode=(chan[i].std.wave+1)&7; + if (chan[i].std.wave.had) { + chan[i].psgMode=(chan[i].std.wave.val+1)&7; if (isMuted[i]) { rWrite(0x08+i,0); } else { rWrite(0x08+i,(chan[i].outVol&31)|((chan[i].psgMode&4)<<3)); } } - if (chan[i].std.hadEx1) { // duty - rWrite(0x16+i,chan[i].std.ex1); + if (chan[i].std.ex1.had) { // duty + rWrite(0x16+i,chan[i].std.ex1.val); } - if (chan[i].std.hadEx2) { - ayEnvMode[i]=chan[i].std.ex2; + if (chan[i].std.ex2.had) { + ayEnvMode[i]=chan[i].std.ex2.val; rWrite(regMode[i],ayEnvMode[i]); } - if (chan[i].std.hadEx3) { - chan[i].autoEnvNum=chan[i].std.ex3; + if (chan[i].std.ex3.had) { + chan[i].autoEnvNum=chan[i].std.ex3.val; chan[i].freqChanged=true; - if (!chan[i].std.willAlg) chan[i].autoEnvDen=1; + if (!chan[i].std.alg.will) chan[i].autoEnvDen=1; } - if (chan[i].std.hadAlg) { - chan[i].autoEnvDen=chan[i].std.alg; + if (chan[i].std.alg.had) { + chan[i].autoEnvDen=chan[i].std.alg.val; chan[i].freqChanged=true; - if (!chan[i].std.willEx3) chan[i].autoEnvNum=1; + if (!chan[i].std.ex3.will) chan[i].autoEnvNum=1; } - if (chan[i].std.hadFb) { - ayNoiseAnd=chan[i].std.fb; + if (chan[i].std.fb.had) { + ayNoiseAnd=chan[i].std.fb.val; immWrite(0x19,ayNoiseAnd); } - if (chan[i].std.hadFms) { - ayNoiseOr=chan[i].std.fms; + if (chan[i].std.fms.had) { + ayNoiseOr=chan[i].std.fms.val; immWrite(0x1a,ayNoiseOr); } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { @@ -222,7 +256,7 @@ void DivPlatformAY8930::tick() { if (chan[i].freq>65535) chan[i].freq=65535; if (chan[i].keyOn) { if (chan[i].insChanged) { - if (!chan[i].std.willEx1) immWrite(0x16+i,chan[i].duty); + if (!chan[i].std.ex1.will) immWrite(0x16+i,chan[i].duty); chan[i].insChanged=false; } } @@ -262,13 +296,7 @@ void DivPlatformAY8930::tick() { } } - rWrite(0x07, - ~((chan[0].psgMode&1)| - ((chan[1].psgMode&1)<<1)| - ((chan[2].psgMode&1)<<2)| - ((chan[0].psgMode&2)<<2)| - ((chan[1].psgMode&2)<<3)| - ((chan[2].psgMode&2)<<4))); + updateOutSel(); for (int i=0; i<32; i++) { if (pendingWrites[i]!=oldWrites[i]) { @@ -308,7 +336,7 @@ int DivPlatformAY8930::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (isMuted[c.chan]) { @@ -372,7 +400,7 @@ int DivPlatformAY8930::dispatch(DivCommand c) { } } else { chan[c.chan].duty=c.value&15; - immWrite(0x16,chan[c.chan].duty); + immWrite(0x16+c.chan,chan[c.chan].duty); } break; case DIV_CMD_STD_NOISE_FREQ: @@ -420,6 +448,23 @@ int DivPlatformAY8930::dispatch(DivCommand c) { chan[c.chan].autoEnvDen=c.value&15; chan[c.chan].freqChanged=true; break; + case DIV_CMD_AY_IO_WRITE: + if (c.value==255) { + immWrite(0x1f,c.value2); + break; + } + if (c.value) { // port B + ioPortB=true; + portBVal=c.value2; + logI("AY I/O port B write: %x",portBVal); + } else { // port A + ioPortA=true; + portAVal=c.value2; + logI("AY I/O port A write: %x",portAVal); + } + updateOutSel(true); + immWrite(14+(c.value?1:0),(c.value?portBVal:portAVal)); + break; case DIV_ALWAYS_SET_VOLUME: return 0; break; @@ -499,6 +544,11 @@ void DivPlatformAY8930::reset() { extMode=false; bank=false; + ioPortA=false; + ioPortB=false; + portAVal=0; + portBVal=0; + immWrite(0x0d,0xa0); immWrite(0x19,2); // and mask immWrite(0x1a,0x00); // or mask @@ -552,6 +602,12 @@ void DivPlatformAY8930::setFlags(unsigned int flags) { case 8: chipClock=COLOR_PAL*3.0/16.0; break; + case 9: + chipClock=COLOR_PAL/4.0; + break; + case 10: + chipClock=2097152; + break; default: chipClock=COLOR_NTSC/2.0; break; diff --git a/src/engine/platform/ay8930.h b/src/engine/platform/ay8930.h index 2568fd7a9..6bd51bdcd 100644 --- a/src/engine/platform/ay8930.h +++ b/src/engine/platform/ay8930.h @@ -54,6 +54,8 @@ class DivPlatformAY8930: public DivDispatch { int delay; bool extMode, stereo; + bool ioPortA, ioPortB; + unsigned char portAVal, portBVal; short oldWrites[32]; short pendingWrites[32]; @@ -64,6 +66,8 @@ class DivPlatformAY8930: public DivDispatch { short* ayBuf[3]; size_t ayBufLen; + void updateOutSel(bool immediate=false); + friend void putDispatchChan(void*,int,int); public: diff --git a/src/engine/platform/bubsyswsg.cpp b/src/engine/platform/bubsyswsg.cpp new file mode 100644 index 000000000..712496c1c --- /dev/null +++ b/src/engine/platform/bubsyswsg.cpp @@ -0,0 +1,340 @@ +/** + * 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 "bubsyswsg.h" +#include "../engine.h" +#include + +#define CHIP_DIVIDER 32 + +#define rWrite(a,v) {if(!skipRegisterWrites) {regPool[a]=v; if(dumpWrites) addWrite(a,v); }} + +const char* regCheatSheetBubSysWSG[]={ + // K005289 timer + "Freq_A", "0", + "Freq_B", "1", + // PROM, DAC control from External logic (Connected to AY PSG ports on Bubble System) + "WaveVol_A", "2", + "WaveVol_B", "3", + NULL +}; + +const char** DivPlatformBubSysWSG::getRegisterSheet() { + return regCheatSheetBubSysWSG; +} + +const char* DivPlatformBubSysWSG::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + } + return NULL; +} + +void DivPlatformBubSysWSG::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; htick(); + + // Wavetable part + for (int i=0; i<2; i++) { + if (isMuted[i]) continue; + out+=chan[i].waveROM[k005289->addr(i)]*(regPool[2+i]&0xf); + } + + out<<=6; // scale output to 16 bit + + if (out<-32768) out=-32768; + if (out>32767) out=32767; + + //printf("out: %d\n",out); + bufL[h]=bufR[h]=out; + } +} + +void DivPlatformBubSysWSG::updateWave(int ch) { + //DivWavetable* wt=parent->getWave(chan[ch].wave); + for (int i=0; i<32; i++) { + // convert to signed + chan[ch].waveROM[i]=chan[ch].ws.output[i]-8; + } + if (chan[ch].active) { + rWrite(2+ch,(ch<<5)|chan[ch].outVol); + } +} + +void DivPlatformBubSysWSG::tick() { + for (int i=0; i<2; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol&15)*MIN(15,chan[i].std.vol.val))/15; + rWrite(2+i,(chan[i].wave<<5)|chan[i].outVol); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (chan[i].active) { + if (chan[i].ws.tick()) { + updateWave(i); + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + //DivInstrument* ins=parent->getIns(chan[i].ins); + chan[i].freq=0x1000-parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>4095) chan[i].freq=4095; + k005289->load(i,chan[i].freq); + rWrite(i,chan[i].freq); + k005289->update(i); + if (chan[i].keyOn) { + // ??? + } + if (chan[i].keyOff) { + rWrite(2+i,(chan[i].wave<<5)|0); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformBubSysWSG::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + rWrite(2+c.chan,(chan[c.chan].wave<<5)|chan[c.chan].vol); + chan[c.chan].std.init(ins); + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,32,15,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + if (chan[c.chan].active) rWrite(2+c.chan,(chan[c.chan].wave<<5)|chan[c.chan].outVol); + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + chan[c.chan].keyOn=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + 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))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformBubSysWSG::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + //rWrite(2+ch,(chan[ch].wave<<5)|((chan[ch].active && isMuted[ch])?0:chan[ch].outVol)); +} + +void DivPlatformBubSysWSG::forceIns() { + for (int i=0; i<2; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + updateWave(i); + } +} + +void* DivPlatformBubSysWSG::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformBubSysWSG::getRegisterPool() { + return (unsigned char*)regPool; +} + +int DivPlatformBubSysWSG::getRegisterPoolSize() { + return 4; +} + +int DivPlatformBubSysWSG::getRegisterPoolDepth() { + return 16; +} + +void DivPlatformBubSysWSG::reset() { + memset(regPool,0,4*2); + for (int i=0; i<2; i++) { + chan[i]=DivPlatformBubSysWSG::Channel(); + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,32,15,false); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + k005289->reset(); +} + +bool DivPlatformBubSysWSG::isStereo() { + return false; +} + +bool DivPlatformBubSysWSG::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformBubSysWSG::notifyWaveChange(int wave) { + for (int i=0; i<2; i++) { + if (chan[i].wave==wave) { + chan[i].ws.changeWave1(chan[i].wave); + updateWave(i); + } + } +} + +void DivPlatformBubSysWSG::notifyInsDeletion(void* ins) { + for (int i=0; i<2; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformBubSysWSG::setFlags(unsigned int flags) { + chipClock=COLOR_NTSC; + rate=chipClock; +} + +void DivPlatformBubSysWSG::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformBubSysWSG::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformBubSysWSG::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<2; i++) { + isMuted[i]=false; + } + setFlags(flags); + k005289=new k005289_core(); + reset(); + return 2; +} + +void DivPlatformBubSysWSG::quit() { + delete k005289; +} + +DivPlatformBubSysWSG::~DivPlatformBubSysWSG() { +} diff --git a/src/engine/platform/bubsyswsg.h b/src/engine/platform/bubsyswsg.h new file mode 100644 index 000000000..6c0e2261e --- /dev/null +++ b/src/engine/platform/bubsyswsg.h @@ -0,0 +1,86 @@ +/** + * 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 _K005289_H +#define _K005289_H + +#include "../dispatch.h" +#include +#include "../macroInt.h" +#include "../waveSynth.h" +#include "sound/k005289/k005289.hpp" + +class DivPlatformBubSysWSG: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta; + signed char vol, outVol, wave; + signed char waveROM[32] = {0}; // 4 bit PROM per channel on bubble system + DivMacroInt std; + DivWaveSynth ws; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + vol(15), + outVol(15), + wave(-1) {} + }; + Channel chan[2]; + bool isMuted[2]; + + k005289_core* k005289; + unsigned short regPool[4]; + void updateWave(int ch); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + int getRegisterPoolDepth(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformBubSysWSG(); +}; + +#endif diff --git a/src/engine/platform/c64.cpp b/src/engine/platform/c64.cpp index 6257daead..b7521f035 100644 --- a/src/engine/platform/c64.cpp +++ b/src/engine/platform/c64.cpp @@ -118,50 +118,50 @@ void DivPlatformC64::acquire(short* bufL, short* bufR, size_t start, size_t len) void DivPlatformC64::updateFilter() { rWrite(0x15,filtCut&7); rWrite(0x16,filtCut>>3); - rWrite(0x17,(filtRes<<4)|(chan[2].filter<<2)|(chan[1].filter<<1)|(chan[0].filter)); + rWrite(0x17,(filtRes<<4)|(chan[2].filter<<2)|(chan[1].filter<<1)|(int)(chan[0].filter)); rWrite(0x18,(filtControl<<4)|vol); } void DivPlatformC64::tick() { for (int i=0; i<3; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { + if (chan[i].std.vol.had) { DivInstrument* ins=parent->getIns(chan[i].ins); if (ins->c64.volIsCutoff) { if (ins->c64.filterIsAbs) { - filtCut=MIN(2047,chan[i].std.vol); + filtCut=MIN(2047,chan[i].std.vol.val); } else { - filtCut-=((signed char)chan[i].std.vol-18)*7; + filtCut-=((signed char)chan[i].std.vol.val-18)*7; if (filtCut>2047) filtCut=2047; if (filtCut<0) filtCut=0; } updateFilter(); } else { - vol=MIN(15,chan[i].std.vol); + vol=MIN(15,chan[i].std.vol.val); updateFilter(); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp); + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { + if (chan[i].std.duty.had) { DivInstrument* ins=parent->getIns(chan[i].ins); if (ins->c64.dutyIsAbs) { - chan[i].duty=chan[i].std.duty; + chan[i].duty=chan[i].std.duty.val; } else { - chan[i].duty-=((signed char)chan[i].std.duty-12)*4; + chan[i].duty-=((signed char)chan[i].std.duty.val-12)*4; } rWrite(i*7+2,chan[i].duty&0xff); rWrite(i*7+3,chan[i].duty>>8); @@ -175,21 +175,21 @@ void DivPlatformC64::tick() { } } } - if (chan[i].std.hadWave) { - chan[i].wave=chan[i].std.wave; - rWrite(i*7+4,(isMuted[i]?8:(chan[i].wave<<4))|(chan[i].ring<<2)|(chan[i].sync<<1)|chan[i].active); + if (chan[i].std.wave.had) { + chan[i].wave=chan[i].std.wave.val; + rWrite(i*7+4,(isMuted[i]?8:(chan[i].wave<<4))|(chan[i].ring<<2)|(chan[i].sync<<1)|(int)(chan[i].active)); } - if (chan[i].std.hadEx1) { - filtControl=chan[i].std.ex1&15; + if (chan[i].std.ex1.had) { + filtControl=chan[i].std.ex1.val&15; updateFilter(); } - if (chan[i].std.hadEx2) { - filtRes=chan[i].std.ex2&15; + if (chan[i].std.ex2.had) { + filtRes=chan[i].std.ex2.val&15; updateFilter(); } - if (chan[i].std.hadEx3) { - chan[i].sync=chan[i].std.ex3&1; - chan[i].ring=chan[i].std.ex3&2; + if (chan[i].std.ex3.had) { + chan[i].sync=chan[i].std.ex3.val&1; + chan[i].ring=chan[i].std.ex3.val&2; chan[i].freqChanged=true; } @@ -226,13 +226,13 @@ int DivPlatformC64::dispatch(DivCommand c) { } chan[c.chan].active=true; chan[c.chan].keyOn=true; - if (chan[c.chan].insChanged || chan[c.chan].resetDuty || ins->std.waveMacroLen>0) { + if (chan[c.chan].insChanged || chan[c.chan].resetDuty || ins->std.waveMacro.len>0) { chan[c.chan].duty=ins->c64.duty; rWrite(c.chan*7+2,chan[c.chan].duty&0xff); rWrite(c.chan*7+3,chan[c.chan].duty>>8); } if (chan[c.chan].insChanged) { - chan[c.chan].wave=(ins->c64.noiseOn<<3)|(ins->c64.pulseOn<<2)|(ins->c64.sawOn<<1)|(ins->c64.triOn); + chan[c.chan].wave=(ins->c64.noiseOn<<3)|(ins->c64.pulseOn<<2)|(ins->c64.sawOn<<1)|(int)(ins->c64.triOn); chan[c.chan].attack=ins->c64.a; chan[c.chan].decay=(ins->c64.s==15)?0:ins->c64.d; chan[c.chan].sustain=ins->c64.s; @@ -245,7 +245,7 @@ int DivPlatformC64::dispatch(DivCommand c) { if (ins->c64.initFilter) { filtCut=ins->c64.cut; filtRes=ins->c64.res; - filtControl=ins->c64.lp|(ins->c64.bp<<1)|(ins->c64.hp<<2)|(ins->c64.ch3off<<3); + filtControl=(int)(ins->c64.lp)|(ins->c64.bp<<1)|(ins->c64.hp<<2)|(ins->c64.ch3off<<3); } updateFilter(); } @@ -279,7 +279,7 @@ int DivPlatformC64::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; vol=chan[c.chan].outVol; } else { @@ -330,10 +330,10 @@ int DivPlatformC64::dispatch(DivCommand c) { break; case DIV_CMD_WAVE: chan[c.chan].wave=c.value; - rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|chan[c.chan].active); + rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|(int)(chan[c.chan].active)); break; case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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].freqChanged=true; chan[c.chan].note=c.value; break; @@ -411,11 +411,11 @@ int DivPlatformC64::dispatch(DivCommand c) { break; case 4: chan[c.chan].ring=c.value; - rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|chan[c.chan].active); + rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|(int)(chan[c.chan].active)); break; case 5: chan[c.chan].sync=c.value; - rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|chan[c.chan].active); + rWrite(c.chan*7+4,(isMuted[c.chan]?8:(chan[c.chan].wave<<4))|(chan[c.chan].ring<<2)|(chan[c.chan].sync<<1)|(int)(chan[c.chan].active)); break; case 6: filtControl&=7; @@ -434,7 +434,7 @@ int DivPlatformC64::dispatch(DivCommand c) { void DivPlatformC64::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - rWrite(ch*7+4,(isMuted[ch]?8:(chan[ch].wave<<4))|(chan[ch].ring<<2)|(chan[ch].sync<<1)|chan[ch].active); + rWrite(ch*7+4,(isMuted[ch]?8:(chan[ch].wave<<4))|(chan[ch].ring<<2)|(chan[ch].sync<<1)|(int)(chan[ch].active)); } void DivPlatformC64::forceIns() { @@ -509,10 +509,17 @@ void DivPlatformC64::setChipModel(bool is6581) { } void DivPlatformC64::setFlags(unsigned int flags) { - if (flags&1) { - rate=COLOR_PAL*2.0/9.0; - } else { - rate=COLOR_NTSC*2.0/7.0; + switch (flags&0xf) { + case 0x0: // NTSC C64 + rate=COLOR_NTSC*2.0/7.0; + break; + case 0x1: // PAL C64 + rate=COLOR_PAL*2.0/9.0; + break; + case 0x2: // SSI 2001 + default: + rate=14318180.0/16.0; + break; } chipClock=rate; } diff --git a/src/engine/platform/fds.cpp b/src/engine/platform/fds.cpp new file mode 100644 index 000000000..9960a0d74 --- /dev/null +++ b/src/engine/platform/fds.cpp @@ -0,0 +1,475 @@ +/** + * 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 "fds.h" +#include "sound/nes/cpu_inline.h" +#include "../engine.h" +#include + +#define CHIP_FREQBASE 262144 + +#define rWrite(a,v) if (!skipRegisterWrites) {fds_wr_mem(fds,a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } + +const char* regCheatSheetFDS[]={ + "IOCtrl", "4023", + "Wave", "4040", + "Volume", "4080", + "FreqL", "4082", + "FreqH", "4083", + "ModCtrl", "4084", + "ModCount", "4085", + "ModFreqL", "4086", + "ModFreqH", "4087", + "ModWrite", "4088", + "WaveCtrl", "4089", + "EnvSpeed", "408A", + "ReadVol", "4090", + "ReadPos", "4091", + "ReadModV", "4092", + "ReadModP", "4093", + "ReadModCG", "4094", + "ReadModInc", "4095", + "ReadWave", "4096", + "ReadModCount", "4097", + NULL +}; + +const char** DivPlatformFDS::getRegisterSheet() { + return regCheatSheetFDS; +} + +const char* DivPlatformFDS::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + case 0x11: + return "11xx: Set modulation depth"; + break; + case 0x12: + return "12xy: Set modulation speed high byte (x: enable; y: value)"; + break; + case 0x13: + return "13xx: Set modulation speed low byte"; + break; + case 0x14: + return "14xx: Set modulator position"; + break; + case 0x15: + return "15xx: Set modulator table to waveform"; + break; + } + return NULL; +} + +void DivPlatformFDS::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; isnd.main.output; + if (sample>32767) sample=32767; + if (sample<-32768) sample=-32768; + bufL[i]=sample; + } +} + +void DivPlatformFDS::updateWave() { + // TODO: master volume + rWrite(0x4089,0x80); + for (int i=0; i<64; i++) { + rWrite(0x4040+i,ws.output[i]); + } + rWrite(0x4089,0); +} + +void DivPlatformFDS::tick() { + for (int i=0; i<1; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + // ok, why are the volumes like that? + chan[i].outVol=MIN(32,chan[i].std.vol.val)-(32-MIN(32,chan[i].vol)); + if (chan[i].outVol<0) chan[i].outVol=0; + rWrite(0x4080,0x80|chan[i].outVol); + } + if (chan[i].std.arp.had) { + if (i==3) { // noise + if (chan[i].std.arp.mode) { + chan[i].baseFreq=chan[i].std.arp.val; + } else { + chan[i].baseFreq=chan[i].note+chan[i].std.arp.val; + } + if (chan[i].baseFreq>255) chan[i].baseFreq=255; + if (chan[i].baseFreq<0) chan[i].baseFreq=0; + } else { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+chan[i].std.arp.val); + } + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); + chan[i].freqChanged=true; + } + } + /* + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + if (i==3) { + if (parent->song.properNoiseLayout) { + chan[i].duty&=1; + } else if (chan[i].duty>1) { + chan[i].duty=1; + } + } + if (i!=2) { + rWrite(0x4000+i*4,0x30|chan[i].outVol|((chan[i].duty&3)<<6)); + } + if (i==3) { // noise + chan[i].freqChanged=true; + } + }*/ + if (chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val || ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + ws.changeWave1(chan[i].wave); + //if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (chan[i].active) { + if (ws.tick()) { + updateWave(); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (chan[i].std.ex1.had) { // mod depth + chan[i].modOn=chan[i].std.ex1.val; + chan[i].modDepth=chan[i].std.ex1.val; + rWrite(0x4084,(chan[i].modOn<<7)|0x40|chan[i].modDepth); + } + if (chan[i].std.ex2.had) { // mod speed + chan[i].modFreq=chan[i].std.ex2.val; + rWrite(0x4086,chan[i].modFreq&0xff); + rWrite(0x4087,chan[i].modFreq>>8); + } + if (chan[i].std.ex3.had) { // mod position + chan[i].modPos=chan[i].std.ex3.val; + rWrite(0x4087,0x80|chan[i].modFreq>>8); + rWrite(0x4085,chan[i].modPos); + rWrite(0x4087,chan[i].modFreq>>8); + } + 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) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false); + if (chan[i].freq>4095) chan[i].freq=4095; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].keyOn) { + // ??? + } + if (chan[i].keyOff) { + rWrite(0x4080,0x80); + } + rWrite(0x4082,chan[i].freq&0xff); + rWrite(0x4083,(chan[i].freq>>8)&15); + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformFDS::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + if (chan[c.chan].insChanged) { + if (ins->fds.initModTableWithFirstWave) { // compatible + if (chan[c.chan].wave==-1) { + DivWavetable* wt=parent->getWave(0); + for (int i=0; i<32; i++) { + if (wt->max<1 || wt->len<1) { + chan[c.chan].modTable[i]=0; + } else { + int data=wt->data[i*MIN(32,wt->len)/32]*7/wt->max; + if (data<0) data=0; + if (data>7) data=7; + chan[c.chan].modTable[i]=data; + } + } + rWrite(0x4087,0x80|chan[c.chan].modFreq>>8); + for (int i=0; i<32; i++) { + rWrite(0x4088,chan[c.chan].modTable[i]); + } + rWrite(0x4087,chan[c.chan].modFreq>>8); + } + } else { // The Familiar Way + chan[c.chan].modDepth=ins->fds.modDepth; + chan[c.chan].modOn=ins->fds.modDepth; + chan[c.chan].modFreq=ins->fds.modSpeed; + rWrite(0x4084,(chan[c.chan].modOn<<7)|0x40|chan[c.chan].modDepth); + rWrite(0x4086,chan[c.chan].modFreq&0xff); + + rWrite(0x4087,0x80|chan[c.chan].modFreq>>8); + for (int i=0; i<32; i++) { + chan[c.chan].modTable[i]=ins->fds.modTable[i]&7; + rWrite(0x4088,chan[c.chan].modTable[i]); + } + rWrite(0x4087,chan[c.chan].modFreq>>8); + } + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(ins); + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + ws.changeWave1(chan[c.chan].wave); + } + ws.init(ins,64,63,chan[c.chan].insChanged); + rWrite(0x4080,0x80|chan[c.chan].vol); + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + rWrite(0x4080,0x80|chan[c.chan].vol); + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + if (chan[c.chan].wave!=c.value) { + chan[c.chan].wave=c.value; + ws.changeWave1(chan[c.chan].wave); + } + break; + case DIV_CMD_FDS_MOD_DEPTH: + chan[c.chan].modDepth=c.value; + rWrite(0x4084,(chan[c.chan].modOn<<7)|0x40|chan[c.chan].modDepth); + break; + case DIV_CMD_FDS_MOD_HIGH: + chan[c.chan].modOn=c.value>>4; + chan[c.chan].modFreq=((c.value&15)<<8)|(chan[c.chan].modFreq&0xff); + rWrite(0x4084,(chan[c.chan].modOn<<7)|0x40|chan[c.chan].modDepth); + rWrite(0x4087,chan[c.chan].modFreq>>8); + break; + case DIV_CMD_FDS_MOD_LOW: + chan[c.chan].modFreq=(chan[c.chan].modFreq&0xf00)|c.value; + rWrite(0x4086,chan[c.chan].modFreq&0xff); + break; + case DIV_CMD_FDS_MOD_POS: + chan[c.chan].modPos=c.value&0x7f; + rWrite(0x4087,0x80|chan[c.chan].modFreq>>8); + rWrite(0x4085,chan[c.chan].modPos); + rWrite(0x4087,chan[c.chan].modFreq>>8); + break; + case DIV_CMD_FDS_MOD_WAVE: { + DivWavetable* wt=parent->getWave(c.value); + for (int i=0; i<32; i++) { + if (wt->max<1 || wt->len<1) { + rWrite(0x4040+i,0); + } else { + int data=wt->data[i*MIN(32,wt->len)/32]*7/wt->max; + if (data<0) data=0; + if (data>7) data=7; + chan[c.chan].modTable[i]=data; + } + } + rWrite(0x4087,0x80|chan[c.chan].modFreq>>8); + for (int i=0; i<32; i++) { + rWrite(0x4088,chan[c.chan].modTable[i]); + } + rWrite(0x4087,chan[c.chan].modFreq>>8); + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: + if (c.chan==3) break; + 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].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 32; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformFDS::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformFDS::forceIns() { + for (int i=0; i<1; i++) { + chan[i].insChanged=true; + chan[i].prevFreq=65535; + } + updateWave(); +} + +void* DivPlatformFDS::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformFDS::getRegisterPool() { + return regPool; +} + +int DivPlatformFDS::getRegisterPoolSize() { + return 128; +} + +void DivPlatformFDS::reset() { + for (int i=0; i<1; i++) { + chan[i]=DivPlatformFDS::Channel(); + } + ws.setEngine(parent); + ws.init(NULL,64,63,false); + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + fds_reset(fds); + memset(regPool,0,128); + + rWrite(0x4023,0); + rWrite(0x4023,0x83); + rWrite(0x4089,0); +} + +bool DivPlatformFDS::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformFDS::setFlags(unsigned int flags) { + if (flags==2) { // Dendy + rate=COLOR_PAL*2.0/5.0; + } else if (flags==1) { // PAL + rate=COLOR_PAL*3.0/8.0; + } else { // NTSC + rate=COLOR_NTSC/2.0; + } + chipClock=rate; +} + +void DivPlatformFDS::notifyInsDeletion(void* ins) { + for (int i=0; i<5; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformFDS::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformFDS::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformFDS::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + apuType=flags; + dumpWrites=false; + skipRegisterWrites=false; + fds=new struct _fds; + for (int i=0; i<1; i++) { + isMuted[i]=false; + } + setFlags(flags); + + reset(); + return 5; +} + +void DivPlatformFDS::quit() { + delete fds; +} + +DivPlatformFDS::~DivPlatformFDS() { +} diff --git a/src/engine/platform/fds.h b/src/engine/platform/fds.h new file mode 100644 index 000000000..d8d588a23 --- /dev/null +++ b/src/engine/platform/fds.h @@ -0,0 +1,94 @@ +/** + * 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 _FDS_H +#define _FDS_H + +#include "../dispatch.h" +#include "../macroInt.h" +#include "../waveSynth.h" + +class DivPlatformFDS: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, prevFreq, note, modFreq; + unsigned char ins, duty, sweep, modDepth, modPos; + bool active, insChanged, freqChanged, sweepChanged, keyOn, keyOff, inPorta, modOn; + signed char vol, outVol, wave; + signed char modTable[32]; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + prevFreq(65535), + note(0), + modFreq(0), + ins(-1), + duty(0), + sweep(8), + modDepth(0), + modPos(0), + active(false), + insChanged(true), + freqChanged(false), + sweepChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + modOn(false), + vol(32), + outVol(32), + wave(-1) { + memset(modTable,0,32); + } + }; + Channel chan[1]; + bool isMuted[1]; + DivWaveSynth ws; + unsigned char apuType; + struct _fds* fds; + unsigned char regPool[128]; + + void updateWave(); + + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformFDS(); +}; + +#endif diff --git a/src/engine/platform/gb.cpp b/src/engine/platform/gb.cpp index f02ea1970..13eac1f32 100644 --- a/src/engine/platform/gb.cpp +++ b/src/engine/platform/gb.cpp @@ -91,16 +91,11 @@ void DivPlatformGB::acquire(short* bufL, short* bufR, size_t start, size_t len) } void DivPlatformGB::updateWave() { - DivWavetable* wt=parent->getWave(chan[2].wave); rWrite(0x1a,0); for (int i=0; i<16; i++) { - if (wt->max<1 || wt->len<1) { - rWrite(0x30+i,0); - } else { - unsigned char nibble1=15-((wt->data[(i*2)*wt->len/32]*15)/wt->max); - unsigned char nibble2=15-((wt->data[(1+i*2)*wt->len/32]*15)/wt->max); - rWrite(0x30+i,(nibble1<<4)|nibble2); - } + int nibble1=15-ws.output[i<<1]; + int nibble2=15-ws.output[1+(i<<1)]; + rWrite(0x30+i,(nibble1<<4)|nibble2); } } @@ -154,46 +149,52 @@ static unsigned char noiseTable[256]={ void DivPlatformGB::tick() { for (int i=0; i<4; i++) { chan[i].std.next(); - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (i==3) { // noise - if (chan[i].std.arpMode) { - chan[i].baseFreq=chan[i].std.arp+24; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=chan[i].std.arp.val+24; } else { - chan[i].baseFreq=chan[i].note+chan[i].std.arp; + chan[i].baseFreq=chan[i].note+chan[i].std.arp.val; } if (chan[i].baseFreq>255) chan[i].baseFreq=255; if (chan[i].baseFreq<0) chan[i].baseFreq=0; } else { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp+24); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val+24); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); } } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - chan[i].duty=chan[i].std.duty; + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; DivInstrument* ins=parent->getIns(chan[i].ins); if (i!=2) { rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); } else { if (parent->song.waveDutyIsVol) { - rWrite(16+i*5+2,gbVolMap[(chan[i].std.duty&3)<<2]); + rWrite(16+i*5+2,gbVolMap[(chan[i].std.duty.val&3)<<2]); } } } - if (chan[i].std.hadWave) { - if (chan[i].wave!=chan[i].std.wave) { - chan[i].wave=chan[i].std.wave; - if (i==2) { + if (i==2 && chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val || ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + ws.changeWave1(chan[i].wave); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (i==2) { + if (chan[i].active) { + if (ws.tick()) { updateWave(); if (!chan[i].keyOff) chan[i].keyOn=true; } @@ -218,10 +219,6 @@ void DivPlatformGB::tick() { } if (chan[i].keyOn) { if (i==2) { // wave - if (chan[i].wave<0) { - chan[i].wave=0; - updateWave(); - } rWrite(16+i*5,0x80); rWrite(16+i*5+2,gbVolMap[chan[i].vol]); } else { @@ -257,7 +254,8 @@ void DivPlatformGB::muteChannel(int ch, bool mute) { int DivPlatformGB::dispatch(DivCommand c) { switch (c.cmd) { - case DIV_CMD_NOTE_ON: + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); if (c.value!=DIV_NOTE_NULL) { if (c.chan==3) { // noise chan[c.chan].baseFreq=c.value; @@ -269,8 +267,17 @@ int DivPlatformGB::dispatch(DivCommand c) { } chan[c.chan].active=true; chan[c.chan].keyOn=true; - chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + chan[c.chan].std.init(ins); + if (c.chan==2) { + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + ws.changeWave1(chan[c.chan].wave); + } + ws.init(ins,32,15,chan[c.chan].insChanged); + } + chan[c.chan].insChanged=false; break; + } case DIV_CMD_NOTE_OFF: chan[c.chan].active=false; chan[c.chan].keyOff=true; @@ -283,8 +290,13 @@ int DivPlatformGB::dispatch(DivCommand c) { case DIV_CMD_INSTRUMENT: if (chan[c.chan].ins!=c.value || c.value2==1) { chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; if (c.chan!=2) { - chan[c.chan].vol=parent->getIns(chan[c.chan].ins)->gb.envVol; + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + 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)); + } } } break; @@ -304,7 +316,7 @@ int DivPlatformGB::dispatch(DivCommand c) { case DIV_CMD_WAVE: if (c.chan!=2) break; chan[c.chan].wave=c.value; - updateWave(); + ws.changeWave1(chan[c.chan].wave); chan[c.chan].keyOn=true; break; case DIV_CMD_NOTE_PORTA: { @@ -340,13 +352,14 @@ int DivPlatformGB::dispatch(DivCommand c) { case DIV_CMD_PANNING: { lastPan&=~(0x11<0)|(((c.value>>4)>0)<<4); lastPan|=c.value<getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + if (writes.size()<16) { + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + } } if (++dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { dacPos=s->loopStart; } else { dacSample=-1; + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + } } } dacPeriod+=MAX(40,dacRate); @@ -118,7 +123,7 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s //printf("write: %x = %.2x\n",w.addr,w.val); lastBusy=0; regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); } else { lastBusy++; if (fm.write_busy==0) { @@ -133,17 +138,11 @@ void DivPlatformGenesis::acquire_nuked(short* bufL, short* bufR, size_t start, s //OPN2_Write(&fm,0,0); } - psgClocks+=psg.rate; - while (psgClocks>=rate) { - psgOut=(psg.acquireOne()*3)>>3; - psgClocks-=rate; - } - - os[0]=(os[0]<<5)+psgOut; + os[0]=(os[0]<<5); if (os[0]<-32768) os[0]=-32768; if (os[0]>32767) os[0]=32767; - os[1]=(os[1]<<5)+psgOut; + os[1]=(os[1]<<5); if (os[1]<-32768) os[1]=-32768; if (os[1]>32767) os[1]=32767; @@ -162,13 +161,18 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[5]) { - immWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + if (writes.size()<16) { + urgentWrite(0x2a,(unsigned char)s->data8[dacPos]+0x80); + } } if (++dacPos>=s->samples) { - if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { dacPos=s->loopStart; } else { dacSample=-1; + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + } } } dacPeriod+=MAX(40,dacRate); @@ -184,7 +188,7 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si fm_ymfm->write(0x0+((w.addr>>8)<<1),w.addr); fm_ymfm->write(0x1+((w.addr>>8)<<1),w.val); regPool[w.addr&0x1ff]=w.val; - writes.pop(); + writes.pop_front(); lastBusy=1; } @@ -197,17 +201,9 @@ void DivPlatformGenesis::acquire_ymfm(short* bufL, short* bufR, size_t start, si os[1]=out_ymfm.data[1]; //OPN2_Write(&fm,0,0); - psgClocks+=psg.rate; - while (psgClocks>=rate) { - psgOut=(psg.acquireOne()*3)>>3; - psgClocks-=rate; - } - - os[0]=os[0]+psgOut; if (os[0]<-32768) os[0]=-32768; if (os[0]>32767) os[0]=32767; - os[1]=os[1]+psgOut; if (os[1]<-32768) os[1]=-32768; if (os[1]>32767) os[1]=32767; @@ -229,8 +225,8 @@ void DivPlatformGenesis::tick() { if (i==2 && extMode) continue; chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -246,24 +242,24 @@ void DivPlatformGenesis::tick() { } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp); + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadAlg) { - chan[i].state.alg=chan[i].std.alg; + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); if (!parent->song.algMacroBehavior) for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; @@ -279,48 +275,48 @@ void DivPlatformGenesis::tick() { } } } - if (chan[i].std.hadFb) { - chan[i].state.fb=chan[i].std.fb; + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); } - if (chan[i].std.hadFms) { - chan[i].state.fms=chan[i].std.fms; + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); } - if (chan[i].std.hadAms) { - chan[i].state.ams=chan[i].std.ams; + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; DivMacroInt::IntOp& m=chan[i].std.op[j]; - if (m.hadAm) { - op.am=m.am; + if (m.am.had) { + op.am=m.am.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadAr) { - op.ar=m.ar; + if (m.ar.had) { + op.ar=m.ar.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDr) { - op.dr=m.dr; + if (m.dr.had) { + op.dr=m.dr.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadMult) { - op.mult=m.mult; + if (m.mult.had) { + op.mult=m.mult.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadRr) { - op.rr=m.rr; + if (m.rr.had) { + op.rr=m.rr.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadSl) { - op.sl=m.sl; + if (m.sl.had) { + op.sl=m.sl.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadTl) { - op.tl=127-m.tl; + if (m.tl.had) { + op.tl=127-m.tl.val; if (isMuted[i]) { rWrite(baseAddr+ADDR_TL,127); } else { @@ -331,26 +327,44 @@ void DivPlatformGenesis::tick() { } } } - if (m.hadRs) { - op.rs=m.rs; + if (m.rs.had) { + op.rs=m.rs.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDt) { - op.dt=m.dt; + if (m.dt.had) { + op.dt=m.dt.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadD2r) { - op.d2r=m.d2r; + if (m.d2r.had) { + op.d2r=m.d2r.val; rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); } - if (m.hadSsg) { - op.ssgEnv=m.ssg; + if (m.ssg.had) { + op.ssgEnv=m.ssg.val; rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); } } if (chan[i].keyOn || chan[i].keyOff) { + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + immWrite(baseAddr+ADDR_SL_RR,0x0f); + immWrite(baseAddr+ADDR_TL,0x7f); + oldWrites[baseAddr+ADDR_SL_RR]=-1; + oldWrites[baseAddr+ADDR_TL]=-1; + //rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + } immWrite(0x28,0x00|konOffs[i]); + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + for (int k=0; k<5; k++) { + immWrite(baseAddr+ADDR_SL_RR,0x0f); + } + } + } chan[i].keyOff=false; } } @@ -391,13 +405,6 @@ void DivPlatformGenesis::tick() { chan[i].keyOn=false; } } - - psg.tick(); - - for (DivRegWrite& i: psg.getRegisterWrites()) { - if (dumpWrites) addWrite(i.addr,i.val); - } - psg.getRegisterWrites().clear(); } int DivPlatformGenesis::octave(int freq) { @@ -442,10 +449,6 @@ int DivPlatformGenesis::toFreq(int freq) { } void DivPlatformGenesis::muteChannel(int ch, bool mute) { - if (ch>5) { - psg.muteChannel(ch-6,mute); - return; - } isMuted[ch]=mute; for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[ch]|opOffs[j]; @@ -464,10 +467,6 @@ void DivPlatformGenesis::muteChannel(int ch, bool mute) { } int DivPlatformGenesis::dispatch(DivCommand c) { - if (c.chan>5) { - c.chan-=6; - return psg.dispatch(c); - } switch (c.cmd) { case DIV_CMD_NOTE_ON: { DivInstrument* ins=parent->getIns(chan[c.chan].ins); @@ -489,6 +488,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (dumpWrites) addWrite(0xffff0002,0); break; } else { + rWrite(0x2b,1<<7); if (dumpWrites) addWrite(0xffff0000,dacSample); } dacPos=0; @@ -506,6 +506,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (dumpWrites) addWrite(0xffff0002,0); break; } else { + rWrite(0x2b,1<<7); if (dumpWrites) addWrite(0xffff0000,dacSample); } dacPos=0; @@ -522,7 +523,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { } chan[c.chan].std.init(ins); - if (!chan[c.chan].std.willVol) { + if (!chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } @@ -559,6 +560,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].portaPause=false; chan[c.chan].note=c.value; chan[c.chan].freqChanged=true; } @@ -570,6 +572,10 @@ int DivPlatformGenesis::dispatch(DivCommand c) { if (c.chan==5) { dacSample=-1; if (dumpWrites) addWrite(0xffff0002,0); + if (parent->song.brokenDACMode) { + rWrite(0x2b,0); + if (dacMode) break; + } } chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; @@ -590,7 +596,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } for (int i=0; i<4; i++) { @@ -619,16 +625,10 @@ int DivPlatformGenesis::dispatch(DivCommand c) { chan[c.chan].ins=c.value; break; case DIV_CMD_PANNING: { - switch (c.value) { - case 0x01: - chan[c.chan].pan=1; - break; - case 0x10: - chan[c.chan].pan=2; - break; - default: - chan[c.chan].pan=3; - break; + if (c.value==0) { + chan[c.chan].pan=3; + } else { + chan[c.chan].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); } rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); break; @@ -733,9 +733,11 @@ int DivPlatformGenesis::dispatch(DivCommand c) { unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - break; } + case DIV_CMD_FM_HARD_RESET: + chan[c.chan].hardReset=c.value; + break; case DIV_ALWAYS_SET_VOLUME: return 0; break; @@ -786,16 +788,13 @@ void DivPlatformGenesis::forceIns() { rWrite(0x2b,0x80); } immWrite(0x22,lfoValue); - psg.forceIns(); } void DivPlatformGenesis::toggleRegisterDump(bool enable) { DivDispatch::toggleRegisterDump(enable); - psg.toggleRegisterDump(enable); } void* DivPlatformGenesis::getChanState(int ch) { - if (ch>5) return psg.getChanState(ch-6); return &chan[ch]; } @@ -808,7 +807,7 @@ int DivPlatformGenesis::getRegisterPoolSize() { } void DivPlatformGenesis::reset() { - while (!writes.empty()) writes.pop(); + while (!writes.empty()) writes.pop_front(); memset(regPool,0,512); if (useYMFM) { fm_ymfm->reset(); @@ -844,12 +843,6 @@ void DivPlatformGenesis::reset() { immWrite(0x22,lfoValue); delay=0; - - // PSG - psg.reset(); - psg.getRegisterWrites().clear(); - psgClocks=0; - psgOut=0; } bool DivPlatformGenesis::isStereo() { @@ -865,17 +858,14 @@ bool DivPlatformGenesis::keyOffAffectsPorta(int ch) { } void DivPlatformGenesis::notifyInsChange(int ins) { - for (int i=0; i<10; i++) { - if (i>5) { - psg.notifyInsChange(ins); - } else if (chan[i].ins==ins) { + for (int i=0; i<6; i++) { + if (chan[i].ins==ins) { chan[i].insChanged=true; } } } void DivPlatformGenesis::notifyInsDeletion(void* ins) { - psg.notifyInsDeletion(ins); } void DivPlatformGenesis::poke(unsigned int addr, unsigned short val) { @@ -904,7 +894,6 @@ void DivPlatformGenesis::setFlags(unsigned int flags) { } else { chipClock=COLOR_NTSC*15.0/7.0; } - psg.setFlags(flags==1); ladder=flags&0x80000000; OPN2_SetChipType(ladder?ym3438_mode_ym2612:0); if (useYMFM) { @@ -929,7 +918,6 @@ int DivPlatformGenesis::init(DivEngine* p, int channels, int sugRate, unsigned i isMuted[i]=false; } fm_ymfm=NULL; - psg.init(p,4,sugRate,flags==1); setFlags(flags); reset(); @@ -938,7 +926,6 @@ int DivPlatformGenesis::init(DivEngine* p, int channels, int sugRate, unsigned i void DivPlatformGenesis::quit() { if (fm_ymfm!=NULL) delete fm_ymfm; - psg.quit(); } DivPlatformGenesis::~DivPlatformGenesis() { diff --git a/src/engine/platform/genesis.h b/src/engine/platform/genesis.h index 717c6c3dc..51d98d807 100644 --- a/src/engine/platform/genesis.h +++ b/src/engine/platform/genesis.h @@ -20,7 +20,7 @@ #ifndef _GENESIS_H #define _GENESIS_H #include "../dispatch.h" -#include +#include #include "../../../extern/Nuked-OPN2/ym3438.h" #include "sound/ymfm/ymfm_opn.h" @@ -38,7 +38,7 @@ class DivPlatformGenesis: public DivDispatch { unsigned char freqH, freqL; int freq, baseFreq, pitch, note; unsigned char ins; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta, hardReset; int vol, outVol; unsigned char pan; Channel(): @@ -57,6 +57,7 @@ class DivPlatformGenesis: public DivDispatch { portaPause(false), furnaceDac(false), inPorta(false), + hardReset(false), vol(0), pan(3) {} }; @@ -68,11 +69,8 @@ class DivPlatformGenesis: public DivDispatch { bool addrOrVal; QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} }; - std::queue writes; + std::deque writes; ym3438_t fm; - DivPlatformSMS psg; - int psgClocks; - int psgOut; int delay; unsigned char lastBusy; diff --git a/src/engine/platform/genesisext.cpp b/src/engine/platform/genesisext.cpp index 364505db2..60f877b7e 100644 --- a/src/engine/platform/genesisext.cpp +++ b/src/engine/platform/genesisext.cpp @@ -73,6 +73,7 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { opChan[ch].baseFreq=NOTE_FREQUENCY(c.value); + opChan[ch].portaPause=false; opChan[ch].freqChanged=true; } opChan[ch].keyOn=true; @@ -106,18 +107,17 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { opChan[ch].ins=c.value; break; case DIV_CMD_PANNING: { - switch (c.value) { - case 0x01: - opChan[ch].pan=1; - break; - case 0x10: - opChan[ch].pan=2; - break; - default: - opChan[ch].pan=3; - break; + if (c.value==0) { + opChan[ch].pan=3; + } else { + opChan[ch].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); + } + if (parent->song.sharedExtStat) { + for (int i=0; i<4; i++) { + if (ch==i) continue; + opChan[i].pan=opChan[ch].pan; + } } - // TODO: ??? rWrite(chanOffs[2]+0xb4,(opChan[ch].pan<<6)|(chan[2].state.fms&7)|((chan[2].state.ams&3)<<4)); break; } @@ -164,6 +164,11 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { opChan[ch].freqChanged=true; break; } + case DIV_CMD_FM_LFO: { + lfoValue=(c.value&7)|((c.value>>4)<<3); + rWrite(0x22,lfoValue); + break; + } case DIV_CMD_FM_MULT: { // TODO unsigned short baseAddr=chanOffs[2]|opOffs[orderedOps[c.value]]; DivInstrumentFM::Operator& op=chan[2].state.op[orderedOps[c.value]]; @@ -175,7 +180,9 @@ int DivPlatformGenesisExt::dispatch(DivCommand c) { unsigned short baseAddr=chanOffs[2]|opOffs[orderedOps[c.value]]; DivInstrumentFM::Operator& op=chan[2].state.op[orderedOps[c.value]]; op.tl=c.value2; - if (isOutput[chan[2].state.alg][c.value]) { + if (isOpMuted[ch]) { + rWrite(baseAddr+0x40,127); + } else if (isOutput[chan[2].state.alg][c.value]) { rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[ch].vol&0x7f))/127)); } else { rWrite(baseAddr+0x40,op.tl); @@ -313,7 +320,47 @@ void DivPlatformGenesisExt::tick() { } void DivPlatformGenesisExt::forceIns() { - DivPlatformGenesis::forceIns(); + for (int i=0; i<6; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (i==2) { // extended channel + if (isOpMuted[j]) { + rWrite(baseAddr+0x40,127); + } else if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[j].vol&0x7f))/127)); + } else { + rWrite(baseAddr+0x40,op.tl); + } + } else { + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + 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)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + if (dacMode) { + rWrite(0x2b,0x80); + } + immWrite(0x22,lfoValue); for (int i=0; i<4; i++) { opChan[i].insChanged=true; if (opChan[i].active) { @@ -378,4 +425,4 @@ void DivPlatformGenesisExt::quit() { } DivPlatformGenesisExt::~DivPlatformGenesisExt() { -} \ No newline at end of file +} diff --git a/src/engine/platform/genesisshared.h b/src/engine/platform/genesisshared.h index 22e4cd826..d58d339e6 100644 --- a/src/engine/platform/genesisshared.h +++ b/src/engine/platform/genesisshared.h @@ -35,7 +35,7 @@ static bool isOutput[8][4]={ {true ,true ,true ,true}, }; static unsigned char dtTable[8]={ - 7,6,5,0,1,2,3,0 + 7,6,5,0,1,2,3,4 }; static int orderedOps[4]={ @@ -43,6 +43,18 @@ static int orderedOps[4]={ }; #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} -#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define immWrite(a,v) if (!skipRegisterWrites) {writes.push_back(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } +#define urgentWrite(a,v) if (!skipRegisterWrites) { \ + if (writes.empty()) { \ + writes.push_back(QueuedWrite(a,v)); \ + } else if (writes.size()>16 || writes.front().addrOrVal) { \ + writes.push_back(QueuedWrite(a,v)); \ + } else { \ + writes.push_front(QueuedWrite(a,v)); \ + } \ + if (dumpWrites) { \ + addWrite(a,v); \ + } \ +} -#include "fmshared_OPN.h" \ No newline at end of file +#include "fmshared_OPN.h" diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 2783ba5d5..70e7bc413 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -82,7 +82,44 @@ static int32_t clamp(int32_t v, int32_t lo, int32_t hi) } const char* regCheatSheetLynx[]={ - "DATA", "0", + "AUDIO0_VOLCNTRL", "20", + "AUDIO0_FEEDBACK", "21", + "AUDIO0_OUTPUT", "22", + "AUDIO0_SHIFT", "23", + "AUDIO0_BACKUP", "24", + "AUDIO0_CONTROL", "25", + "AUDIO0_COUNTER", "26", + "AUDIO0_OTHER", "27", + "AUDIO1_VOLCNTRL", "28", + "AUDIO1_FEEDBACK", "29", + "AUDIO1_OUTPUT", "2a", + "AUDIO1_SHIFT", "2b", + "AUDIO1_BACKUP", "2c", + "AUDIO1_CONTROL", "2d", + "AUDIO1_COUNTER", "2e", + "AUDIO1_OTHER", "2f", + "AUDIO2_VOLCNTRL", "30", + "AUDIO2_FEEDBACK", "31", + "AUDIO2_OUTPUT", "32", + "AUDIO2_SHIFT", "33", + "AUDIO2_BACKUP", "34", + "AUDIO2_CONTROL", "35", + "AUDIO2_COUNTER", "36", + "AUDIO2_OTHER", "37", + "AUDIO3_VOLCNTRL", "38", + "AUDIO3_FEEDBACK", "39", + "AUDIO3_OUTPUT", "3a", + "AUDIO3_SHIFT", "3b", + "AUDIO3_BACKUP", "3c", + "AUDIO3_CONTROL", "3d", + "AUDIO3_COUNTER", "3e", + "AUDIO3_OTHER", "3f", + "ATTENREG0", "40", + "ATTENREG1", "41", + "ATTENREG2", "42", + "ATTENREG3", "43", + "MPAN", "44", + "MSTEREO", "50", NULL }; @@ -111,23 +148,23 @@ void DivPlatformLynx::acquire(short* bufL, short* bufR, size_t start, size_t len void DivPlatformLynx::tick() { for (int i=0; i<4; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol&127)*MIN(127,chan[i].std.vol))>>7; + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol&127)*MIN(127,chan[i].std.vol.val))>>7; WRITE_VOLUME(i,(isMuted[i]?0:(chan[i].outVol&127))); } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); - chan[i].actualNote=chan[i].std.arp; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + chan[i].actualNote=chan[i].std.arp.val; } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); - chan[i].actualNote=chan[i].note+chan[i].std.arp; + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + chan[i].actualNote=chan[i].note+chan[i].std.arp.val; } chan[i].freqChanged=true; } } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].actualNote=chan[i].note; chan[i].freqChanged=true; @@ -141,15 +178,15 @@ void DivPlatformLynx::tick() { chan[i].lfsr=-1; } chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); - if (chan[i].std.hadDuty) { - chan[i].duty=chan[i].std.duty; + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; WRITE_FEEDBACK(i, chan[i].duty.feedback); } WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); WRITE_BACKUP( i, chan[i].fd.backup ); } - else if (chan[i].std.hadDuty) { - chan[i].duty = chan[i].std.duty; + else if (chan[i].std.duty.had) { + chan[i].duty = chan[i].std.duty.val; WRITE_FEEDBACK(i, chan[i].duty.feedback); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); } @@ -191,18 +228,18 @@ int DivPlatformLynx::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (chan[c.chan].active) WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); } break; case DIV_CMD_PANNING: - chan[c.chan].pan=((c.value&0x0f)<<4)|((c.value&0xf0)>>4); + chan[c.chan].pan=c.value; WRITE_ATTEN(c.chan,chan[c.chan].pan); break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { + if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; @@ -235,7 +272,7 @@ int DivPlatformLynx::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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; chan[c.chan].actualNote=c.value; diff --git a/src/engine/platform/mmc5.cpp b/src/engine/platform/mmc5.cpp new file mode 100644 index 000000000..79d9fc61e --- /dev/null +++ b/src/engine/platform/mmc5.cpp @@ -0,0 +1,414 @@ +/** + * 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 "mmc5.h" +#include "sound/nes/mmc5.h" +#include "../engine.h" +#include + +#define CHIP_DIVIDER 16 + +#define rWrite(a,v) if (!skipRegisterWrites) {extcl_cpu_wr_mem_MMC5(mmc5,a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } + +const char* regCheatSheetMMC5[]={ + "S0Volume", "5000", + "S0PeriodL", "5002", + "S0PeriodH", "5003", + "S1Volume", "5004", + "S1PeriodL", "5006", + "S1PeriodH", "5007", + "PCMControl", "4010", + "PCMWrite", "4011", + "APUControl", "4015", + NULL +}; + +const char** DivPlatformMMC5::getRegisterSheet() { + return regCheatSheetMMC5; +} + +const char* DivPlatformMMC5::getEffectName(unsigned char effect) { + switch (effect) { + case 0x12: + return "12xx: Set duty cycle/noise mode (pulse: 0 to 3; noise: 0 or 1)"; + break; + } + return NULL; +} + +void DivPlatformMMC5::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; i=rate) { + DivSample* s=parent->getSample(dacSample); + if (s->samples>0) { + if (!isMuted[4]) { + 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; + } + } + dacPeriod-=rate; + } else { + dacSample=-1; + } + } + } + + extcl_envelope_clock_MMC5(mmc5); + extcl_length_clock_MMC5(mmc5); + extcl_apu_tick_MMC5(mmc5); + if (mmc5->clocked) { + mmc5->clocked=false; + } + int sample=isMuted[0]?0:(mmc5->S3.output*10); + if (!isMuted[1]) { + sample+=mmc5->S4.output*10; + } + if (!isMuted[2]) { + sample+=mmc5->pcm.output*2; + } + if (sample>32767) sample=32767; + if (sample<-32768) sample=-32768; + bufL[i]=sample; + } +} + +void DivPlatformMMC5::tick() { + for (int i=0; i<2; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + // ok, why are the volumes like that? + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); + if (chan[i].outVol<0) chan[i].outVol=0; + rWrite(0x5000+i*4,0x30|chan[i].outVol|((chan[i].duty&3)<<6)); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + rWrite(0x5000+i*4,0x30|chan[i].outVol|((chan[i].duty&3)<<6)); + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; + if (chan[i].freq>2047) chan[i].freq=2047; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].keyOn) { + //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)); + } + if (chan[i].keyOff) { + //rWrite(16+i*5+2,8); + rWrite(0x5000+i*4,0x30); + } + rWrite(0x5002+i*4,chan[i].freq&0xff); + if ((chan[i].prevFreq>>8)!=(chan[i].freq>>8)) { + rWrite(0x5003+i*4,0xf8|(chan[i].freq>>8)); + } + if (chan[i].freq!=65535 && chan[i].freq!=0) { + chan[i].prevFreq=chan[i].freq; + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } + + // PCM + if (chan[4].freqChanged) { + chan[4].freq=parent->calcFreq(chan[4].baseFreq,chan[4].pitch,false); + if (chan[4].furnaceDac) { + double off=1.0; + if (dacSample>=0 && dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(dacSample); + off=(double)s->centerRate/8363.0; + } + dacRate=MIN(chan[4].freq*off,32000); + if (dumpWrites) addWrite(0xffff0001,dacRate); + } + chan[4].freqChanged=false; + } +} + +int DivPlatformMMC5::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if (c.chan==2) { // PCM + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (ins->type==DIV_INS_AMIGA) { + dacSample=ins->amiga.initSample; + if (dacSample<0 || dacSample>=parent->song.sampleLen) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000,dacSample); + } + dacPos=0; + dacPeriod=0; + chan[c.chan].baseFreq=parent->song.tuning*pow(2.0f,((float)(c.value+3)/12.0f)); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].furnaceDac=true; + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + } + dacSample=12*sampleBank+chan[c.chan].note%12; + if (dacSample>=parent->song.sampleLen) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000,dacSample); + } + dacPos=0; + dacPeriod=0; + dacRate=parent->getSample(dacSample)->rate; + if (dumpWrites) addWrite(0xffff0001,dacRate); + chan[c.chan].furnaceDac=false; + } + break; + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + rWrite(0x5000+c.chan*4,0x30|chan[c.chan].vol|((chan[c.chan].duty&3)<<6)); + break; + case DIV_CMD_NOTE_OFF: + if (c.chan==2) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + } + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + if (chan[c.chan].active) { + rWrite(0x5000+c.chan*4,0x30|chan[c.chan].vol|((chan[c.chan].duty&3)<<6)); + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + chan[c.chan].duty=c.value; + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + 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))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformMMC5::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformMMC5::forceIns() { + for (int i=0; i<3; i++) { + chan[i].insChanged=true; + chan[i].prevFreq=65535; + } +} + +void* DivPlatformMMC5::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformMMC5::getRegisterPool() { + return regPool; +} + +int DivPlatformMMC5::getRegisterPoolSize() { + return 32; +} + +float DivPlatformMMC5::getPostAmp() { + return 64.0f; +} + +void DivPlatformMMC5::reset() { + for (int i=0; i<3; i++) { + chan[i]=DivPlatformMMC5::Channel(); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + dacPeriod=0; + dacPos=0; + dacRate=0; + dacSample=-1; + sampleBank=0; + + map_init_MMC5(mmc5); + memset(regPool,0,128); + + rWrite(0x5015,0x03); + rWrite(0x5010,0x00); +} + +bool DivPlatformMMC5::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformMMC5::setFlags(unsigned int flags) { + if (flags==2) { // Dendy + rate=COLOR_PAL*2.0/5.0; + } else if (flags==1) { // PAL + rate=COLOR_PAL*3.0/8.0; + } else { // NTSC + rate=COLOR_NTSC/2.0; + } + chipClock=rate; +} + +void DivPlatformMMC5::notifyInsDeletion(void* ins) { + for (int i=0; i<3; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformMMC5::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformMMC5::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformMMC5::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + apuType=flags; + dumpWrites=false; + skipRegisterWrites=false; + mmc5=new struct _mmc5; + for (int i=0; i<3; i++) { + isMuted[i]=false; + //mmc5->muted[i]=false; // TODO + } + setFlags(flags); + + init_nla_table(500,500); + reset(); + return 5; +} + +void DivPlatformMMC5::quit() { + delete mmc5; +} + +DivPlatformMMC5::~DivPlatformMMC5() { +} diff --git a/src/engine/platform/mmc5.h b/src/engine/platform/mmc5.h new file mode 100644 index 000000000..6b364d5ad --- /dev/null +++ b/src/engine/platform/mmc5.h @@ -0,0 +1,89 @@ +/** + * 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 _MMC5_H +#define _MMC5_H + +#include "../dispatch.h" +#include "../macroInt.h" + +class DivPlatformMMC5: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, prevFreq, note; + unsigned char ins, duty, sweep; + bool active, insChanged, freqChanged, sweepChanged, keyOn, keyOff, inPorta, furnaceDac; + signed char vol, outVol, wave; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + prevFreq(65535), + note(0), + ins(-1), + duty(0), + sweep(8), + active(false), + insChanged(true), + freqChanged(false), + sweepChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + furnaceDac(false), + vol(15), + outVol(15), + wave(-1) {} + }; + Channel chan[5]; + bool isMuted[5]; + int dacPeriod, dacRate; + unsigned int dacPos; + int dacSample; + unsigned char sampleBank; + unsigned char apuType; + struct _mmc5* mmc5; + unsigned char regPool[128]; + + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + float getPostAmp(); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformMMC5(); +}; + +#endif diff --git a/src/engine/platform/n163.cpp b/src/engine/platform/n163.cpp new file mode 100644 index 000000000..7c9188d49 --- /dev/null +++ b/src/engine/platform/n163.cpp @@ -0,0 +1,665 @@ +/** + * 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 "n163.h" +#include "../engine.h" +#include + +#define rRead(a,v) n163.addr_w(a); n163.data_r(v); +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define rWriteMask(a,v,m) if (!skipRegisterWrites) {writes.emplace(a,v,m); if (dumpWrites) {addWrite(a,v);} } +#define chWrite(c,a,v) \ + if (c<=chanMax) { \ + rWrite(0x78-(c<<3)+(a&7),v) \ + } + +#define chWriteMask(c,a,v,m) \ + if (c<=chanMax) { \ + rWriteMask(0x78-(c<<3)+(a&7),v,m) \ + } + +#define CHIP_FREQBASE (15*32768) + +const char* regCheatSheetN163[]={ + "FreqL7", "40", + "AccL7", "41", + "FreqM7", "42", + "AccM7", "43", + "WavLen_FreqH7", "44", + "AccH7", "45", + "WavPos7", "46", + "Vol7", "47", + "FreqL6", "48", + "AccL6", "49", + "FreqM6", "4A", + "AccM6", "4B", + "WavLen_FreqH6", "4C", + "AccH6", "4D", + "WavPos6", "4E", + "Vol6", "4F", + "FreqL5", "50", + "AccL5", "51", + "FreqM5", "52", + "AccM5", "53", + "WavLen_FreqH5", "54", + "AccH5", "55", + "WavPos5", "56", + "Vol5", "57", + "FreqL4", "58", + "AccL4", "59", + "FreqM4", "5A", + "AccM4", "5B", + "WavLen_FreqH4", "5C", + "AccH4", "5D", + "WavPos4", "5E", + "Vol4", "5F", + "FreqL3", "60", + "AccL3", "61", + "FreqM3", "62", + "AccM3", "63", + "WavLen_FreqH3", "64", + "AccH3", "65", + "WavPos3", "66", + "Vol3", "67", + "FreqL2", "68", + "AccL2", "69", + "FreqM2", "6A", + "AccM2", "6B", + "WavLen_FreqH2", "6C", + "AccH2", "6D", + "WavPos2", "6E", + "Vol2", "6F", + "FreqL1", "70", + "AccL1", "71", + "FreqM1", "72", + "AccM1", "73", + "WavLen_FreqH1", "74", + "AccH1", "75", + "WavPos1", "76", + "Vol1", "77", + "FreqL0", "78", + "AccL0", "79", + "FreqM0", "7A", + "AccM0", "7B", + "WavLen_FreqH0", "7C", + "AccH0", "7D", + "WavPos0", "7E", + "ChanMax_Vol0", "7F", + NULL +}; + +const char** DivPlatformN163::getRegisterSheet() { + return regCheatSheetN163; +} + +const char* DivPlatformN163::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Select waveform"; + break; + case 0x11: + return "11xx: Set waveform position in RAM (single nibble unit)"; + break; + case 0x12: + return "12xx: Set waveform length in RAM (04 to FC, 4 nibble unit)"; + break; + case 0x13: + return "130x: Change waveform update mode (0: off, bit 0: update now, bit 1: update when every waveform changes)"; + break; + case 0x14: + return "14xx: Select waveform for load to RAM"; + break; + case 0x15: + return "15xx: Set waveform position for load to RAM (single nibble unit)"; + break; + case 0x16: + return "16xx: Set waveform length for load to RAM (04 to FC, 4 nibble unit)"; + break; + case 0x17: + return "170x: Change waveform load mode (0: off, bit 0: load now, bit 1: load when every waveform changes)"; + break; + case 0x18: + return "180x: Change channel limits (0 to 7, x + 1)"; + break; + case 0x20: + return "20xx: (Global) Select waveform for load to RAM"; + break; + case 0x21: + return "21xx: (Global) Set waveform position for load to RAM (single nibble unit)"; + break; + case 0x22: + return "22xx: (Global) Set waveform length for load to RAM (04 to FC, 4 nibble unit)"; + break; + case 0x23: + return "230x: (Global) Change waveform load mode (0: off, bit 0: load now, bit 1: load when every waveform changes)"; + break; + } + return NULL; +} + +void DivPlatformN163::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; i32767) out=32767; + if (out<-32768) out=-32768; + bufL[i]=bufR[i]=out; + + // command queue + while (!writes.empty()) { + QueuedWrite w=writes.front(); + n163.addr_w(w.addr); + n163.data_w((n163.data_r()&~w.mask)|(w.val&w.mask)); + writes.pop(); + } + } +} + +void DivPlatformN163::updateWave(int wave, int pos, int len) { + len&=0xfc; // 4 nibble boundary + DivWavetable* wt=parent->getWave(wave); + for (int i=0; i=((0x78-(chanMax<<3))<<1)) { // avoid conflict with channel register area + break; + } + unsigned char mask=(addr&1)?0xf0:0x0f; + if (wt->max<1 || wt->len<1) { + rWriteMask(addr>>1,0,mask); + } else { + int data=wt->data[i*wt->len/len]*15/wt->max; + if (data<0) data=0; + if (data>15) data=15; + rWriteMask(addr>>1,(addr&1)?(data<<4):(data&0xf),mask); + } + } +} + +void DivPlatformN163::updateWaveCh(int ch) { + if (ch<=chanMax) { + updateWave(chan[ch].wave,chan[ch].wavePos,chan[ch].waveLen); + if (chan[ch].active && !isMuted[ch]) { + chan[ch].volumeChanged=true; + } + } +} + +void DivPlatformN163::tick() { + for (int i=0; i<=chanMax; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=(MIN(15,chan[i].std.vol.val)*(chan[i].vol&15))/15; + if (chan[i].outVol<0) chan[i].outVol=0; + if (chan[i].outVol>15) chan[i].outVol=15; + if (chan[i].resVol!=chan[i].outVol) { + chan[i].resVol=chan[i].outVol; + if (!isMuted[i]) { + chan[i].volumeChanged=true; + } + } + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.duty.had) { + if (chan[i].wavePos!=chan[i].std.duty.val) { + chan[i].wavePos=chan[i].std.duty.val; + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + } + chan[i].waveChanged=true; + } + } + if (chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val) { + chan[i].wave=chan[i].std.wave.val; + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + } + } + } + if (chan[i].std.ex1.had) { + if (chan[i].waveLen!=(chan[i].std.ex1.val&0xfc)) { + chan[i].waveLen=chan[i].std.ex1.val&0xfc; + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + } + chan[i].freqChanged=true; + } + } + if (chan[i].std.ex2.had) { + if ((chan[i].waveMode&0x2)!=(chan[i].std.ex2.val&0x2)) { // update when every waveform changed + chan[i].waveMode=(chan[i].waveMode&~0x2)|(chan[i].std.ex2.val&0x2); + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + chan[i].waveChanged=true; + } + } + if ((chan[i].waveMode&0x1)!=(chan[i].std.ex2.val&0x1)) { // update waveform now + chan[i].waveMode=(chan[i].waveMode&~0x1)|(chan[i].std.ex2.val&0x1); + if (chan[i].waveMode&0x1) { // rising edge + chan[i].waveUpdated=true; + chan[i].waveChanged=true; + } + } + } + if (chan[i].std.ex3.had) { + if (chan[i].loadWave!=chan[i].std.ex3.val) { + chan[i].loadWave=chan[i].std.ex3.val; + if (chan[i].loadMode&0x2) { + updateWave(chan[i].loadWave,chan[i].loadPos,chan[i].loadLen&0xfc); + } + } + } + if (chan[i].std.alg.had) { + if (chan[i].loadPos!=chan[i].std.alg.val) { + chan[i].loadPos=chan[i].std.alg.val; + } + } + if (chan[i].std.fb.had) { + if (chan[i].loadLen!=(chan[i].std.fb.val&0xfc)) { + chan[i].loadLen=chan[i].std.fb.val&0xfc; + } + } + if (chan[i].std.fms.had) { + if ((chan[i].loadMode&0x2)!=(chan[i].std.fms.val&0x2)) { // load when every waveform changes + chan[i].loadMode=(chan[i].loadMode&~0x2)|(chan[i].std.fms.val&0x2); + } + if ((chan[i].loadMode&0x1)!=(chan[i].std.fms.val&0x1)) { // load now + chan[i].loadMode=(chan[i].loadMode&~0x1)|(chan[i].std.fms.val&0x1); + if (chan[i].loadMode&0x1) { // rising edge + updateWave(chan[i].loadWave,chan[i].loadPos,chan[i].loadLen&0xfc); + } + } + } + if (chan[i].volumeChanged) { + if (chan[i].active && !isMuted[i]) { + chWriteMask(i,0x7,chan[i].resVol&0xf,0xf); + } else { + chWriteMask(i,0x7,0,0xf); + } + chan[i].volumeChanged=false; + } + if (chan[i].waveChanged) { + chWrite(i,0x6,chan[i].wavePos); + if (chan[i].active) { + chan[i].freqChanged=true; + } + chan[i].waveChanged=false; + } + if (chan[i].waveUpdated) { + updateWaveCh(i); + if (chan[i].active) { + if (!chan[i].keyOff) chan[i].keyOn=true; + } + chan[i].waveUpdated=false; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + chan[i].freq=parent->calcFreq((((chan[i].baseFreq*chan[i].waveLen)*(chanMax+1))/16),chan[i].pitch,false,0); + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>0x3ffff) chan[i].freq=0x3ffff; + if (chan[i].keyOn) { + if (chan[i].wave<0) { + chan[i].wave=0; + if (chan[i].waveMode&0x2) { + updateWaveCh(i); + } + } + } + if (chan[i].keyOff && !isMuted[i]) { + chWriteMask(i,0x7,0,0xf); + } + chWrite(i,0x0,chan[i].freq&0xff); + chWrite(i,0x2,chan[i].freq>>8); + chWrite(i,0x4,((256-chan[i].waveLen)&0xfc)|((chan[i].freq>>16)&3)); + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformN163::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (chan[c.chan].insChanged) { + chan[c.chan].wave=ins->n163.wave; + chan[c.chan].wavePos=ins->n163.wavePos; + chan[c.chan].waveLen=ins->n163.waveLen; + chan[c.chan].waveMode=ins->n163.waveMode; + chan[c.chan].waveChanged=true; + if (chan[c.chan].waveMode&0x3) { + chan[c.chan].waveUpdated=true; + } + chan[c.chan].insChanged=false; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].resVol=chan[c.chan].vol; + if (!isMuted[c.chan]) { + chan[c.chan].volumeChanged=true; + } + chan[c.chan].std.init(ins); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + //chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].std.release(); + break; + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].insChanged=true; + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + chan[c.chan].resVol=chan[c.chan].outVol; + } else { + chan[c.chan].resVol=chan[c.chan].vol; + } + if (!isMuted[c.chan]) { + chan[c.chan].volumeChanged=true; + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + if (chan[c.chan].waveMode&0x2) { + chan[c.chan].waveUpdated=true; + } + chan[c.chan].keyOn=true; + break; + case DIV_CMD_N163_WAVE_POSITION: + chan[c.chan].wavePos=c.value; + if (chan[c.chan].waveMode&0x2) { + chan[c.chan].waveUpdated=true; + } + chan[c.chan].waveChanged=true; + break; + case DIV_CMD_N163_WAVE_LENGTH: + chan[c.chan].waveLen=c.value&0xfc; + if (chan[c.chan].waveMode&0x2) { + chan[c.chan].waveUpdated=true; + } + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_N163_WAVE_MODE: + chan[c.chan].waveMode=c.value&0x3; + if (chan[c.chan].waveMode&0x3) { // update now + chan[c.chan].waveUpdated=true; + chan[c.chan].waveChanged=true; + } + break; + case DIV_CMD_N163_WAVE_LOAD: + chan[c.chan].loadWave=c.value; + if (chan[c.chan].loadMode&0x2) { // load when every waveform changes + updateWave(chan[c.chan].loadWave,chan[c.chan].loadPos,chan[c.chan].loadLen); + } + break; + case DIV_CMD_N163_WAVE_LOADPOS: + chan[c.chan].loadPos=c.value; + break; + case DIV_CMD_N163_WAVE_LOADLEN: + chan[c.chan].loadLen=c.value&0xfc; + break; + case DIV_CMD_N163_WAVE_LOADMODE: + chan[c.chan].loadMode=c.value&0x3; + if (chan[c.chan].loadMode&0x1) { // load now + updateWave(chan[c.chan].loadWave,chan[c.chan].loadPos,chan[c.chan].loadLen); + } + break; + case DIV_CMD_N163_GLOBAL_WAVE_LOAD: + loadWave=c.value; + if (loadMode&0x2) { // load when every waveform changes + updateWave(loadWave,loadPos,loadLen); + } + break; + case DIV_CMD_N163_GLOBAL_WAVE_LOADPOS: + loadPos=c.value; + break; + case DIV_CMD_N163_GLOBAL_WAVE_LOADLEN: + loadLen=c.value&0xfc; + break; + case DIV_CMD_N163_GLOBAL_WAVE_LOADMODE: + loadMode=c.value&0x3; + if (loadMode&0x3) { // load now + updateWave(loadWave,loadPos,loadLen); + } + break; + case DIV_CMD_N163_CHANNEL_LIMIT: + if (chanMax!=(c.value&0x7)) { + chanMax=c.value&0x7; + rWriteMask(0x7f,chanMax<<4,0x70); + forceIns(); + } + 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].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) { + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + chan[c.chan].keyOn=true; + } + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformN163::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + chan[ch].volumeChanged=true; +} + +void DivPlatformN163::forceIns() { + for (int i=0; i<=chanMax; i++) { + chan[i].insChanged=true; + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + chan[i].volumeChanged=true; + chan[i].waveChanged=true; + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + } + } + } +} + +void DivPlatformN163::notifyWaveChange(int wave) { + for (int i=0; i<8; i++) { + if (chan[i].wave==wave) { + if (chan[i].waveMode&0x2) { + chan[i].waveUpdated=true; + } + } + } +} + +void DivPlatformN163::notifyInsChange(int ins) { + for (int i=0; i<8; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformN163::notifyInsDeletion(void* ins) { + for (int i=0; i<8; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void* DivPlatformN163::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformN163::getRegisterPool() { + for (int i=0; i<128; i++) { + regPool[i]=n163.reg(i); + } + return regPool; +} + +int DivPlatformN163::getRegisterPoolSize() { + return 128; +} + +void DivPlatformN163::reset() { + while (!writes.empty()) writes.pop(); + for (int i=0; i<8; i++) { + chan[i]=DivPlatformN163::Channel(); + } + + n163.reset(); + memset(regPool,0,128); + + n163.set_disable(false); + n163.set_multiplex(multiplex); + chanMax=initChanMax; + loadWave=-1; + loadPos=0; + loadLen=0; + loadMode=0; + rWrite(0x7f,initChanMax<<4); +} + +void DivPlatformN163::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformN163::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +void DivPlatformN163::setFlags(unsigned int flags) { + switch (flags&0xf) { + case 0x0: // NTSC + rate=COLOR_NTSC/2.0; + break; + case 0x1: // PAL + rate=COLOR_PAL*3.0/8.0; + break; + case 0x2: // Dendy + rate=COLOR_PAL*2.0/5.0; + break; + } + initChanMax=chanMax=(flags>>4)&7; + multiplex=((flags>>7)&1)?false:true; // not accurate in real hardware + chipClock=rate; + rate/=15; + n163.set_multiplex(multiplex); + rWrite(0x7f,initChanMax<<4); +} + +int DivPlatformN163::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<8; i++) { + isMuted[i]=false; + } + setFlags(flags); + + reset(); + + return 8; +} + +void DivPlatformN163::quit() { +} + +DivPlatformN163::~DivPlatformN163() { +} diff --git a/src/engine/platform/n163.h b/src/engine/platform/n163.h new file mode 100644 index 000000000..70f842adf --- /dev/null +++ b/src/engine/platform/n163.h @@ -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. + */ + +#ifndef _N163_H +#define _N163_H + +#include "../dispatch.h" +#include +#include "../macroInt.h" +#include "sound/n163/n163.hpp" + +class DivPlatformN163: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + short ins, wave, wavePos, waveLen; + unsigned char waveMode; + short loadWave, loadPos, loadLen; + unsigned char loadMode; + bool active, insChanged, freqChanged, volumeChanged, waveChanged, waveUpdated, keyOn, keyOff, inPorta; + signed char vol, outVol, resVol; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + wave(-1), + wavePos(0), + waveLen(0), + waveMode(0), + loadWave(-1), + loadPos(0), + loadLen(0), + loadMode(0), + active(false), + insChanged(true), + freqChanged(false), + volumeChanged(false), + waveChanged(false), + waveUpdated(false), + keyOn(false), + keyOff(false), + inPorta(false), + vol(15), + outVol(15), + resVol(15) {} + }; + Channel chan[8]; + bool isMuted[8]; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + unsigned char mask; + QueuedWrite(unsigned char a, unsigned char v, unsigned char m=~0): addr(a), val(v), mask(m) {} + }; + std::queue writes; + unsigned char initChanMax; + unsigned char chanMax; + short loadWave, loadPos, loadLen; + unsigned char loadMode; + bool multiplex; + + n163_core n163; + unsigned char regPool[128]; + void updateWave(int wave, int pos, int len); + void updateWaveCh(int ch); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void setFlags(unsigned int flags); + void notifyWaveChange(int wave); + void notifyInsChange(int ins); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformN163(); +}; + +#endif diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 897d8e842..9f56e4e56 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -23,6 +23,8 @@ #include #include +struct _nla_table nla_table; + #define CHIP_DIVIDER 16 #define rWrite(a,v) if (!skipRegisterWrites) {apu_wr_reg(nes,a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} } @@ -78,10 +80,17 @@ void DivPlatformNES::acquire(short* bufL, short* bufR, size_t start, size_t len) DivSample* s=parent->getSample(dacSample); if (s->samples>0) { if (!isMuted[4]) { - rWrite(0x4011,((unsigned char)s->data8[dacPos]+0x80)>>1); + unsigned char next=((unsigned char)s->data8[dacPos]+0x80)>>1; + if (dacAntiClickOn && dacAntiClick=s->samples) { - if (s->loopStart>=0 && s->loopStart<=(int)s->samples) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { dacPos=s->loopStart; } else { dacSample=-1; @@ -99,7 +108,7 @@ void DivPlatformNES::acquire(short* bufL, short* bufR, size_t start, size_t len) if (nes->apu.clocked) { nes->apu.clocked=false; } - int sample=(pulse_output(nes)+tnd_output(nes)-128)<<7; + int sample=(pulse_output(nes)+tnd_output(nes)); if (sample>32767) sample=32767; if (sample<-32768) sample=-32768; bufL[i]=sample; @@ -134,9 +143,9 @@ static unsigned char noiseTable[253]={ void DivPlatformNES::tick() { for (int i=0; i<4; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { + if (chan[i].std.vol.had) { // ok, why are the volumes like that? - chan[i].outVol=MIN(15,chan[i].std.vol)-(15-(chan[i].vol&15)); + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); if (chan[i].outVol<0) chan[i].outVol=0; if (i==2) { // triangle rWrite(0x4000+i*4,(chan[i].outVol==0)?0:255); @@ -145,33 +154,33 @@ void DivPlatformNES::tick() { rWrite(0x4000+i*4,0x30|chan[i].outVol|((chan[i].duty&3)<<6)); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (i==3) { // noise - if (chan[i].std.arpMode) { - chan[i].baseFreq=chan[i].std.arp; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=chan[i].std.arp.val; } else { - chan[i].baseFreq=chan[i].note+chan[i].std.arp; + chan[i].baseFreq=chan[i].note+chan[i].std.arp.val; } if (chan[i].baseFreq>255) chan[i].baseFreq=255; if (chan[i].baseFreq<0) chan[i].baseFreq=0; } else { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); } } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - chan[i].duty=chan[i].std.duty; + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; if (i==3) { if (parent->song.properNoiseLayout) { chan[i].duty&=1; @@ -201,6 +210,7 @@ void DivPlatformNES::tick() { } else { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; if (chan[i].freq>2047) chan[i].freq=2047; + if (chan[i].freq<0) chan[i].freq=0; } if (chan[i].keyOn) { //rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); @@ -334,7 +344,7 @@ int DivPlatformNES::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (chan[c.chan].active) { @@ -403,7 +413,7 @@ int DivPlatformNES::dispatch(DivCommand c) { break; case DIV_CMD_LEGATO: if (c.chan==3) break; - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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; @@ -451,6 +461,10 @@ int DivPlatformNES::getRegisterPoolSize() { return 32; } +float DivPlatformNES::getPostAmp() { + return 128.0f; +} + void DivPlatformNES::reset() { for (int i=0; i<5; i++) { chan[i]=DivPlatformNES::Channel(); @@ -473,6 +487,9 @@ void DivPlatformNES::reset() { rWrite(0x4015,0x1f); rWrite(0x4001,chan[0].sweep); rWrite(0x4005,chan[1].sweep); + + dacAntiClickOn=true; + dacAntiClick=0; } bool DivPlatformNES::keyOffAffectsArp(int ch) { diff --git a/src/engine/platform/nes.h b/src/engine/platform/nes.h index c1b699bcf..1dbb4411b 100644 --- a/src/engine/platform/nes.h +++ b/src/engine/platform/nes.h @@ -54,10 +54,11 @@ class DivPlatformNES: public DivDispatch { Channel chan[5]; bool isMuted[5]; int dacPeriod, dacRate; - unsigned int dacPos; + unsigned int dacPos, dacAntiClick; int dacSample; unsigned char sampleBank; unsigned char apuType; + bool dacAntiClickOn; struct NESAPU* nes; unsigned char regPool[128]; @@ -74,6 +75,7 @@ class DivPlatformNES: public DivDispatch { void tick(); void muteChannel(int ch, bool mute); bool keyOffAffectsArp(int ch); + float getPostAmp(); void setFlags(unsigned int flags); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp new file mode 100644 index 000000000..3e222eec1 --- /dev/null +++ b/src/engine/platform/opl.cpp @@ -0,0 +1,1094 @@ +/** + * 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 "opl.h" +#include "../engine.h" +#include +#include + +#define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } + +#define CHIP_FREQBASE chipFreqBase + +// N = invalid +#define N 255 + +const unsigned char slotsOPL2i[4][20]={ + {0, 1, 2, 6, 7, 8, 12, 13, 14}, // OP1 + {3, 4, 5, 9, 10, 11, 15, 16, 17}, // OP2 + {N, N, N, N, N, N, N, N, N}, + {N, N, N, N, N, N, N, N, N} +}; + +const unsigned char slotsOPL2Drumsi[4][20]={ + {0, 1, 2, 6, 7, 8, 12, 16, 14, 17, 13}, // OP1 + {3, 4, 5, 9, 10, 11, 15, N, N, N, N}, // OP2 + {N, N, N, N, N, N, N, N, N, N, N}, + {N, N, N, N, N, N, N, N, N, N, N} +}; + +const unsigned short chanMapOPL2[20]={ + 0, 1, 2, 3, 4, 5, 6, 7, 8, N, N, N, N, N, N, N, N, N, N, N +}; + +const unsigned short chanMapOPL2Drums[20]={ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, N, N, N, N, N, N, N, N, N +}; + +const unsigned char* slotsOPL2[4]={ + slotsOPL2i[0], + slotsOPL2i[1], + slotsOPL2i[2], + slotsOPL2i[3] +}; + +const unsigned char* slotsOPL2Drums[4]={ + slotsOPL2Drumsi[0], + slotsOPL2Drumsi[1], + slotsOPL2Drumsi[2], + slotsOPL2Drumsi[3] +}; + +const unsigned char slotsOPL3i[4][20]={ + {0, 6, 1, 7, 2, 8, 18, 24, 19, 25, 20, 26, 30, 31, 32, 12, 13, 14}, // OP1 + {3, 9, 4, 10, 5, 11, 21, 27, 22, 28, 23, 29, 33, 34, 35, 15, 16, 17}, // OP2 + {6, N, 7, N, 8, N, 24, N, 25, N, 26, N, N, N, N, N, N, N}, // OP3 + {9, N, 10, N, 11, N, 27, N, 28, N, 29, N, N, N, N, N, N, N} // OP4 +}; + +const unsigned char slotsOPL3Drumsi[4][20]={ + {0, 6, 1, 7, 2, 8, 18, 24, 19, 25, 20, 26, 30, 31, 32, 12, 16, 14, 17, 13}, // OP1 + {3, 9, 4, 10, 5, 11, 21, 27, 22, 28, 23, 29, 33, 34, 35, 15, N, N, N, N}, // OP2 + {6, N, 7, N, 8, N, 24, N, 25, N, 26, N, N, N, N, N, N, N, N, N}, // OP3 + {9, N, 10, N, 11, N, 27, N, 28, N, 29, N, N, N, N, N, N, N, N, N} // OP4 +}; + +const unsigned short chanMapOPL3[20]={ + 0, 3, 1, 4, 2, 5, 0x100, 0x103, 0x101, 0x104, 0x102, 0x105, 0x106, 0x107, 0x108, 6, 7, 8, N, N +}; + +const unsigned short chanMapOPL3Drums[20]={ + 0, 3, 1, 4, 2, 5, 0x100, 0x103, 0x101, 0x104, 0x102, 0x105, 0x106, 0x107, 0x108, 6, 7, 8, 8, 7 +}; + +const unsigned char* slotsOPL3[4]={ + slotsOPL3i[0], + slotsOPL3i[1], + slotsOPL3i[2], + slotsOPL3i[3] +}; + +const unsigned char* slotsOPL3Drums[4]={ + slotsOPL3Drumsi[0], + slotsOPL3Drumsi[1], + slotsOPL3Drumsi[2], + slotsOPL3Drumsi[3] +}; + +const unsigned int slotMap[36]={ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + + 0x100, 0x101, 0x102, 0x103, 0x104, 0x105, + 0x108, 0x109, 0x10a, 0x10b, 0x10c, 0x10d, + 0x110, 0x111, 0x112, 0x113, 0x114, 0x115, +}; + +const bool isOutputL[2][4][4]={ + { // 2-op + {false, true, false, false}, // 1 > 2 + { true, true, false, false}, // 1 + 2 + {false, true, false, false}, // ditto, 0 + { true, true, false, false}, // ditto, 1 + }, + { // 4-op + {false, false, false, true}, // 1 > 2 > 3 > 4 + { true, false, false, true}, // 1 + (2 > 3 > 4) + {false, true, false, true}, // (1 > 2) + (3 > 4) + { true, false, true, true} // 1 + (2 > 3) + 4 + } +}; + +#undef N + +const int orderedOpsL[4]={ + 0,2,1,3 +}; + +#define ADDR_AM_VIB_SUS_KSR_MULT 0x20 +#define ADDR_KSL_TL 0x40 +#define ADDR_AR_DR 0x60 +#define ADDR_SL_RR 0x80 +#define ADDR_WS 0xe0 + +#define ADDR_FREQ 0xa0 +#define ADDR_FREQH 0xb0 +#define ADDR_LR_FB_ALG 0xc0 + +const char* DivPlatformOPL::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Set global AM depth (0: 1dB, 1: 4.8dB)"; + break; + case 0x11: + return "11xx: Set feedback (0 to 7)"; + break; + case 0x12: + return "12xx: Set level of operator 1 (0 highest, 3F lowest)"; + break; + case 0x13: + return "13xx: Set level of operator 2 (0 highest, 3F lowest)"; + break; + case 0x14: + return "14xx: Set level of operator 3 (0 highest, 3F lowest; 4-op only)"; + break; + case 0x15: + return "15xx: Set level of operator 4 (0 highest, 3F lowest; 4-op only)"; + break; + case 0x16: + return "16xy: Set operator multiplier (x: operator from 1 to 4; y: multiplier)"; + break; + case 0x17: + return "17xx: Set global vibrato depth (0: normal, 1: double)"; + break; + case 0x18: + if (properDrumsSys) { + return "18xx: Toggle drums mode (1: enabled; 0: disabled)"; + } + break; + case 0x19: + return "19xx: Set attack of all operators (0 to F)"; + break; + case 0x1a: + return "1Axx: Set attack of operator 1 (0 to F)"; + break; + case 0x1b: + return "1Bxx: Set attack of operator 2 (0 to F)"; + break; + case 0x1c: + return "1Cxx: Set attack of operator 3 (0 to F; 4-op only)"; + break; + case 0x1d: + return "1Dxx: Set attack of operator 4 (0 to F; 4-op only)"; + break; + } + return NULL; +} + +void DivPlatformOPL::acquire_nuked(short* bufL, short* bufR, size_t start, size_t len) { + static short o[2]; + static int os[2]; + + for (size_t h=start; h32767) os[0]=32767; + + if (os[1]<-32768) os[1]=-32768; + if (os[1]>32767) os[1]=32767; + + bufL[h]=os[0]; + bufR[h]=os[1]; + } +} + +void DivPlatformOPL::acquire(short* bufL, short* bufR, size_t start, size_t len) { + //if (useYMFM) { + // acquire_ymfm(bufL,bufR,start,len); + //} else { + acquire_nuked(bufL,bufR,start,len); + //} +} + +void DivPlatformOPL::tick() { + for (int i=0; i>1)&1)|(chan[i].state.fb<<1)); + } + } else { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + } + } + } + + for (int j=0; j1) { + if (m.ws.had) { + op.ws=m.ws.val; + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); + } + } + + if (m.tl.had) { + op.tl=63-m.tl.val; + } + if (m.ksl.had) { + op.ksl=m.ksl.val; + } + if (m.tl.had || m.ksl.had) { + if (isMuted[i]) { + rWrite(baseAddr+ADDR_KSL_TL,63|(op.ksl<<6)); + } else { + if (isOutputL[ops==4][chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_KSL_TL,(63-(((63-op.tl)*(chan[i].outVol&0x3f))/63))|(op.ksl<<6)); + } else { + rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + } + } + } + } + + if (icalcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); + if (chan[i].freq>131071) chan[i].freq=131071; + int freqt=toFreq(chan[i].freq); + chan[i].freqH=freqt>>8; + chan[i].freqL=freqt&0xff; + immWrite(chanMap[i]+ADDR_FREQ,chan[i].freqL); + } + if (i=OPLL_C_NUM*64) { + return 128; + } else if (freq>=OPLL_C_NUM*32) { + return 64; + } else if (freq>=OPLL_C_NUM*16) { + return 32; + } else if (freq>=OPLL_C_NUM*8) { + return 16; + } else if (freq>=OPLL_C_NUM*4) { + return 8; + } else if (freq>=OPLL_C_NUM*2) { + return 4; + } else if (freq>=OPLL_C_NUM) { + return 2; + } else { + return 1; + } + return 1; +} + +int DivPlatformOPL::toFreq(int freq) { + if (freq>=OPLL_C_NUM*64) { + return 0x1c00|((freq>>7)&0x3ff); + } else if (freq>=OPLL_C_NUM*32) { + return 0x1800|((freq>>6)&0x3ff); + } else if (freq>=OPLL_C_NUM*16) { + return 0x1400|((freq>>5)&0x3ff); + } else if (freq>=OPLL_C_NUM*8) { + return 0x1000|((freq>>4)&0x3ff); + } else if (freq>=OPLL_C_NUM*4) { + return 0xc00|((freq>>3)&0x3ff); + } else if (freq>=OPLL_C_NUM*2) { + return 0x800|((freq>>2)&0x3ff); + } else if (freq>=OPLL_C_NUM) { + return 0x400|((freq>>1)&0x3ff); + } else { + return freq&0x3ff; + } +} + +void DivPlatformOPL::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + int ops=(slots[3][ch]!=255 && chan[ch].state.ops==4 && oplType==3)?4:2; + chan[ch].fourOp=(ops==4); + update4OpMask=true; + for (int i=0; i>1)&1)|(chan[ch].state.fb<<1)); + } + } else { + rWrite(chanMap[ch]+ADDR_LR_FB_ALG,(chan[ch].state.alg&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[ch+1]+ADDR_LR_FB_ALG,((chan[ch].state.alg>>1)&1)|(chan[ch].state.fb<<1)|((chan[ch].pan&3)<<4)); + } + } +} + +int DivPlatformOPL::dispatch(DivCommand c) { + if (c.chan>=totalChans) return 0; + // ineffective in 4-op mode + if (oplType==3 && c.chan<14 && (c.chan&1) && c.cmd!=DIV_CMD_GET_VOLMAX && c.cmd!=DIV_ALWAYS_SET_VOLUME) { + if (chan[c.chan-1].fourOp) return 0; + } + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + + if (chan[c.chan].insChanged) { + chan[c.chan].state=ins->fm; + } + + chan[c.chan].std.init(ins); + if (!chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + if (chan[c.chan].insChanged) { + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + chan[c.chan].fourOp=(ops==4); + update4OpMask=true; + for (int i=0; i1) { + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); + } + } + + if (isMuted[c.chan]) { + oldWrites[chanMap[c.chan]+ADDR_LR_FB_ALG]=-1; + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)); + if (ops==4) { + oldWrites[chanMap[c.chan+1]+ADDR_LR_FB_ALG]=-1; + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)); + } + } else { + oldWrites[chanMap[c.chan]+ADDR_LR_FB_ALG]=-1; + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + if (ops==4) { + oldWrites[chanMap[c.chan+1]+ADDR_LR_FB_ALG]=-1; + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + } + } + } + + chan[c.chan].insChanged=false; + + if (c.value!=DIV_NOTE_NULL) { + if (c.chan>=melodicChans && chan[c.chan].state.opllPreset==16 && chan[c.chan].state.fixedDrums) { // drums + if (c.chan==melodicChans) { + chan[c.chan].baseFreq=(chan[c.chan].state.kickFreq&1023)<<(chan[c.chan].state.kickFreq>>10); + } else if (c.chan==melodicChans+1 || c.chan==melodicChans+4) { + chan[c.chan].baseFreq=(chan[c.chan].state.snareHatFreq&1023)<<(chan[c.chan].state.snareHatFreq>>10); + } else if (c.chan==melodicChans+2 || c.chan==melodicChans+3) { + chan[c.chan].baseFreq=(chan[c.chan].state.tomTopFreq&1023)<<(chan[c.chan].state.tomTopFreq>>10); + } else { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + } + } else { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + } + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + } + chan[c.chan].keyOn=true; + chan[c.chan].active=true; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + break; + case DIV_CMD_NOTE_OFF_ENV: + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + chan[c.chan].std.release(); + break; + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_VOLUME: { + if (pretendYMU) { + c.value=pow(((double)c.value/127.0),0.5)*63.0; + if (c.value<0) c.value=0; + if (c.value>63) c.value=63; + } + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + for (int i=0; i0)<<1)|((c.value>>4)>0); + } + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (isMuted[c.chan]) { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)); + } + } else { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + } + } + break; + } + case DIV_CMD_PITCH: { + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + int newFreq; + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + newFreq=chan[c.chan].baseFreq+c.value*octave(chan[c.chan].baseFreq); + if (newFreq>=destFreq) { + newFreq=destFreq; + return2=true; + } + } else { + newFreq=chan[c.chan].baseFreq-c.value*octave(chan[c.chan].baseFreq); + if (newFreq<=destFreq) { + newFreq=destFreq; + return2=true; + } + } + if (!chan[c.chan].portaPause) { + if (octave(chan[c.chan].baseFreq)!=octave(newFreq)) { + chan[c.chan].portaPause=true; + break; + } + } + chan[c.chan].baseFreq=newFreq; + chan[c.chan].portaPause=false; + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_FM_LFO: { + if (c.value&2) { + dvb=c.value&1; + } else { + dam=c.value&1; + } + immWrite(0xbd,(dam<<7)|(dvb<<6)|(properDrums<<5)|drumState); + break; + } + case DIV_CMD_FM_FB: { + chan[c.chan].state.fb=c.value&7; + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (isMuted[c.chan]) { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)); + } + } else { + rWrite(chanMap[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[c.chan+1]+ADDR_LR_FB_ALG,((chan[c.chan].state.alg>>1)&1)|(chan[c.chan].state.fb<<1)|((chan[c.chan].pan&3)<<4)); + } + } + break; + } + case DIV_CMD_FM_MULT: { + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (c.value>=ops) break; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[(ops==4)?orderedOpsL[c.value]:c.value]; + op.mult=c.value2&15; + unsigned char slot=slots[c.value][c.chan]; + if (slot==255) break; + unsigned short baseAddr=slotMap[slot]; + rWrite(baseAddr+ADDR_AM_VIB_SUS_KSR_MULT,(op.am<<7)|(op.vib<<6)|(op.sus<<5)|(op.ksr<<4)|op.mult); + break; + } + case DIV_CMD_FM_TL: { + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (c.value>=ops) break; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[(ops==4)?orderedOpsL[c.value]:c.value]; + op.tl=c.value2&63; + unsigned char slot=slots[c.value][c.chan]; + if (slot==255) break; + unsigned short baseAddr=slotMap[slot]; + if (isMuted[c.chan]) { + rWrite(baseAddr+ADDR_KSL_TL,63|(op.ksl<<6)); + } else { + if (isOutputL[ops==4][chan[c.chan].state.alg][c.value]) { + rWrite(baseAddr+ADDR_KSL_TL,(63-(((63-op.tl)*(chan[c.chan].outVol&0x3f))/63))|(op.ksl<<6)); + } else { + rWrite(baseAddr+ADDR_KSL_TL,op.tl|(op.ksl<<6)); + } + } + break; + } + case DIV_CMD_FM_AR: { + int ops=(slots[3][c.chan]!=255 && chan[c.chan].state.ops==4 && oplType==3)?4:2; + if (c.value<0) { + for (int i=0; i=ops) break; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[(ops==4)?orderedOpsL[c.value]:c.value]; + op.ar=c.value2&15; + unsigned char slot=slots[c.value][c.chan]; + if (slot==255) break; + unsigned short baseAddr=slotMap[slot]; + rWrite(baseAddr+ADDR_AR_DR,(op.ar<<4)|op.dr); + } + break; + } + case DIV_CMD_FM_EXTCH: { + if (!properDrumsSys) break; + properDrums=c.value; + immWrite(0xbd,(dam<<7)|(dvb<<6)|(properDrums<<5)|drumState); + slots=properDrums?slotsDrums:slotsNonDrums; + if (oplType==3) { + chanMap=properDrums?chanMapOPL3Drums:chanMapOPL3; + melodicChans=properDrums?15:18; + totalChans=properDrums?20:18; + } else { + chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; + melodicChans=properDrums?6:9; + totalChans=properDrums?11:9; + } + break; + } + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_GET_VOLMAX: + if (pretendYMU) return 127; + return 63; + break; + case DIV_CMD_PRE_PORTA: + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_PRE_NOTE: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +void DivPlatformOPL::forceIns() { + if (oplType==3) { + chanMap=properDrums?chanMapOPL3Drums:chanMapOPL3; + melodicChans=properDrums?15:18; + totalChans=properDrums?20:18; + } else { + chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; + melodicChans=properDrums?6:9; + totalChans=properDrums?11:9; + } + for (int i=0; i1) { + rWrite(baseAddr+ADDR_WS,op.ws&((oplType==3)?7:3)); + } + } + + if (isMuted[i]) { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)); + } + } else { + rWrite(chanMap[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + if (ops==4) { + rWrite(chanMap[i+1]+ADDR_LR_FB_ALG,((chan[i].state.alg>>1)&1)|(chan[i].state.fb<<1)|((chan[i].pan&3)<<4)); + } + } + } + immWrite(0xbd,(dam<<7)|(dvb<<6)|(properDrums<<5)|drumState); + update4OpMask=true; +} + +void DivPlatformOPL::toggleRegisterDump(bool enable) { + DivDispatch::toggleRegisterDump(enable); +} + +void* DivPlatformOPL::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformOPL::getRegisterPool() { + return regPool; +} + +int DivPlatformOPL::getRegisterPoolSize() { + return 512; +} + +void DivPlatformOPL::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,512); + /* + if (useYMFM) { + fm_ymfm->reset(); + } + */ + OPL3_Reset(&fm,rate); + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + properDrums=properDrumsSys; + if (oplType==3) { + chanMap=properDrums?chanMapOPL3Drums:chanMapOPL3; + melodicChans=properDrums?15:18; + totalChans=properDrums?20:18; + } else { + chanMap=properDrums?chanMapOPL2Drums:chanMapOPL2; + melodicChans=properDrums?6:9; + totalChans=properDrums?11:9; + } + + for (int i=0; i& wlist) { + for (DivRegWrite& i: wlist) immWrite(i.addr,i.val); +} + +int DivPlatformOPL::getPortaFloor(int ch) { + return (ch>5)?12:0; +} + +void DivPlatformOPL::setYMFM(bool use) { + useYMFM=use; +} + +void DivPlatformOPL::setOPLType(int type, bool drums) { + pretendYMU=false; + switch (type) { + case 1: case 2: + slotsNonDrums=slotsOPL2; + slotsDrums=slotsOPL2Drums; + slots=drums?slotsDrums:slotsNonDrums; + chanMap=drums?chanMapOPL2Drums:chanMapOPL2; + chipFreqBase=9440540*0.25; + chans=9; + melodicChans=drums?6:9; + totalChans=drums?11:9; + break; + case 3: case 759: + slotsNonDrums=slotsOPL3; + slotsDrums=slotsOPL3Drums; + slots=drums?slotsDrums:slotsNonDrums; + chanMap=drums?chanMapOPL3Drums:chanMapOPL3; + chipFreqBase=9440540; + chans=18; + melodicChans=drums?15:18; + totalChans=drums?20:18; + if (type==759) pretendYMU=true; + break; + } + if (type==759) { + oplType=3; + } else { + oplType=type; + } + properDrumsSys=drums; +} + +void DivPlatformOPL::setFlags(unsigned int flags) { + /* + if (flags==3) { + chipClock=COLOR_NTSC*12.0/7.0; + } else if (flags==2) { + chipClock=8000000.0; + } else if (flags==1) { + chipClock=COLOR_PAL*12.0/7.0; + } else { + chipClock=COLOR_NTSC*15.0/7.0; + } + ladder=flags&0x80000000; + OPN2_SetChipType(ladder?ym3438_mode_ym2612:0); + if (useYMFM) { + if (fm_ymfm!=NULL) delete fm_ymfm; + if (ladder) { + fm_ymfm=new ymfm::ym2612(iface); + } else { + fm_ymfm=new ymfm::ym3438(iface); + } + rate=chipClock/144; + } else { + rate=chipClock/36; + }*/ + + if (oplType==3) { + chipClock=COLOR_NTSC*4.0; + rate=chipClock/288; + } else { + chipClock=COLOR_NTSC; + rate=chipClock/72; + } + + if (pretendYMU) { + rate=48000; + chipClock=rate*288; + } +} + +int DivPlatformOPL::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<20; i++) { + isMuted[i]=false; + } + setFlags(flags); + + reset(); + return totalChans; +} + +void DivPlatformOPL::quit() { +} + +DivPlatformOPL::~DivPlatformOPL() { +} diff --git a/src/engine/platform/opl.h b/src/engine/platform/opl.h new file mode 100644 index 000000000..44d80fda2 --- /dev/null +++ b/src/engine/platform/opl.h @@ -0,0 +1,126 @@ +/** + * 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 _OPL_H +#define _OPL_H +#include "../dispatch.h" +#include "../macroInt.h" +#include +#include "../../../extern/Nuked-OPL3/opl3.h" + +class DivPlatformOPL: public DivDispatch { + protected: + struct Channel { + DivInstrumentFM state; + DivMacroInt std; + unsigned char freqH, freqL; + int freq, baseFreq, pitch, note; + unsigned char ins; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta, fourOp; + int vol, outVol; + unsigned char pan; + Channel(): + freqH(0), + freqL(0), + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + portaPause(false), + furnaceDac(false), + inPorta(false), + fourOp(false), + vol(0), + pan(3) { + state.ops=2; + } + }; + Channel chan[20]; + bool isMuted[20]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + bool addrOrVal; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} + }; + std::queue writes; + opl3_chip fm; + const unsigned char** slotsNonDrums; + const unsigned char** slotsDrums; + const unsigned char** slots; + const unsigned short* chanMap; + double chipFreqBase; + int delay, oplType, chans, melodicChans, totalChans; + unsigned char lastBusy; + unsigned char drumState; + unsigned char drumVol[5]; + + unsigned char regPool[512]; + + bool properDrums, properDrumsSys, dam, dvb; + + unsigned char lfoValue; + + bool useYMFM, update4OpMask, pretendYMU; + + short oldWrites[512]; + short pendingWrites[512]; + + int octave(int freq); + int toFreq(int freq); + + friend void putDispatchChan(void*,int,int); + + void acquire_nuked(short* bufL, short* bufR, size_t start, size_t len); + //void acquire_ymfm(short* bufL, short* bufR, size_t start, size_t len); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + void setYMFM(bool use); + void setOPLType(int type, bool drums); + bool keyOffAffectsArp(int ch); + bool keyOffAffectsPorta(int ch); + void toggleRegisterDump(bool enable); + void setFlags(unsigned int flags); + void notifyInsChange(int ins); + void notifyInsDeletion(void* ins); + int getPortaFloor(int ch); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformOPL(); +}; +#endif diff --git a/src/engine/platform/opll.cpp b/src/engine/platform/opll.cpp index 0ca5a6c2b..ee07463c1 100644 --- a/src/engine/platform/opll.cpp +++ b/src/engine/platform/opll.cpp @@ -25,58 +25,46 @@ #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} #define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } -#define CHIP_FREQBASE 295017 +#define CHIP_FREQBASE 1180068 const char* DivPlatformOPLL::getEffectName(unsigned char effect) { switch (effect) { - case 0x10: - return "10xy: Setup LFO (x: enable; y: speed)"; - break; case 0x11: return "11xx: Set feedback (0 to 7)"; break; case 0x12: - return "12xx: Set level of operator 1 (0 highest, 7F lowest)"; + return "12xx: Set level of operator 1 (0 highest, 3F lowest)"; break; case 0x13: - return "13xx: Set level of operator 2 (0 highest, 7F lowest)"; - break; - case 0x14: - return "14xx: Set level of operator 3 (0 highest, 7F lowest)"; - break; - case 0x15: - return "15xx: Set level of operator 4 (0 highest, 7F lowest)"; + return "13xx: Set level of operator 2 (0 highest, F lowest)"; break; case 0x16: - return "16xy: Set operator multiplier (x: operator from 1 to 4; y: multiplier)"; - break; - case 0x17: - return "17xx: Enable channel 6 DAC"; + return "16xy: Set operator multiplier (x: operator from 1 to 2; y: multiplier)"; break; case 0x18: - return "18xx: Toggle extended channel 3 mode"; + if (properDrumsSys) { + return "18xx: Toggle drums mode (1: enabled; 0: disabled)"; + } break; case 0x19: - return "19xx: Set attack of all operators (0 to 1F)"; + return "19xx: Set attack of all operators (0 to F)"; break; case 0x1a: - return "1Axx: Set attack of operator 1 (0 to 1F)"; + return "1Axx: Set attack of operator 1 (0 to F)"; break; case 0x1b: - return "1Bxx: Set attack of operator 2 (0 to 1F)"; - break; - case 0x1c: - return "1Cxx: Set attack of operator 3 (0 to 1F)"; - break; - case 0x1d: - return "1Dxx: Set attack of operator 4 (0 to 1F)"; + return "1Bxx: Set attack of operator 2 (0 to F)"; break; } return NULL; } const unsigned char cycleMapOPLL[18]={ - 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 6, 7, 8, 6, 7, 8, 0 + 8, 7, 6, 7, 8, 7, 8, 6, 0, 1, 2, 7, 8, 9, 3, 4, 5, 9 +}; + +const unsigned char drumSlot[11]={ + 0, 0, 0, 0, 0, 0, 6, 7, 8, 8, 7 }; void DivPlatformOPLL::acquire_nuked(short* bufL, short* bufR, size_t start, size_t len) { @@ -103,9 +91,9 @@ void DivPlatformOPLL::acquire_nuked(short* bufL, short* bufR, size_t start, size } } - unsigned char nextOut=cycleMapOPLL[(fm.cycles+17)%18]; OPLL_Clock(&fm,o); - if (!isMuted[nextOut]) { + unsigned char nextOut=cycleMapOPLL[fm.cycles]; + if ((nextOut>=6 && properDrums) || !isMuted[nextOut]) { os+=(o[0]+o[1]); } } @@ -124,107 +112,130 @@ void DivPlatformOPLL::acquire(short* bufL, short* bufR, size_t start, size_t len } void DivPlatformOPLL::tick() { - for (int i=0; i<9; i++) { + for (int i=0; i<11; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; - rWrite(0x30+i,(15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)|(chan[i].state.opllPreset<<4)); + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(15,chan[i].std.vol.val))/15; + if (i<9) { + rWrite(0x30+i,((15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)&15)|(chan[i].state.opllPreset<<4)); + } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp); + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } } -/* - if (chan[i].std.hadAlg) { - chan[i].state.alg=chan[i].std.alg; - rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + + if (chan[i].std.wave.had && chan[i].state.opllPreset!=16) { + chan[i].state.opllPreset=chan[i].std.wave.val; + if (i<9) { + rWrite(0x30+i,((15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)&15)|(chan[i].state.opllPreset<<4)); + } } - if (chan[i].std.hadFb) { - chan[i].state.fb=chan[i].std.fb; - } - if (chan[i].std.hadFms) { - chan[i].state.fms=chan[i].std.fms; - } - if (chan[i].std.hadAms) { - chan[i].state.ams=chan[i].std.ams; - } - for (int j=0; j<2; j++) { - unsigned short baseAddr=chanOffs[i]|opOffs[j]; - DivInstrumentFM::Operator& op=chan[i].state.op[j]; - DivMacroInt::IntOp& m=chan[i].std.op[j]; - if (m.hadAm) { - op.am=m.am; - rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + + if (chan[i].state.opllPreset==0) { + if (chan[i].std.alg.had) { // SUS + chan[i].state.alg=chan[i].std.alg.val; + chan[i].freqChanged=true; } - if (m.hadAr) { - op.ar=m.ar; - rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; + rWrite(0x03,(chan[i].state.op[1].ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); } - if (m.hadDr) { - op.dr=m.dr; - rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; + rWrite(0x03,(chan[i].state.op[1].ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); } - if (m.hadMult) { - op.mult=m.mult; - rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; + rWrite(0x03,(chan[i].state.op[1].ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); } - if (m.hadRr) { - op.rr=m.rr; - rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); - } - if (m.hadSl) { - op.sl=m.sl; - rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); - } - if (m.hadTl) { - op.tl=127-m.tl; - if (isMuted[i]) { - rWrite(baseAddr+ADDR_TL,127); - } else { - if (isOutput[chan[i].state.alg][j]) { - rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + + for (int j=0; j<2; j++) { + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + DivMacroInt::IntOp& m=chan[i].std.op[j]; + + if (m.am.had) { + op.am=m.am.val; + rWrite(0x00+j,(op.am<<7)|(op.vib<<6)|((op.ssgEnv&8)<<2)|(op.ksr<<4)|(op.mult)); + } + if (m.ar.had) { + op.ar=m.ar.val; + rWrite(0x04+j,(op.ar<<4)|(op.dr)); + } + if (m.dr.had) { + op.dr=m.dr.val; + rWrite(0x04+j,(op.ar<<4)|(op.dr)); + } + if (m.mult.had) { + op.mult=m.mult.val; + rWrite(0x00+j,(op.am<<7)|(op.vib<<6)|((op.ssgEnv&8)<<2)|(op.ksr<<4)|(op.mult)); + } + if (m.rr.had) { + op.rr=m.rr.val; + rWrite(0x06+j,(op.sl<<4)|(op.rr)); + } + if (m.sl.had) { + op.sl=m.sl.val; + rWrite(0x06+j,(op.sl<<4)|(op.rr)); + } + if (m.tl.had) { + op.tl=((j==1)?15:63)-m.tl.val; + if (j==1) { + if (i<9) { + rWrite(0x30+i,((15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)&15)|(chan[i].state.opllPreset<<4)); + } } else { - rWrite(baseAddr+ADDR_TL,op.tl); + rWrite(0x02,(chan[i].state.op[0].ksl<<6)|(op.tl&63)); } } + + if (m.egt.had) { + op.ssgEnv=(m.egt.val&1)?8:0; + rWrite(0x00+j,(op.am<<7)|(op.vib<<6)|((op.ssgEnv&8)<<2)|(op.ksr<<4)|(op.mult)); + } + if (m.ksl.had) { + op.ksl=m.ksl.val; + if (j==1) { + rWrite(0x02,(chan[i].state.op[0].ksl<<6)|(chan[i].state.op[0].tl&63)); + } else { + rWrite(0x03,(chan[i].state.op[1].ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); + } + } + if (m.ksr.had) { + op.ksr=m.ksr.val; + rWrite(0x00+j,(op.am<<7)|(op.vib<<6)|((op.ssgEnv&8)<<2)|(op.ksr<<4)|(op.mult)); + } + if (m.vib.had) { + op.vib=m.vib.val; + rWrite(0x00+j,(op.am<<7)|(op.vib<<6)|((op.ssgEnv&8)<<2)|(op.ksr<<4)|(op.mult)); + } } - if (m.hadRs) { - op.rs=m.rs; - rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); - } - if (m.hadDt) { - op.dt=m.dt; - rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); - } - if (m.hadD2r) { - op.d2r=m.d2r; - rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); - } - if (m.hadSsg) { - op.ssgEnv=m.ssg; - rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); - } - }*/ + } if (chan[i].keyOn || chan[i].keyOff) { - if (i>=6 && drums) { + if (i>=6 && properDrums) { + drumState&=~(0x10>>(i-6)); + immWrite(0x0e,0x20|drumState); + } else if (i>=6 && drums) { drumState&=~(0x10>>(chan[i].note%12)); immWrite(0x0e,0x20|drumState); } else { - immWrite(0x20+i,(chan[i].freqH)/*|(chan[i].state.alg?0x20:0)*/); + if (i<9) { + immWrite(0x20+i,(chan[i].freqH)|(chan[i].state.alg?0x20:0)); + } } //chan[i].keyOn=false; chan[i].keyOff=false; @@ -238,25 +249,40 @@ void DivPlatformOPLL::tick() { } } - for (int i=0; i<9; i++) { + for (int i=0; i<11; i++) { if (chan[i].freqChanged) { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); if (chan[i].freq>262143) chan[i].freq=262143; int freqt=toFreq(chan[i].freq); - chan[i].freqH=freqt>>8; chan[i].freqL=freqt&0xff; - if (i<6 || !drums) { - immWrite(0x10+i,freqt&0xff); + if (i>=6 && properDrums) { + immWrite(0x10+drumSlot[i],freqt&0xff); + immWrite(0x20+drumSlot[i],freqt>>8); + } else if (i<6 || !drums) { + if (i<9) { + immWrite(0x10+i,freqt&0xff); + } } + chan[i].freqH=freqt>>8; } - if (chan[i].keyOn && i>=6 && drums) { + if (chan[i].keyOn && i>=6 && properDrums) { + if (!isMuted[i]) { + drumState|=(0x10>>(i-6)); + immWrite(0x0e,0x20|drumState); + } + chan[i].keyOn=false; + } else if (chan[i].keyOn && i>=6 && drums) { //printf("%d\n",chan[i].note%12); drumState|=(0x10>>(chan[i].note%12)); immWrite(0x0e,0x20|drumState); chan[i].keyOn=false; - } else if (chan[i].keyOn || chan[i].freqChanged) { + } else if ((chan[i].keyOn || chan[i].freqChanged) && i<9) { //immWrite(0x28,0xf0|konOffs[i]); - immWrite(0x20+i,(chan[i].freqH)|(chan[i].active<<4)|(chan[i].state.alg?0x20:0)); + if (!(i>=6 && properDrums)) { + if (i<9) { + immWrite(0x20+i,(chan[i].freqH)|(chan[i].active<<4)|(chan[i].state.alg?0x20:0)); + } + } chan[i].keyOn=false; } chan[i].freqChanged=false; @@ -328,15 +354,45 @@ void DivPlatformOPLL::muteChannel(int ch, bool mute) { int DivPlatformOPLL::dispatch(DivCommand c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: { + if (c.chan>=9 && !properDrums) return 0; DivInstrument* ins=parent->getIns(chan[c.chan].ins); if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; } chan[c.chan].std.init(ins); - if (!chan[c.chan].std.willVol) { + if (!chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } + + if (c.chan>=6 && properDrums) { // drums mode + chan[c.chan].insChanged=false; + if (c.value!=DIV_NOTE_NULL) { + if (chan[c.chan].state.opllPreset==16 && chan[c.chan].state.fixedDrums) { + switch (c.chan) { + case 6: + chan[c.chan].baseFreq=(chan[c.chan].state.kickFreq&511)<<(chan[c.chan].state.kickFreq>>9); + break; + case 7: case 10: + chan[c.chan].baseFreq=(chan[c.chan].state.snareHatFreq&511)<<(chan[c.chan].state.snareHatFreq>>9); + break; + case 8: case 9: + chan[c.chan].baseFreq=(chan[c.chan].state.tomTopFreq&511)<<(chan[c.chan].state.tomTopFreq>>9); + break; + default: + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + break; + } + } else { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + } + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + } + chan[c.chan].keyOn=true; + chan[c.chan].active=true; + break; + } if (chan[c.chan].insChanged) { // update custom preset @@ -345,12 +401,13 @@ int DivPlatformOPLL::dispatch(DivCommand c) { DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; rWrite(0x00,(mod.am<<7)|(mod.vib<<6)|((mod.ssgEnv&8)<<2)|(mod.ksr<<4)|(mod.mult)); rWrite(0x01,(car.am<<7)|(car.vib<<6)|((car.ssgEnv&8)<<2)|(car.ksr<<4)|(car.mult)); - rWrite(0x02,(car.ksl<<6)|(mod.tl&63)); - rWrite(0x03,(mod.ksl<<6)|((chan[c.chan].state.fms&1)<<4)|((chan[c.chan].state.ams&1)<<3)|chan[c.chan].state.fb); + rWrite(0x02,(mod.ksl<<6)|(mod.tl&63)); + rWrite(0x03,(car.ksl<<6)|((chan[c.chan].state.fms&1)<<4)|((chan[c.chan].state.ams&1)<<3)|chan[c.chan].state.fb); rWrite(0x04,(mod.ar<<4)|(mod.dr)); rWrite(0x05,(car.ar<<4)|(car.dr)); rWrite(0x06,(mod.sl<<4)|(mod.rr)); rWrite(0x07,(car.sl<<4)|(car.rr)); + lastCustomMemory=c.chan; } if (chan[c.chan].state.opllPreset==16) { // compatible drums mode if (c.chan>=6) { @@ -368,10 +425,14 @@ int DivPlatformOPLL::dispatch(DivCommand c) { } } else { if (c.chan>=6) { - drums=false; - immWrite(0x0e,0); + if (drums) { + drums=false; + immWrite(0x0e,0); + } + } + if (c.chan<9) { + rWrite(0x30+c.chan,((15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)&15)|(chan[c.chan].state.opllPreset<<4)); } - rWrite(0x30+c.chan,(15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)|(chan[c.chan].state.opllPreset<<4)); } } @@ -410,26 +471,38 @@ int DivPlatformOPLL::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_OFF: + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; chan[c.chan].active=false; break; case DIV_CMD_NOTE_OFF_ENV: + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; chan[c.chan].active=false; chan[c.chan].std.release(); break; case DIV_CMD_ENV_RELEASE: + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].std.release(); break; case DIV_CMD_VOLUME: { + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } - if (c.chan<6 || !drums) { - rWrite(0x30+c.chan,(15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)|(chan[c.chan].state.opllPreset<<4)); + if (c.chan>=6 && properDrums) { + drumVol[c.chan-6]=15-chan[c.chan].outVol; + rWrite(0x36,drumVol[0]); + rWrite(0x37,drumVol[1]|(drumVol[4]<<4)); + rWrite(0x38,drumVol[3]|(drumVol[2]<<4)); + break; + } else if (c.chan<6 || !drums) { + if (c.chan<9) { + rWrite(0x30+c.chan,((15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)&15)|(chan[c.chan].state.opllPreset<<4)); + } } break; } @@ -444,11 +517,13 @@ int DivPlatformOPLL::dispatch(DivCommand c) { chan[c.chan].ins=c.value; break; case DIV_CMD_PITCH: { + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].pitch=c.value; chan[c.chan].freqChanged=true; break; } case DIV_CMD_NOTE_PORTA: { + if (c.chan>=9 && !properDrums) return 0; int destFreq=NOTE_FREQUENCY(c.value2); int newFreq; bool return2=false; @@ -481,20 +556,23 @@ int DivPlatformOPLL::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: { + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); chan[c.chan].note=c.value; chan[c.chan].freqChanged=true; break; } case DIV_CMD_FM_FB: { - DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; - //DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; + if (c.chan>=9 && !properDrums) return 0; + //DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; + DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; chan[c.chan].state.fb=c.value&7; - rWrite(0x03,(mod.ksl<<6)|((chan[c.chan].state.fms&1)<<4)|((chan[c.chan].state.ams&1)<<3)|chan[c.chan].state.fb); + rWrite(0x03,(car.ksl<<6)|((chan[c.chan].state.fms&1)<<4)|((chan[c.chan].state.ams&1)<<3)|chan[c.chan].state.fb); break; } case DIV_CMD_FM_MULT: { + if (c.chan>=9 && !properDrums) return 0; if (c.value==0) { DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; mod.mult=c.value2&15; @@ -502,24 +580,28 @@ int DivPlatformOPLL::dispatch(DivCommand c) { } else { DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; car.mult=c.value2&15; - rWrite(0x30+c.chan,(15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)|(chan[c.chan].state.opllPreset<<4)); - } - break; - } - case DIV_CMD_FM_TL: { - if (c.value==0) { - DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; - DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; - mod.tl=c.value2&63; - rWrite(0x02,(car.ksl<<6)|(mod.tl&63)); - } else { - DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; - car.tl=c.value2&15; rWrite(0x01,(car.am<<7)|(car.vib<<6)|((car.ssgEnv&8)<<2)|(car.ksr<<4)|(car.mult)); } break; } + case DIV_CMD_FM_TL: { + if (c.chan>=9 && !properDrums) return 0; + if (c.value==0) { + DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; + //DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; + mod.tl=c.value2&63; + rWrite(0x02,(mod.ksl<<6)|(mod.tl&63)); + } else { + DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; + car.tl=c.value2&15; + if (c.chan<9) { + rWrite(0x30+c.chan,((15-(chan[c.chan].outVol*(15-chan[c.chan].state.op[1].tl))/15)&15)|(chan[c.chan].state.opllPreset<<4)); + } + } + break; + } case DIV_CMD_FM_AR: { + if (c.chan>=9 && !properDrums) return 0; DivInstrumentFM::Operator& mod=chan[c.chan].state.op[0]; DivInstrumentFM::Operator& car=chan[c.chan].state.op[1]; if (c.value<0) { @@ -536,6 +618,18 @@ int DivPlatformOPLL::dispatch(DivCommand c) { rWrite(0x05,(car.ar<<4)|(car.dr)); break; } + case DIV_CMD_FM_EXTCH: + if (!properDrumsSys) break; + if ((int)properDrums==c.value) break; + if (c.value) { + properDrums=true; + immWrite(0x0e,0x20); + } else { + properDrums=false; + immWrite(0x0e,0x00); + drumState=0; + } + break; case DIV_ALWAYS_SET_VOLUME: return 0; break; @@ -543,6 +637,7 @@ int DivPlatformOPLL::dispatch(DivCommand c) { return 15; break; case DIV_CMD_PRE_PORTA: + if (c.chan>=9 && !properDrums) return 0; chan[c.chan].inPorta=c.value; break; case DIV_CMD_PRE_NOTE: @@ -556,26 +651,32 @@ int DivPlatformOPLL::dispatch(DivCommand c) { void DivPlatformOPLL::forceIns() { for (int i=0; i<9; i++) { + if (i>=6 && properDrums) continue; // update custom preset - if (chan[i].state.opllPreset==0) { + if (chan[i].state.opllPreset==0 && i==lastCustomMemory) { DivInstrumentFM::Operator& mod=chan[i].state.op[0]; DivInstrumentFM::Operator& car=chan[i].state.op[1]; rWrite(0x00,(mod.am<<7)|(mod.vib<<6)|((mod.ssgEnv&8)<<2)|(mod.ksr<<4)|(mod.mult)); rWrite(0x01,(car.am<<7)|(car.vib<<6)|((car.ssgEnv&8)<<2)|(car.ksr<<4)|(car.mult)); - rWrite(0x02,(car.ksl<<6)|(mod.tl&63)); - rWrite(0x03,(mod.ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); + rWrite(0x02,(mod.ksl<<6)|(mod.tl&63)); + rWrite(0x03,(car.ksl<<6)|((chan[i].state.fms&1)<<4)|((chan[i].state.ams&1)<<3)|chan[i].state.fb); rWrite(0x04,(mod.ar<<4)|(mod.dr)); rWrite(0x05,(car.ar<<4)|(car.dr)); rWrite(0x06,(mod.sl<<4)|(mod.rr)); rWrite(0x07,(car.sl<<4)|(car.rr)); } - rWrite(0x30+i,(15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)|(chan[i].state.opllPreset<<4)); - if (chan[i].active) { - chan[i].keyOn=true; - chan[i].freqChanged=true; + if (i<9) { + rWrite(0x30+i,((15-(chan[i].outVol*(15-chan[i].state.op[1].tl))/15)&15)|(chan[i].state.opllPreset<<4)); + } + if (!(i>=6 && properDrums)) { + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + chan[i].insChanged=true; + } } } - if (drums) { + if (drums) { // WHAT?! FIX THIS! immWrite(0x16,0x20); immWrite(0x26,0x05); immWrite(0x16,0x20); @@ -587,6 +688,13 @@ void DivPlatformOPLL::forceIns() { immWrite(0x18,0xC0); immWrite(0x28,0x01); } + // restore drum volumes + if (properDrums) { + rWrite(0x36,drumVol[0]); + rWrite(0x37,drumVol[1]|(drumVol[4]<<4)); + rWrite(0x38,drumVol[3]|(drumVol[2]<<4)); + } + drumState=0; } void DivPlatformOPLL::toggleRegisterDump(bool enable) { @@ -599,6 +707,7 @@ void DivPlatformOPLL::setVRC7(bool vrc) { void DivPlatformOPLL::setProperDrums(bool pd) { properDrums=pd; + properDrumsSys=pd; } @@ -621,11 +730,25 @@ void DivPlatformOPLL::reset() { OPLL_Reset(&fm,opll_type_ds1001); } else { OPLL_Reset(&fm,opll_type_ym2413); + switch (patchSet) { + case 0: + fm.patchrom=OPLL_GetPatchROM(opll_type_ym2413); + break; + case 1: + fm.patchrom=OPLL_GetPatchROM(opll_type_ymf281); + break; + case 2: + fm.patchrom=OPLL_GetPatchROM(opll_type_ym2423); + break; + case 3: + fm.patchrom=OPLL_GetPatchROM(opll_type_ds1001); + break; + } } if (dumpWrites) { addWrite(0xffffffff,0); } - for (int i=0; i<9; i++) { + for (int i=0; i<11; i++) { chan[i]=DivPlatformOPLL::Channel(); chan[i].vol=15; chan[i].outVol=15; @@ -638,6 +761,7 @@ void DivPlatformOPLL::reset() { lastBusy=60; drumState=0; + lastCustomMemory=-1; drumVol[0]=0; drumVol[1]=0; @@ -646,6 +770,12 @@ void DivPlatformOPLL::reset() { drumVol[4]=0; delay=0; + drums=false; + properDrums=properDrumsSys; + + if (properDrums) { + immWrite(0x0e,0x20); + } } bool DivPlatformOPLL::keyOffAffectsArp(int ch) { @@ -657,7 +787,7 @@ bool DivPlatformOPLL::keyOffAffectsPorta(int ch) { } void DivPlatformOPLL::notifyInsChange(int ins) { - for (int i=0; i<9; i++) { + for (int i=0; i<11; i++) { if (chan[i].ins==ins) { chan[i].insChanged=true; } @@ -684,23 +814,25 @@ void DivPlatformOPLL::setYMFM(bool use) { } void DivPlatformOPLL::setFlags(unsigned int flags) { - if (flags==3) { - chipClock=COLOR_NTSC; - } else if (flags==2) { - chipClock=8000000.0; - } else if (flags==1) { - chipClock=COLOR_PAL*1.0/5.0; + if ((flags&15)==3) { + chipClock=COLOR_NTSC/2.0; + } else if ((flags&15)==2) { + chipClock=4000000.0; + } else if ((flags&15)==1) { + chipClock=COLOR_PAL*4.0/5.0; } else { - chipClock=COLOR_NTSC/4.0; + chipClock=COLOR_NTSC; } - rate=chipClock/9; + rate=chipClock/36; + patchSet=flags>>4; } int DivPlatformOPLL::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { parent=p; dumpWrites=false; skipRegisterWrites=false; - for (int i=0; i<9; i++) { + patchSet=0; + for (int i=0; i<11; i++) { isMuted[i]=false; } setFlags(flags); diff --git a/src/engine/platform/opll.h b/src/engine/platform/opll.h index 87e970579..4a7a4c97f 100644 --- a/src/engine/platform/opll.h +++ b/src/engine/platform/opll.h @@ -67,7 +67,7 @@ class DivPlatformOPLL: public DivDispatch { }; std::queue writes; opll_t fm; - int delay; + int delay, lastCustomMemory; unsigned char lastBusy; unsigned char drumState; unsigned char drumVol[5]; @@ -76,8 +76,10 @@ class DivPlatformOPLL: public DivDispatch { bool useYMFM; bool drums; - bool properDrums; + bool properDrums, properDrumsSys; bool vrc7; + + unsigned char patchSet; short oldWrites[256]; short pendingWrites[256]; diff --git a/src/engine/platform/pce.cpp b/src/engine/platform/pce.cpp index 7b45dfa7b..0789c804d 100644 --- a/src/engine/platform/pce.cpp +++ b/src/engine/platform/pce.cpp @@ -91,7 +91,7 @@ void DivPlatformPCE::acquire(short* bufL, short* bufR, size_t start, size_t len) 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) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { chan[i].dacPos=s->loopStart; } else { chan[i].dacSample=-1; @@ -131,15 +131,10 @@ void DivPlatformPCE::acquire(short* bufL, short* bufR, size_t start, size_t len) } void DivPlatformPCE::updateWave(int ch) { - DivWavetable* wt=parent->getWave(chan[ch].wave); chWrite(ch,0x04,0x5f); chWrite(ch,0x04,0x1f); for (int i=0; i<32; i++) { - if (wt->max<1 || wt->len<1) { - chWrite(ch,0x06,0); - } else { - chWrite(ch,0x06,wt->data[i*wt->len/32]*31/wt->max); - } + chWrite(ch,0x06,chan[ch].ws.output[i]); } if (chan[ch].active) { chWrite(ch,0x04,0x80|chan[ch].outVol); @@ -154,32 +149,39 @@ static unsigned char noiseFreq[12]={ void DivPlatformPCE::tick() { for (int i=0; i<6; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol&31)*MIN(31,chan[i].std.vol))>>5; + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol&31)*MIN(31,chan[i].std.vol.val))>>5; if (chan[i].furnaceDac) { // ignore for now } else { chWrite(i,0x04,0x80|chan[i].outVol); } } - if (chan[i].std.hadArp) { + if (chan[i].std.duty.had && i>=4) { + chan[i].noise=chan[i].std.duty.val; + chan[i].freqChanged=true; + int noiseSeek=chan[i].note; + if (noiseSeek<0) noiseSeek=0; + chWrite(i,0x07,chan[i].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); + } + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); // noise - int noiseSeek=chan[i].std.arp; + int noiseSeek=chan[i].std.arp.val; if (noiseSeek<0) noiseSeek=0; chWrite(i,0x07,chan[i].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); - int noiseSeek=chan[i].note+chan[i].std.arp; + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + int noiseSeek=chan[i].note+chan[i].std.arp.val; if (noiseSeek<0) noiseSeek=0; chWrite(i,0x07,chan[i].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); int noiseSeek=chan[i].note; if (noiseSeek<0) noiseSeek=0; @@ -187,13 +189,18 @@ void DivPlatformPCE::tick() { chan[i].freqChanged=true; } } - if (chan[i].std.hadWave && !chan[i].pcm) { - if (chan[i].wave!=chan[i].std.wave) { - chan[i].wave=chan[i].std.wave; - updateWave(i); + if (chan[i].std.wave.had && !chan[i].pcm) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); if (!chan[i].keyOff) chan[i].keyOn=true; } } + if (chan[i].active) { + if (chan[i].ws.tick()) { + updateWave(i); + } + } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { //DivInstrument* ins=parent->getIns(chan[i].ins); chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); @@ -214,10 +221,6 @@ void DivPlatformPCE::tick() { chWrite(i,0x02,chan[i].freq&0xff); chWrite(i,0x03,chan[i].freq>>8); if (chan[i].keyOn) { - if (chan[i].wave<0) { - chan[i].wave=0; - updateWave(i); - } //rWrite(16+i*5,0x80); //chWrite(i,0x04,0x80|chan[i].vol); } @@ -300,6 +303,12 @@ int DivPlatformPCE::dispatch(DivCommand c) { chan[c.chan].keyOn=true; chWrite(c.chan,0x04,0x80|chan[c.chan].vol); chan[c.chan].std.init(ins); + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,32,31,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; break; } case DIV_CMD_NOTE_OFF: @@ -317,19 +326,20 @@ int DivPlatformPCE::dispatch(DivCommand c) { case DIV_CMD_INSTRUMENT: if (chan[c.chan].ins!=c.value || c.value2==1) { chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; } break; case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; if (chan[c.chan].active) chWrite(c.chan,0x04,0x80|chan[c.chan].outVol); } } break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { + if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; @@ -340,7 +350,7 @@ int DivPlatformPCE::dispatch(DivCommand c) { break; case DIV_CMD_WAVE: chan[c.chan].wave=c.value; - updateWave(c.chan); + chan[c.chan].ws.changeWave1(chan[c.chan].wave); chan[c.chan].keyOn=true; break; case DIV_CMD_PCE_LFO_MODE: @@ -399,7 +409,7 @@ int DivPlatformPCE::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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; @@ -452,6 +462,8 @@ void DivPlatformPCE::reset() { memset(regPool,0,128); for (int i=0; i<6; i++) { chan[i]=DivPlatformPCE::Channel(); + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,32,31,false); } if (dumpWrites) { addWrite(0xffffffff,0); @@ -489,6 +501,7 @@ bool DivPlatformPCE::keyOffAffectsArp(int ch) { void DivPlatformPCE::notifyWaveChange(int wave) { for (int i=0; i<6; i++) { if (chan[i].wave==wave) { + chan[i].ws.changeWave1(wave); updateWave(i); } } diff --git a/src/engine/platform/pce.h b/src/engine/platform/pce.h index 2a5bd4fba..2e8614ba8 100644 --- a/src/engine/platform/pce.h +++ b/src/engine/platform/pce.h @@ -23,6 +23,7 @@ #include "../dispatch.h" #include #include "../macroInt.h" +#include "../waveSynth.h" #include "sound/pce_psg.h" class DivPlatformPCE: public DivDispatch { @@ -35,6 +36,7 @@ class DivPlatformPCE: public DivDispatch { bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, furnaceDac; signed char vol, outVol, wave; DivMacroInt std; + DivWaveSynth ws; Channel(): freq(0), baseFreq(0), diff --git a/src/engine/platform/pcspkr.cpp b/src/engine/platform/pcspkr.cpp new file mode 100644 index 000000000..ee6ba7ef8 --- /dev/null +++ b/src/engine/platform/pcspkr.cpp @@ -0,0 +1,413 @@ +/** + * 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 "pcspkr.h" +#include "../engine.h" +#include + +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#endif + +#define PCSPKR_DIVIDER 4 +#define CHIP_DIVIDER 1 + +const char* regCheatSheetPCSpeaker[]={ + "Period", "0", + NULL +}; + +const char** DivPlatformPCSpeaker::getRegisterSheet() { + return regCheatSheetPCSpeaker; +} + +const char* DivPlatformPCSpeaker::getEffectName(unsigned char effect) { + return NULL; +} + +const float cut=0.05; +const float reso=0.06; + +void DivPlatformPCSpeaker::acquire_unfilt(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; i(freq>>1) && !isMuted[0])?32767:0; + } else { + bufL[i]=0; + } + } +} + +void DivPlatformPCSpeaker::acquire_cone(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; i((freq+16)>>1) && !isMuted[0])?1:0; + low+=0.04*band; + band+=0.04*(next-low-band); + float out=(low+band)*0.75; + if (out>1.0) out=1.0; + if (out<-1.0) out=-1.0; + bufL[i]=out*32767; + } else { + bufL[i]=0; + } + } +} + +void DivPlatformPCSpeaker::acquire_piezo(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; i((freq+64)>>1) && !isMuted[0])?1:0; + low+=cut*band; + band+=cut*(next-low-(reso*band)); + float out=band*0.15-(next-low)*0.06; + if (out>1.0) out=1.0; + if (out<-1.0) out=-1.0; + bufL[i]=out*32767; + } else { + bufL[i]=0; + } + } +} + +void DivPlatformPCSpeaker::beepFreq(int freq) { +#ifdef __linux__ + static struct input_event ie; + if (beepFD>=0) { + gettimeofday(&ie.time,NULL); + ie.type=EV_SND; + ie.code=SND_TONE; + if (freq>0) { + ie.value=chipClock/freq; + } else { + ie.value=0; + } + if (write(beepFD,&ie,sizeof(struct input_event))<0) { + perror("error while writing frequency!"); + } else { + //printf("writing freq: %d\n",freq); + } + } +#endif +} + +void DivPlatformPCSpeaker::acquire_real(short* bufL, short* bufR, size_t start, size_t len) { + if (lastOn!=on || lastFreq!=freq) { + lastOn=on; + lastFreq=freq; + beepFreq((on && !isMuted[0])?freq:0); + } + for (size_t i=start; icalcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>65535) chan[i].freq=65535; + if (chan[i].keyOn) { + on=true; + } + if (chan[i].keyOff) { + on=false; + } + freq=chan[i].freq; + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformPCSpeaker::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + if (chan[c.chan].active) { + on=chan[c.chan].vol; + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: + if (c.chan==3) break; + 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; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 1; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformPCSpeaker::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformPCSpeaker::forceIns() { + for (int i=0; i<1; i++) { + chan[i].insChanged=true; + } +} + +void* DivPlatformPCSpeaker::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformPCSpeaker::getRegisterPool() { + if (on) { + regPool[0]=freq; + regPool[1]=freq>>8; + } else { + regPool[0]=0; + regPool[1]=0; + } + return regPool; +} + +int DivPlatformPCSpeaker::getRegisterPoolSize() { + return 2; +} + +void DivPlatformPCSpeaker::reset() { + for (int i=0; i<1; i++) { + chan[i]=DivPlatformPCSpeaker::Channel(); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + on=false; + lastOn=false; + freq=0; + lastFreq=0; + pos=0; + flip=false; + low=0; + band=0; + + if (speakerType==3) { +#ifdef __linux__ + if (beepFD==-1) { + beepFD=open("/dev/input/by-path/platform-pcspkr-event-spkr",O_WRONLY); + if (beepFD<0) { + perror("error while opening PC speaker"); + } + } +#endif + beepFreq(0); + } else { + beepFreq(0); + } + + memset(regPool,0,2); +} + +bool DivPlatformPCSpeaker::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformPCSpeaker::setFlags(unsigned int flags) { + chipClock=COLOR_NTSC/3.0; + rate=chipClock/PCSPKR_DIVIDER; + speakerType=flags&3; +} + +void DivPlatformPCSpeaker::notifyInsDeletion(void* ins) { + for (int i=0; i<1; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformPCSpeaker::notifyPlaybackStop() { + beepFreq(0); +} + +void DivPlatformPCSpeaker::poke(unsigned int addr, unsigned short val) { + // ??? +} + +void DivPlatformPCSpeaker::poke(std::vector& wlist) { + // ??? +} + +int DivPlatformPCSpeaker::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + beepFD=-1; + for (int i=0; i<1; i++) { + isMuted[i]=false; + } + setFlags(flags); + + reset(); + return 5; +} + +void DivPlatformPCSpeaker::quit() { + if (speakerType==3) { + beepFreq(0); + } +#ifdef __linux__ + if (beepFD>=0) close(beepFD); +#endif +} + +DivPlatformPCSpeaker::~DivPlatformPCSpeaker() { +} diff --git a/src/engine/platform/pcspkr.h b/src/engine/platform/pcspkr.h new file mode 100644 index 000000000..17dedf247 --- /dev/null +++ b/src/engine/platform/pcspkr.h @@ -0,0 +1,95 @@ +/** + * 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 _PCSPKR_H +#define _PCSPKR_H + +#include "../dispatch.h" +#include "../macroInt.h" + +class DivPlatformPCSpeaker: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins, duty, sweep; + bool active, insChanged, freqChanged, sweepChanged, keyOn, keyOff, inPorta, furnaceDac; + signed char vol, outVol, wave; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + duty(0), + sweep(8), + active(false), + insChanged(true), + freqChanged(false), + sweepChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + furnaceDac(false), + vol(15), + outVol(15), + wave(-1) {} + }; + Channel chan[1]; + bool isMuted[1]; + bool on, flip, lastOn; + int pos, speakerType, beepFD; + float low, band; + float low2, high2, band2; + float low3, band3; + unsigned short freq, lastFreq; + unsigned char regPool[2]; + + friend void putDispatchChan(void*,int,int); + + void beepFreq(int freq); + + void acquire_unfilt(short* bufL, short* bufR, size_t start, size_t len); + void acquire_cone(short* bufL, short* bufR, size_t start, size_t len); + void acquire_piezo(short* bufL, short* bufR, size_t start, size_t len); + void acquire_real(short* bufL, short* bufR, size_t start, size_t len); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + void notifyPlaybackStop(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformPCSpeaker(); +}; + +#endif diff --git a/src/engine/platform/pet.cpp b/src/engine/platform/pet.cpp new file mode 100644 index 000000000..e46277e0a --- /dev/null +++ b/src/engine/platform/pet.cpp @@ -0,0 +1,285 @@ +/** + * 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 "pet.h" +#include "../engine.h" +#include + +#define rWrite(a,v) {regPool[(a)]=(v)&0xff; if((a)==10) {chan.sreg=(v); chan.cnt=2;}} + +#define CHIP_DIVIDER 16 +#define SAMP_DIVIDER 4 + +const char* regCheatSheet6522[]={ + "T2L", "08", + "SR", "0A", + "ACR", "0B", + NULL +}; + +const char** DivPlatformPET::getRegisterSheet() { + return regCheatSheet6522; +} + +const char* DivPlatformPET::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + } + return NULL; +} + +void DivPlatformPET::acquire(short* bufL, short* bufR, size_t start, size_t len) { + // high-level emulation of 6522 shift register for now + int t2=regPool[8]*2+4; + if (((regPool[11]>>2)&7)==4) { + for (size_t h=start; h0) { + int adv=MIN(cycs,chan.cnt); + chan.cnt-=adv; + cycs-=adv; + if (chan.cnt==0) { + chan.out=(chan.sreg&1)*32767; + chan.sreg=(chan.sreg>>1)|((chan.sreg&1)<<7); + chan.cnt=t2; + } + } + bufL[h]=chan.out; + bufR[h]=chan.out; + } + } else { + chan.out=0; + for (size_t h=start; h0) { + if (regPool[11]!=16) { + rWrite(11,16); + rWrite(10,chan.wave); + } + } else { + rWrite(11,0); + } +} + +void DivPlatformPET::tick() { + chan.std.next(); + if (chan.std.vol.had) { + chan.outVol=chan.std.vol.val&chan.vol; + writeOutVol(); + } + if (chan.std.arp.had) { + if (!chan.inPorta) { + if (chan.std.arp.mode) { + chan.baseFreq=NOTE_PERIODIC(chan.std.arp.val); + } else { + chan.baseFreq=NOTE_PERIODIC(chan.note+chan.std.arp.val); + } + } + chan.freqChanged=true; + } else { + if (chan.std.arp.mode && chan.std.arp.finished) { + chan.baseFreq=NOTE_PERIODIC(chan.note); + chan.freqChanged=true; + } + } + if (chan.std.wave.had) { + if (chan.wave!=chan.std.wave.val) { + chan.wave=chan.std.wave.val; + rWrite(10,chan.wave); + } + } + if (chan.freqChanged || chan.keyOn || chan.keyOff) { + chan.freq=parent->calcFreq(chan.baseFreq,chan.pitch,true); + if (chan.freq>257) chan.freq=257; + if (chan.freq<2) chan.freq=2; + rWrite(8,chan.freq-2); + if (chan.keyOn) { + if (!chan.std.vol.will) { + chan.outVol=chan.vol; + writeOutVol(); + } + chan.keyOn=false; + } + if (chan.keyOff) { + rWrite(11,0); + chan.keyOff=false; + } + chan.freqChanged=false; + } +} + +int DivPlatformPET::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan.ins); + if (c.value!=DIV_NOTE_NULL) { + chan.baseFreq=NOTE_PERIODIC(c.value); + chan.freqChanged=true; + chan.note=c.value; + } + chan.active=true; + chan.keyOn=true; + chan.std.init(ins); + break; + } + case DIV_CMD_NOTE_OFF: + chan.active=false; + chan.keyOff=true; + chan.std.init(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; + } + break; + case DIV_CMD_VOLUME: + if (chan.vol!=c.value) { + chan.vol=c.value; + if (!chan.std.vol.had) { + chan.outVol=chan.vol; + writeOutVol(); + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan.vol; + break; + case DIV_CMD_PITCH: + chan.pitch=c.value; + chan.freqChanged=true; + break; + case DIV_CMD_WAVE: + chan.wave=c.value; + rWrite(10,chan.wave); + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(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=NOTE_PERIODIC(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.std.init(parent->getIns(chan.ins)); + } + chan.inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 1; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformPET::muteChannel(int ch, bool mute) { + isMuted=mute; + writeOutVol(); +} + +void DivPlatformPET::forceIns() { + chan.insChanged=true; + chan.freqChanged=true; + writeOutVol(); +} + +void* DivPlatformPET::getChanState(int ch) { + return &chan; +} + +unsigned char* DivPlatformPET::getRegisterPool() { + return regPool; +} + +int DivPlatformPET::getRegisterPoolSize() { + return 16; +} + +void DivPlatformPET::reset() { + memset(regPool,0,16); + chan=Channel(); +} + +bool DivPlatformPET::isStereo() { + return false; +} + +void DivPlatformPET::notifyInsDeletion(void* ins) { + chan.std.notifyInsDeletion((DivInstrument*)ins); +} + +void DivPlatformPET::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformPET::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformPET::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + chipClock=1000000; + rate=chipClock/SAMP_DIVIDER; // = 250000kHz + isMuted=false; + reset(); + return 1; +} + +DivPlatformPET::~DivPlatformPET() { +} diff --git a/src/engine/platform/pet.h b/src/engine/platform/pet.h new file mode 100644 index 000000000..3b6af48d9 --- /dev/null +++ b/src/engine/platform/pet.h @@ -0,0 +1,82 @@ +/** + * 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 _PET_H +#define _PET_H + +#include "../dispatch.h" +#include "../macroInt.h" + +class DivPlatformPET: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta; + int vol, outVol, wave; + unsigned char sreg; + int cnt; + short out; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + vol(1), + outVol(1), + wave(0b00001111), + sreg(0), + cnt(0), + out(0) {} + }; + Channel chan; + bool isMuted; + + unsigned char regPool[16]; + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void notifyInsDeletion(void* ins); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + ~DivPlatformPET(); + private: + void writeOutVol(); +}; + +#endif diff --git a/src/engine/platform/qsound.cpp b/src/engine/platform/qsound.cpp index 0e0637d8a..0fd25ec72 100644 --- a/src/engine/platform/qsound.cpp +++ b/src/engine/platform/qsound.cpp @@ -277,8 +277,8 @@ void DivPlatformQSound::acquire(short* bufL, short* bufR, size_t start, size_t l void DivPlatformQSound::tick() { for (int i=0; i<16; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol&0xff)*MIN(255,chan[i].std.vol<<2))>>8; + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol&0xff)*chan[i].std.vol.val)>>6; // Check if enabled and write volume if (chan[i].active) { rWrite(q1_reg_map[Q1V_VOL][i], chan[i].outVol << 4); @@ -311,17 +311,17 @@ void DivPlatformQSound::tick() { qsound_loop = length - s->loopStart; } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].note+chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=off*QS_NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } @@ -336,9 +336,9 @@ void DivPlatformQSound::tick() { 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); - //logW("ch %d bank=%04x, addr=%04x, end=%04x, loop=%04x!\n",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.hadVol) { + if (!chan[i].std.vol.had) { rWrite(q1_reg_map[Q1V_VOL][i], chan[i].vol << 4); } } @@ -347,7 +347,7 @@ void DivPlatformQSound::tick() { rWrite(q1_reg_map[Q1V_VOL][i], 0); rWrite(q1_reg_map[Q1V_FREQ][i], 0); } else if (chan[i].active) { - //logW("ch %d frequency set to %04x, off=%f, note=%d, %04x!\n",i,chan[i].freq,off,chan[i].note,QS_NOTE_FREQUENCY(chan[i].note)); + //logV("ch %d frequency set to %04x, off=%f, note=%d, %04x!",i,chan[i].freq,off,chan[i].note,QS_NOTE_FREQUENCY(chan[i].note)); rWrite(q1_reg_map[Q1V_FREQ][i], chan[i].freq); } if (chan[i].keyOn) chan[i].keyOn=false; @@ -404,7 +404,7 @@ int DivPlatformQSound::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { // Check if enabled and write volume chan[c.chan].outVol=c.value; if (chan[c.chan].active && c.chan < 16) { @@ -414,7 +414,7 @@ int DivPlatformQSound::dispatch(DivCommand c) { } break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { + if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; @@ -477,7 +477,7 @@ int DivPlatformQSound::dispatch(DivCommand c) { off=(double)s->centerRate/24038.0/16.0; } } - chan[c.chan].baseFreq=off*QS_NOTE_FREQUENCY(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp-12):(0))); + chan[c.chan].baseFreq=off*QS_NOTE_FREQUENCY(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val-12):(0))); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; break; diff --git a/src/engine/platform/saa.cpp b/src/engine/platform/saa.cpp index 0bafaf157..708fbc7ab 100644 --- a/src/engine/platform/saa.cpp +++ b/src/engine/platform/saa.cpp @@ -135,8 +135,8 @@ inline unsigned char applyPan(unsigned char vol, unsigned char pan) { void DivPlatformSAA1099::tick() { for (int i=0; i<6; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=MIN(15,chan[i].std.vol)-(15-(chan[i].vol&15)); + if (chan[i].std.vol.had) { + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(i,0); @@ -144,30 +144,30 @@ void DivPlatformSAA1099::tick() { rWrite(i,applyPan(chan[i].outVol&15,chan[i].pan)); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadDuty) { - saaNoise[i/3]=chan[i].std.duty&3; + if (chan[i].std.duty.had) { + saaNoise[i/3]=chan[i].std.duty.val&3; rWrite(0x16,saaNoise[0]|(saaNoise[1]<<4)); } - if (chan[i].std.hadWave) { - chan[i].psgMode=chan[i].std.wave&3; + if (chan[i].std.wave.had) { + chan[i].psgMode=chan[i].std.wave.val&3; } - if (chan[i].std.hadEx1) { - saaEnv[i/3]=chan[i].std.ex1; + if (chan[i].std.ex1.had) { + saaEnv[i/3]=chan[i].std.ex1.val; rWrite(0x18+(i/3),saaEnv[i/3]); } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { @@ -253,7 +253,7 @@ int DivPlatformSAA1099::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (isMuted[c.chan]) { @@ -414,6 +414,16 @@ void DivPlatformSAA1099::reset() { extMode=false; + rWrite(8,255); + rWrite(9,255); + rWrite(10,255); + rWrite(11,255); + rWrite(12,255); + rWrite(13,255); + rWrite(16,0x77); + rWrite(17,0x77); + rWrite(18,0x77); + rWrite(0x1c,2); rWrite(0x1c,1); } diff --git a/src/engine/platform/segapcm.cpp b/src/engine/platform/segapcm.cpp index 7872e0b69..b87ee9a86 100644 --- a/src/engine/platform/segapcm.cpp +++ b/src/engine/platform/segapcm.cpp @@ -54,7 +54,7 @@ void DivPlatformSegaPCM::acquire(short* bufL, short* bufR, size_t start, size_t } 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) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { chan[i].pcm.pos=s->loopStart<<8; } else { chan[i].pcm.sample=-1; @@ -80,22 +80,22 @@ void DivPlatformSegaPCM::tick() { for (int i=0; i<16; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=(chan[i].std.arp<<6)+baseFreqOff; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=(chan[i].std.arp.val<<6); } else { - chan[i].baseFreq=((chan[i].note+(signed char)chan[i].std.arp)<<6)+baseFreqOff; + chan[i].baseFreq=((chan[i].note+(signed char)chan[i].std.arp.val)<<6); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { - chan[i].baseFreq=(chan[i].note<<6)+baseFreqOff; + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=(chan[i].note<<6); chan[i].freqChanged=true; } } @@ -113,9 +113,9 @@ void DivPlatformSegaPCM::tick() { DivSample* s=parent->getSample(chan[i].pcm.sample); off=(double)s->centerRate/8363.0; } - chan[i].pcm.freq=MIN(255,((off*parent->song.tuning*pow(2.0,double(chan[i].freq+256)/(64.0*12.0)))*255)/31250); - if (dumpWrites && i>=8) { - addWrite(0x10007+((i-8)<<3),chan[i].pcm.freq); + chan[i].pcm.freq=MIN(255,(15625+(off*parent->song.tuning*pow(2.0,double(chan[i].freq+256)/(64.0*12.0)))*255)/31250); + if (dumpWrites) { + addWrite(0x10007+(i<<3),chan[i].pcm.freq); } } chan[i].freqChanged=false; @@ -214,7 +214,7 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } chan[c.chan].chVolL=c.value; @@ -250,17 +250,17 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { - int destFreq=(c.value2<<6)+baseFreqOff; + int destFreq=(c.value2<<6); int newFreq; bool return2=false; if (destFreq>chan[c.chan].baseFreq) { - newFreq=chan[c.chan].baseFreq+c.value; + newFreq=chan[c.chan].baseFreq+c.value*4; if (newFreq>=destFreq) { newFreq=destFreq; return2=true; } } else { - newFreq=chan[c.chan].baseFreq-c.value; + newFreq=chan[c.chan].baseFreq-c.value*4; if (newFreq<=destFreq) { newFreq=destFreq; return2=true; @@ -275,7 +275,7 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: { - chan[c.chan].baseFreq=(c.value<<6)+baseFreqOff; + chan[c.chan].baseFreq=(c.value<<6); chan[c.chan].freqChanged=true; break; } diff --git a/src/engine/platform/sms.cpp b/src/engine/platform/sms.cpp index 114d67b05..f4bbc46e1 100644 --- a/src/engine/platform/sms.cpp +++ b/src/engine/platform/sms.cpp @@ -56,31 +56,37 @@ int DivPlatformSMS::acquireOne() { void DivPlatformSMS::tick() { for (int i=0; i<4; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol&15)*MIN(15,chan[i].std.vol))>>4; + if (chan[i].std.vol.had) { + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); + if (chan[i].outVol<0) chan[i].outVol=0; + // old formula + // ((chan[i].vol&15)*MIN(15,chan[i].std.vol.val))>>4; rWrite(0x90|(i<<5)|(isMuted[i]?15:(15-(chan[i].outVol&15)))); } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); - chan[i].actualNote=chan[i].std.arp; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + chan[i].actualNote=chan[i].std.arp.val; } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); - chan[i].actualNote=chan[i].note+chan[i].std.arp; + // TODO: check whether this weird octave boundary thing applies to other systems as well + int areYouSerious=chan[i].note+chan[i].std.arp.val; + while (areYouSerious>0x60) areYouSerious-=12; + chan[i].baseFreq=NOTE_PERIODIC(areYouSerious); + chan[i].actualNote=areYouSerious; } chan[i].freqChanged=true; } } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); chan[i].actualNote=chan[i].note; chan[i].freqChanged=true; } } - if (i==3) if (chan[i].std.hadDuty) { - snNoiseMode=chan[i].std.duty; - if (chan[i].std.duty<2) { + if (i==3) if (chan[i].std.duty.had) { + snNoiseMode=chan[i].std.duty.val; + if (chan[i].std.duty.val<2) { chan[3].freqChanged=false; } updateSNMode=true; @@ -93,10 +99,11 @@ void DivPlatformSMS::tick() { if (chan[i].actualNote>0x5d) chan[i].freq=0x01; rWrite(0x80|i<<5|(chan[i].freq&15)); rWrite(chan[i].freq>>4); - if (i==2 && snNoiseMode&2) { + // what? + /*if (i==2 && snNoiseMode&2) { chan[3].baseFreq=chan[2].baseFreq; chan[3].actualNote=chan[2].actualNote; - } + }*/ chan[i].freqChanged=false; } } @@ -123,11 +130,11 @@ void DivPlatformSMS::tick() { } } else { // 3 fixed values unsigned char value; - if (chan[3].std.hadArp) { - if (chan[3].std.arpMode) { - value=chan[3].std.arp%12; + if (chan[3].std.arp.had) { + if (chan[3].std.arp.mode) { + value=chan[3].std.arp.val%12; } else { - value=(chan[3].note+chan[3].std.arp)%12; + value=(chan[3].note+chan[3].std.arp.val)%12; } } else { value=chan[3].note%12; @@ -174,14 +181,14 @@ int DivPlatformSMS::dispatch(DivCommand c) { case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (chan[c.chan].active) rWrite(0x90|c.chan<<5|(isMuted[c.chan]?15:(15-(chan[c.chan].vol&15)))); } break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { + if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; @@ -218,7 +225,7 @@ int DivPlatformSMS::dispatch(DivCommand c) { updateSNMode=true; break; case DIV_CMD_LEGATO: - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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; chan[c.chan].actualNote=c.value; diff --git a/src/engine/platform/sound/ay8910.cpp b/src/engine/platform/sound/ay8910.cpp index 2d734f931..8bb387885 100644 --- a/src/engine/platform/sound/ay8910.cpp +++ b/src/engine/platform/sound/ay8910.cpp @@ -1120,7 +1120,7 @@ void ay8910_device::sound_stream_update(short** outputs, int outLen) for (int chan = 0; chan < NUM_CHANNELS; chan++) { tone = &m_tone[chan]; - m_vol_enabled[chan] = (tone->output | tone_enable(chan)) & (noise_output() | noise_enable(chan)); + m_vol_enabled[chan] = (tone->output | (unsigned char)tone_enable(chan)) & (noise_output() | (unsigned char)noise_enable(chan)); } /* update envelope */ diff --git a/src/engine/platform/sound/k005289/k005289.cpp b/src/engine/platform/sound/k005289/k005289.cpp new file mode 100644 index 000000000..4763b63f0 --- /dev/null +++ b/src/engine/platform/sound/k005289/k005289.cpp @@ -0,0 +1,45 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami K005289 emulation core + + This chip is used at infamous Konami Bubble System, for part of Wavetable sound generator. + But seriously, It is just to 2 internal 12 bit timer and address generators, rather than sound generator. + + Everything except for internal counter and address are done by external logic, the chip is only has external address, frequency registers and its update pins. + + Frequency calculation: Input clock / (4096 - Pitch input) +*/ + +#include "k005289.hpp" + +void k005289_core::tick() +{ + for (auto & elem : m_voice) + elem.tick(); +} + +void k005289_core::reset() +{ + for (auto & elem : m_voice) + elem.reset(); +} + +void k005289_core::voice_t::tick() +{ + if (bitfield(++counter, 0, 12) == 0) + { + addr = bitfield(addr + 1, 0, 5); + counter = freq; + } +} + +void k005289_core::voice_t::reset() +{ + addr = 0; + pitch = 0; + freq = 0; + counter = 0; +} diff --git a/src/engine/platform/sound/k005289/k005289.hpp b/src/engine/platform/sound/k005289/k005289.hpp new file mode 100644 index 000000000..fe5d50ac9 --- /dev/null +++ b/src/engine/platform/sound/k005289/k005289.hpp @@ -0,0 +1,67 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami K005289 emulation core + + See k005289.cpp for more info. +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_K005289_HPP +#define _VGSOUND_EMU_K005289_HPP + +#pragma once + +namespace k005289 +{ + typedef unsigned char u8; + typedef unsigned short u16; + typedef signed short s16; + + // get bitfield, bitfield(input, position, len) + template T bitfield(T in, u8 pos, u8 len = 1) + { + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); + } +} + +using namespace k005289; +class k005289_core +{ +public: + // accessors, getters, setters + u8 addr(int voice) { return m_voice[voice & 1].addr; } // 1QA...E/2QA...E pin + void load(int voice, u16 addr) { m_voice[voice & 1].load(addr); } // LD1/2 pin, A0...11 pin + void update(int voice) { m_voice[voice & 1].update(); } // TG1/2 pin + + // internal state + void reset(); + void tick(); + +private: + // k005289 voice structs + struct voice_t + { + // internal state + void reset(); + void tick(); + + // accessors, getters, setters + void load(u16 addr) { pitch = addr; } // Load pitch data (address pin) + void update() { freq = pitch; } // Replace current frequency to lastest loaded pitch + + // registers + u8 addr = 0; // external address pin + u16 pitch = 0; // pitch + u16 freq = 0; // current frequency + s16 counter = 0; // frequency counter + }; + + voice_t m_voice[2]; +}; + +#endif diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index ad112baf1..5d12bbb86 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -435,8 +435,8 @@ public: case ATTENREG2: case ATTENREG3: mRegisterPool[8*4+idx] = value; - mAttenuationLeft[idx] = ( value & 0x0f ) << 2; - mAttenuationRight[idx] = ( value & 0xf0 ) >> 2; + mAttenuationRight[idx] = ( value & 0x0f ) << 2; + mAttenuationLeft[idx] = ( value & 0xf0 ) >> 2; break; case MPAN: mPan = value; diff --git a/src/engine/platform/sound/n163/n163.cpp b/src/engine/platform/sound/n163/n163.cpp new file mode 100644 index 000000000..b18f146ba --- /dev/null +++ b/src/engine/platform/sound/n163/n163.cpp @@ -0,0 +1,160 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Namco 163 Sound emulation core + + This chip is one of NES mapper with sound expansion, This one is by Namco. + + It has 1 to 8 wavetable channels, All channel registers and waveforms are stored to internal RAM. + 4 bit Waveforms are freely allocatable, and its length is variables; its can be stores many short waveforms or few long waveforms in RAM. + + But waveforms are needs to squash, reallocate to avoid conflict with channel register area, each channel register size is 8 bytes per channels. + + Sound output is time division multiplexed, it's can be captured only single channels output at once. in reason, More activated channels are less sound quality. + + Sound register layout + + Address Bit Description + 7654 3210 + + 78-7f Channel 0 + 78 xxxx xxxx Channel 0 Pitch input bit 0-7 + 79 xxxx xxxx Channel 0 Accumulator bit 0-7* + 7a xxxx xxxx Channel 0 Pitch input bit 8-15 + 7b xxxx xxxx Channel 0 Accumulator bit 8-15* + 7c xxxx xx-- Channel 0 Waveform length, 256 - (x * 4) + ---- --xx Channel 0 Pitch input bit 16-17 + 7d xxxx xxxx Channel 0 Accumulator bit 16-23* + 7e xxxx xxxx Channel 0 Waveform base offset + xxxx xxx- RAM byte (0 to 127) + ---- ---x RAM nibble + ---- ---0 Low nibble + ---- ---1 High nibble + 7f ---- xxxx Channel 0 Volume + + 7f Number of active channels + 7f -xxx ---- Number of active channels + -000 ---- Channel 0 activated + -001 ---- Channel 1 activated + -010 ---- Channel 2 activated + ... + -110 ---- Channel 6 activated + -111 ---- Channel 7 activated + + 70-77 Channel 1 (Optional if activated) + 68-6f Channel 2 (Optional if activated) + ... + 48-4f Channel 6 (Optional if activated) + 40-47 Channel 7 (Optional if activated) + + Rest of RAM area are for 4 bit Waveform and/or scratchpad. + Each waveform byte has 2 nibbles packed, fetches LSB first, MSB next. + ---- xxxx 4 bit waveform, LSB + xxxx ---- Same as above, MSB + + Waveform address: Waveform base offset + Bit 16 to 23 of Accumulator, 1 LSB of result is nibble select, 7 MSB of result is Byte address in RAM. + + Frequency formula: + Frequency: Pitch input * ((Input clock * 15 * Number of activated voices) / 65536) + + There's to way for reduce N163 noises: reduce channel limit and demultiplex + - Channel limit is runtime changeable and it makes some usable effects. + - Demultiplex is used for "non-ear destroyable" emulators, but less hardware accurate. (when LPF and RF filter is not considered) + This core is support both, You can choose output behavior + +*/ + +#include "n163.hpp" + +void n163_core::tick() +{ + if (m_multiplex) + m_out = 0; + // 0xe000-0xe7ff Disable sound bits (bit 6, bit 0 to 5 are CPU ROM Bank 0x8000-0x9fff select.) + if (m_disable) + { + if (!m_multiplex) + m_out = 0; + return; + } + + // tick per each clock + const u32 freq = m_ram[m_voice_cycle + 0] | (u32(m_ram[m_voice_cycle + 2]) << 8) | (bitfield(m_ram[m_voice_cycle + 4], 0, 2) << 16); // 18 bit frequency + u32 accum = m_ram[m_voice_cycle + 1] | (u32(m_ram[m_voice_cycle + 3]) << 8) | ( u32(m_ram[m_voice_cycle + 5]) << 16); // 24 bit accumulator + const u16 length = 256 - (m_ram[m_voice_cycle + 4] & 0xfc); + const u8 addr = m_ram[m_voice_cycle + 6] + bitfield(accum, 16, 8); + const s16 wave = (bitfield(m_ram[bitfield(addr, 1, 7)], bitfield(addr, 0) << 2, 4) - 8); + const s16 volume = bitfield(m_ram[m_voice_cycle + 7], 0, 4); + + // accumulate address + accum = bitfield(accum + freq, 0, 24); + if (bitfield(accum, 16, 8) >= length) + accum = bitfield(accum, 0, 18); + + // writeback to register + m_ram[m_voice_cycle + 1] = bitfield(accum, 0, 8); + m_ram[m_voice_cycle + 3] = bitfield(accum, 8, 8); + m_ram[m_voice_cycle + 5] = bitfield(accum, 16, 8); + + // update voice cycle + bool flush = m_multiplex ? true : false; + m_voice_cycle -= 0x8; + if (m_voice_cycle < (0x78 - (bitfield(m_ram[0x7f], 4, 3) << 3))) + { + if (!m_multiplex) + flush = true; + m_voice_cycle = 0x78; + } + + // output 4 bit waveform and volume, multiplexed + m_acc += wave * volume; + if (flush) + { + m_out = m_acc / (m_multiplex ? 1 : (bitfield(m_ram[0x7f], 4, 3) + 1)); + m_acc = 0; + } +} + +void n163_core::reset() +{ + // reset this chip + m_disable = false; + m_multiplex = true; + std::fill(std::begin(m_ram), std::end(m_ram), 0); + m_voice_cycle = 0x78; + m_addr_latch.reset(); + m_out = 0; + m_acc = 0; +} + +// accessor +void n163_core::addr_w(u8 data) +{ + // 0xf800-0xffff Sound address, increment + m_addr_latch.addr = bitfield(data, 0, 7); + m_addr_latch.incr = bitfield(data, 7); +} + +void n163_core::data_w(u8 data, bool cpu_access) +{ + // 0x4800-0x4fff Sound data write + m_ram[m_addr_latch.addr] = data; + + // address latch increment + if (cpu_access && m_addr_latch.incr) + m_addr_latch.addr = bitfield(m_addr_latch.addr + 1, 0, 7); +} + +u8 n163_core::data_r(bool cpu_access) +{ + // 0x4800-0x4fff Sound data read + const u8 ret = m_ram[m_addr_latch.addr]; + + // address latch increment + if (cpu_access && m_addr_latch.incr) + m_addr_latch.addr = bitfield(m_addr_latch.addr + 1, 0, 7); + + return ret; +} diff --git a/src/engine/platform/sound/n163/n163.hpp b/src/engine/platform/sound/n163/n163.hpp new file mode 100644 index 000000000..a31827572 --- /dev/null +++ b/src/engine/platform/sound/n163/n163.hpp @@ -0,0 +1,82 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Namco 163 Sound emulation core +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_N163_HPP +#define _VGSOUND_EMU_N163_HPP + +#pragma once + +namespace n163 +{ + typedef unsigned char u8; + typedef unsigned short u16; + typedef unsigned int u32; + typedef signed short s16; + + // get bitfield, bitfield(input, position, len) + template T bitfield(T in, u8 pos, u8 len = 1) + { + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); + } +}; + +using namespace n163; +class n163_core +{ +public: + // accessors, getters, setters + void addr_w(u8 data); + void data_w(u8 data, bool cpu_access = false); + u8 data_r(bool cpu_access = false); + + void set_disable(bool disable) { m_disable = disable; } + + // internal state + void reset(); + void tick(); + + // sound output pin + s16 out() { return m_out; } + + // register pool + u8 reg(u8 addr) { return m_ram[addr & 0x7f]; } + void set_multiplex(bool multiplex = true) { m_multiplex = multiplex; } + +private: + // Address latch + struct addr_latch_t + { + addr_latch_t() + : addr(0) + , incr(0) + { }; + + void reset() + { + addr = 0; + incr = 0; + } + + u8 addr : 7; + u8 incr : 1; + }; + + bool m_disable = false; + u8 m_ram[0x80] = {0}; // internal 128 byte RAM + u8 m_voice_cycle = 0x78; // Voice cycle for processing + addr_latch_t m_addr_latch; // address latch + s16 m_out = 0; // output + // demultiplex related + bool m_multiplex = true; // multiplex flag, but less noisy = inaccurate! + s16 m_acc = 0; // accumulated output +}; + +#endif diff --git a/src/engine/platform/sound/nes/apu.c b/src/engine/platform/sound/nes/apu.c index a278cbf74..9e177a780 100644 --- a/src/engine/platform/sound/nes/apu.c +++ b/src/engine/platform/sound/nes/apu.c @@ -177,8 +177,8 @@ void apu_tick(struct NESAPU* a, BYTE *hwtick) { * eseguo un ticket per ogni canale * valorizzandone l'output. */ - square_tick(a->S1, 0, a->apu) - square_tick(a->S2, 0, a->apu) + square_tick(a->S1, 0, a->apu.clocked) + square_tick(a->S2, 0, a->apu.clocked) triangle_tick() noise_tick() dmc_tick() @@ -213,7 +213,8 @@ void apu_turn_on(struct NESAPU* a, BYTE apu_type) { a->S2.sweep.delay = 1; a->S2.sweep.divider = 1; a->TR.frequency = 1; - a->TR.sequencer = 0; + /* questo era 0 ma produce click nell'audio */ + a->TR.sequencer = 7; a->NS.frequency = 1; a->NS.shift = 1; a->DMC.frequency = 1; @@ -226,4 +227,6 @@ void apu_turn_on(struct NESAPU* a, BYTE apu_type) { a->DMC.length = 1; a->DMC.address_start = 0xC000; a->apu.odd_cycle = 0; + // come non viene inizializzato? Vorrei qualche spiegazione... + a->r4011.frames = 0; } diff --git a/src/engine/platform/sound/nes/apu.h b/src/engine/platform/sound/nes/apu.h index 93cf72fef..224ea2a04 100644 --- a/src/engine/platform/sound/nes/apu.h +++ b/src/engine/platform/sound/nes/apu.h @@ -137,12 +137,12 @@ enum apu_mode { APU_60HZ, APU_48HZ }; #define dmc_output()\ a->DMC.output = a->DMC.counter & 0x7F /* tick */ -#define square_tick(square, swap, type)\ +#define square_tick(square, swap, type_clocked)\ if (!(--square.frequency)) {\ square_output(square, swap)\ square.frequency = (square.timer + 1) << 1;\ square.sequencer = (square.sequencer + 1) & 0x07;\ - type.clocked = TRUE;\ + type_clocked = TRUE;\ } #define triangle_tick()\ if (!(--a->TR.frequency)) {\ @@ -302,7 +302,7 @@ enum apu_mode { APU_60HZ, APU_48HZ }; #define square_reg2(square)\ /* timer (low 8 bits) */\ square.timer = (square.timer & 0x0700) | value -#define square_reg3(square)\ +#define square_reg3(square,length_clocked)\ /* length counter */\ /*\ * se non disabilitato, una scrittura in\ @@ -312,7 +312,7 @@ enum apu_mode { APU_60HZ, APU_48HZ }; * momento del clock di un length counter e\ * con il length diverso da zero.\ */\ - if (square.length.enabled && !(a->apu.length_clocked && square.length.value)) {\ + if (square.length.enabled && !(length_clocked && square.length.value)) {\ square.length.value = length_table[value >> 3];\ }\ /* envelope */\ @@ -510,9 +510,11 @@ typedef struct _apuDMC { #endif EXTERNC struct _nla_table { - SWORD pulse[32]; - SWORD tnd[203]; -} nla_table; + SWORD pulse[32]; + SWORD tnd[203]; +}; + +extern struct _nla_table nla_table; EXTERNC struct NESAPU { _apu apu; diff --git a/src/engine/platform/sound/nes/cpu_inline.h b/src/engine/platform/sound/nes/cpu_inline.h index 665fa01ee..c9af64e9e 100644 --- a/src/engine/platform/sound/nes/cpu_inline.h +++ b/src/engine/platform/sound/nes/cpu_inline.h @@ -21,6 +21,7 @@ #include #include "apu.h" +#include "fds.h" #define mod_cycles_op(op, vl) cpu.cycles op vl #define r2006_during_rendering()\ @@ -57,7 +58,7 @@ INLINE static void apu_wr_reg(struct NESAPU* a, WORD address, BYTE value) { return; } if (address == 0x4003) { - square_reg3(a->S1); + square_reg3(a->S1,a->apu.length_clocked); sweep_silence(a->S1) return; } @@ -80,7 +81,7 @@ INLINE static void apu_wr_reg(struct NESAPU* a, WORD address, BYTE value) { return; } if (address == 0x4007) { - square_reg3(a->S2); + square_reg3(a->S2,a->apu.length_clocked); sweep_silence(a->S2) return; } @@ -314,4 +315,83 @@ INLINE static void apu_wr_reg(struct NESAPU* a, WORD address, BYTE value) { return; } + +INLINE static BYTE fds_wr_mem(struct _fds* fds, WORD address, BYTE value) { + if (address == 0x4023) { + fds->enabled_snd_reg=value&0x02; + } + if ((address >= 0x4040) && (address <= 0x408A)) { + if (fds->enabled_snd_reg) { + if ((address >= 0x4040) && (address <= 0x407F)) { + fds->snd.wave.data[address & 0x003F] = value & 0x3F; + return (TRUE); + } + if (address == 0x4080) { + fds->snd.volume.speed = value & 0x3F; + fds->snd.volume.increase = value & 0x40; + fds->snd.volume.mode = value & 0x80; + return (TRUE); + } + if (address == 0x4082) { + fds->snd.main.frequency = (fds->snd.main.frequency & 0xFF00) | value; + return (TRUE); + } + if (address == 0x4083) { + fds->snd.main.frequency = ((value & 0x0F) << 8) | (fds->snd.main.frequency & 0x00FF); + fds->snd.envelope.disabled = value & 0x40; + fds->snd.main.silence = value & 0x80; + return (TRUE); + } + if (address == 0x4084) { + fds->snd.sweep.speed = value & 0x3F; + fds->snd.sweep.increase = value & 0x40; + fds->snd.sweep.mode = value & 0x80; + return (TRUE); + } + if (address == 0x4085) { + fds->snd.sweep.bias = ((SBYTE) (value << 1)) / 2; + fds->snd.modulation.index = 0; + return (TRUE); + } + if (address == 0x4086) { + fds->snd.modulation.frequency = (fds->snd.modulation.frequency & 0xFF00) | value; + return (TRUE); + } + if (address == 0x4087) { + fds->snd.modulation.frequency = ((value & 0x0F) << 8) + | (fds->snd.modulation.frequency & 0x00FF); + fds->snd.modulation.disabled = value & 0x80; + return (TRUE); + } + if (address == 0x4088) { + BYTE i; + + // 0,2,4,6,-8,-6,-4,-2 + for (i = 0; i < 32; i++) { + BYTE a = i << 1; + + if (i < 31) { + fds->snd.modulation.data[a] = fds->snd.modulation.data[a + 2]; + } else { + BYTE tmp = ((value & 0x03) | (0x3F * (value & 0x04))); + fds->snd.modulation.data[a] = (SBYTE) tmp; + } + fds->snd.modulation.data[a + 1] = fds->snd.modulation.data[a]; + } + return (TRUE); + } + if (address == 0x4089) { + fds->snd.wave.writable = value & 0x80; + fds->snd.wave.volume = value & 0x03; + return (TRUE); + } + if (address == 0x408A) { + fds->snd.envelope.speed = value; + return (TRUE); + } + } + } + + return (FALSE); +} #endif /* CPU_INLINE_H_ */ diff --git a/src/engine/platform/sound/nes/fds.c b/src/engine/platform/sound/nes/fds.c new file mode 100644 index 000000000..e72e90389 --- /dev/null +++ b/src/engine/platform/sound/nes/fds.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010-2019 Fabio Cavallo (aka FHorse) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include +#include +#include "fds.h" + +enum { TRANSFERED_8BIT = 0x02, END_OF_HEAD = 0x40 }; + +static const BYTE volume_wave[4] = { 39, 26, 19, 15 }; + +void fds_reset(struct _fds* fds) { + memset(fds,0,sizeof(struct _fds)); +} + +void extcl_apu_tick_FDS(struct _fds* fds) { + SWORD freq; + + /* volume unit */ + if (fds->snd.volume.mode) { + fds->snd.volume.gain = fds->snd.volume.speed; + } else if (!fds->snd.envelope.disabled && fds->snd.envelope.speed) { + if (fds->snd.volume.counter) { + fds->snd.volume.counter--; + } else { + fds->snd.volume.counter = (fds->snd.envelope.speed << 3) * (fds->snd.volume.speed + 1); + if (fds->snd.volume.increase) { + if (fds->snd.volume.gain < 32) { + fds->snd.volume.gain++; + } + } else if (fds->snd.volume.gain) { + fds->snd.volume.gain--; + } + } + } + + /* sweep unit */ + if (fds->snd.sweep.mode) { + fds->snd.sweep.gain = fds->snd.sweep.speed; + } else if (!fds->snd.envelope.disabled && fds->snd.envelope.speed) { + if (fds->snd.sweep.counter) { + fds->snd.sweep.counter--; + } else { + fds->snd.sweep.counter = (fds->snd.envelope.speed << 3) * (fds->snd.sweep.speed + 1); + if (fds->snd.sweep.increase) { + if (fds->snd.sweep.gain < 32) { + fds->snd.sweep.gain++; + } + } else if (fds->snd.sweep.gain) { + fds->snd.sweep.gain--; + } + } + } + + /* modulation unit */ + freq = fds->snd.main.frequency; + + if (!fds->snd.modulation.disabled && fds->snd.modulation.frequency) { + if ((fds->snd.modulation.counter -= fds->snd.modulation.frequency) < 0) { + SWORD temp, temp2, a, d; + SBYTE adj = fds->snd.modulation.data[fds->snd.modulation.index]; + + fds->snd.modulation.counter += 65536; + + if (++fds->snd.modulation.index == 64) { + fds->snd.modulation.index = 0; + } + + if (adj == -4) { + fds->snd.sweep.bias = 0; + } else { + fds->snd.sweep.bias += adj; + } + + temp = fds->snd.sweep.bias * ((fds->snd.sweep.gain < 32) ? fds->snd.sweep.gain : 32); + + a = 64; + d = 0; + + if (temp <= 0) { + d = 15; + } else if (temp < 3040) { //95 * 32 + a = 66; + d = -31; + } + + temp2 = a + (SBYTE) ((temp - d) / 16 - a); + + fds->snd.modulation.mod = freq * temp2 / 64; + } + + if (freq) { + freq += fds->snd.modulation.mod; + } + } + + /* main unit */ + if (fds->snd.main.silence) { + fds->snd.main.output = 0; + return; + } + + if (freq && !fds->snd.wave.writable) { + if ((fds->snd.wave.counter -= freq) < 0) { + WORD level; + + fds->snd.wave.counter += 65536; + + level = (fds->snd.volume.gain < 32 ? fds->snd.volume.gain : 32) + * volume_wave[fds->snd.wave.volume]; + + /* valore massimo dell'output (63 * (39 * 32)) = 78624 */ + /*fds->snd.main.output = (fds->snd.wave.data[fds->snd.wave.index] * level) >> 4;*/ + fds->snd.main.output = (fds->snd.wave.data[fds->snd.wave.index] * level) >> 3; + + if (++fds->snd.wave.index == 64) { + fds->snd.wave.index = 0; + } + + fds->snd.wave.clocked = TRUE; + } + } +} diff --git a/src/engine/platform/sound/nes/fds.h b/src/engine/platform/sound/nes/fds.h new file mode 100644 index 000000000..f1ef45e72 --- /dev/null +++ b/src/engine/platform/sound/nes/fds.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2010-2019 Fabio Cavallo (aka FHorse) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef FDS_H_ +#define FDS_H_ + +#include "common.h" + +enum fds_operations { FDS_OP_NONE, FDS_OP_READ, FDS_OP_WRITE }; + +#if defined (__cplusplus) +#define EXTERNC extern "C" +#else +#define EXTERNC +#endif + +EXTERNC struct _fds { + // snd + BYTE enabled_snd_reg; + struct _fds_snd { + struct _fds_snd_wave { + BYTE data[64]; + BYTE writable; + BYTE volume; + + BYTE index; + int32_t counter; + + /* ------------------------------------------------------- */ + /* questi valori non e' necessario salvarli nei savestates */ + /* ------------------------------------------------------- */ + /* */ BYTE clocked; /* */ + /* ------------------------------------------------------- */ + } wave; + struct _fds_snd_envelope { + BYTE speed; + BYTE disabled; + } envelope; + struct _fds_snd_main { + BYTE silence; + WORD frequency; + + SWORD output; + } main; + struct _fds_snd_volume { + BYTE speed; + BYTE mode; + BYTE increase; + + BYTE gain; + uint32_t counter; + } volume; + struct _fds_snd_sweep { + SBYTE bias; + BYTE mode; + BYTE increase; + BYTE speed; + + BYTE gain; + uint32_t counter; + } sweep; + struct _fds_snd_modulation { + SBYTE data[64]; + WORD frequency; + BYTE disabled; + + BYTE index; + int32_t counter; + SWORD mod; + } modulation; + } snd; +}; + +EXTERNC void extcl_apu_tick_FDS(struct _fds* fds); +EXTERNC void fds_reset(struct _fds* fds); + +#undef EXTERNC + +#endif /* FDS_H_ */ diff --git a/src/engine/platform/sound/nes/mmc5.c b/src/engine/platform/sound/nes/mmc5.c new file mode 100644 index 000000000..9272bc44b --- /dev/null +++ b/src/engine/platform/sound/nes/mmc5.c @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010-2019 Fabio Cavallo (aka FHorse) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include +#include "apu.h" +#include "mmc5.h" + +enum { MODE0, MODE1, MODE2, MODE3 }; +enum { CHR_S, CHR_B }; +enum { SPLIT_LEFT, SPLIT_RIGHT = 0x40 }; + +const BYTE filler_attrib[4] = {0x00, 0x55, 0xAA, 0xFF}; +BYTE prg_ram_mode; + +void map_init_MMC5(struct _mmc5* mmc5) { + memset(mmc5,0,sizeof(struct _mmc5)); + + mmc5->S3.frequency = 1; + mmc5->S4.frequency = 1; + mmc5->S3.length.enabled = 0; + mmc5->S3.length.value = 0; + mmc5->S4.length.enabled = 0; + mmc5->S4.length.value = 0; +} +void extcl_cpu_wr_mem_MMC5(struct _mmc5* mmc5, WORD address, BYTE value) { + if (address < 0x5000) { + return; + } + + switch (address) { + case 0x5000: + square_reg0(mmc5->S3); + return; + case 0x5001: + /* lo sweep non e' utilizzato */ + return; + case 0x5002: + square_reg2(mmc5->S3); + return; + case 0x5003: + square_reg3(mmc5->S3,0); + return; + case 0x5004: + square_reg0(mmc5->S4); + return; + case 0x5005: + /* lo sweep non e' utilizzato */ + return; + case 0x5006: + square_reg2(mmc5->S4); + return; + case 0x5007: + square_reg3(mmc5->S4,0); + return; + case 0x5010: + mmc5->pcm.enabled = ~value & 0x01; + mmc5->pcm.output = 0; + if (mmc5->pcm.enabled) { + mmc5->pcm.output = mmc5->pcm.amp; + } + mmc5->clocked = TRUE; + return; + case 0x5011: + mmc5->pcm.amp = value; + mmc5->pcm.output = 0; + if (mmc5->pcm.enabled) { + mmc5->pcm.output = mmc5->pcm.amp; + } + mmc5->clocked = TRUE; + return; + case 0x5015: + if (!(mmc5->S3.length.enabled = value & 0x01)) { + mmc5->S3.length.value = 0; + } + if (!(mmc5->S4.length.enabled = value & 0x02)) { + mmc5->S4.length.value = 0; + } + return; + } +} +void extcl_length_clock_MMC5(struct _mmc5* mmc5) { + length_run(mmc5->S3) + length_run(mmc5->S4) +} +void extcl_envelope_clock_MMC5(struct _mmc5* mmc5) { + envelope_run(mmc5->S3) + envelope_run(mmc5->S4) +} +void extcl_apu_tick_MMC5(struct _mmc5* mmc5) { + square_tick(mmc5->S3, 0, mmc5->clocked) + square_tick(mmc5->S4, 0, mmc5->clocked) +} \ No newline at end of file diff --git a/src/engine/platform/sound/nes/mmc5.h b/src/engine/platform/sound/nes/mmc5.h new file mode 100644 index 000000000..3d1fbff93 --- /dev/null +++ b/src/engine/platform/sound/nes/mmc5.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010-2019 Fabio Cavallo (aka FHorse) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef MAPPER_MMC5_H_ +#define MAPPER_MMC5_H_ + +#include "apu.h" + +#if defined (__cplusplus) +#define EXTERNC extern "C" +#else +#define EXTERNC +#endif + +EXTERNC struct _mmc5 { + BYTE prg_mode; + BYTE chr_mode; + BYTE ext_mode; + BYTE nmt_mode[4]; + BYTE prg_ram_write[2]; + BYTE prg_bank[4]; + uint32_t prg_ram_bank[4][2]; + BYTE chr_last; + WORD chr_high; + WORD chr_s[8]; + WORD chr_b[4]; + BYTE ext_ram[0x400]; + BYTE fill_table[0x400]; + BYTE fill_tile; + BYTE fill_attr; + BYTE split; + BYTE split_st_tile; + BYTE split_side; + BYTE split_scrl; + BYTE split_in_reg; + BYTE split_x; + BYTE split_y; + WORD split_tile; + uint32_t split_bank; + BYTE factor[2]; + WORD product; + _apuSquare S3, S4; + struct _mmc5_pcm { + BYTE enabled; + BYTE output; + BYTE amp; + } pcm; + BYTE filler[50]; + + /* ------------------------------------------------------- */ + /* questi valori non e' necessario salvarli nei savestates */ + /* ------------------------------------------------------- */ + /* */ BYTE clocked; /* */ + /* ------------------------------------------------------- */ +}; + +EXTERNC void map_init_MMC5(struct _mmc5* mmc5); +EXTERNC void extcl_cpu_wr_mem_MMC5(struct _mmc5* mmc5, WORD address, BYTE value); +EXTERNC void extcl_length_clock_MMC5(struct _mmc5* mmc5); +EXTERNC void extcl_envelope_clock_MMC5(struct _mmc5* mmc5); +EXTERNC void extcl_apu_tick_MMC5(struct _mmc5* mmc5); + +#undef EXTERNC + +#endif /* MAPPER_MMC5_H_ */ diff --git a/src/engine/platform/sound/pce_psg.cpp b/src/engine/platform/sound/pce_psg.cpp index ed71e0fd2..f9c1ee099 100644 --- a/src/engine/platform/sound/pce_psg.cpp +++ b/src/engine/platform/sound/pce_psg.cpp @@ -157,12 +157,6 @@ void PCE_PSG::RecalcUOFunc(int chnum) //printf("UO Update: %d, %02x\n", chnum, ch->control); - // what is this? - if (lfoctrl&3 && chnum==1) { - ch->UpdateOutput = &PCE_PSG::UpdateOutput_Off; - return; - } - if((revision != REVISION_HUC6280 && !(ch->control & 0xC0)) || (revision == REVISION_HUC6280 && !(ch->control & 0x80))) ch->UpdateOutput = &PCE_PSG::UpdateOutput_Off; else if(ch->noisectrl & ch->control & 0x80) diff --git a/src/engine/platform/sound/swan.cpp b/src/engine/platform/sound/swan.cpp new file mode 100644 index 000000000..685fefe94 --- /dev/null +++ b/src/engine/platform/sound/swan.cpp @@ -0,0 +1,409 @@ +/******************************************************************************/ +/* Mednafen - Multi-system Emulator */ +/******************************************************************************/ +/* sound.cpp - WonderSwan Sound Emulation +** Copyright (C) 2007-2017 Mednafen Team +** Copyright (C) 2016 Alex 'trap15' Marshall - http://daifukkat.su/ +** +** 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 "swan.h" +#include + +#define MK_SAMPLE_CACHE \ + { \ + int sample; \ + sample = (((wsRAM[(/*(SampleRAMPos << 6) + */(sample_pos[ch] >> 1) + (ch << 4)) ] >> ((sample_pos[ch] & 1) ? 4 : 0)) & 0x0F)); \ + sample_cache[ch][0] = sample * ((volume[ch] >> 4) & 0x0F); \ + sample_cache[ch][1] = sample * ((volume[ch] >> 0) & 0x0F); \ + } + +#define MK_SAMPLE_CACHE_NOISE \ + { \ + int sample; \ + sample = ((nreg & 1) ? 0xF : 0x0); \ + sample_cache[ch][0] = sample * ((volume[ch] >> 4) & 0x0F); \ + sample_cache[ch][1] = sample * ((volume[ch] >> 0) & 0x0F); \ + } + +#define MK_SAMPLE_CACHE_VOICE \ + { \ + int sample, half; \ + sample = volume[ch]; \ + half = sample >> 1; \ + sample_cache[ch][0] = (voice_volume & 4) ? sample : (voice_volume & 8) ? half : 0; \ + sample_cache[ch][1] = (voice_volume & 1) ? sample : (voice_volume & 2) ? half : 0; \ + } + + +#define SYNCSAMPLE(wt) /* \ + { \ + int32_t left = sample_cache[ch][0], right = sample_cache[ch][1]; \ + WaveSynth.offset_inline(wt, left - last_val[ch][0], sbuf[0]); \ + WaveSynth.offset_inline(wt, right - last_val[ch][1], sbuf[1]); \ + last_val[ch][0] = left; \ + last_val[ch][1] = right; \ + } */ + +#define SYNCSAMPLE_NOISE(wt) SYNCSAMPLE(wt) + +void WSwan::SoundUpdate(uint32_t v30mz_timestamp) +{ + int32_t run_time; + + //printf("%d\n", v30mz_timestamp); + //printf("%02x %02x\n", control, noise_control); + run_time = v30mz_timestamp - last_ts; + + for(int y = 0; y < 2; y++) + sbuf[y] = 0; + + for(unsigned int ch = 0; ch < 4; ch++) + { + // Channel is disabled? + if(!(control & (1 << ch))) + continue; + + if(ch == 1 && (control & 0x20)) // Direct D/A mode? + { + MK_SAMPLE_CACHE_VOICE; + SYNCSAMPLE(v30mz_timestamp); + } + else if(ch == 2 && (control & 0x40) && sweep_value) // Sweep + { + uint32_t tmp_pt = 2048 - period[ch]; + uint32_t tmp_run_time = run_time; + + while(tmp_run_time) + { + int32_t sub_run_time = tmp_run_time; + + if(sub_run_time > sweep_8192_divider) + sub_run_time = sweep_8192_divider; + + sweep_8192_divider -= sub_run_time; + if(sweep_8192_divider <= 0) + { + sweep_8192_divider += 8192; + sweep_counter--; + if(sweep_counter <= 0) + { + sweep_counter = sweep_step + 1; + period[ch] = (period[ch] + (int8_t)sweep_value) & 0x7FF; + } + } + + if(tmp_pt > 4) + { + period_counter[ch] -= sub_run_time; + while(period_counter[ch] <= 0) + { + sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F; + + MK_SAMPLE_CACHE; + period_counter[ch] += tmp_pt; + } + } + tmp_run_time -= sub_run_time; + } + } + else if(ch == 3 && (control & 0x80) && (noise_control & 0x10)) // Noise + { + uint32_t tmp_pt = 2048 - period[ch]; + + period_counter[ch] -= run_time; + while(period_counter[ch] <= 0) + { + static const uint8_t stab[8] = { 14, 10, 13, 4, 8, 6, 9, 11 }; + + nreg = ((nreg << 1) | ((1 ^ (nreg >> 7) ^ (nreg >> stab[noise_control & 0x7])) & 1)) & 0x7FFF; + + if(control & 0x80) + { + MK_SAMPLE_CACHE_NOISE; + SYNCSAMPLE_NOISE(v30mz_timestamp + period_counter[ch]); + } + else if(tmp_pt > 4) + { + sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F; + MK_SAMPLE_CACHE; + SYNCSAMPLE(v30mz_timestamp + period_counter[ch]); + } + period_counter[ch] += tmp_pt; + } + } + else + { + uint32_t tmp_pt = 2048 - period[ch]; + + if(tmp_pt > 4) + { + period_counter[ch] -= run_time; + while(period_counter[ch] <= 0) + { + sample_pos[ch] = (sample_pos[ch] + 1) & 0x1F; + + MK_SAMPLE_CACHE; + SYNCSAMPLE(v30mz_timestamp + period_counter[ch]); // - period_counter[ch]); + period_counter[ch] += tmp_pt; + } + } + } + sbuf[0] += sample_cache[ch][0]; + sbuf[1] += sample_cache[ch][1]; + } + + if(HVoiceCtrl & 0x80) + { + int16_t sample = (uint8_t)HyperVoice; + + switch(HVoiceCtrl & 0xC) + { + case 0x0: sample = (uint16_t)sample << (8 - (HVoiceCtrl & 3)); break; + case 0x4: sample = (uint16_t)(sample | -0x100) << (8 - (HVoiceCtrl & 3)); break; + case 0x8: sample = (uint16_t)((int8_t)sample) << (8 - (HVoiceCtrl & 3)); break; + case 0xC: sample = (uint16_t)sample << 8; break; + } + // bring back to 11bit, keeping signedness + sample >>= 5; + + int32_t left, right; + left = (HVoiceChanCtrl & 0x40) ? sample : 0; + right = (HVoiceChanCtrl & 0x20) ? sample : 0; + + // WaveSynth.offset_inline(v30mz_timestamp, left - last_hv_val[0], sbuf[0]); + // WaveSynth.offset_inline(v30mz_timestamp, right - last_hv_val[1], sbuf[1]); + // last_hv_val[0] = left; + // last_hv_val[1] = right; + sbuf[0] += left; + sbuf[1] += right; + } + last_ts = v30mz_timestamp; +} + +void WSwan::SoundWrite(uint32_t A, uint8_t V) +{ + if(A >= 0x80 && A <= 0x87) + { + int ch = (A - 0x80) >> 1; + + if(A & 1) + period[ch] = (period[ch] & 0x00FF) | ((V & 0x07) << 8); + else + period[ch] = (period[ch] & 0x0700) | ((V & 0xFF) << 0); + + //printf("Period %d: 0x%04x --- %f\n", ch, period[ch], 3072000.0 / (2048 - period[ch])); + } + else if(A >= 0x88 && A <= 0x8B) + { + volume[A - 0x88] = V; + } + else if(A == 0x8C) + sweep_value = V; + else if(A == 0x8D) + { + sweep_step = V; + sweep_counter = sweep_step + 1; + sweep_8192_divider = 8192; + } + else if(A == 0x8E) + { + //printf("NOISECONTROL: %02x\n", V); + if(V & 0x8) + nreg = 0; + + noise_control = V & 0x17; + } + else if(A == 0x90) + { + for(int n = 0; n < 4; n++) + { + if(!(control & (1 << n)) && (V & (1 << n))) + { + period_counter[n] = 1; + sample_pos[n] = 0x1F; + } + } + control = V; + //printf("Sound Control: %02x\n", V); + } + else if(A == 0x91) + { + output_control = V & 0xF; + //printf("%02x, %02x\n", V, (V >> 1) & 3); + } + else if(A == 0x92) + nreg = (nreg & 0xFF00) | (V << 0); + else if(A == 0x93) + nreg = (nreg & 0x00FF) | ((V & 0x7F) << 8); + else if(A == 0x94) + { + voice_volume = V & 0xF; + //printf("%02x\n", V); + } + else switch(A) + { + case 0x6A: HVoiceCtrl = V; break; + case 0x6B: HVoiceChanCtrl = V & 0x6F; break; + case 0x8F: SampleRAMPos = V; break; + case 0x95: HyperVoice = V; break; // Pick a port, any port?! + //default: printf("%04x:%02x\n", A, V); break; + } +} + +uint8_t WSwan::SoundRead(uint32_t A) +{ + if(A >= 0x80 && A <= 0x87) + { + int ch = (A - 0x80) >> 1; + + if(A & 1) + return(period[ch] >> 8); + else + return(period[ch]); + } + else if(A >= 0x88 && A <= 0x8B) + return(volume[A - 0x88]); + else switch(A) + { + default: /*printf("SoundRead: %04x\n", A);*/ return(0); + case 0x6A: return(HVoiceCtrl); + case 0x6B: return(HVoiceChanCtrl); + case 0x8C: return(sweep_value); + case 0x8D: return(sweep_step); + case 0x8E: return(noise_control); + case 0x8F: return(SampleRAMPos); + case 0x90: return(control); + case 0x91: return(output_control | 0x80); + case 0x92: return((nreg >> 0) & 0xFF); + case 0x93: return((nreg >> 8) & 0xFF); + case 0x94: return(voice_volume); + } +} + +void WSwan::RAMWrite(uint32_t A, uint8_t V) +{ + wsRAM[A & 0x3F] = V; +} + +int32_t WSwan::SoundFlush(int16_t *SoundBuf, const int32_t MaxSoundFrames) +{ + int32_t FrameCount = 0; + + if(SoundBuf) + { + for(int y = 0; y < 2; y++) + { + // sbuf[y]->end_frame(v30mz_timestamp); + // FrameCount = sbuf[y]->read_samples(SoundBuf + y, MaxSoundFrames, true); + int32_t left = sbuf[0]; + int32_t right = sbuf[1]; + if (left >= 0x400) left = 0x3FF; + else if (left < -0x400) left = -0x400; + if (right >= 0x400) left = 0x3FF; + else if (right < -0x400) left = -0x400; + SoundBuf[0] = (int16_t)left << 5; + SoundBuf[1] = (int16_t)right << 5; + } + } + + last_ts = 0; + + return(FrameCount); +} + +// Call before wsRAM is updated +// void WSwan::SoundCheckRAMWrite(uint32_t A) +// { +// if((A >> 6) == SampleRAMPos) +// SoundUpdate(); +// } + +// static void RedoVolume(void) +// { +// WaveSynth.volume(2.5); +// } + +// void WSwan::SoundInit(void) +// { +// for(int i = 0; i < 2; i++) +// { +// sbuf[i] = new Blip_Buffer(); + +// sbuf[i]->set_sample_rate(0 ? 0 : 44100, 60); +// sbuf[i]->clock_rate((long)(3072000)); +// sbuf[i]->bass_freq(20); +// } + +// RedoVolume(); +// } + +// void WSwan::SoundKill(void) +// { +// for(int i = 0; i < 2; i++) +// { +// if(sbuf[i]) +// { +// delete sbuf[i]; +// sbuf[i] = NULL; +// } +// } + +// } + +// bool WSwan::SetSoundRate(uint32_t rate) +// { +// for(int i = 0; i < 2; i++) +// sbuf[i]->set_sample_rate(rate?rate:44100, 60); + +// return(true); +// } + +void WSwan::SoundReset(void) +{ + memset(period, 0, sizeof(period)); + memset(volume, 0, sizeof(volume)); + voice_volume = 0; + sweep_step = 0; + sweep_value = 0; + noise_control = 0; + control = 0; + output_control = 0; + + sweep_8192_divider = 8192; + sweep_counter = 1; + SampleRAMPos = 0; + + for(unsigned ch = 0; ch < 4; ch++) + period_counter[ch] = 1; + + memset(sample_pos, 0, sizeof(sample_pos)); + nreg = 0; + + memset(sample_cache, 0, sizeof(sample_cache)); + // memset(last_val, 0, sizeof(last_val)); + last_v_val = 0; + + HyperVoice = 0; + last_hv_val[0] = last_hv_val[1] = 0; + HVoiceCtrl = 0; + HVoiceChanCtrl = 0; + + for(int y = 0; y < 2; y++) + // sbuf[y]->clear(); + sbuf[y] = 0; + last_ts = 0; +} diff --git a/src/engine/platform/sound/swan.h b/src/engine/platform/sound/swan.h new file mode 100644 index 000000000..a1d01fa54 --- /dev/null +++ b/src/engine/platform/sound/swan.h @@ -0,0 +1,81 @@ +/******************************************************************************/ +/* Mednafen - Multi-system Emulator */ +/******************************************************************************/ +/* sound.h - WonderSwan Sound Emulation +** Copyright (C) 2007-2016 Mednafen Team +** +** 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 __WSWAN_SOUND_H +#define __WSWAN_SOUND_H + +#include + +class WSwan +{ +public: + int32_t SoundFlush(int16_t *SoundBuf, const int32_t MaxSoundFrames); + + // void SoundInit(void); + // void SoundKill(void); + // void SetSoundMultiplier(double multiplier); + // bool SetSoundRate(uint32_t rate); + + void SoundWrite(uint32_t, uint8_t); + uint8_t SoundRead(uint32_t); + void SoundReset(void); + // void SoundCheckRAMWrite(uint32_t A); + + void SoundUpdate(uint32_t); + void RAMWrite(uint32_t, uint8_t); + +private: + // Blip_Synth WaveSynth; + + // Blip_Buffer *sbuf[2] = { NULL }; + int32_t sbuf[2]; + + uint16_t period[4]; + uint8_t volume[4]; // left volume in upper 4 bits, right in lower 4 bits + uint8_t voice_volume; + + uint8_t sweep_step, sweep_value; + uint8_t noise_control; + uint8_t control; + uint8_t output_control; + + int32_t sweep_8192_divider; + uint8_t sweep_counter; + uint8_t SampleRAMPos; + + int32_t sample_cache[4][2]; + + int32_t last_v_val; + + uint8_t HyperVoice; + int32_t last_hv_val[2]; + uint8_t HVoiceCtrl, HVoiceChanCtrl; + + int32_t period_counter[4]; + // int32_t last_val[4][2]; // Last outputted value, l&r + uint8_t sample_pos[4]; + uint16_t nreg; + uint32_t last_ts; + + uint8_t wsRAM[64]; +}; + +#endif diff --git a/src/engine/platform/sound/vera_pcm.c b/src/engine/platform/sound/vera_pcm.c new file mode 100644 index 000000000..740f5c821 --- /dev/null +++ b/src/engine/platform/sound/vera_pcm.c @@ -0,0 +1,135 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#include "vera_pcm.h" +#include +#include + +static uint8_t volume_lut[16] = {0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18, 23, 30, 38, 49, 64}; + +static void +fifo_reset(struct VERA_PCM* pcm) +{ + pcm->fifo_wridx = 0; + pcm->fifo_rdidx = 0; + pcm->fifo_cnt = 0; + memset(pcm->fifo,0,sizeof(pcm->fifo)); +} + +void +pcm_reset(struct VERA_PCM* pcm) +{ + fifo_reset(pcm); + pcm->ctrl = 0; + pcm->rate = 0; + pcm->cur_l = 0; + pcm->cur_r = 0; + pcm->phase = 0; +} + +void +pcm_write_ctrl(struct VERA_PCM* pcm, uint8_t val) +{ + if (val & 0x80) { + fifo_reset(pcm); + } + + pcm->ctrl = val & 0x3F; +} + +uint8_t +pcm_read_ctrl(struct VERA_PCM* pcm) +{ + uint8_t result = pcm->ctrl; + if (pcm->fifo_cnt == sizeof(pcm->fifo)) { + result |= 0x80; + } + return result; +} + +void +pcm_write_rate(struct VERA_PCM* pcm, uint8_t val) +{ + pcm->rate = val; +} + +uint8_t +pcm_read_rate(struct VERA_PCM* pcm) +{ + return pcm->rate; +} + +void +pcm_write_fifo(struct VERA_PCM* pcm, uint8_t val) +{ + if (pcm->fifo_cnt < sizeof(pcm->fifo)) { + pcm->fifo[pcm->fifo_wridx++] = val; + if (pcm->fifo_wridx == sizeof(pcm->fifo)) { + pcm->fifo_wridx = 0; + } + pcm->fifo_cnt++; + } +} + +static uint8_t +read_fifo(struct VERA_PCM* pcm) +{ + if (pcm->fifo_cnt == 0) { + return 0; + } + uint8_t result = pcm->fifo[pcm->fifo_rdidx++]; + if (pcm->fifo_rdidx == sizeof(pcm->fifo)) { + pcm->fifo_rdidx = 0; + } + pcm->fifo_cnt--; + return result; +} + +bool +pcm_is_fifo_almost_empty(struct VERA_PCM* pcm) +{ + return pcm->fifo_cnt < 1024; +} + +void +pcm_render(struct VERA_PCM* pcm, int16_t* buf_l, int16_t* buf_r, unsigned num_samples) +{ + while (num_samples--) { + uint8_t old_phase = pcm->phase; + pcm->phase += pcm->rate; + if ((old_phase & 0x80) != (pcm->phase & 0x80)) { + switch ((pcm->ctrl >> 4) & 3) { + case 0: { // mono 8-bit + pcm->cur_l = (int16_t)read_fifo(pcm) << 8; + pcm->cur_r = pcm->cur_l; + break; + } + case 1: { // stereo 8-bit + pcm->cur_l = read_fifo(pcm) << 8; + pcm->cur_r = read_fifo(pcm) << 8; + break; + } + case 2: { // mono 16-bit + pcm->cur_l = read_fifo(pcm); + pcm->cur_l |= read_fifo(pcm) << 8; + pcm->cur_r = pcm->cur_l; + break; + } + case 3: { // stereo 16-bit + pcm->cur_l = read_fifo(pcm); + pcm->cur_l |= read_fifo(pcm) << 8; + pcm->cur_r = read_fifo(pcm); + pcm->cur_r |= read_fifo(pcm) << 8; + break; + } + } + } + + *(buf_l) += ((int)pcm->cur_l * (int)volume_lut[pcm->ctrl & 0xF]) >> 6; + *(buf_r) += ((int)pcm->cur_r * (int)volume_lut[pcm->ctrl & 0xF]) >> 6; + + buf_l++; + buf_r++; + } +} diff --git a/src/engine/platform/sound/vera_pcm.h b/src/engine/platform/sound/vera_pcm.h new file mode 100644 index 000000000..d9b600d0b --- /dev/null +++ b/src/engine/platform/sound/vera_pcm.h @@ -0,0 +1,31 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#pragma once + +#include +#include + +struct VERA_PCM { + uint8_t fifo[4096 - 1]; // Actual hardware FIFO is 4kB, but you can only use 4095 bytes. + unsigned fifo_wridx; + unsigned fifo_rdidx; + unsigned fifo_cnt; + + uint8_t ctrl; + uint8_t rate; + + int16_t cur_l, cur_r; + uint8_t phase; +}; + + +void pcm_reset(struct VERA_PCM* pcm); +void pcm_write_ctrl(struct VERA_PCM* pcm, uint8_t val); +uint8_t pcm_read_ctrl(struct VERA_PCM* pcm); +void pcm_write_rate(struct VERA_PCM* pcm, uint8_t val); +uint8_t pcm_read_rate(struct VERA_PCM* pcm); +void pcm_write_fifo(struct VERA_PCM* pcm, uint8_t val); +void pcm_render(struct VERA_PCM* pcm, int16_t* buf_l, int16_t* buf_r, unsigned num_samples); +bool pcm_is_fifo_almost_empty(struct VERA_PCM* pcm); diff --git a/src/engine/platform/sound/vera_psg.c b/src/engine/platform/sound/vera_psg.c new file mode 100644 index 000000000..afdf69cec --- /dev/null +++ b/src/engine/platform/sound/vera_psg.c @@ -0,0 +1,103 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#include "vera_psg.h" + +#include +#include + +enum waveform { + WF_PULSE = 0, + WF_SAWTOOTH, + WF_TRIANGLE, + WF_NOISE, +}; + +static uint8_t volume_lut[64] = {0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 14, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 28, 29, 31, 33, 35, 37, 39, 42, 44, 47, 50, 52, 56, 59, 63}; + +void +psg_reset(struct VERA_PSG* psg) +{ + memset(psg->channels, 0, sizeof(psg->channels)); + psg->noiseState=1; + psg->noiseOut=0; +} + +void +psg_writereg(struct VERA_PSG* psg, uint8_t reg, uint8_t val) +{ + reg &= 0x3f; + + int ch = reg / 4; + int idx = reg & 3; + + switch (idx) { + case 0: psg->channels[ch].freq = (psg->channels[ch].freq & 0xFF00) | val; break; + case 1: psg->channels[ch].freq = (psg->channels[ch].freq & 0x00FF) | (val << 8); break; + case 2: { + psg->channels[ch].right = (val & 0x80) != 0; + psg->channels[ch].left = (val & 0x40) != 0; + psg->channels[ch].volume = volume_lut[val & 0x3F]; + break; + } + case 3: { + psg->channels[ch].pw = val & 0x3F; + psg->channels[ch].waveform = val >> 6; + break; + } + } +} + +static inline void +render(struct VERA_PSG* psg, int16_t *left, int16_t *right) +{ + int l = 0; + int r = 0; + psg->noiseOut=((psg->noiseOut<<1)|(psg->noiseState&1))&63; + psg->noiseState=(psg->noiseState<<1)|(((psg->noiseState>>1)^(psg->noiseState>>2)^(psg->noiseState>>4)^(psg->noiseState>>15))&1); + + for (int i = 0; i < 16; i++) { + struct VERAChannel *ch = &psg->channels[i]; + + unsigned new_phase = (ch->phase + ch->freq) & 0x1FFFF; + if ((ch->phase & 0x10000) != (new_phase & 0x10000)) { + ch->noiseval = psg->noiseOut; + } + ch->phase = new_phase; + + uint8_t v = 0; + switch (ch->waveform) { + case WF_PULSE: v = (ch->phase >> 10) > ch->pw ? 0 : 63; break; + case WF_SAWTOOTH: v = ch->phase >> 11; break; + case WF_TRIANGLE: v = (ch->phase & 0x10000) ? (~(ch->phase >> 10) & 0x3F) : ((ch->phase >> 10) & 0x3F); break; + case WF_NOISE: v = ch->noiseval; break; + } + int8_t sv = (v ^ 0x20); + if (sv & 0x20) { + sv |= 0xC0; + } + + int val = (int)sv * (int)ch->volume; + + if (ch->left) { + l += val; + } + if (ch->right) { + r += val; + } + } + + *left = l; + *right = r; +} + +void +psg_render(struct VERA_PSG* psg, int16_t *bufL, int16_t *bufR, unsigned num_samples) +{ + while (num_samples--) { + render(psg, bufL, bufR); + bufL++; + bufR++; + } +} diff --git a/src/engine/platform/sound/vera_psg.h b/src/engine/platform/sound/vera_psg.h new file mode 100644 index 000000000..7a6a7f01d --- /dev/null +++ b/src/engine/platform/sound/vera_psg.h @@ -0,0 +1,28 @@ +// Commander X16 Emulator +// Copyright (c) 2020 Frank van den Hoef +// All rights reserved. License: 2-clause BSD + +#pragma once + +#include +#include + +struct VERAChannel { + uint16_t freq; + uint8_t volume; + bool left, right; + uint8_t pw; + uint8_t waveform; + + unsigned phase; + uint8_t noiseval; +}; + +struct VERA_PSG { + unsigned int noiseState, noiseOut; + struct VERAChannel channels[16]; +}; + +void psg_reset(struct VERA_PSG* psg); +void psg_writereg(struct VERA_PSG* psg, uint8_t reg, uint8_t val); +void psg_render(struct VERA_PSG* psg, int16_t *bufL, int16_t *bufR, unsigned num_samples); diff --git a/src/engine/platform/sound/vic20sound.c b/src/engine/platform/sound/vic20sound.c new file mode 100644 index 000000000..a3c90f204 --- /dev/null +++ b/src/engine/platform/sound/vic20sound.c @@ -0,0 +1,250 @@ +/* + * vic20sound.c - Implementation of VIC20 sound code. + * + * Written by + * Rami Rasanen + * Ville-Matias Heikkila + * + * This file is part of VICE, the Versatile Commodore Emulator. + * See README for copyright notice. + * + * 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., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA. + * + */ + +#include +#include +#include + +#include "vic20sound.h" + +/* ---------------------------------------------------------------------*/ + +static float voltagefunction[] = { + 0.00f, 148.28f, 296.55f, 735.97f, 914.88f, 1126.89f, 1321.86f, 1503.07f, 1603.50f, + 1758.00f, 1913.98f, 2070.94f, 2220.36f, 2342.91f, 2488.07f, 3188.98f, 3285.76f, 3382.53f, + 3479.31f, 3576.08f, 3672.86f, 3769.63f, 3866.41f, 3963.18f, 4059.96f, 4248.10f, 4436.24f, + 4624.38f, 4812.53f, 5000.67f, 5188.81f, 5192.91f, 5197.00f, 5338.52f, 5480.04f, 5621.56f, + 5763.07f, 5904.59f, 6046.11f, 6187.62f, 6329.14f, 6609.31f, 6889.47f, 7169.64f, 7449.80f, + 7729.97f, 7809.36f, 7888.75f, 7968.13f, 8047.52f, 8126.91f, 8206.30f, 8285.69f, 8365.07f, + 8444.46f, 8523.85f, 8603.24f, 8905.93f, 9208.63f, 9511.32f, 9814.02f, 9832.86f, 9851.70f, + 9870.54f, 9889.38f, 9908.22f, 9927.07f, 9945.91f, 9964.75f, 9983.59f, 10002.43f, 10021.27f, + 10040.12f, 10787.23f, 11534.34f, 12281.45f, 12284.98f, 12288.50f, 12292.03f, 12295.56f, 12299.09f, + 12302.62f, 12306.15f, 12309.68f, 12313.21f, 12316.74f, 12320.26f, 12323.79f, 12327.32f, 13113.05f, + 13898.78f, 13910.58f, 13922.39f, 13934.19f, 13945.99f, 13957.80f, 13969.60f, 13981.40f, 13993.21f, + 14005.01f, 14016.81f, 14028.62f, 14040.42f, 14052.22f, 14064.03f, 16926.31f, 16987.04f, 17047.77f, + 17108.50f, 17169.23f, 17229.96f, 17290.69f, 17351.42f, 17412.15f, 17472.88f, 17533.61f, 17594.34f, + 17655.07f, 17715.80f, 17776.53f, 17837.26f, 18041.51f, 18245.77f, 18450.02f, 18654.28f, 18858.53f, + 19062.78f, 19267.04f, 19471.29f, 19675.55f, 19879.80f, 20084.05f, 20288.31f, 20417.74f, 20547.17f, + 20676.61f, 20774.26f, 20871.91f, 20969.55f, 21067.20f, 21164.85f, 21262.50f, 21360.15f, 21457.80f, + 21555.45f, 21653.09f, 21750.74f, 21848.39f, 21946.04f, 22043.69f, 22141.34f, 22212.33f, 22283.33f, + 22354.33f, 22425.33f, 22496.32f, 22567.32f, 22638.32f, 22709.32f, 22780.31f, 22851.31f, 22922.31f, + 22993.31f, 23064.30f, 23135.30f, 23206.30f, 23255.45f, 23304.60f, 23353.75f, 23402.91f, 23452.06f, + 23501.21f, 23550.36f, 23599.51f, 23648.67f, 23768.81f, 23888.96f, 24009.11f, 24129.26f, 24249.41f, + 24369.56f, 24451.92f, 24534.28f, 24616.63f, 24698.99f, 24781.35f, 24863.70f, 24946.06f, 25028.42f, + 25110.77f, 25193.13f, 25275.49f, 25357.84f, 25440.20f, 25522.56f, 25604.92f, 25658.87f, 25712.83f, + 25766.79f, 25820.75f, 25874.71f, 25928.66f, 25982.62f, 26036.58f, 26090.54f, 26144.49f, 26198.45f, + 26252.41f, 26306.37f, 26360.33f, 26414.28f, 26501.23f, 26588.17f, 26675.12f, 26762.06f, 26849.01f, + 26935.95f, 27022.90f, 27109.84f, 27196.78f, 27283.73f, 27370.67f, 27457.62f, 27544.56f, 27631.51f, + 27718.45f, 27726.89f, 27735.33f, 27743.78f, 27752.22f, 27760.66f, 27769.10f, 27777.54f, 27785.98f, + 27794.43f, 27802.87f, 27811.31f, 27819.75f, 27828.19f, 27836.63f, 27845.08f, 27853.52f, 27861.96f, + 27870.40f, 27878.84f, 27887.28f, 27895.73f, 27904.17f, 27912.61f, 27921.05f, 27929.49f, 27937.93f, + 27946.38f, 27954.82f, 27963.26f, 27971.70f, 27980.14f, 27988.58f, 27997.03f, 28005.47f, 28013.91f, + 28022.35f, 28030.79f, 28039.23f, 28047.68f, 28056.12f, 28064.56f, 28073.00f, 28081.44f, 28089.88f, + 28098.33f, 28106.77f, 28115.21f, 28123.65f, 28132.09f, 28140.53f, 28148.98f, 28157.42f, 28165.86f, + 28174.30f, 28182.74f, 28191.18f, 28199.63f, 28208.07f, 28216.51f, 28224.95f, 28233.39f, 28241.83f, + 28250.28f, 28258.72f, 28267.16f, 28275.60f, 28284.04f, 28292.48f, 28300.93f, 28309.37f, 28317.81f, + 28326.25f, 28334.69f, 28343.13f, 28351.58f, 28360.02f, 28368.46f, 28376.90f, 28385.34f, 28393.78f, + 28402.23f, 28410.67f, 28419.11f, 28427.55f, 28435.99f, 28444.43f, 28452.88f, 28461.32f, 28469.76f, + 28478.20f, 28486.64f, 28495.08f, 28503.53f, 28511.97f, 28520.41f, 28528.85f, 28537.29f, 28545.73f, + 28554.18f, 28562.62f, 28571.06f, 28579.50f, 28587.94f, 28596.38f, 28604.83f, 28613.27f, 28621.71f, + 28630.15f, 28638.59f, 28647.03f, 28655.48f, 28663.92f, 28672.36f, 28680.80f, 28689.24f, 28697.68f, + 28706.13f, 28714.57f, 28723.01f, 28731.45f, 28739.89f, 28748.33f, 28756.78f, 28765.22f, 28773.66f, + 28782.10f, 28790.54f, 28798.98f, 28807.43f, 28815.87f, 28824.31f, 28832.75f, 28841.19f, 28849.63f, + 28858.08f, 28866.52f, 28874.96f, 28883.40f, 28891.84f, 28900.28f, 28908.73f, 28917.17f, 28925.61f, + 28934.05f, 28942.49f, 28950.93f, 28959.38f, 28967.82f, 28976.26f, 28984.70f, 28993.14f, 29001.58f, + 29010.03f, 29018.47f, 29026.91f, 29035.35f, 29043.79f, 29052.23f, 29060.68f, 29069.12f, 29077.56f, + 29086.00f, 29094.44f, 29102.88f, 29111.33f, 29119.77f, 29128.21f, 29136.65f, 29145.09f, 29153.53f, + 29161.98f, 29170.42f, 29178.86f, 29187.30f, 29195.74f, 29204.18f, 29212.63f, 29221.07f, 29229.51f, + 29237.95f, 29246.39f, 29254.83f, 29263.28f, 29271.72f, 29280.16f, 29288.60f, 29297.04f, 29305.48f, + 29313.93f, 29322.37f, 29330.81f, 29339.25f, 29347.69f, 29356.13f, 29364.58f, 29373.02f, 29381.46f, + 29389.90f, 29398.34f, 29406.78f, 29415.23f, 29423.67f, 29432.11f, 29440.55f, 29448.99f, 29457.43f, + 29465.88f, 29474.32f, 29482.76f, 29491.20f +}; + +void vic_sound_clock(sound_vic20_t *snd, uint32_t cycles); + +int vic_sound_machine_calculate_samples(sound_vic20_t *snd, int16_t *pbuf, int nr, int soc, int scc, uint32_t delta_t) +{ + int s = 0; + int i; + float o; + int16_t vicbuf; + int samples_to_do; + + while (s < nr && delta_t >= snd->cycles_per_sample - snd->leftover_cycles) { + samples_to_do = (int)(snd->cycles_per_sample - snd->leftover_cycles); + snd->leftover_cycles += samples_to_do - snd->cycles_per_sample; + vic_sound_clock(snd, samples_to_do); + + o = snd->lowpassbuf - snd->highpassbuf; + snd->highpassbuf += snd->highpassbeta * (snd->lowpassbuf - snd->highpassbuf); + snd->lowpassbuf += snd->lowpassbeta * (voltagefunction[(((snd->accum * 7) / snd->accum_cycles) + 1) * snd->volume] - snd->lowpassbuf); + + if (o < -32768) { + vicbuf = -32768; + } else if (o > 32767) { + vicbuf = 32767; + } else { + vicbuf = (int16_t)o; + } + + for (i = 0; i < soc; i++) { + pbuf[(s * soc) + i] = vicbuf; + } + s++; + snd->accum = 0; + snd->accum_cycles = 0; + delta_t -= samples_to_do; + } + if (delta_t > 0) { + snd->leftover_cycles += delta_t; + vic_sound_clock(snd, delta_t); + delta_t = 0; + } + return s; +} + +void vic_sound_clock(sound_vic20_t *snd, uint32_t cycles) +{ + uint32_t i; + int j, enabled; + + if (cycles <= 0) { + return; + } + + for (j = 0; j < 4; j++) { + int chspeed = "\4\3\2\1"[j]; + + if (snd->ch[j].ctr > cycles) { + snd->accum += snd->ch[j].out * cycles; + snd->ch[j].ctr -= cycles; + } else { + for (i = cycles; i; i--) { + snd->ch[j].ctr--; + if (snd->ch[j].ctr <= 0) { + int a = (~snd->ch[j].reg) & 127; + int edge_trigger; + a = a ? a : 128; + snd->ch[j].ctr += a << chspeed; + enabled = (snd->ch[j].reg & 128) >> 7; + edge_trigger = (snd->noise_LFSR & 1) & !snd->noise_LFSR0_old; + + if((j != 3) || ((j == 3) && edge_trigger)) { + uint8_t shift = snd->ch[j].shift; + shift = ((shift << 1) | (((((shift & 128) >> 7)) ^ 1) & enabled)); + snd->ch[j].shift = shift; + } + if(j == 3) { + int bit3 = (snd->noise_LFSR >> 3) & 1; + int bit12 = (snd->noise_LFSR >> 12) & 1; + int bit14 = (snd->noise_LFSR >> 14) & 1; + int bit15 = (snd->noise_LFSR >> 15) & 1; + int gate1 = bit3 ^ bit12; + int gate2 = bit14 ^ bit15; + int gate3 = (gate1 ^ gate2) ^ 1; + int gate4 = (gate3 & enabled) ^ 1; + snd->noise_LFSR0_old = snd->noise_LFSR & 1; + snd->noise_LFSR = (snd->noise_LFSR << 1) | gate4; + } + snd->ch[j].out = snd->ch[j].shift & (j == 3 ? enabled : 1); + } + snd->accum += snd->ch[j].out; /* FIXME: doesn't take DC offset into account */ + } + } + } + + snd->accum_cycles += cycles; +} + +void vic_sound_machine_store(sound_vic20_t *snd, uint16_t addr, uint8_t value) +{ + switch (addr) { + case 0xA: + snd->ch[0].reg = value; + break; + case 0xB: + snd->ch[1].reg = value; + break; + case 0xC: + snd->ch[2].reg = value; + break; + case 0xD: + snd->ch[3].reg = value; + break; + case 0xE: + snd->volume = value & 0x0f; + break; + } +} + +int vic_sound_machine_init(sound_vic20_t *snd, int speed, int cycles_per_sec) +{ + uint32_t i; + float dt; + + memset(snd, 0, sizeof(sound_vic20_t)); + + snd->cycles_per_sample = (float)cycles_per_sec / speed; + snd->leftover_cycles = 0.0f; + + snd->lowpassbuf = 0.0f; + snd->highpassbuf = 0.0f; + + snd->speed = speed; + + dt = 1.f / speed; + + /* + Audio output stage + + 5V + -----+ + audio| 1k | + +---+---R---+--------(K) +----- + out | | | | |audio + -----+ C .01 C .1 | 1 uF | + | uF | uF +-----C-----+ 1K + | | + GND GND R 470 | amp + | +----- + + GND + + */ + + /* Low-pass: R = 1 kOhm, C = 100 nF; RC = 1e3*1e-7 = 1e-4 (1591 Hz) */ + snd->lowpassbeta = dt / ( dt + 1e-4f ); + /* High-pass: R = 1 kOhm, C = 1 uF; RC = 1e3*1e-6 = 1e-3 ( 159 Hz) */ + snd->highpassbeta = dt / ( dt + 1e-3f ); + + for (i = 0; i < 16; i++) { + vic_sound_machine_store(snd, (uint16_t)i, 0); + } + + return 1; +} diff --git a/src/engine/platform/sound/vic20sound.h b/src/engine/platform/sound/vic20sound.h new file mode 100644 index 000000000..7685bc555 --- /dev/null +++ b/src/engine/platform/sound/vic20sound.h @@ -0,0 +1,73 @@ +/* + * vic20sound.h - implementation of VIC20 sound code + * + * Written by + * Teemu Rantanen + * + * This file is part of VICE, the Versatile Commodore Emulator. + * See README for copyright notice. + * + * 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., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA. + * + */ + +#ifndef VICE_VIC20SOUND_H +#define VICE_VIC20SOUND_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct sound_vic20_s { + unsigned char div; + struct { + unsigned char out; + unsigned char reg; + unsigned char shift; + signed short ctr; + } ch[4]; + unsigned short noisectr; + unsigned char volume; + int cyclecount; + + int accum; + int accum_cycles; + + float cycles_per_sample; + float leftover_cycles; + int speed; + + float highpassbuf; + float highpassbeta; + float lowpassbuf; + float lowpassbeta; + + uint16_t noise_LFSR; + uint8_t noise_LFSR0_old; +}; +typedef struct sound_vic20_s sound_vic20_t; + +int vic_sound_machine_init(sound_vic20_t *snd, int speed, int cycles_per_sec); +void vic_sound_machine_store(sound_vic20_t *snd, uint16_t addr, uint8_t value); +int vic_sound_machine_calculate_samples(sound_vic20_t *snd, int16_t *pbuf, int nr, int soc, int scc, uint32_t delta_t); +void vic_sound_clock(sound_vic20_t *snd, uint32_t cycles); + +#ifdef __cplusplus +}; +#endif +#endif diff --git a/src/engine/platform/sound/vrcvi/vrcvi.cpp b/src/engine/platform/sound/vrcvi/vrcvi.cpp new file mode 100644 index 000000000..19152ed74 --- /dev/null +++ b/src/engine/platform/sound/vrcvi/vrcvi.cpp @@ -0,0 +1,311 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami VRC VI sound emulation core + + It's one of NES mapper with built-in sound chip, and also one of 2 Konami VRCs with this feature. (rest one has OPLL derivatives.) + + It's also DACless like other sound chip and mapper-with-sound manufactured by konami, + the Chips 6 bit digital sound output is needs converted to analog sound output when you it want to make some sounds, or send to sound mixer. + + Its are used for Akumajou Densetsu (Japan release of Castlevania III), Madara, Esper Dream 2. + + The chip is installed in 351951 PCB and 351949A PCB. + + 351951 PCB is used exclusivly for Akumajou Densetsu, Small board has VRC VI, PRG and CHR ROM. + - It's configuration also calls VRC6a, iNES mapper 024. + + 351949A PCB is for Last 2 titles with VRC VI, Bigger board has VRC VI, PRG and CHR ROM, and Battery Backed 8K x 8 bit SRAM. + - Additionally, It's PRG A0 and A1 bit to VRC VI input is swapped, compare to above. + - It's configuration also calls VRC6b, iNES mapper 026. + + The chip itself has 053328, 053329, 053330 Revision, but Its difference between revision is unknown. + + Like other mappers for NES, It has internal timer - Its timer can be sync with scanline like other Konami mapper in this era. + + Register layout (Sound and Timer only; 351951 PCB case, 351949A swaps xxx1 and xxx2): + + Address Bits Description + 7654 3210 + + 9000-9002 Pulse 1 + + 9000 x--- ---- Pulse 1 Duty ignore + -xxx ---- Pulse 1 Duty cycle + ---- xxxx Pulse 1 Volume + 9001 xxxx xxxx Pulse 1 Pitch bit 0-7 + 9002 x--- ---- Pulse 1 Enable + ---- xxxx Pulse 1 Pitch bit 8-11 + + 9003 Sound control + + 9003 ---- -x-- 4 bit Frequency mode + ---- -0x- 8 bit Frequency mode + ---- ---x Halt + + a000-a002 Pulse 2 + + a000 x--- ---- Pulse 2 Duty ignore + -xxx ---- Pulse 2 Duty cycle + ---- xxxx Pulse 2 Volume + a001 xxxx xxxx Pulse 2 Pitch bit 0-7 + a002 x--- ---- Pulse 2 Enable + ---- xxxx Pulse 2 Pitch bit 8-11 + + b000-b002 Sawtooth + + b000 --xx xxxx Sawtooth Accumulate Rate + b001 xxxx xxxx Sawtooth Pitch bit 0-7 + b002 x--- ---- Sawtooth Enable + ---- xxxx Sawtooth Pitch bit 8-11 + + f000-f002 IRQ Timer + + f000 xxxx xxxx IRQ Timer latch + f001 ---- -0-- Sync with scanline + ---- --x- Enable timer + ---- ---x Enable timer after IRQ Acknowledge + f002 ---- ---- IRQ Acknowledge + + Frequency calculations: + + if 4 bit Frequency Mode then + Frequency: Input clock / (bit 8 to 11 of Pitch + 1) + end else if 8 bit Frequency Mode then + Frequency: Input clock / (bit 4 to 11 of Pitch + 1) + end else then + Frequency: Input clock / (Pitch + 1) + end +*/ + +#include "vrcvi.hpp" + +void vrcvi_core::tick() +{ + m_out = 0; + if (!m_control.m_halt) // Halt flag + { + // tick per each clock + for (auto & elem : m_pulse) + { + if (elem.tick()) + m_out += elem.m_control.m_volume; // add 4 bit pulse output + } + if (m_sawtooth.tick()) + m_out += bitfield(m_sawtooth.m_accum, 3, 5); // add 5 bit sawtooth output + } + if (m_timer.tick()) + m_timer.counter_tick(); +} + +void vrcvi_core::reset() +{ + for (auto & elem : m_pulse) + elem.reset(); + + m_sawtooth.reset(); + m_timer.reset(); + m_control.reset(); + m_out = 0; +} + +bool vrcvi_core::alu_t::tick() +{ + if (m_divider.m_enable) + { + const u16 temp = m_counter; + // post decrement + if (bitfield(m_host.m_control.m_shift, 1)) + { + m_counter = (m_counter & 0x0ff) | (bitfield(bitfield(m_counter, 8, 4) - 1, 0, 4) << 8); + m_counter = (m_counter & 0xf00) | (bitfield(bitfield(m_counter, 0, 8) - 1, 0, 8) << 0); + } + else if (bitfield(m_host.m_control.m_shift, 0)) + { + m_counter = (m_counter & 0x00f) | (bitfield(bitfield(m_counter, 4, 8) - 1, 0, 8) << 4); + m_counter = (m_counter & 0xff0) | (bitfield(bitfield(m_counter, 0, 4) - 1, 0, 4) << 0); + } + else + m_counter = bitfield(bitfield(m_counter, 0, 12) - 1, 0, 12); + + // carry handling + bool carry = bitfield(m_host.m_control.m_shift, 1) ? (bitfield(temp, 8, 4) == 0) : + (bitfield(m_host.m_control.m_shift, 0) ? (bitfield(temp, 4, 8) == 0) : + (bitfield(temp, 0, 12) == 0)); + if (carry) + m_counter = m_divider.m_divider; + + return carry; + } + return false; +} + +bool vrcvi_core::pulse_t::tick() +{ + if (!m_divider.m_enable) + return false; + + if (vrcvi_core::alu_t::tick()) + m_cycle = bitfield(m_cycle + 1, 0, 4); + + return m_control.m_mode ? true : ((m_cycle > m_control.m_duty) ? true : false); +} + +bool vrcvi_core::sawtooth_t::tick() +{ + if (!m_divider.m_enable) + return false; + + if (vrcvi_core::alu_t::tick()) + { + if (bitfield(m_cycle++, 0)) // Even step only + m_accum += m_rate; + if (m_cycle >= 14) // Reset accumulator at every 14 cycles + { + m_accum = 0; + m_cycle = 0; + } + } + return (m_accum == 0) ? false : true; +} + +void vrcvi_core::alu_t::reset() +{ + m_divider.reset(); + m_counter = 0; + m_cycle = 0; +} + +void vrcvi_core::pulse_t::reset() +{ + vrcvi_core::alu_t::reset(); + m_control.reset(); +} + +void vrcvi_core::sawtooth_t::reset() +{ + vrcvi_core::alu_t::reset(); + m_rate = 0; + m_accum = 0; +} + +bool vrcvi_core::timer_t::tick() +{ + if (m_timer_control.m_enable) + { + if (!m_timer_control.m_sync) // scanline sync mode + { + m_prescaler -= 3; + if (m_prescaler <= 0) + { + m_prescaler += 341; + return true; + } + } + } + return (m_timer_control.m_enable && m_timer_control.m_sync) ? true : false; +} + +void vrcvi_core::timer_t::counter_tick() +{ + if (bitfield(++m_counter, 0, 8) == 0) + { + m_counter = m_counter_latch; + irq_set(); + } +} + +void vrcvi_core::timer_t::reset() +{ + m_timer_control.reset(); + m_prescaler = 341; + m_counter = m_counter_latch = 0; + irq_clear(); +} + +// Accessors + +void vrcvi_core::alu_t::divider_t::write(bool msb, u8 data) +{ + if (msb) + { + m_divider = (m_divider & ~0xf00) | (bitfield(data, 0, 4) << 8); + m_enable = bitfield(data, 7); + } + else + m_divider = (m_divider & ~0x0ff) | data; +} + + +void vrcvi_core::pulse_w(u8 voice, u8 address, u8 data) +{ + pulse_t &v = m_pulse[voice]; + switch (address) + { + case 0x00: // Control - 0x9000 (Pulse 1), 0xa000 (Pulse 2) + v.m_control.m_mode = bitfield(data, 7); + v.m_control.m_duty = bitfield(data, 4, 3); + v.m_control.m_volume = bitfield(data, 0, 4); + break; + case 0x01: // Pitch LSB - 0x9001/0x9002 (Pulse 1), 0xa001/0xa002 (Pulse 2) + v.m_divider.write(false, data); + break; + case 0x02: // Pitch MSB, Enable/Disable - 0x9002/0x9001 (Pulse 1), 0xa002/0xa001 (Pulse 2) + v.m_divider.write(true, data); + if (!v.m_divider.m_enable) // Reset duty cycle + v.m_cycle = 0; + break; + } +} + +void vrcvi_core::saw_w(u8 address, u8 data) +{ + switch (address) + { + case 0x00: // Sawtooth Accumulate - 0xb000 + m_sawtooth.m_rate = bitfield(data, 0, 6); + break; + case 0x01: // Pitch LSB - 0xb001/0xb002 (Sawtooth) + m_sawtooth.m_divider.write(false, data); + break; + case 0x02: // Pitch MSB, Enable/Disable - 0xb002/0xb001 (Sawtooth) + m_sawtooth.m_divider.write(true, data); + if (!m_sawtooth.m_divider.m_enable) // Reset accumulator + m_sawtooth.m_accum = 0; + break; + } +} + +void vrcvi_core::timer_w(u8 address, u8 data) +{ + switch (address) + { + case 0x00: // Timer latch - 0xf000 + m_timer.m_counter_latch = data; + break; + case 0x01: // Timer control - 0xf001/0xf002 + m_timer.m_timer_control.m_sync = bitfield(data, 2); + m_timer.m_timer_control.m_enable = bitfield(data, 1); + m_timer.m_timer_control.m_enable_ack = bitfield(data, 0); + if (m_timer.m_timer_control.m_enable) + { + m_timer.m_counter = m_timer.m_counter_latch; + m_timer.m_prescaler = 341; + } + m_timer.irq_clear(); + break; + case 0x02: // IRQ Acknowledge - 0xf002/0xf001 + m_timer.irq_clear(); + m_timer.m_timer_control.m_enable = m_timer.m_timer_control.m_enable_ack; + break; + } +} + +void vrcvi_core::control_w(u8 data) +{ + // Global control - 0x9003 + m_control.m_halt = bitfield(data, 0); + m_control.m_shift = bitfield(data, 1, 2); +} diff --git a/src/engine/platform/sound/vrcvi/vrcvi.hpp b/src/engine/platform/sound/vrcvi/vrcvi.hpp new file mode 100644 index 000000000..40b4245e3 --- /dev/null +++ b/src/engine/platform/sound/vrcvi/vrcvi.hpp @@ -0,0 +1,238 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami VRC VI sound emulation core + + See vrcvi.cpp to more infos. +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_VRCVI_HPP +#define _VGSOUND_EMU_VRCVI_HPP + +#pragma once + +namespace vrcvi +{ + typedef unsigned char u8; + typedef unsigned short u16; + typedef unsigned int u32; + typedef signed char s8; + typedef signed short s16; + + // get bitfield, bitfield(input, position, len) + template T bitfield(T in, u8 pos, u8 len = 1) + { + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); + } +}; + +class vrcvi_intf +{ +public: + virtual void irq_w(bool irq) { } +}; + +using namespace vrcvi; +class vrcvi_core +{ +public: + friend class vrcvi_intf; + // constructor + vrcvi_core(vrcvi_intf &intf) + : m_pulse{*this,*this} + , m_sawtooth(*this) + , m_timer(*this) + , m_intf(intf) + { + } + // accessors, getters, setters + void pulse_w(u8 voice, u8 address, u8 data); + void saw_w(u8 address, u8 data); + void timer_w(u8 address, u8 data); + void control_w(u8 data); + + // internal state + void reset(); + void tick(); + + // 6 bit output + s8 out() { return m_out; } +private: + // Common ALU for sound channels + struct alu_t + { + alu_t(vrcvi_core &host) + : m_host(host) + { }; + + + virtual void reset(); + virtual bool tick(); + + struct divider_t + { + divider_t() + : m_divider(0) + , m_enable(0) + { }; + + void reset() + { + m_divider = 0; + m_enable = 0; + } + + void write(bool msb, u8 data); + + u16 m_divider : 12; // divider (pitch) + u16 m_enable : 1; // channel enable flag + }; + + vrcvi_core &m_host; + divider_t m_divider; + u16 m_counter = 0; // clock counter + u8 m_cycle = 0; // clock cycle + }; + + // 2 Pulse channels + struct pulse_t : alu_t + { + pulse_t(vrcvi_core &host) + : alu_t(host) + { }; + + virtual void reset() override; + virtual bool tick() override; + + // Control bits + struct pulse_control_t + { + pulse_control_t() + : m_mode(0) + , m_duty(0) + , m_volume(0) + { }; + + void reset() + { + m_mode = 0; + m_duty = 0; + m_volume = 0; + } + + u8 m_mode : 1; // duty toggle flag + u8 m_duty : 3; // 3 bit duty cycle + u8 m_volume : 4; // 4 bit volume + }; + + pulse_control_t m_control; + }; + + // 1 Sawtooth channel + struct sawtooth_t : alu_t + { + sawtooth_t(vrcvi_core &host) + : alu_t(host) + { }; + + virtual void reset() override; + virtual bool tick() override; + + u8 m_rate = 0; // sawtooth accumulate rate + u8 m_accum = 0; // sawtooth accumulator, high 5 bit is accumulated to output + }; + + // Internal timer + struct timer_t + { + timer_t(vrcvi_core &host) + : m_host(host) + { }; + + void reset(); + bool tick(); + void counter_tick(); + + // IRQ update + void update() { m_host.m_intf.irq_w(m_timer_control.m_irq_trigger); } + void irq_set() + { + if (!m_timer_control.m_irq_trigger) + { + m_timer_control.m_irq_trigger = 1; + update(); + } + } + void irq_clear() + { + if (m_timer_control.m_irq_trigger) + { + m_timer_control.m_irq_trigger = 0; + update(); + } + } + + // Control bits + struct timer_control_t + { + timer_control_t() + : m_irq_trigger(0) + , m_enable_ack(0) + , m_enable(0) + , m_sync(0) + { }; + + void reset() + { + m_irq_trigger = 0; + m_enable_ack = 0; + m_enable = 0; + m_sync = 0; + } + + u8 m_irq_trigger : 1; + u8 m_enable_ack : 1; + u8 m_enable : 1; + u8 m_sync : 1; + }; + + vrcvi_core &m_host; // host core + timer_control_t m_timer_control; // timer control bits + s16 m_prescaler = 341; // prescaler + u8 m_counter = 0; // clock counter + u8 m_counter_latch = 0; // clock counter latch + }; + + struct global_control_t + { + global_control_t() + : m_halt(0) + , m_shift(0) + { }; + + void reset() + { + m_halt = 0; + m_shift = 0; + } + + u8 m_halt : 1; // halt sound + u8 m_shift : 2; // 4/8 bit right shift + }; + + pulse_t m_pulse[2]; // 2 pulse channels + sawtooth_t m_sawtooth; // sawtooth channel + timer_t m_timer; // internal timer + global_control_t m_control; // control + + vrcvi_intf &m_intf; + + s8 m_out = 0; // 6 bit output +}; + +#endif diff --git a/src/engine/platform/sound/x1_010/x1_010.cpp b/src/engine/platform/sound/x1_010/x1_010.cpp new file mode 100644 index 000000000..150928e66 --- /dev/null +++ b/src/engine/platform/sound/x1_010/x1_010.cpp @@ -0,0 +1,224 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holders: cam900 + Seta/Allumer X1-010 Emulation core + + the chip has 16 voices, all voices can be switchable to Wavetable or PCM sample playback mode. + It has also 2 output channels, but no known hardware using this feature for stereo sound. + + Wavetable needs to paired with envelope, it's always enabled and similar as AY PSG's one + but its shape is stored at RAM. + + PCM volume is stored by each register. + + Both volume is 4bit per output. + + Everything except PCM sample is stored at paired 8 bit RAM. + + RAM layout (common case: Address bit 12 is swapped when RAM is shared with CPU) + + ----------------------------- + 0000...007f Voice Registers + + 0000...0007 Voice 0 Register + + Address Bits Description + 7654 3210 + 0 x--- ---- Frequency divider* + ---- -x-- Envelope one-shot mode + ---- --x- Sound format + ---- --0- PCM + ---- --1- Wavetable + ---- ---x Keyon/off + PCM case: + 1 xxxx xxxx Volume (Each nibble is for each output) + + 2 xxxx xxxx Frequency* + + 4 xxxx xxxx Start address / 4096 + + 5 xxxx xxxx 0x100 - (End address / 4096) + Wavetable case: + 1 ---x xxxx Wavetable data select + + 2 xxxx xxxx Frequency LSB* + 3 xxxx xxxx "" MSB + + 4 xxxx xxxx Envelope period (.10 fixed point, Low 8 bit) + + 5 ---x xxxx Envelope shape select (!= 0 : Reserved for Voice registers) + + 0008...000f Voice 1 Register + ... + 0078...007f Voice 15 Register + ----------------------------- + 0080...0fff Envelope shape data (Same as volume; Each nibble is for each output) + + 0080...00ff Envelope shape data 1 + 0100...017f Envelope shape data 2 + ... + 0f80...0fff Envelope shape data 31 + ----------------------------- + 1000...1fff Wavetable data + + 1000...107f Wavetable data 0 + 1080...10ff Wavetable data 1 + ... + 1f80...1fff Wavetable data 31 + ----------------------------- + + * Frequency is 4.4 fixed point for PCM, + 6.10 for Wavetable. + Frequency divider is higher precision or just right shift? + needs verification. +*/ + +#include "x1_010.hpp" + +void x1_010_core::tick() +{ + // reset output + m_out[0] = m_out[1] = 0; + for (int i = 0; i < 16; i++) + { + voice_t &v = m_voice[i]; + v.tick(); + m_out[0] += v.data * v.vol_out[0]; + m_out[1] += v.data * v.vol_out[1]; + } +} + +void x1_010_core::voice_t::tick() +{ + data = vol_out[0] = vol_out[1] = 0; + if (flag.keyon) + { + if (flag.wavetable) // Wavetable + { + // envelope, each nibble is for each output + u8 vol = m_host.m_envelope[(bitfield(end_envshape, 0, 5) << 7) | bitfield(env_acc, 10, 7)]; + vol_out[0] = bitfield(vol, 4, 4); + vol_out[1] = bitfield(vol, 0, 4); + env_acc += start_envfreq; + if (flag.env_oneshot && bitfield(env_acc, 17)) + flag.keyon = false; + else + env_acc = bitfield(env_acc, 0, 17); + // get wavetable data + data = m_host.m_wave[(bitfield(vol_wave, 0, 5) << 7) | bitfield(acc, 10, 7)]; + acc = bitfield(acc + (freq >> flag.div), 0, 17); + } + else // PCM sample + { + // volume register, each nibble is for each output + vol_out[0] = bitfield(vol_wave, 4, 4); + vol_out[1] = bitfield(vol_wave, 0, 4); + // get PCM sample + data = m_host.m_intf.read_byte(bitfield(acc, 4, 20)); + acc += bitfield(freq, 0, 8) >> flag.div; + if ((acc >> 16) > (0xff ^ end_envshape)) + flag.keyon = false; + } + } +} + +u8 x1_010_core::ram_r(u16 offset) +{ + if (offset & 0x1000) // wavetable data + return m_wave[offset & 0xfff]; + else if (offset & 0xf80) // envelope shape data + return m_envelope[offset & 0xfff]; + else // channel register + return m_voice[bitfield(offset, 3, 4)].reg_r(offset & 0x7); +} + +void x1_010_core::ram_w(u16 offset, u8 data) +{ + if (offset & 0x1000) // wavetable data + m_wave[offset & 0xfff] = data; + else if (offset & 0xf80) // envelope shape data + m_envelope[offset & 0xfff] = data; + else // channel register + m_voice[bitfield(offset, 3, 4)].reg_w(offset & 0x7, data); +} + +u8 x1_010_core::voice_t::reg_r(u8 offset) +{ + switch (offset & 0x7) + { + case 0x00: return (flag.div << 7) + | (flag.env_oneshot << 2) + | (flag.wavetable << 1) + | (flag.keyon << 0); + case 0x01: return vol_wave; + case 0x02: return bitfield(freq, 0, 8); + case 0x03: return bitfield(freq, 8, 8); + case 0x04: return start_envfreq; + case 0x05: return end_envshape; + default: break; + } + return 0; +} + +void x1_010_core::voice_t::reg_w(u8 offset, u8 data) +{ + switch (offset & 0x7) + { + case 0x00: + { + const bool prev_keyon = flag.keyon; + flag.div = bitfield(data, 7); + flag.env_oneshot = bitfield(data, 2); + flag.wavetable = bitfield(data, 1); + flag.keyon = bitfield(data, 0); + if (!prev_keyon && flag.keyon) // Key on + { + acc = flag.wavetable ? 0 : (u32(start_envfreq) << 16); + env_acc = 0; + } + break; + } + case 0x01: + vol_wave = data; + break; + case 0x02: + freq = (freq & 0xff00) | data; + break; + case 0x03: + freq = (freq & 0x00ff) | (u16(data) << 8); + break; + case 0x04: + start_envfreq = data; + break; + case 0x05: + end_envshape = data; + break; + default: + break; + } +} + +void x1_010_core::voice_t::reset() +{ + flag.reset(); + vol_wave = 0; + freq = 0; + start_envfreq = 0; + end_envshape = 0; + acc = 0; + env_acc = 0; + data = 0; + vol_out[0] = vol_out[1] = 0; +} + +void x1_010_core::reset() +{ + for (auto & elem : m_voice) + elem.reset(); + + std::fill_n(&m_envelope[0], 0x1000, 0); + std::fill_n(&m_wave[0], 0x1000, 0); + m_out[0] = m_out[1] = 0; +} diff --git a/src/engine/platform/sound/x1_010/x1_010.hpp b/src/engine/platform/sound/x1_010/x1_010.hpp new file mode 100644 index 000000000..e8f5e3e40 --- /dev/null +++ b/src/engine/platform/sound/x1_010/x1_010.hpp @@ -0,0 +1,132 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holders: cam900 + Seta/Allumer X1-010 Emulation core + + See x1_010.cpp for more info. +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_X1_010_HPP +#define _VGSOUND_EMU_X1_010_HPP + +#pragma once + +namespace x1_010 +{ + typedef unsigned char u8; + typedef unsigned short u16; + typedef unsigned int u32; + typedef signed char s8; + typedef signed int s32; + + template T bitfield(T in, u8 pos, u8 len = 1) + { + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); + } +} + +using namespace x1_010; +class x1_010_mem_intf +{ +public: + virtual u8 read_byte(u32 address) { return 0; } +}; + +using namespace x1_010; +class x1_010_core +{ + friend class x1_010_mem_intf; +public: + // constructor + x1_010_core(x1_010_mem_intf &intf) + : m_voice{*this,*this,*this,*this, + *this,*this,*this,*this, + *this,*this,*this,*this, + *this,*this,*this,*this} + , m_intf(intf) + { + m_envelope = std::make_unique(0x1000); + m_wave = std::make_unique(0x1000); + + std::fill_n(&m_envelope[0], 0x1000, 0); + std::fill_n(&m_wave[0], 0x1000, 0); + } + + // register accessor + u8 ram_r(u16 offset); + void ram_w(u16 offset, u8 data); + + // getters + s32 output(u8 channel) { return m_out[channel & 1]; } + + // internal state + void reset(); + void tick(); + +private: + // 16 voices in chip + struct voice_t + { + // constructor + voice_t(x1_010_core &host) : m_host(host) {} + + // internal state + void reset(); + void tick(); + + // register accessor + u8 reg_r(u8 offset); + void reg_w(u8 offset, u8 data); + + // registers + x1_010_core &m_host; + struct flag_t + { + u8 div : 1; + u8 env_oneshot : 1; + u8 wavetable : 1; + u8 keyon : 1; + void reset() + { + div = 0; + env_oneshot = 0; + wavetable = 0; + keyon = 0; + } + flag_t() + : div(0) + , env_oneshot(0) + , wavetable(0) + , keyon(0) + { } + }; + flag_t flag; + u8 vol_wave = 0; + u16 freq = 0; + u8 start_envfreq = 0; + u8 end_envshape = 0; + + // internal registers + u32 acc = 0; + u32 env_acc = 0; + s8 data = 0; + u8 vol_out[2] = {0}; + }; + voice_t m_voice[16]; + + // RAM + std::unique_ptr m_envelope = nullptr; + std::unique_ptr m_wave = nullptr; + + // output data + s32 m_out[2] = {0}; + + x1_010_mem_intf &m_intf; +}; + +#endif diff --git a/src/engine/platform/sound/ymfm/ymfm.h b/src/engine/platform/sound/ymfm/ymfm.h index 906e3211a..7b98f8499 100644 --- a/src/engine/platform/sound/ymfm/ymfm.h +++ b/src/engine/platform/sound/ymfm/ymfm.h @@ -33,7 +33,7 @@ #pragma once -#ifdef _MSC_VER +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS #endif diff --git a/src/engine/platform/sound/ymfm/ymfm_fm.h b/src/engine/platform/sound/ymfm/ymfm_fm.h index 01cb22e77..3239880e5 100644 --- a/src/engine/platform/sound/ymfm/ymfm_fm.h +++ b/src/engine/platform/sound/ymfm/ymfm_fm.h @@ -426,7 +426,7 @@ protected: void assign_operators(); // update the state of the given timer - void update_timer(uint32_t which, uint32_t enable); + void update_timer(uint32_t which, uint32_t enable, int32_t delta_clocks); // internal state ymfm_interface &m_intf; // reference to the system interface @@ -436,6 +436,7 @@ protected: uint8_t m_irq_mask; // mask of which bits signal IRQs uint8_t m_irq_state; // current IRQ state uint8_t m_timer_running[2]; // current timer running state + uint8_t m_total_clocks; // low 8 bits of the total number of clocks processed uint32_t m_active_channels; // mask of active channels (computed by prepare) uint32_t m_modified_channels; // mask of channels that have been modified uint32_t m_prepare_count; // counter to do periodic prepare sweeps diff --git a/src/engine/platform/sound/ymfm/ymfm_fm.ipp b/src/engine/platform/sound/ymfm/ymfm_fm.ipp index 410569458..84948aedb 100644 --- a/src/engine/platform/sound/ymfm/ymfm_fm.ipp +++ b/src/engine/platform/sound/ymfm/ymfm_fm.ipp @@ -1236,6 +1236,7 @@ void fm_engine_base::save_restore(ymfm_saved_state &state) state.save_restore(m_irq_state); state.save_restore(m_timer_running[0]); state.save_restore(m_timer_running[1]); + state.save_restore(m_total_clocks); // save the register/family data m_regs.save_restore(state); @@ -1261,6 +1262,9 @@ void fm_engine_base::save_restore(ymfm_saved_state &state) template uint32_t fm_engine_base::clock(uint32_t chanmask) { + // update the clock counter + m_total_clocks++; + // if something was modified, prepare // also prepare every 4k samples to catch ending notes if (m_modified_channels != 0 || m_prepare_count++ >= 4096) @@ -1455,7 +1459,7 @@ void fm_engine_base::assign_operators() //------------------------------------------------- template -void fm_engine_base::update_timer(uint32_t tnum, uint32_t enable) +void fm_engine_base::update_timer(uint32_t tnum, uint32_t enable, int32_t delta_clocks) { // if the timer is live, but not currently enabled, set the timer if (enable && !m_timer_running[tnum]) @@ -1463,6 +1467,9 @@ void fm_engine_base::update_timer(uint32_t tnum, uint32_t enable) // period comes from the registers, and is different for each uint32_t period = (tnum == 0) ? (1024 - m_regs.timer_a_value()) : 16 * (256 - m_regs.timer_b_value()); + // caller can also specify a delta to account for other effects + period += delta_clocks; + // reset it m_intf.ymfm_set_timer(tnum, period * OPERATORS * m_clock_prescale); m_timer_running[tnum] = 1; @@ -1499,7 +1506,7 @@ void fm_engine_base::engine_timer_expired(uint32_t tnum) // reset m_timer_running[tnum] = false; - update_timer(tnum, 1); + update_timer(tnum, 1, 0); } @@ -1557,9 +1564,11 @@ void fm_engine_base::engine_mode_write(uint8_t data) reset_mask |= RegisterType::STATUS_TIMERA; set_reset_status(0, reset_mask); - // load timers - update_timer(1, m_regs.load_timer_b()); - update_timer(0, m_regs.load_timer_a()); + // load timers; note that timer B gets a small negative adjustment because + // the *16 multiplier is free-running, so the first tick of the clock + // is a bit shorter + update_timer(1, m_regs.load_timer_b(), -(m_total_clocks & 15)); + update_timer(0, m_regs.load_timer_a(), 0); } } diff --git a/src/engine/platform/sound/ymfm/ymfm_opm.cpp b/src/engine/platform/sound/ymfm/ymfm_opm.cpp index 9fd447e8c..31d0a7467 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opm.cpp +++ b/src/engine/platform/sound/ymfm/ymfm_opm.cpp @@ -74,6 +74,7 @@ opm_registers::opm_registers() : m_lfo_waveform[2][index] = am | (pm << 8); // waveform 3 is noise; it is filled in dynamically + m_lfo_waveform[3][index] = 0; } } @@ -86,6 +87,8 @@ void opm_registers::reset() { std::fill_n(&m_regdata[0], REGISTERS, 0); + m_lfo_counter = 0; + // enable output on both channels by default m_regdata[0x20] = m_regdata[0x21] = m_regdata[0x22] = m_regdata[0x23] = 0xc0; m_regdata[0x24] = m_regdata[0x25] = m_regdata[0x26] = m_regdata[0x27] = 0xc0; @@ -194,8 +197,13 @@ int32_t opm_registers::clock_noise_and_lfo() // treat the rate as a 4.4 floating-point step value with implied // leading 1; this matches exactly the frequencies in the application // manual, though it might not be implemented exactly this way on chip + // note from tildearrow: + // - in fact it doesn't. the strings in Scherzo Di Notte totally go out + // tune after a bit (and this doesn't happen in Nuked-OPM). uint32_t rate = lfo_rate(); - m_lfo_counter += (0x10 | bitfield(rate, 0, 4)) << bitfield(rate, 4, 4); + if (rate != 0) { + m_lfo_counter += (0x10 | bitfield(rate, 0, 4)) << bitfield(rate, 4, 4); + } // bit 1 of the test register is officially undocumented but has been // discovered to hold the LFO in reset while active diff --git a/src/engine/platform/sound/ymfm/ymfm_opn.cpp b/src/engine/platform/sound/ymfm/ymfm_opn.cpp index 4a334a63b..00ac2cf63 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opn.cpp +++ b/src/engine/platform/sound/ymfm/ymfm_opn.cpp @@ -2398,8 +2398,8 @@ void ym2612::generate(output_data *output, uint32_t numsamples) // a better sound mixer than we usually have, so just average over the six // channels; also apply a 64/65 factor to account for the discontinuity // adjustment above - output->data[0] = (output->data[0] << 7) * 64 / (6 * 65); - output->data[1] = (output->data[1] << 7) * 64 / (6 * 65); + output->data[0] = (output->data[0] * 128) * 64 / (6 * 65); + output->data[1] = (output->data[1] * 128) * 64 / (6 * 65); } } @@ -2432,8 +2432,8 @@ void ym3438::generate(output_data *output, uint32_t numsamples) // YM3438 doesn't have the same DAC discontinuity, though its output is // multiplexed like the YM2612 - output->data[0] = (output->data[0] << 7) / 6; - output->data[1] = (output->data[1] << 7) / 6; + output->data[0] = (output->data[0] * 128) / 6; + output->data[1] = (output->data[1] * 128) / 6; } } diff --git a/src/engine/platform/sound/ymfm/ymfm_opn.h b/src/engine/platform/sound/ymfm/ymfm_opn.h index 8d9b42de0..f4136c731 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opn.h +++ b/src/engine/platform/sound/ymfm/ymfm_opn.h @@ -763,7 +763,7 @@ public: protected: // simulate the DAC discontinuity - int32_t dac_discontinuity(int32_t value) const { return (value < 0) ? (value - 2) : (value + 3); } + constexpr int32_t dac_discontinuity(int32_t value) const { return (value < 0) ? (value - 2) : (value + 3); } // internal state uint16_t m_address; // address register diff --git a/src/engine/platform/sound/ymfm/ymfm_opz.cpp b/src/engine/platform/sound/ymfm/ymfm_opz.cpp index adeefd79f..62a3d4d9c 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opz.cpp +++ b/src/engine/platform/sound/ymfm/ymfm_opz.cpp @@ -270,6 +270,7 @@ bool opz_registers::write(uint16_t index, uint8_t data, uint32_t &channel, uint3 bool is_setting_preset = (bitfield(m_regdata[0x100 + (index & 0x1f)], 7) != 0); if (is_setting_preset) { + //printf("ISP\n"); if ((index & 0xe0) == 0xe0) { m_regdata[0x140 + (index & 0x1f)] = data; @@ -280,10 +281,14 @@ bool opz_registers::write(uint16_t index, uint8_t data, uint32_t &channel, uint3 } // handle writes to the key on index - if ((index & 0xf8) == 0x20 && bitfield(index, 0, 3) == bitfield(m_regdata[0x08], 0, 3)) + + // note from tildearrow: + // - are you kidding? I have to write to this "load preset" register before keying on? + if ((index & 0xf8) == 0x20 /*&& bitfield(index, 0, 3) == bitfield(m_regdata[0x08], 0, 3)*/) { channel = bitfield(index, 0, 3); opmask = ch_key_on(channel) ? 0xf : 0; + //printf("%d opmask is %d\n",opmask,channel); // according to the TX81Z manual, the sync option causes the LFOs // to reset at each note on @@ -333,8 +338,12 @@ int32_t opz_registers::clock_noise_and_lfo() // manual, though it might not be implemented exactly this way on chip uint32_t rate0 = lfo_rate(); uint32_t rate1 = lfo2_rate(); - m_lfo_counter[0] += (0x10 | bitfield(rate0, 0, 4)) << bitfield(rate0, 4, 4); - m_lfo_counter[1] += (0x10 | bitfield(rate1, 0, 4)) << bitfield(rate1, 4, 4); + if (rate0 != 0) { + m_lfo_counter[0] += (0x10 | bitfield(rate0, 0, 4)) << bitfield(rate0, 4, 4); + } + if (rate1 != 0) { + m_lfo_counter[1] += (0x10 | bitfield(rate1, 0, 4)) << bitfield(rate1, 4, 4); + } uint32_t lfo0 = bitfield(m_lfo_counter[0], 22, 8); uint32_t lfo1 = bitfield(m_lfo_counter[1], 22, 8); diff --git a/src/engine/platform/sound/ymfm/ymfm_opz.h b/src/engine/platform/sound/ymfm/ymfm_opz.h index 997ba32f9..4bc4663a0 100644 --- a/src/engine/platform/sound/ymfm/ymfm_opz.h +++ b/src/engine/platform/sound/ymfm/ymfm_opz.h @@ -220,8 +220,8 @@ public: // per-channel registers uint32_t ch_volume(uint32_t choffs) const { return byte(0x00, 0, 8, choffs); } - uint32_t ch_output_any(uint32_t choffs) const { return byte(0x20, 7, 1, choffs) | byte(0x30, 0, 1, choffs); } - uint32_t ch_output_0(uint32_t choffs) const { return byte(0x30, 0, 1, choffs); } + uint32_t ch_output_any(uint32_t choffs) const { return 1; } + uint32_t ch_output_0(uint32_t choffs) const { return byte(0x30, 0, 1, choffs) | (!byte(0x20, 7, 1, choffs)); } uint32_t ch_output_1(uint32_t choffs) const { return byte(0x20, 7, 1, choffs) | byte(0x30, 0, 1, choffs); } uint32_t ch_output_2(uint32_t choffs) const { return 0; } uint32_t ch_output_3(uint32_t choffs) const { return 0; } diff --git a/src/engine/platform/swan.cpp b/src/engine/platform/swan.cpp new file mode 100644 index 000000000..802f27b86 --- /dev/null +++ b/src/engine/platform/swan.cpp @@ -0,0 +1,523 @@ +/** + * 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 "swan.h" +#include "../engine.h" +#include + +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);}} + +#define CHIP_DIVIDER 32 + +const char* regCheatSheetWS[]={ + "CH1_Pitch", "00", + "CH2_Pitch", "02", + "CH3_Pitch", "04", + "CH4_Pitch", "06", + "CH1_Vol", "08", + "CH2_Vol", "09", + "CH3_Vol", "0A", + "CH4_Vol", "0B", + "Sweep_Value", "0C", + "Sweep_Time", "0D", + "Noise", "0E", + "Wave_Base", "0F", + "Ctrl", "10", + "Output", "11", + "Random", "12", + "Voice_Ctrl", "14", + "Wave_Mem", "40", + NULL +}; + +const char** DivPlatformSwan::getRegisterSheet() { + return regCheatSheetWS; +} + +const char* DivPlatformSwan::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + case 0x11: + return "11xx: Setup noise mode (0: disabled; 1-8: enabled/tap)"; + break; + case 0x12: + return "12xx: Setup sweep period (0: disabled; 1-20: enabled/period)"; + break; + case 0x13: + return "13xx: Set sweep amount"; + break; + case 0x17: + return "17xx: Toggle PCM mode"; + break; + } + return NULL; +} + +void DivPlatformSwan::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; hrate) { + DivSample* s=parent->getSample(dacSample); + if (s->samples<=0) { + dacSample=-1; + 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; + } + } + dacPeriod-=rate; + } + } + + // the rest + while (!writes.empty()) { + QueuedWrite w=writes.front(); + regPool[w.addr]=w.val; + if (w.addr<0x40) ws->SoundWrite(w.addr|0x80,w.val); + else ws->RAMWrite(w.addr&0x3f,w.val); + writes.pop(); + } + int16_t samp[2]{0, 0}; + ws->SoundUpdate(16); + ws->SoundFlush(samp, 1); + bufL[h]=samp[0]; + bufR[h]=samp[1]; + } +} + +void DivPlatformSwan::updateWave(int ch) { + unsigned char addr=0x40+ch*16; + for (int i=0; i<16; i++) { + int nibble1=chan[ch].ws.output[i<<1]; + int nibble2=chan[ch].ws.output[1+(i<<1)]; + rWrite(addr+i,nibble1|(nibble2<<4)); + } +} + +void DivPlatformSwan::calcAndWriteOutVol(int ch, int env) { + int vl=chan[ch].vol*((chan[ch].pan>>4)&0x0f)*env/225; + int vr=chan[ch].vol*(chan[ch].pan&0x0f)*env/225; + if (ch==1&&pcm) { + vl=(vl>0)?((vl>7)?3:2):0; + vr=(vr>0)?((vr>7)?3:2):0; + chan[1].outVol=vr|(vl<<2); + } else { + chan[ch].outVol=vr|(vl<<4); + } + writeOutVol(ch); +} + +void DivPlatformSwan::writeOutVol(int ch) { + unsigned char val=isMuted[ch]?0:chan[ch].outVol; + if (ch==1&&pcm) { + rWrite(0x14,val) + } else { + rWrite(0x08+ch,val); + } +} + +void DivPlatformSwan::tick() { + unsigned char sndCtrl=(pcm?0x20:0)|(sweep?0x40:0)|((noise>0)?0x80:0); + for (int i=0; i<4; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + int env=chan[i].std.vol.val; + if(parent->getIns(chan[i].ins)->type==DIV_INS_AMIGA) { + env=MIN(env/4,15); + } + calcAndWriteOutVol(i,env); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.wave.had && !(i==1 && pcm)) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); + } + } + if (chan[i].active) { + sndCtrl|=(1<calcFreq(chan[i].baseFreq,chan[i].pitch,true); + if (i==1 && pcm && furnaceDac) { + double off=1.0; + if (dacSample>=0 && dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(dacSample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + dacRate=((double)chipClock/2)/MAX(1,off*chan[i].freq); + if (dumpWrites) addWrite(0xffff0001,dacRate); + } + if (chan[i].freq>2048) chan[i].freq=2048; + if (chan[i].freq<1) chan[i].freq=1; + int rVal=2048-chan[i].freq; + rWrite(i*2,rVal&0xff); + rWrite(i*2+1,rVal>>8); + if (chan[i].keyOn) { + if (!chan[i].std.vol.will) { + calcAndWriteOutVol(i,15); + } + chan[i].keyOn=false; + } + if (chan[i].keyOff) { + chan[i].keyOff=false; + } + chan[i].freqChanged=false; + } + } + if (chan[3].std.duty.had) { + noise=chan[3].std.duty.val; + if (noise>0) { + rWrite(0x0e,((noise-1)&0x07)|0x18); + sndCtrl|=0x80; + } else { + sndCtrl&=~0x80; + } + } + rWrite(0x10,sndCtrl); +} + +int DivPlatformSwan::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (c.chan==1) { + if (ins->type==DIV_INS_AMIGA) { + pcm=true; + } else if (furnaceDac) { + pcm=false; + } + if (pcm) { + if (skipRegisterWrites) break; + dacPos=0; + dacPeriod=0; + if (ins->type==DIV_INS_AMIGA) { + dacSample=ins->amiga.initSample; + if (dacSample<0 || dacSample>=parent->song.sampleLen) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + break; + } else { + if (dumpWrites) { + addWrite(0xffff0000,dacSample); + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[1].baseFreq=NOTE_PERIODIC(c.value); + chan[1].freqChanged=true; + chan[1].note=c.value; + } + chan[1].active=true; + chan[1].keyOn=true; + chan[1].std.init(ins); + furnaceDac=true; + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[1].note=c.value; + } + dacSample=12*sampleBank+chan[1].note%12; + if (dacSample>=parent->song.sampleLen) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000,dacSample); + } + dacRate=parent->getSample(dacSample)->rate; + if (dumpWrites) { + addWrite(0xffff0001,dacRate); + } + chan[1].active=true; + chan[1].keyOn=true; + furnaceDac=false; + } + break; + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(ins); + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,32,15,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + if (c.chan==1&&pcm) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + pcm=false; + } + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.had) { + calcAndWriteOutVol(c.chan,15); + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + chan[c.chan].keyOn=true; + break; + case DIV_CMD_WS_SWEEP_TIME: + if (c.chan==2) { + if (c.value==0) { + sweep=false; + } else { + sweep=true; + rWrite(0x0d,(c.value-1)&0xff); + } + } + break; + case DIV_CMD_WS_SWEEP_AMOUNT: + if (c.chan==2) { + rWrite(0x0c,c.value&0xff); + } + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + if (c.chan==3) { + noise=c.value&0xff; + if (noise>0) rWrite(0x0e,((noise-1)&0x07)|0x18); + } + break; + case DIV_CMD_SAMPLE_MODE: + if (c.chan==1) pcm=c.value; + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_PANNING: { + chan[c.chan].pan=c.value; + calcAndWriteOutVol(c.chan,chan[c.chan].std.vol.will?chan[c.chan].std.vol.val:15); + 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))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformSwan::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + writeOutVol(ch); +} + +void DivPlatformSwan::forceIns() { + for (int i=0; i<4; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + updateWave(i); + writeOutVol(i); + } +} + +void* DivPlatformSwan::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformSwan::getRegisterPool() { + // get Random from emulator + regPool[0x12]=ws->SoundRead(0x92); + regPool[0x13]=ws->SoundRead(0x93); + return regPool; +} + +int DivPlatformSwan::getRegisterPoolSize() { + return 128; +} + +void DivPlatformSwan::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,128); + for (int i=0; i<4; i++) { + chan[i]=Channel(); + chan[i].vol=15; + chan[i].pan=0xff; + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,32,15,false); + rWrite(0x08+i,0xff); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + ws->SoundReset(); + pcm=false; + sweep=false; + furnaceDac=false; + noise=0; + dacPeriod=0; + dacRate=0; + dacPos=0; + dacSample=-1; + sampleBank=0; + rWrite(0x0f,0x00); // wave table at 0x0000 + rWrite(0x11,0x09); // enable speakers +} + +bool DivPlatformSwan::isStereo() { + return true; +} + +void DivPlatformSwan::notifyWaveChange(int wave) { + for (int i=0; i<4; i++) { + if (chan[i].wave==wave) { + chan[i].ws.changeWave1(wave); + updateWave(i); + } + } +} + +void DivPlatformSwan::notifyInsDeletion(void* ins) { + for (int i=0; i<4; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformSwan::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformSwan::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformSwan::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + chipClock=3072000; + rate=chipClock/16; // = 192000kHz, should be enough + for (int i=0; i<4; i++) { + isMuted[i]=false; + } + ws=new WSwan(); + reset(); + return 4; +} + +void DivPlatformSwan::quit() { + delete ws; +} + +DivPlatformSwan::~DivPlatformSwan() { +} diff --git a/src/engine/platform/swan.h b/src/engine/platform/swan.h new file mode 100644 index 000000000..610884b00 --- /dev/null +++ b/src/engine/platform/swan.h @@ -0,0 +1,97 @@ +/** + * 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 _SWAN_H +#define _SWAN_H + +#include "../dispatch.h" +#include "../macroInt.h" +#include "../waveSynth.h" +#include "sound/swan.h" +#include + +class DivPlatformSwan: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins, pan; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta; + int vol, outVol, wave; + DivMacroInt std; + DivWaveSynth ws; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + pan(255), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + vol(15), + outVol(15), + wave(-1) {} + }; + Channel chan[4]; + bool isMuted[4]; + bool pcm, sweep, furnaceDac; + unsigned char sampleBank, noise; + int dacPeriod, dacRate; + unsigned int dacPos; + int dacSample; + + unsigned char regPool[0x80]; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {} + }; + std::queue writes; + WSwan* ws; + void updateWave(int ch); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformSwan(); + private: + void calcAndWriteOutVol(int ch, int env); + void writeOutVol(int ch); +}; + +#endif diff --git a/src/engine/platform/tia.cpp b/src/engine/platform/tia.cpp index 2b99731fe..e06c5c74f 100644 --- a/src/engine/platform/tia.cpp +++ b/src/engine/platform/tia.cpp @@ -87,8 +87,8 @@ unsigned char DivPlatformTIA::dealWithFreq(unsigned char shape, int base, int pi void DivPlatformTIA::tick() { for (int i=0; i<2; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=MIN(15,chan[i].std.vol)-(15-(chan[i].vol&15)); + if (chan[i].std.vol.had) { + chan[i].outVol=MIN(15,chan[i].std.vol.val)-(15-(chan[i].vol&15)); if (chan[i].outVol<0) chan[i].outVol=0; if (isMuted[i]) { rWrite(0x19+i,0); @@ -96,29 +96,29 @@ void DivPlatformTIA::tick() { rWrite(0x19+i,chan[i].outVol&15); } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=0x80000000|chan[i].std.arp; + if (chan[i].std.arp.mode) { + chan[i].baseFreq=0x80000000|chan[i].std.arp.val; } else { - chan[i].baseFreq=(chan[i].note+chan[i].std.arp)<<8; + chan[i].baseFreq=(chan[i].note+chan[i].std.arp.val)<<8; } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=chan[i].note<<8; chan[i].freqChanged=true; } } - if (chan[i].std.hadWave) { - chan[i].shape=chan[i].std.wave&15; + if (chan[i].std.wave.had) { + chan[i].shape=chan[i].std.wave.val&15; rWrite(0x15+i,chan[i].shape); chan[i].freqChanged=true; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { if (chan[i].insChanged) { - if (!chan[i].std.willWave) { + if (!chan[i].std.wave.will) { chan[i].shape=4; rWrite(0x15+i,chan[i].shape); } @@ -179,7 +179,7 @@ int DivPlatformTIA::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (isMuted[c.chan]) { diff --git a/src/engine/platform/tx81z.cpp b/src/engine/platform/tx81z.cpp new file mode 100644 index 000000000..7ea634b26 --- /dev/null +++ b/src/engine/platform/tx81z.cpp @@ -0,0 +1,788 @@ +/** + * 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 "tx81z.h" +#include "../engine.h" +#include +#include + +#include "fmshared_OPM.h" + +// actually 0x40 but the upper bit of data selects address +#define ADDR_WS_FINE 0x100 +// actually 0xc0 but bit 5 of data selects address +#define ADDR_EGS_REV 0x120 + +static unsigned short chanOffs[8]={ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 +}; +static unsigned short opOffs[4]={ + 0x00, 0x08, 0x10, 0x18 +}; +static bool isOutput[8][4]={ + // 1 3 2 4 + {false,false,false,true}, + {false,false,false,true}, + {false,false,false,true}, + {false,false,false,true}, + {false,false,true ,true}, + {false,true ,true ,true}, + {false,true ,true ,true}, + {true ,true ,true ,true}, +}; +static unsigned char dtTable[8]={ + 7,6,5,0,1,2,3,4 +}; + +static int orderedOps[4]={ + 0,2,1,3 +}; + +#define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } + +#define NOTE_LINEAR(x) (((x)<<6)+baseFreqOff+log2(parent->song.tuning/440.0)*12.0*64.0) + +const char* regCheatSheetOPZ[]={ + "Test", "00", + "NoteCtl", "08", + "NoiseCtl", "0F", + "ClockA1", "10", + "ClockA2", "11", + "ClockB", "12", + "Control", "14", + "LFOFreq", "18", + "AMD_PMD", "19", + "LFOWave", "1B", + "L_R_FB_ALG", "20", + "KC", "28", + "KF", "30", + "PMS_AMS", "38", + "DT_MULT", "40", + "TL", "60", + "KS_AR", "80", + "AM_DR", "A0", + "DT2_SR", "C0", + "SL_RR", "E0", + NULL +}; + +const char** DivPlatformTX81Z::getRegisterSheet() { + return regCheatSheetOPZ; +} + +const char* DivPlatformTX81Z::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Set noise frequency (xx: value; 0 disables noise)"; + break; + case 0x11: + return "11xx: Set feedback (0 to 7)"; + break; + case 0x12: + return "12xx: Set level of operator 1 (0 highest, 7F lowest)"; + break; + case 0x13: + return "13xx: Set level of operator 2 (0 highest, 7F lowest)"; + break; + case 0x14: + return "14xx: Set level of operator 3 (0 highest, 7F lowest)"; + break; + case 0x15: + return "15xx: Set level of operator 4 (0 highest, 7F lowest)"; + break; + case 0x16: + return "16xy: Set operator multiplier (x: operator from 1 to 4; y: multiplier)"; + break; + case 0x17: + return "17xx: Set LFO speed"; + break; + case 0x18: + return "18xx: Set LFO waveform (0 saw, 1 square, 2 triangle, 3 noise)"; + break; + case 0x19: + return "19xx: Set attack of all operators (0 to 1F)"; + break; + case 0x1a: + return "1Axx: Set attack of operator 1 (0 to 1F)"; + break; + case 0x1b: + return "1Bxx: Set attack of operator 2 (0 to 1F)"; + break; + case 0x1c: + return "1Cxx: Set attack of operator 3 (0 to 1F)"; + break; + case 0x1d: + return "1Dxx: Set attack of operator 4 (0 to 1F)"; + break; + case 0x1e: + return "1Exx: Set AM depth (0 to 7F)"; + break; + case 0x1f: + return "1Fxx: Set PM depth (0 to 7F)"; + break; + case 0x30: + return "30xx: Toggle hard envelope reset on new notes"; + break; + } + return NULL; +} + +void DivPlatformTX81Z::acquire(short* bufL, short* bufR, size_t start, size_t len) { + static int os[2]; + + for (size_t h=start; hwrite(0x0+((w.addr>>8)<<1),w.addr); + fm_ymfm->write(0x1+((w.addr>>8)<<1),w.val); + regPool[w.addr&0xff]=w.val; + writes.pop(); + delay=1; + } + } + + fm_ymfm->generate(&out_ymfm); + + os[0]=out_ymfm.data[0]; + if (os[0]<-32768) os[0]=-32768; + if (os[0]>32767) os[0]=32767; + + os[1]=out_ymfm.data[1]; + if (os[1]<-32768) os[1]=-32768; + if (os[1]>32767) os[1]=32767; + + bufL[h]=os[0]; + bufR[h]=os[1]; + } +} + +static unsigned char noteMap[12]={ + 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14 +}; + +inline int hScale(int note) { + return ((note/12)<<4)+(noteMap[note%12]); +} + +void DivPlatformTX81Z::tick() { + for (int i=0; i<8; i++) { + chan[i].std.next(); + + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_LINEAR(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_LINEAR(chan[i].note+(signed char)chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_LINEAR(chan[i].note); + chan[i].freqChanged=true; + } + } + + if (chan[i].std.duty.had) { + if (chan[i].std.duty.val>0) { + rWrite(0x0f,0x80|(0x20-chan[i].std.duty.val)); + } else { + rWrite(0x0f,0); + } + } + + if (chan[i].std.wave.had) { + rWrite(0x1b,chan[i].std.wave.val&3); + } + + if (chan[i].std.ex1.had) { + amDepth=chan[i].std.ex1.val; + immWrite(0x19,amDepth); + } + + if (chan[i].std.ex2.had) { + pmDepth=chan[i].std.ex2.val; + immWrite(0x19,0x80|pmDepth); + } + + if (chan[i].std.ex3.had) { + immWrite(0x18,chan[i].std.ex3.val); + } + + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; + if (isMuted[i]) { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|0x40); + } else { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|(chan[i].active?0:0x40)|(chan[i].chVolR<<7)); + } + if (!parent->song.algMacroBehavior) for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + } + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; + if (isMuted[i]) { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|0x40); + } else { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|(chan[i].active?0:0x40)|(chan[i].chVolR<<7)); + } + } + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; + rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); + } + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; + rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); + } + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + DivMacroInt::IntOp& m=chan[i].std.op[j]; + if (m.am.had) { + op.am=m.am.val; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.ar.had) { + op.ar=m.ar.val; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + } + if (m.dr.had) { + op.dr=m.dr.val; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.mult.had) { + op.mult=m.mult.val; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.rr.had) { + op.rr=m.rr.val; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.sl.had) { + op.sl=m.sl.val; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.tl.had) { + op.tl=127-m.tl.val; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + if (m.rs.had) { + op.rs=m.rs.val; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + } + if (m.dt.had) { + op.dt=m.dt.val; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.d2r.had) { + op.d2r=m.d2r.val; + rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); + } + if (m.dt2.had) { + op.dt2=m.dt2.val; + rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); + } + } + if (chan[i].keyOn || chan[i].keyOff) { + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + immWrite(baseAddr+ADDR_SL_RR,0x0f); + immWrite(baseAddr+ADDR_TL,0x7f); + oldWrites[baseAddr+ADDR_SL_RR]=-1; + oldWrites[baseAddr+ADDR_TL]=-1; + } + } + if (isMuted[i]) { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|0x00); + } else { + //if (chan[i].keyOn) immWrite(0x08,i); + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|0x00|(chan[i].chVolR<<7)); + } + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + for (int k=0; k<9; k++) { + immWrite(baseAddr+ADDR_SL_RR,0x0f); + } + } + } + chan[i].keyOff=false; + } + } + + for (int i=0; i<256; i++) { + if (pendingWrites[i]!=oldWrites[i]) { + immWrite(i,pendingWrites[i]&0xff); + oldWrites[i]=pendingWrites[i]; + } + } + for (int i=256; i<288; i++) { + if (pendingWrites[i]!=oldWrites[i]) { + immWrite(0x40+(i&0x1f),0x80|(pendingWrites[i]&0x7f)); + oldWrites[i]=pendingWrites[i]; + } + } + for (int i=288; i<320; i++) { + if (pendingWrites[i]!=oldWrites[i]) { + immWrite(0xc0+(i&0x1f),0x20|(pendingWrites[i]&0xdf)); + oldWrites[i]=pendingWrites[i]; + } + } + + for (int i=0; i<8; i++) { + if (chan[i].freqChanged) { + chan[i].freq=chan[i].baseFreq+(chan[i].pitch>>1)-64; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>=(95<<6)) chan[i].freq=(95<<6)-1; + immWrite(i+0x28,hScale(chan[i].freq>>6)); + immWrite(i+0x30,(chan[i].freq<<2)|(chan[i].chVolL==chan[i].chVolR)); + chan[i].freqChanged=false; + } + if (chan[i].keyOn) { + if (isMuted[i]) { + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + } else { + //immWrite(0x08,i); + immWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|0x40|(chan[i].chVolR<<7)); + } + chan[i].keyOn=false; + } + } +} + +void DivPlatformTX81Z::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + // TODO: use volume registers! + /* + if (isMuted[ch]) { + immWrite(chanOffs[ch]+ADDR_LR_FB_ALG,(chan[ch].state.alg&7)|(chan[ch].state.fb<<3)); + } else { + immWrite(chanOffs[ch]+ADDR_LR_FB_ALG,(chan[ch].state.alg&7)|(chan[ch].state.fb<<3)|((chan[ch].chVolL&1)<<6)|((chan[ch].chVolR&1)<<7)); + }*/ +} + +int DivPlatformTX81Z::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + + if (chan[c.chan].insChanged) { + chan[c.chan].state=ins->fm; + } + + chan[c.chan].std.init(ins); + if (!chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator op=chan[c.chan].state.op[i]; + if (isOutput[chan[c.chan].state.alg][i]) { + if (!chan[c.chan].active || chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } + } else { + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+ADDR_WS_FINE,(op.dvb&15)|(op.ws<<4)); + rWrite(baseAddr+ADDR_EGS_REV,(op.dam&7)|(op.ksl<<6)); + } + } + if (chan[c.chan].insChanged) { + /* + if (isMuted[c.chan]) { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + } else { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)|((chan[c.chan].chVolL&1)<<6)|((chan[c.chan].chVolR&1)<<7)); + }*/ + rWrite(chanOffs[c.chan]+ADDR_FMS_AMS,((chan[c.chan].state.fms&7)<<4)|(chan[c.chan].state.ams&3)); + //rWrite(chanOffs[c.chan]+ADDR_FMS_AMS,0x84|((chan[c.chan].state.fms2&7)<<4)|(chan[c.chan].state.ams2&3)); + } + chan[c.chan].insChanged=false; + + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_LINEAR(c.value); + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + } + chan[c.chan].keyOn=true; + chan[c.chan].active=true; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + break; + case DIV_CMD_NOTE_OFF_ENV: + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + chan[c.chan].std.release(); + break; + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_VOLUME: { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + if (isOutput[chan[c.chan].state.alg][i]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + break; + } + case DIV_CMD_GET_VOLUME: { + return chan[c.chan].vol; + break; + } + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].insChanged=true; + } + chan[c.chan].ins=c.value; + break; + case DIV_CMD_PANNING: { + chan[c.chan].chVolL=((c.value>>4)>0); + chan[c.chan].chVolR=((c.value&15)>0); + chan[c.chan].freqChanged=true; + /* + if (isMuted[c.chan]) { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + } else { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)|((chan[c.chan].chVolL&1)<<6)|((chan[c.chan].chVolR&1)<<7)); + }*/ + break; + } + case DIV_CMD_PITCH: { + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_LINEAR(c.value2); + int newFreq; + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + newFreq=chan[c.chan].baseFreq+c.value; + if (newFreq>=destFreq) { + newFreq=destFreq; + return2=true; + } + } else { + newFreq=chan[c.chan].baseFreq-c.value; + if (newFreq<=destFreq) { + newFreq=destFreq; + return2=true; + } + } + chan[c.chan].baseFreq=newFreq; + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_LINEAR(c.value); + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_FM_LFO: { + rWrite(0x18,c.value); + break; + } + case DIV_CMD_FM_LFO_WAVE: { + rWrite(0x1b,c.value&3); + break; + } + case DIV_CMD_FM_FB: { + chan[c.chan].state.fb=c.value&7; + /* + if (isMuted[c.chan]) { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + } else { + rWrite(chanOffs[c.chan]+ADDR_LR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)|((chan[c.chan].chVolL&1)<<6)|((chan[c.chan].chVolR&1)<<7)); + }*/ + break; + } + case DIV_CMD_FM_MULT: { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.mult=c.value2&15; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + break; + } + case DIV_CMD_FM_TL: { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.tl=c.value2; + if (isOutput[chan[c.chan].state.alg][c.value]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + break; + } + case DIV_CMD_FM_AR: { + if (c.value<0) { + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + } + } else { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + } + break; + } + case DIV_CMD_FM_AM_DEPTH: { + amDepth=c.value; + immWrite(0x19,amDepth); + break; + } + case DIV_CMD_FM_PM_DEPTH: { + pmDepth=c.value; + immWrite(0x19,0x80|pmDepth); + break; + } + case DIV_CMD_FM_HARD_RESET: + chan[c.chan].hardReset=c.value; + break; + case DIV_CMD_STD_NOISE_FREQ: { + if (c.chan!=7) break; + if (c.value) { + if (c.value>0x1f) { + rWrite(0x0f,0x80); + } else { + rWrite(0x0f,0x80|(0x1f-c.value)); + } + } else { + rWrite(0x0f,0); + } + break; + } + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_GET_VOLMAX: + return 127; + break; + case DIV_CMD_PRE_PORTA: + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_PRE_NOTE: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +void DivPlatformTX81Z::forceIns() { + for (int i=0; i<8; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator op=chan[i].state.op[j]; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.egt<<5)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,(op.d2r&31)|(op.dt2<<6)); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+ADDR_WS_FINE,(op.dvb&15)|(op.ws<<4)); + rWrite(baseAddr+ADDR_EGS_REV,(op.dam&7)|(op.ksl<<6)); + } + /* + if (isMuted[i]) { + rWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + } else { + rWrite(chanOffs[i]+ADDR_LR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)|((chan[i].chVolL&1)<<6)|((chan[i].chVolR&1)<<7)); + }*/ + rWrite(chanOffs[i]+ADDR_FMS_AMS,((chan[i].state.fms&7)<<4)|(chan[i].state.ams&3)); + //rWrite(chanOffs[i]+ADDR_FMS_AMS,0x84|((chan[i].state.fms2&7)<<4)|(chan[i].state.ams2&3)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + immWrite(0x19,amDepth); + immWrite(0x19,0x80|pmDepth); +} + +void DivPlatformTX81Z::notifyInsChange(int ins) { + for (int i=0; i<8; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void* DivPlatformTX81Z::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformTX81Z::getRegisterPool() { + return regPool; +} + +int DivPlatformTX81Z::getRegisterPoolSize() { + return 330; +} + +void DivPlatformTX81Z::poke(unsigned int addr, unsigned short val) { + immWrite(addr,val); +} + +void DivPlatformTX81Z::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) immWrite(i.addr,i.val); +} + +void DivPlatformTX81Z::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,330); + fm_ymfm->reset(); + if (dumpWrites) { + addWrite(0xffffffff,0); + } + for (int i=0; i<8; i++) { + chan[i]=DivPlatformTX81Z::Channel(); + chan[i].vol=0x7f; + chan[i].outVol=0x7f; + } + + for (int i=0; i<330; i++) { + oldWrites[i]=-1; + pendingWrites[i]=-1; + } + + lastBusy=60; + pcmCycles=0; + pcmL=0; + pcmR=0; + delay=0; + amDepth=0x7f; + pmDepth=0x7f; + + //rWrite(0x18,0x10); + immWrite(0x19,amDepth); + immWrite(0x19,0x80|pmDepth); + //rWrite(0x1b,0x00); + + extMode=false; +} + +void DivPlatformTX81Z::setFlags(unsigned int flags) { + if (flags==2) { + chipClock=4000000.0; + baseFreqOff=-122; + } else if (flags==1) { + chipClock=COLOR_PAL*4.0/5.0; + baseFreqOff=12; + } else { + chipClock=COLOR_NTSC; + baseFreqOff=0; + } + rate=chipClock/64; +} + +bool DivPlatformTX81Z::isStereo() { + return true; +} + +int DivPlatformTX81Z::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<8; i++) { + isMuted[i]=false; + } + setFlags(flags); + fm_ymfm=new ymfm::ym2414(iface); + reset(); + + return 8; +} + +void DivPlatformTX81Z::quit() { + delete fm_ymfm; +} + +DivPlatformTX81Z::~DivPlatformTX81Z() { +} diff --git a/src/engine/platform/tx81z.h b/src/engine/platform/tx81z.h new file mode 100644 index 000000000..363d5c234 --- /dev/null +++ b/src/engine/platform/tx81z.h @@ -0,0 +1,118 @@ +/** + * 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 _TX81Z_H +#define _TX81Z_H +#include "../dispatch.h" +#include "../instrument.h" +#include +#include "sound/ymfm/ymfm_opz.h" +#include "../macroInt.h" + +class DivTXInterface: public ymfm::ymfm_interface { + +}; + +class DivPlatformTX81Z: public DivDispatch { + protected: + struct Channel { + DivInstrumentFM state; + DivMacroInt std; + unsigned char freqH, freqL; + int freq, baseFreq, pitch, note; + unsigned char ins; + signed char konCycles; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, portaPause, furnacePCM, hardReset; + int vol, outVol; + unsigned char chVolL, chVolR; + Channel(): + freqH(0), + freqL(0), + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + portaPause(false), + furnacePCM(false), + hardReset(false), + vol(0), + outVol(0), + chVolL(127), + chVolR(127) {} + }; + Channel chan[8]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + bool addrOrVal; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} + }; + std::queue writes; + int delay, baseFreqOff; + int pcmL, pcmR, pcmCycles; + unsigned char lastBusy; + unsigned char amDepth, pmDepth; + + ymfm::ym2414* fm_ymfm; + ymfm::ym2414::output_data out_ymfm; + DivTXInterface iface; + + unsigned char regPool[330]; + + bool extMode; + + bool isMuted[8]; + + short oldWrites[330]; + short pendingWrites[330]; + + int octave(int freq); + int toFreq(int freq); + + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void notifyInsChange(int ins); + void setFlags(unsigned int flags); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformTX81Z(); +}; +#endif diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp new file mode 100644 index 000000000..77f5c8050 --- /dev/null +++ b/src/engine/platform/vera.cpp @@ -0,0 +1,426 @@ +/** + * 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 "vera.h" +#include "../engine.h" +#include +#include + +extern "C" { + #include "sound/vera_psg.h" + #include "sound/vera_pcm.h" +} + +#define rWrite(c,a,d) {regPool[(c)*4+(a)]=(d); psg_writereg(psg,((c)*4+(a)),(d));} +#define rWriteLo(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0x3f))|((d)&0x3f)) +#define rWriteHi(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0xc0))|(((d)<<6)&0xc0)) +#define rWritePCMCtrl(d) {regPool[64]=(d); pcm_write_ctrl(pcm,d);} +#define rWritePCMRate(d) {regPool[65]=(d); pcm_write_rate(pcm,d);} +#define rWritePCMData(d) {regPool[66]=(d); pcm_write_fifo(pcm,d);} +#define rWritePCMVol(d) rWritePCMCtrl((regPool[64]&(~0x3f))|((d)&0x3f)) + +const char* regCheatSheetVERA[]={ + "CHxFreq", "00+x*4", + "CHxVol", "02+x*4", + "CHxWave", "03+x*4", + + "AUDIO_CTRL", "40", + "AUDIO_RATE", "41", + "AUDIO_DATA", "42", + + NULL +}; + +const char** DivPlatformVERA::getRegisterSheet() { + return regCheatSheetVERA; +} + +const char* DivPlatformVERA::getEffectName(unsigned char effect) { + switch (effect) { + case 0x20: + return "20xx: Change waveform"; + break; + case 0x22: + return "22xx: Set duty cycle (0 to 3F)"; + break; + } + return NULL; +} + +void DivPlatformVERA::acquire(short* bufL, short* bufR, size_t start, size_t len) { + // both PSG part and PCM part output a full 16-bit range, putting bufL/R + // argument right into both could cause an overflow + short buf[4][128]; + size_t pos=start; + DivSample* s=parent->getSample(chan[16].pcm.sample); + while (len>0) { + if (s->samples>0) { + while (pcm_is_fifo_almost_empty(pcm)) { + short tmp_l=0; + short tmp_r=0; + if (!isMuted[16]) { + // TODO stereo samples once DivSample has a support for it + if (chan[16].pcm.depth16) { + tmp_l=s->data16[chan[16].pcm.pos]; + tmp_r=tmp_l; + } else { + tmp_l=s->data8[chan[16].pcm.pos]; + tmp_r=tmp_l; + } + if (!(chan[16].pan&1)) tmp_l=0; + if (!(chan[16].pan&2)) tmp_r=0; + } + if (chan[16].pcm.depth16) { + rWritePCMData(tmp_l&0xff); + rWritePCMData((tmp_l>>8)&0xff); + rWritePCMData(tmp_r&0xff); + rWritePCMData((tmp_r>>8)&0xff); + } else { + rWritePCMData(tmp_l&0xff); + 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; + } + } + } + } else { + // just let the buffer run out + chan[16].pcm.sample=-1; + } + int curLen=MIN(len,128); + memset(buf,0,sizeof(buf)); + psg_render(psg,buf[0],buf[1],curLen); + pcm_render(pcm,buf[2],buf[3],curLen); + for (int i=0; icalcBaseFreq(chipClock,2097152,note,false); + } else { + double off=1.0; + if (chan[ch].pcm.sample>=0 && chan[ch].pcm.samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[ch].pcm.sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=s->centerRate/8363.0; + } + } + return (int)(off*parent->calcBaseFreq(chipClock,65536,note,false)); + } +} + +void DivPlatformVERA::tick() { + for (int i=0; i<16; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=MAX(chan[i].vol+chan[i].std.vol.val-63,0); + rWriteLo(i,2,chan[i].outVol); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=calcNoteFreq(0,chan[i].std.arp.val); + } else { + chan[i].baseFreq=calcNoteFreq(0,chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=calcNoteFreq(0,chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.duty.had) { + rWriteLo(i,3,chan[i].std.duty.val); + } + if (chan[i].std.wave.had) { + rWriteHi(i,3,chan[i].std.wave.val); + } + if (chan[i].freqChanged) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,8); + if (chan[i].freq>65535) chan[i].freq=65535; + rWrite(i,0,chan[i].freq&0xff); + rWrite(i,1,(chan[i].freq>>8)&0xff); + chan[i].freqChanged=false; + } + } + // PCM + chan[16].std.next(); + if (chan[16].std.vol.had) { + chan[16].outVol=MAX(chan[16].vol+MIN(chan[16].std.vol.val/4,15)-15,0); + rWritePCMVol(chan[16].outVol&15); + } + if (chan[16].std.arp.had) { + if (!chan[16].inPorta) { + if (chan[16].std.arp.mode) { + chan[16].baseFreq=calcNoteFreq(16,chan[16].std.arp.val); + } else { + chan[16].baseFreq=calcNoteFreq(16,chan[16].note+chan[16].std.arp.val); + } + } + chan[16].freqChanged=true; + } else { + if (chan[16].std.arp.mode && chan[16].std.arp.finished) { + chan[16].baseFreq=calcNoteFreq(16,chan[16].note); + chan[16].freqChanged=true; + } + } + if (chan[16].freqChanged) { + chan[16].freq=parent->calcFreq(chan[16].baseFreq,chan[16].pitch,false,8); + if (chan[16].freq>128) chan[16].freq=128; + rWritePCMRate(chan[16].freq&0xff); + chan[16].freqChanged=false; + } +} + +int DivPlatformVERA::dispatch(DivCommand c) { + int tmp; + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if(c.chan<16) { + rWriteLo(c.chan,2,chan[c.chan].vol) + } else { + chan[16].pcm.sample=parent->getIns(chan[16].ins)->amiga.initSample; + if (chan[16].pcm.sample<0 || chan[16].pcm.sample>=parent->song.sampleLen) { + chan[16].pcm.sample=-1; + } + 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) { + chan[16].pcm.depth16=true; + ctrl|=0x20; + } else { + chan[16].pcm.depth16=false; + if (s->depth!=8) chan[16].pcm.sample=-1; + } + rWritePCMCtrl(ctrl); + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=calcNoteFreq(c.chan,c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + if(c.chan<16) { + rWriteLo(c.chan,2,0) + } else { + chan[16].pcm.sample=-1; + rWritePCMCtrl(0x80); + rWritePCMRate(0); + } + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + chan[c.chan].ins=(unsigned char)c.value; + break; + case DIV_CMD_VOLUME: + if (c.chan<16) { + tmp=c.value&0x3f; + chan[c.chan].vol=tmp; + rWriteLo(c.chan,2,tmp); + } else { + tmp=c.value&0x0f; + chan[c.chan].vol=tmp; + rWritePCMVol(tmp); + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=calcNoteFreq(c.chan,c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=calcNoteFreq(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; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_STD_NOISE_MODE: + if (c.chan<16) rWriteLo(c.chan,3,c.value); + break; + case DIV_CMD_WAVE: + if (c.chan<16) rWriteHi(c.chan,3,c.value); + break; + case DIV_CMD_PANNING: { + tmp=0; + tmp|=(c.value&0x10)?1:0; + tmp|=(c.value&0x01)?2:0; + chan[c.chan].pan=tmp&3; + if (c.chan<16) { + rWriteHi(c.chan,2,isMuted[c.chan]?0:chan[c.chan].pan); + } + break; + } + case DIV_CMD_GET_VOLMAX: + if(c.chan<16) { + return 63; + } else { + return 15; + } + break; + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + default: + break; + } + return 1; +} + +void* DivPlatformVERA::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformVERA::getRegisterPool() { + return regPool; +} + +int DivPlatformVERA::getRegisterPoolSize() { + return 67; +} + +void DivPlatformVERA::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (ch<16) { + rWriteHi(ch,2,mute?0:chan[ch].pan); + } +} + +bool DivPlatformVERA::isStereo() { + return true; +} + +void DivPlatformVERA::notifyInsDeletion(void* ins) { + for (int i=0; i<2; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVERA::poke(unsigned int addr, unsigned short val) { + switch (addr) { + case 64: + rWritePCMCtrl((unsigned char)val); + break; + case 65: + rWritePCMRate((unsigned char)val); + break; + case 66: + rWritePCMData((unsigned char)val); + break; + default: + rWrite(0,addr,(unsigned char)val); + break; + } +} + +void DivPlatformVERA::poke(std::vector& wlist) { + for (auto &i: wlist) poke(i.addr,i.val); +} + +int DivPlatformVERA::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + for (int i=0; i<17; i++) { + isMuted[i]=false; + } + parent=p; + psg=new struct VERA_PSG; + pcm=new struct VERA_PCM; + dumpWrites=false; + skipRegisterWrites=false; + chipClock=25000000; + rate=chipClock/512; + reset(); + return 17; +} + +void DivPlatformVERA::quit() { + delete psg; + delete pcm; +} +DivPlatformVERA::~DivPlatformVERA() { +} diff --git a/src/engine/platform/vera.h b/src/engine/platform/vera.h new file mode 100644 index 000000000..a3773cccb --- /dev/null +++ b/src/engine/platform/vera.h @@ -0,0 +1,78 @@ +/** + * 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 _VERA_H +#define _VERA_H +#include "../dispatch.h" +#include "../instrument.h" +#include "../macroInt.h" + +struct VERA_PSG; +struct VERA_PCM; + +class DivPlatformVERA: public DivDispatch { + protected: + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins, pan; + bool active, freqChanged, inPorta; + int vol, outVol; + unsigned accum; + int noiseval; + DivMacroInt std; + + struct PCMChannel { + int sample; + unsigned pos; + unsigned len; + unsigned char freq; + bool depth16; + PCMChannel(): sample(-1), pos(0), len(0), freq(0), depth16(false) {} + } pcm; + Channel(): freq(0), baseFreq(0), pitch(0), note(0), ins(-1), pan(0), active(false), freqChanged(false), inPorta(false), vol(0), outVol(0), accum(0), noiseval(0) {} + }; + Channel chan[17]; + bool isMuted[17]; + unsigned char regPool[67]; + struct VERA_PSG* psg; + struct VERA_PCM* pcm; + + int calcNoteFreq(int ch, int note); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void tick(); + void muteChannel(int ch, bool mute); + void notifyInsDeletion(void* ins); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformVERA(); +}; +#endif diff --git a/src/engine/platform/vic20.cpp b/src/engine/platform/vic20.cpp new file mode 100644 index 000000000..b45fecfc3 --- /dev/null +++ b/src/engine/platform/vic20.cpp @@ -0,0 +1,335 @@ +/** + * 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 "vic20.h" +#include "../engine.h" +#include + +#define rWrite(a,v) {regPool[(a)]=(v)&0xff; vic_sound_machine_store(vic,a,(v)&0xff);} + +#define CHIP_DIVIDER 32 +#define SAMP_DIVIDER 4 + +const char* regCheatSheetVIC[]={ + "CH1_Pitch", "0A", + "CH2_Pitch", "0B", + "CH3_Pitch", "0C", + "Noise_Pitch", "0D", + "Volume", "0E", + NULL +}; + +const char** DivPlatformVIC20::getRegisterSheet() { + return regCheatSheetVIC; +} + +const char* DivPlatformVIC20::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + } + return NULL; +} + +void DivPlatformVIC20::acquire(short* bufL, short* bufR, size_t start, size_t len) { + const unsigned char loadFreq[3] = {0x7e, 0x7d, 0x7b}; + const unsigned char wavePatterns[16] = { + 0b0, 0b10, 0b100, 0b110, 0b1000, 0b1010, 0b1011, 0b1110, + 0b10010, 0b10100, 0b10110, 0b11000, 0b11010, 0b100100, 0b101010, 0b101100 + }; + for (size_t h=start; h=0) { + if (chan[i].waveWriteCycle>=16*7) { + // empty shift register first + rWrite(10+i,126); + } else if (chan[i].waveWriteCycle>=16) { + unsigned bit=8-(chan[i].waveWriteCycle/16); + rWrite(10+i,loadFreq[i]|((wavePatterns[chan[i].wave]<calcFreq(chan[i].baseFreq,chan[i].pitch,true); + if (i<3) { + chan[i].freq>>=(2-i); + } else { + chan[i].freq>>=1; + } + if (chan[i].freq<1) chan[i].freq=1; + if (chan[i].freq>127) chan[i].freq=0; + if (isMuted[i]) chan[i].keyOn=false; + if (chan[i].keyOn) { + if (i<3) { + // 128*16 cycles for lowest channel to finish counting at lowest freq + // 2*16 cycles for lowest channel to empty first 2 bits + // 7*16 cycles to write 7 bits + chan[i].waveWriteCycle=137*16-1; + hasWaveWrite=true; + } else { + rWrite(10+i,255-chan[i].freq); + } + chan[i].keyOn=false; + } else if (chan[i].freqChanged && chan[i].active && !isMuted[i]) { + rWrite(10+i,255-chan[i].freq); + } + if (chan[i].keyOff) { + rWrite(10+i,0); + chan[i].keyOff=false; + } + chan[i].freqChanged=false; + } + } +} + +int DivPlatformVIC20::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(ins); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.had) { + calcAndWriteOutVol(c.chan,15); + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value&0x0f; + chan[c.chan].keyOn=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + 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))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformVIC20::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (mute) { + chan[ch].keyOff=true; + } else if (chan[ch].active) { + chan[ch].keyOn=true; + } +} + +void DivPlatformVIC20::forceIns() { + for (int i=0; i<4; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + writeOutVol(i); + } +} + +void* DivPlatformVIC20::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformVIC20::getRegisterPool() { + return regPool; +} + +int DivPlatformVIC20::getRegisterPoolSize() { + return 16; +} + +void DivPlatformVIC20::reset() { + memset(regPool,0,16); + for (int i=0; i<4; i++) { + chan[i]=Channel(); + } + vic_sound_machine_init(vic,rate,chipClock); + hasWaveWrite=false; + rWrite(14,15); + // hack: starting noise channel right away after this would result in a dead + // channel as the LFSR state is 0, so clock it a bit + vic_sound_clock(vic,4); +} + +bool DivPlatformVIC20::isStereo() { + return false; +} + +void DivPlatformVIC20::notifyInsDeletion(void* ins) { + for (int i=0; i<4; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVIC20::setFlags(unsigned int flags) { + if (flags&1) { + chipClock=COLOR_PAL/4.0; + } else { + chipClock=COLOR_NTSC*2.0/7.0; + } + rate=chipClock/4; +} + +void DivPlatformVIC20::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformVIC20::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformVIC20::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<4; i++) { + isMuted[i]=false; + } + setFlags(flags); + vic=new sound_vic20_t(); + reset(); + return 4; +} + +void DivPlatformVIC20::quit() { + delete vic; +} + +DivPlatformVIC20::~DivPlatformVIC20() { +} diff --git a/src/engine/platform/vic20.h b/src/engine/platform/vic20.h new file mode 100644 index 000000000..1c4d384b7 --- /dev/null +++ b/src/engine/platform/vic20.h @@ -0,0 +1,86 @@ +/** + * 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 _VIC20_H +#define _VIC20_H + +#include "../dispatch.h" +#include "../macroInt.h" +#include "sound/vic20sound.h" +#include + +class DivPlatformVIC20: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + unsigned char ins, pan; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta; + int vol, outVol, wave, waveWriteCycle; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + pan(255), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + vol(15), + outVol(15), + wave(0), + waveWriteCycle(-1) {} + }; + Channel chan[4]; + bool isMuted[4]; + bool hasWaveWrite; + + unsigned char regPool[16]; + sound_vic20_t* vic; + void updateWave(int ch); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + bool isStereo(); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformVIC20(); + private: + void calcAndWriteOutVol(int ch, int env); + void writeOutVol(int ch); +}; + +#endif diff --git a/src/engine/platform/vrc6.cpp b/src/engine/platform/vrc6.cpp new file mode 100644 index 000000000..31694a62c --- /dev/null +++ b/src/engine/platform/vrc6.cpp @@ -0,0 +1,496 @@ +/** + * 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 "vrc6.h" +#include "../engine.h" +#include +#include + +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define chWrite(c,a,v) rWrite(0x9000+(c<<12)+(a&3),v) + +const char* regCheatSheetVRC6[]={ + "S0DutyVol", "9000", + "S0PeriodL", "9001", + "S0PeriodH", "9002", + "GlobalCtl", "9003", + "S1DutyVol", "A000", + "S1PeriodL", "A001", + "S1PeriodH", "A002", + "SawVolume", "B000", + "SawPeriodL", "B001", + "SawPeriodH", "B002", + "TimerLatch", "F000", + "TimerCtl", "F001", + "IRQAck", "F002", + NULL +}; + +const char** DivPlatformVRC6::getRegisterSheet() { + return regCheatSheetVRC6; +} + +const char* DivPlatformVRC6::getEffectName(unsigned char effect) { + switch (effect) { + case 0x12: + return "12xx: Set duty cycle (pulse: 0 to 7)"; + break; + case 0x17: + return "17xx: Toggle PCM mode (pulse channel)"; + break; + } + return NULL; +} + +void DivPlatformVRC6::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; irate) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->samples<=0) { + chan[i].dacSample=-1; + chWrite(i,0,0); + continue; + } + unsigned char dacData=(((unsigned char)s->data8[chan[i].dacPos]^0x80)>>4); + chan[i].dacOut=MAX(0,MIN(15,(dacData*chan[i].outVol)>>4)); + if (!isMuted[i]) { + 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); + } + } + chan[i].dacPeriod-=rate; + } + } + } + + // VRC6 part + vrc6.tick(); + int sample=vrc6.out()<<9; // scale to 16 bit + if (sample>32767) sample=32767; + if (sample<-32768) sample=-32768; + bufL[i]=bufR[i]=sample; + + // Command part + while (!writes.empty()) { + QueuedWrite w=writes.front(); + switch (w.addr&0xf000) { + case 0x9000: // Pulse 1 + if (w.addr<=0x9003) { + if (w.addr==0x9003) { + vrc6.control_w(w.val); + } else if (w.addr<=0x9002) { + vrc6.pulse_w(0,w.addr&3,w.val); + } + regPool[w.addr-0x9000]=w.val; + } + break; + case 0xa000: // Pulse 2 + if (w.addr<=0xa002) { + vrc6.pulse_w(1,w.addr&3,w.val); + regPool[(w.addr-0xa000)+4]=w.val; + } + break; + case 0xb000: // Sawtooth + if (w.addr<=0xb002) { + vrc6.saw_w(w.addr&3,w.val); + regPool[(w.addr-0xb000)+7]=w.val; + } + break; + case 0xf000: // IRQ/Timer + if (w.addr<=0xf002) { + vrc6.timer_w(w.addr&3,w.val); + regPool[(w.addr-0xf000)+10]=w.val; + } + break; + } + writes.pop(); + } + } +} + +void DivPlatformVRC6::tick() { + for (int i=0; i<3; i++) { + // 16 for pulse; 14 for saw + int CHIP_DIVIDER=(i==2)?14:16; + chan[i].std.next(); + if (chan[i].std.vol.had) { + if (i==2) { // sawtooth + chan[i].outVol=((chan[i].vol&63)*MIN(63,chan[i].std.vol.val))/63; + if (chan[i].outVol<0) chan[i].outVol=0; + if (chan[i].outVol>63) chan[i].outVol=63; + if (!isMuted[i]) { + chWrite(i,0,chan[i].outVol); + } + } else { // pulse + chan[i].outVol=((chan[i].vol&15)*MIN(15,chan[i].std.vol.val))/15; + if (chan[i].outVol<0) chan[i].outVol=0; + if (chan[i].outVol>15) chan[i].outVol=15; + if ((!isMuted[i]) && (!chan[i].pcm)) { + chWrite(i,0,(chan[i].outVol&0xf)|((chan[i].duty&7)<<4)); + } + } + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + if ((!isMuted[i]) && (i!=2) && (!chan[i].pcm)) { // pulse + chWrite(i,0,(chan[i].outVol&0xf)|((chan[i].duty&7)<<4)); + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + if (i==2) { // sawtooth + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; + } else { // pulse + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true)-1; + if (chan[i].furnaceDac) { + double off=1.0; + if (chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + chan[i].dacRate=((double)chipClock)/MAX(1,off*chan[i].freq); + if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dacRate); + } + } + if (chan[i].freq>4095) chan[i].freq=4095; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].keyOff) { + chWrite(i,2,0); + } else { + chWrite(i,1,chan[i].freq&0xff); + chWrite(i,2,0x80|((chan[i].freq>>8)&0xf)); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformVRC6::dispatch(DivCommand c) { + int CHIP_DIVIDER=(c.chan==2)?14:16; + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if (c.chan!=2) { // pulse wave + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].pcm=true; + } else if (chan[c.chan].furnaceDac) { + chan[c.chan].pcm=false; + } + if (chan[c.chan].pcm) { + if (skipRegisterWrites) break; + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].dacSample=ins->amiga.initSample; + if (chan[c.chan].dacSample<0 || chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) { + chWrite(c.chan,2,0x80); + chWrite(c.chan,0,isMuted[c.chan]?0:0x80); + addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].std.init(ins); + //chan[c.chan].keyOn=true; + chan[c.chan].furnaceDac=true; + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + } + chan[c.chan].dacSample=12*sampleBank+chan[c.chan].note%12; + if (chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + chan[c.chan].dacRate=parent->getSample(chan[c.chan].dacSample)->rate; + if (dumpWrites) { + chWrite(c.chan,2,0x80); + chWrite(c.chan,0,isMuted[c.chan]?0:0x80); + addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate); + } + chan[c.chan].furnaceDac=false; + } + break; + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + if (!isMuted[c.chan]) { + if (c.chan==2) { // sawtooth + chWrite(c.chan,0,chan[c.chan].vol); + } else if (!chan[c.chan].pcm) { + chWrite(c.chan,0,(chan[c.chan].vol&0xf)|((chan[c.chan].duty&7)<<4)); + } + } + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + chan[c.chan].pcm=false; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + if (!isMuted[c.chan]) { + if (chan[c.chan].active) { + if (c.chan==2) { // sawtooth + chWrite(c.chan,0,chan[c.chan].vol); + } else if (!chan[c.chan].pcm) { + chWrite(c.chan,0,(chan[c.chan].vol&0xf)|((chan[c.chan].duty&7)<<4)); + } + } + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + if ((c.chan!=2) && (!chan[c.chan].pcm)) { // pulse + chan[c.chan].duty=c.value; + } + break; + case DIV_CMD_SAMPLE_MODE: + if (c.chan!=2) { // pulse + chan[c.chan].pcm=c.value; + } + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + 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))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + if (c.chan==2) return 63; // sawtooth has 6 bit volume + return 15; // pulse has 4 bit volume + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformVRC6::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (isMuted[ch]) { + chWrite(ch,0,0); + } else if (chan[ch].active) { + if (ch==2) { // sawtooth + chWrite(ch,0,chan[ch].outVol); + } else { + chWrite(ch,0,chan[ch].pcm?chan[ch].dacOut:((chan[ch].outVol&0xf)|((chan[ch].duty&7)<<4))); + } + } +} + +void DivPlatformVRC6::forceIns() { + for (int i=0; i<3; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + } +} + +void* DivPlatformVRC6::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformVRC6::getRegisterPool() { + return regPool; +} + +int DivPlatformVRC6::getRegisterPoolSize() { + return 13; +} + +void DivPlatformVRC6::reset() { + for (int i=0; i<3; i++) { + chan[i]=DivPlatformVRC6::Channel(); + } + // a poll may be necessary to decide the default + chan[2].vol=30; + chan[2].outVol=30; + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + sampleBank=0; + + vrc6.reset(); + // Initialize control register + rWrite(0x9003,0); + // Clear internal IRQ + rWrite(0xf000,0); + rWrite(0xf001,0); + rWrite(0xf002,0); +} + +bool DivPlatformVRC6::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformVRC6::setFlags(unsigned int flags) { + if (flags==2) { // Dendy + rate=COLOR_PAL*2.0/5.0; + } else if (flags==1) { // PAL + rate=COLOR_PAL*3.0/8.0; + } else { // NTSC + rate=COLOR_NTSC/2.0; + } + chipClock=rate; +} + +void DivPlatformVRC6::notifyInsDeletion(void* ins) { + for (int i=0; i<3; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVRC6::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformVRC6::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformVRC6::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<3; i++) { + isMuted[i]=false; + } + setFlags(flags); + reset(); + return 3; +} + +void DivPlatformVRC6::quit() { +} + +DivPlatformVRC6::~DivPlatformVRC6() { +} diff --git a/src/engine/platform/vrc6.h b/src/engine/platform/vrc6.h new file mode 100644 index 000000000..05cd89415 --- /dev/null +++ b/src/engine/platform/vrc6.h @@ -0,0 +1,100 @@ +/** + * 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 _VRC6_H +#define _VRC6_H + +#include +#include "../dispatch.h" +#include "../macroInt.h" +#include "sound/vrcvi/vrcvi.hpp" + + +class DivPlatformVRC6: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + int dacPeriod, dacRate, dacOut; + unsigned int dacPos; + int dacSample; + unsigned char ins, duty; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, pcm, furnaceDac; + signed char vol, outVol; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + dacPeriod(0), + dacRate(0), + dacOut(0), + dacPos(0), + dacSample(-1), + ins(-1), + duty(0), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + pcm(false), + furnaceDac(false), + vol(15), + outVol(15) {} + }; + Channel chan[3]; + bool isMuted[3]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v) {} + }; + std::queue writes; + unsigned char sampleBank; + vrcvi_intf intf; + vrcvi_core vrc6; + unsigned char regPool[13]; + + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + DivPlatformVRC6() : vrc6(intf) {}; + ~DivPlatformVRC6(); +}; + +#endif diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp new file mode 100644 index 000000000..124e0ace6 --- /dev/null +++ b/src/engine/platform/x1_010.cpp @@ -0,0 +1,901 @@ +/** + * 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 "x1_010.h" +#include "../engine.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) { x1_010->ram_w(a,v); if (dumpWrites) { addWrite(a,v); } } + +#define chRead(c,a) x1_010->ram_r((c<<3)|(a&7)) +#define chWrite(c,a,v) rWrite((c<<3)|(a&7),v) +#define waveWrite(c,a,v) rWrite(0x1000|(chan[c].waveBank<<11)|(c<<7)|(a&0x7f),(v-128)&0xff) +#define envFill(c,a) rWrite(0x800|(c<<7)|(a&0x7f),(chan[c].lvol<<4)|chan[c].rvol) +#define envWrite(c,a,l,r) rWrite(0x800|(c<<7)|(a&0x7f),(((chan[c].lvol*(l))/15)<<4)|((chan[c].rvol*(r))/15)) + +#define refreshControl(c) chWrite(c,0,chan[c].active?(chan[c].pcm?1:((chan[c].env.flag.envEnable && chan[c].env.flag.envOneshot)?7:3)):0); + +#define CHIP_FREQBASE 4194304 + +const char* regCheatSheetX1_010[]={ + // Channel registers + "Ch00_Control", "0000", + "Ch00_PCMVol_WavSel", "0001", + "Ch00_FreqL", "0002", + "Ch00_FreqH", "0003", + "Ch00_Start_EnvFrq", "0004", + "Ch00_End_EnvSel", "0005", + "Ch01_Control", "0008", + "Ch01_PCMVol_WavSel", "0009", + "Ch01_FreqL", "000A", + "Ch01_FreqH", "000B", + "Ch01_Start_EnvFrq", "000C", + "Ch01_End_EnvSel", "000D", + "Ch02_Control", "0010", + "Ch02_PCMVol_WavSel", "0011", + "Ch02_FreqL", "0012", + "Ch02_FreqH", "0013", + "Ch02_Start_EnvFrq", "0014", + "Ch02_End_EnvSel", "0015", + "Ch03_Control", "0018", + "Ch03_PCMVol_WavSel", "0019", + "Ch03_FreqL", "001A", + "Ch03_FreqH", "001B", + "Ch03_Start_EnvFrq", "001C", + "Ch03_End_EnvSel", "001D", + "Ch04_Control", "0020", + "Ch04_PCMVol_WavSel", "0021", + "Ch04_FreqL", "0022", + "Ch04_FreqH", "0023", + "Ch04_Start_EnvFrq", "0024", + "Ch04_End_EnvSel", "0025", + "Ch05_Control", "0028", + "Ch05_PCMVol_WavSel", "0029", + "Ch05_FreqL", "002A", + "Ch05_FreqH", "002B", + "Ch05_Start_EnvFrq", "002C", + "Ch05_End_EnvSel", "002D", + "Ch06_Control", "0030", + "Ch06_PCMVol_WavSel", "0031", + "Ch06_FreqL", "0032", + "Ch06_FreqH", "0033", + "Ch06_Start_EnvFrq", "0034", + "Ch06_End_EnvSel", "0035", + "Ch07_Control", "0038", + "Ch07_PCMVol_WavSel", "0039", + "Ch07_FreqL", "003A", + "Ch07_FreqH", "003B", + "Ch07_Start_EnvFrq", "003C", + "Ch07_End_EnvSel", "003D", + "Ch08_Control", "0040", + "Ch08_PCMVol_WavSel", "0041", + "Ch08_FreqL", "0042", + "Ch08_FreqH", "0043", + "Ch08_Start_EnvFrq", "0044", + "Ch08_End_EnvSel", "0045", + "Ch09_Control", "0048", + "Ch09_PCMVol_WavSel", "0049", + "Ch09_FreqL", "004A", + "Ch09_FreqH", "004B", + "Ch09_Start_EnvFrq", "004C", + "Ch09_End_EnvSel", "004D", + "Ch10_Control", "0050", + "Ch10_PCMVol_WavSel", "0051", + "Ch10_FreqL", "0052", + "Ch10_FreqH", "0053", + "Ch10_Start_EnvFrq", "0054", + "Ch10_End_EnvSel", "0055", + "Ch11_Control", "0058", + "Ch11_PCMVol_WavSel", "0059", + "Ch11_FreqL", "005A", + "Ch11_FreqH", "005B", + "Ch11_Start_EnvFrq", "005C", + "Ch11_End_EnvSel", "005D", + "Ch12_Control", "0060", + "Ch12_PCMVol_WavSel", "0061", + "Ch12_FreqL", "0062", + "Ch12_FreqH", "0063", + "Ch12_Start_EnvFrq", "0064", + "Ch12_End_EnvSel", "0065", + "Ch13_Control", "0068", + "Ch13_PCMVol_WavSel", "0069", + "Ch13_FreqL", "006A", + "Ch13_FreqH", "006B", + "Ch13_Start_EnvFrq", "006C", + "Ch13_End_EnvSel", "006D", + "Ch14_Control", "0070", + "Ch14_PCMVol_WavSel", "0071", + "Ch14_FreqL", "0072", + "Ch14_FreqH", "0073", + "Ch14_Start_EnvFrq", "0074", + "Ch14_End_EnvSel", "0075", + "Ch15_Control", "0078", + "Ch15_PCMVol_WavSel", "0079", + "Ch15_FreqL", "007A", + "Ch15_FreqH", "007B", + "Ch15_Start_EnvFrq", "007C", + "Ch15_End_EnvSel", "007D", + // Envelope data + "Env01Data", "0080", + "Env02Data", "0100", + "Env03Data", "0180", + "Env04Data", "0200", + "Env05Data", "0280", + "Env06Data", "0300", + "Env07Data", "0380", + "Env08Data", "0400", + "Env09Data", "0480", + "Env10Data", "0500", + "Env11Data", "0580", + "Env12Data", "0600", + "Env13Data", "0680", + "Env14Data", "0700", + "Env15Data", "0780", + "Env16Data", "0800", + "Env17Data", "0880", + "Env18Data", "0900", + "Env19Data", "0980", + "Env20Data", "0A00", + "Env21Data", "0A80", + "Env22Data", "0B00", + "Env23Data", "0B80", + "Env24Data", "0C00", + "Env25Data", "0C80", + "Env26Data", "0D00", + "Env27Data", "0D80", + "Env28Data", "0E00", + "Env29Data", "0E80", + "Env30Data", "0F00", + "Env31Data", "0F80", + // Wavetable data + "Wave00Data", "1000", + "Wave01Data", "1080", + "Wave02Data", "1100", + "Wave03Data", "1180", + "Wave04Data", "1200", + "Wave05Data", "1280", + "Wave06Data", "1300", + "Wave07Data", "1380", + "Wave08Data", "1400", + "Wave09Data", "1480", + "Wave10Data", "1500", + "Wave11Data", "1580", + "Wave12Data", "1600", + "Wave13Data", "1680", + "Wave14Data", "1700", + "Wave15Data", "1780", + "Wave16Data", "1800", + "Wave17Data", "1880", + "Wave18Data", "1900", + "Wave19Data", "1980", + "Wave20Data", "1A00", + "Wave21Data", "1A80", + "Wave22Data", "1B00", + "Wave23Data", "1B80", + "Wave24Data", "1C00", + "Wave25Data", "1C80", + "Wave26Data", "1D00", + "Wave27Data", "1D80", + "Wave28Data", "1E00", + "Wave29Data", "1E80", + "Wave30Data", "1F00", + "Wave31Data", "1F80", + NULL +}; + +const char** DivPlatformX1_010::getRegisterSheet() { + return regCheatSheetX1_010; +} + +const char* DivPlatformX1_010::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Change waveform"; + break; + case 0x11: + return "11xx: Change envelope shape"; + break; + case 0x17: + return "17xx: Toggle PCM mode"; + break; + case 0x20: + return "20xx: Set PCM frequency (1 to FF)"; + break; + case 0x22: + return "22xx: Set envelope mode (bit 0: enable, bit 1: one-shot, bit 2: split shape to L/R, bit 3/5: H.invert right/left, bit 4/6: V.invert right/left)"; + break; + case 0x23: + return "23xx: Set envelope period"; + break; + case 0x25: + return "25xx: Envelope slide up"; + break; + case 0x26: + return "26xx: Envelope slide down"; + break; + case 0x29: + return "29xy: Set auto-envelope (x: numerator; y: denominator)"; + break; + } + return NULL; +} + +void DivPlatformX1_010::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; htick(); + + signed int tempL=x1_010->output(0); + signed int tempR=x1_010->output(1); + + if (tempL<-32768) tempL=-32768; + if (tempL>32767) tempL=32767; + if (tempR<-32768) tempR=-32768; + if (tempR>32767) tempR=32767; + + //printf("tempL: %d tempR: %d\n",tempL,tempR); + bufL[h]=stereo?tempL:((tempL+tempR)>>1); + bufR[h]=stereo?tempR:bufL[h]; + } +} + +double DivPlatformX1_010::NoteX1_010(int ch, int note) { + if (chan[ch].pcm) { // PCM note + double off=1.0; + int sample=chan[ch].sample; + if (sample>=0 && samplesong.sampleLen) { + DivSample* s=parent->getSample(sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=s->centerRate/8363.0; + } + } + return off*parent->calcBaseFreq(chipClock,8192,note,false); + } + // Wavetable note + return NOTE_FREQUENCY(note); +} + +void DivPlatformX1_010::updateWave(int ch) { + if (chan[ch].active) { + chan[ch].waveBank^=1; + } + for (int i=0; i<128; i++) { + int data=chan[ch].ws.output[i]; + waveWrite(ch,i,data); + } + if (!chan[ch].pcm) { + chWrite(ch,1,(chan[ch].waveBank<<4)|(ch&0xf)); + } +} + +void DivPlatformX1_010::updateEnvelope(int ch) { + if (!chan[ch].pcm) { + if (isMuted[ch]) { + for (int i=0; i<128; i++) { + rWrite(0x800|(ch<<7)|(i&0x7f),0); + } + } else { + if (!chan[ch].env.flag.envEnable) { + for (int i=0; i<128; i++) { + envFill(ch,i); + } + } else { + DivWavetable* wt=parent->getWave(chan[ch].env.shape); + for (int i=0; i<128; i++) { + if (wt->max<1 || wt->len<1) { + envFill(ch,i); + } else if (chan[ch].env.flag.envSplit || chan[ch].env.flag.envHinvR || chan[ch].env.flag.envVinvR || chan[ch].env.flag.envHinvL || chan[ch].env.flag.envVinvL) { // Stereo config + int la=i,ra=i; + int lo,ro; + if (chan[ch].env.flag.envHinvR) { ra=127-i; } // horizontal invert right envelope + if (chan[ch].env.flag.envHinvL) { la=127-i; } // horizontal invert left envelope + if (chan[ch].env.flag.envSplit) { // Split shape to left and right half + lo=wt->data[la*(wt->len/128/2)]*15/wt->max; + ro=wt->data[(ra+128)*(wt->len/128/2)]*15/wt->max; + } else { + lo=wt->data[la*wt->len/128]*15/wt->max; + ro=wt->data[ra*wt->len/128]*15/wt->max; + } + if (chan[ch].env.flag.envVinvR) { ro=15-ro; } // vertical invert right envelope + if (chan[ch].env.flag.envVinvL) { lo=15-lo; } // vertical invert left envelope + if (lo<0) lo=0; + if (lo>15) lo=15; + if (ro<0) ro=0; + if (ro>15) ro=15; + envWrite(ch,i,lo,ro); + } else { + int out=wt->data[i*wt->len/128]*15/wt->max; + if (out<0) out=0; + if (out>15) out=15; + envWrite(ch,i,out,out); + } + } + } + } + chWrite(ch,5,0x10|(ch&0xf)); + } else { + chWrite(ch,1,isMuted[ch]?0:((chan[ch].lvol<<4)|chan[ch].rvol)); + } +} + +void DivPlatformX1_010::tick() { + for (int i=0; i<16; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + signed char macroVol=((chan[i].vol&15)*MIN(chan[i].furnacePCM?64:15,chan[i].std.vol.val))/(chan[i].furnacePCM?64:15); + if ((!isMuted[i]) && (macroVol!=chan[i].outVol)) { + chan[i].outVol=macroVol; + chan[i].envChanged=true; + } + } + if ((!chan[i].pcm) || chan[i].furnacePCM) { + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NoteX1_010(i,chan[i].std.arp.val); + } else { + chan[i].baseFreq=NoteX1_010(i,chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NoteX1_010(i,chan[i].note); + chan[i].freqChanged=true; + } + } + } + if (chan[i].std.wave.had && !chan[i].pcm) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + if (!chan[i].pcm) { + chan[i].ws.changeWave1(chan[i].wave); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + } + if (chan[i].std.ex1.had) { + bool nextEnable=(chan[i].std.ex1.val&1); + if (nextEnable!=(chan[i].env.flag.envEnable)) { + chan[i].env.flag.envEnable=nextEnable; + if (!chan[i].pcm) { + if (!isMuted[i]) { + chan[i].envChanged=true; + } + refreshControl(i); + } + } + bool nextOneshot=(chan[i].std.ex1.val&2); + if (nextOneshot!=(chan[i].env.flag.envOneshot)) { + chan[i].env.flag.envOneshot=nextOneshot; + if (!chan[i].pcm) { + refreshControl(i); + } + } + bool nextSplit=(chan[i].std.ex1.val&4); + if (nextSplit!=(chan[i].env.flag.envSplit)) { + chan[i].env.flag.envSplit=nextSplit; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextHinvR=(chan[i].std.ex1.val&8); + if (nextHinvR!=(chan[i].env.flag.envHinvR)) { + chan[i].env.flag.envHinvR=nextHinvR; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextVinvR=(chan[i].std.ex1.val&16); + if (nextVinvR!=(chan[i].env.flag.envVinvR)) { + chan[i].env.flag.envVinvR=nextVinvR; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextHinvL=(chan[i].std.ex1.val&32); + if (nextHinvL!=(chan[i].env.flag.envHinvL)) { + chan[i].env.flag.envHinvL=nextHinvL; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + bool nextVinvL=(chan[i].std.ex1.val&64); + if (nextVinvL!=(chan[i].env.flag.envVinvL)) { + chan[i].env.flag.envVinvL=nextVinvL; + if (!isMuted[i] && !chan[i].pcm) { + chan[i].envChanged=true; + } + } + } + if (chan[i].std.ex2.had) { + if (chan[i].env.shape!=chan[i].std.ex2.val) { + chan[i].env.shape=chan[i].std.ex2.val; + if (!chan[i].pcm) { + if (chan[i].env.flag.envEnable && (!isMuted[i])) { + chan[i].envChanged=true; + } + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + } + if (chan[i].std.ex3.had) { + chan[i].autoEnvNum=chan[i].std.ex3.val; + if (!chan[i].pcm) { + chan[i].freqChanged=true; + if (!chan[i].std.alg.will) chan[i].autoEnvDen=1; + } + } + if (chan[i].std.alg.had) { + chan[i].autoEnvDen=chan[i].std.alg.val; + if (!chan[i].pcm) { + chan[i].freqChanged=true; + if (!chan[i].std.ex3.will) chan[i].autoEnvNum=1; + } + } + if (chan[i].active) { + if (chan[i].ws.tick()) { + updateWave(i); + } + } + if (chan[i].envChanged) { + chan[i].lvol=isMuted[i]?0:(((chan[i].outVol&0xf)*((chan[i].pan>>4)&0xf))/15); + chan[i].rvol=isMuted[i]?0:(((chan[i].outVol&0xf)*((chan[i].pan>>0)&0xf))/15); + updateEnvelope(i); + chan[i].envChanged=false; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false); + if (chan[i].pcm) { + if (chan[i].freq<1) chan[i].freq=1; + if (chan[i].freq>255) chan[i].freq=255; + chWrite(i,2,chan[i].freq&0xff); + } else { + if (chan[i].freq>65535) chan[i].freq=65535; + chWrite(i,2,chan[i].freq&0xff); + chWrite(i,3,(chan[i].freq>>8)&0xff); + if (chan[i].freqChanged && chan[i].autoEnvNum>0 && chan[i].autoEnvDen>0) { + chan[i].env.period=(chan[i].freq*chan[i].autoEnvDen/chan[i].autoEnvNum)>>12; + chWrite(i,4,chan[i].env.period); + } + } + if (chan[i].keyOn || chan[i].keyOff || (chRead(i,0)&1)) { + refreshControl(i); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + if (chan[i].env.slide!=0) { + chan[i].env.slidefrac+=chan[i].env.slide; + while (chan[i].env.slidefrac>0xf) { + chan[i].env.slidefrac-=0x10; + if (chan[i].env.period<0xff) { + chan[i].env.period++; + if (!chan[i].pcm) { + chWrite(i,4,chan[i].env.period); + } + } + } + while (chan[i].env.slidefrac<-0xf) { + chan[i].env.slidefrac+=0x10; + if (chan[i].env.period>0) { + chan[i].env.period--; + if (!chan[i].pcm) { + chWrite(i,4,chan[i].env.period); + } + } + } + } + } +} + +int DivPlatformX1_010::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + chWrite(c.chan,0,0); // reset previous note + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if ((ins->type==DIV_INS_AMIGA) || chan[c.chan].pcm) { + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].furnacePCM=true; + } else { + chan[c.chan].furnacePCM=false; + } + if (skipRegisterWrites) break; + if (chan[c.chan].furnacePCM) { + chan[c.chan].pcm=true; + chan[c.chan].std.init(ins); + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + chWrite(c.chan,4,(s->offX1_010>>12)&0xff); + int end=(s->offX1_010+s->length8+0xfff)&~0xfff; // padded + chWrite(c.chan,5,(0x100-(end>>12))&0xff); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + } else { + chan[c.chan].std.init(NULL); + chan[c.chan].outVol=chan[c.chan].vol; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + chWrite(c.chan,0,0); // reset + chWrite(c.chan,1,0); + chWrite(c.chan,2,0); + chWrite(c.chan,4,0); + chWrite(c.chan,5,0); + break; + } + } + } else { + chan[c.chan].std.init(NULL); + chan[c.chan].outVol=chan[c.chan].vol; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + chWrite(c.chan,0,0); // reset + chWrite(c.chan,1,0); + chWrite(c.chan,2,0); + chWrite(c.chan,4,0); + chWrite(c.chan,5,0); + break; + } + DivSample* s=parent->getSample(12*sampleBank+c.value%12); + chWrite(c.chan,4,(s->offX1_010>>12)&0xff); + int end=(s->offX1_010+s->length8+0xfff)&~0xfff; // padded + chWrite(c.chan,5,(0x100-(end>>12))&0xff); + chan[c.chan].baseFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); + chan[c.chan].freqChanged=true; + } + } else if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].envChanged=true; + chan[c.chan].std.init(ins); + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,128,255,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; + refreshControl(c.chan); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].pcm=false; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + if (chan[c.chan].outVol!=c.value) { + chan[c.chan].outVol=c.value; + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + chan[c.chan].keyOn=true; + break; + case DIV_CMD_X1_010_ENVELOPE_SHAPE: + if (chan[c.chan].env.shape!=c.value) { + chan[c.chan].env.shape=c.value; + if (!chan[c.chan].pcm) { + if (chan[c.chan].env.flag.envEnable && (!isMuted[c.chan])) { + chan[c.chan].envChanged=true; + } + chan[c.chan].keyOn=true; + } + } + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NoteX1_010(c.chan,c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_SAMPLE_MODE: + if (chan[c.chan].pcm!=(c.value&1)) { + chan[c.chan].pcm=c.value&1; + chan[c.chan].freqChanged=true; + chan[c.chan].envChanged=true; + } + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_PANNING: { + if (!stereo) break; + if (chan[c.chan].pan!=c.value) { + chan[c.chan].pan=c.value; + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + } + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note+((chan[c.chan].std.arp.will&&!chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_SAMPLE_FREQ: + if (chan[c.chan].pcm) { + chan[c.chan].freq=MAX(1,c.value&0xff); + chWrite(c.chan,2,chan[c.chan].freq&0xff); + if (chRead(c.chan,0)&1) { + refreshControl(c.chan); + } + } + break; + case DIV_CMD_X1_010_ENVELOPE_MODE: { + bool nextEnable=c.value&1; + if (nextEnable!=(chan[c.chan].env.flag.envEnable)) { + chan[c.chan].env.flag.envEnable=nextEnable; + if (!chan[c.chan].pcm) { + if (!isMuted[c.chan]) { + chan[c.chan].envChanged=true; + } + refreshControl(c.chan); + } + } + bool nextOneshot=c.value&2; + if (nextOneshot!=(chan[c.chan].env.flag.envOneshot)) { + chan[c.chan].env.flag.envOneshot=nextOneshot; + if (!chan[c.chan].pcm) { + refreshControl(c.chan); + } + } + bool nextSplit=c.value&4; + if (nextSplit!=(chan[c.chan].env.flag.envSplit)) { + chan[c.chan].env.flag.envSplit=nextSplit; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextHinvR=c.value&8; + if (nextHinvR!=(chan[c.chan].env.flag.envHinvR)) { + chan[c.chan].env.flag.envHinvR=nextHinvR; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextVinvR=c.value&16; + if (nextVinvR!=(chan[c.chan].env.flag.envVinvR)) { + chan[c.chan].env.flag.envVinvR=nextVinvR; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextHinvL=c.value&32; + if (nextHinvL!=(chan[c.chan].env.flag.envHinvL)) { + chan[c.chan].env.flag.envHinvL=nextHinvL; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + bool nextVinvL=c.value&64; + if (nextVinvL!=(chan[c.chan].env.flag.envVinvL)) { + chan[c.chan].env.flag.envVinvL=nextVinvL; + if (!isMuted[c.chan] && !chan[c.chan].pcm) { + chan[c.chan].envChanged=true; + } + } + break; + } + case DIV_CMD_X1_010_ENVELOPE_PERIOD: + chan[c.chan].env.period=c.value; + if (!chan[c.chan].pcm) { + chWrite(c.chan,4,chan[c.chan].env.period); + } + break; + case DIV_CMD_X1_010_ENVELOPE_SLIDE: + chan[c.chan].env.slide=c.value; + break; + case DIV_CMD_X1_010_AUTO_ENVELOPE: + chan[c.chan].autoEnvNum=c.value>>4; + chan[c.chan].autoEnvDen=c.value&15; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_GET_VOLMAX: + return 15; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformX1_010::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + chan[ch].envChanged=true; +} + +void DivPlatformX1_010::forceIns() { + for (int i=0; i<16; i++) { + chan[i].insChanged=true; + chan[i].envChanged=true; + chan[i].freqChanged=true; + updateWave(i); + } +} + +void* DivPlatformX1_010::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformX1_010::getRegisterPool() { + for (int i=0; i<0x2000; i++) { + regPool[i]=x1_010->ram_r(i); + } + return regPool; +} + +int DivPlatformX1_010::getRegisterPoolSize() { + return 0x2000; +} + +void DivPlatformX1_010::reset() { + memset(regPool,0,0x2000); + for (int i=0; i<16; i++) { + chan[i]=DivPlatformX1_010::Channel(); + chan[i].reset(); + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,128,255,false); + } + x1_010->reset(); + sampleBank=0; + // set per-channel initial panning + for (int i=0; i<16; i++) { + chWrite(i,0,0); + } +} + +bool DivPlatformX1_010::isStereo() { + return stereo; +} + +bool DivPlatformX1_010::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformX1_010::notifyWaveChange(int wave) { + for (int i=0; i<16; i++) { + if (chan[i].wave==wave) { + chan[i].ws.changeWave1(wave); + updateWave(i); + } + } +} + +void DivPlatformX1_010::notifyInsDeletion(void* ins) { + for (int i=0; i<16; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformX1_010::setFlags(unsigned int flags) { + switch (flags&15) { + case 0: // 16MHz (earlier hardwares) + chipClock=16000000; + break; + case 1: // 16.67MHz (later hardwares) + chipClock=50000000.0/3.0; + break; + // Other clock is used + default: + chipClock=16000000; + break; + } + rate=chipClock/512; + stereo=flags&16; +} + +void DivPlatformX1_010::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformX1_010::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformX1_010::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + stereo=false; + for (int i=0; i<16; i++) { + isMuted[i]=false; + } + setFlags(flags); + intf.parent=parent; + x1_010=new x1_010_core(intf); + x1_010->reset(); + reset(); + return 16; +} + +void DivPlatformX1_010::quit() { + delete x1_010; +} + +DivPlatformX1_010::~DivPlatformX1_010() { +} diff --git a/src/engine/platform/x1_010.h b/src/engine/platform/x1_010.h new file mode 100644 index 000000000..2bfb78cc9 --- /dev/null +++ b/src/engine/platform/x1_010.h @@ -0,0 +1,145 @@ +/** + * 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 _X1_010_H +#define _X1_010_H + +#include "../dispatch.h" +#include "../engine.h" +#include "../macroInt.h" +#include "../waveSynth.h" +#include "sound/x1_010/x1_010.hpp" + +class DivX1_010Interface: public x1_010_mem_intf { + public: + DivEngine* parent; + int sampleBank; + virtual u8 read_byte(u32 address) override { + if (parent->x1_010Mem==NULL) return 0; + return parent->x1_010Mem[address & 0xfffff]; + } + DivX1_010Interface(): parent(NULL), sampleBank(0) {} +}; + +class DivPlatformX1_010: public DivDispatch { + struct Channel { + struct Envelope { + struct EnvFlag { + unsigned char envEnable : 1; + unsigned char envOneshot : 1; + unsigned char envSplit : 1; + unsigned char envHinvR : 1; + unsigned char envVinvR : 1; + unsigned char envHinvL : 1; + unsigned char envVinvL : 1; + void reset() { + envEnable=0; + envOneshot=0; + envSplit=0; + envHinvR=0; + envVinvR=0; + envHinvL=0; + envVinvL=0; + } + EnvFlag(): + envEnable(0), + envOneshot(0), + envSplit(0), + envHinvR(0), + envVinvR(0), + envHinvL(0), + envVinvL(0) {} + }; + int shape, period, slide, slidefrac; + EnvFlag flag; + void reset() { + shape=-1; + period=0; + flag.reset(); + } + Envelope(): + shape(-1), + period(0), + slide(0), + slidefrac(0) {} + }; + int freq, baseFreq, pitch, note; + int wave, sample, ins; + unsigned char pan, autoEnvNum, autoEnvDen; + bool active, insChanged, envChanged, freqChanged, keyOn, keyOff, inPorta, furnacePCM, pcm; + int vol, outVol, lvol, rvol; + unsigned char waveBank; + Envelope env; + DivMacroInt std; + DivWaveSynth ws; + void reset() { + freq = baseFreq = pitch = note = 0; + wave = sample = ins = -1; + pan = 255; + autoEnvNum = autoEnvDen = 0; + active = false; + insChanged = envChanged = freqChanged = true; + keyOn = keyOff = inPorta = furnacePCM = pcm = false; + vol = outVol = lvol = rvol = 15; + waveBank = 0; + } + Channel(): + freq(0), baseFreq(0), pitch(0), note(0), + wave(-1), sample(-1), ins(-1), + pan(255), autoEnvNum(0), autoEnvDen(0), + active(false), insChanged(true), envChanged(true), freqChanged(false), keyOn(false), keyOff(false), inPorta(false), furnacePCM(false), pcm(false), + vol(15), outVol(15), lvol(15), rvol(15), + waveBank(0) {} + }; + Channel chan[16]; + bool isMuted[16]; + bool stereo=false; + unsigned char sampleBank; + DivX1_010Interface intf; + x1_010_core* x1_010; + unsigned char regPool[0x2000]; + double NoteX1_010(int ch, int note); + void updateWave(int ch); + void updateEnvelope(int ch); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformX1_010(); +}; + +#endif diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index e9e77d813..dd7220fa5 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -32,6 +32,218 @@ static unsigned char konOffs[4]={ #define CHIP_DIVIDER 32 +const char* regCheatSheetYM2610[]={ + // SSG + "SSG_FreqL_A", "000", + "SSG_FreqH_A", "001", + "SSG_FreqL_B", "002", + "SSG_FreqH_B", "003", + "SSG_FreqL_C", "004", + "SSG_FreqH_C", "005", + "SSG_FreqNoise", "006", + "SSG_Enable", "007", + "SSG_Volume_A", "008", + "SSG_Volume_B", "009", + "SSG_Volume_C", "00A", + "SSG_FreqL_Env", "00B", + "SSG_FreqH_Env", "00C", + "SSG_Control_Env", "00D", + // ADPCM-B + "ADPCMB_Control", "010", + "ADPCMB_L_R", "011", + "ADPCMB_StartL", "012", + "ADPCMB_StartH", "013", + "ADPCMB_EndL", "014", + "ADPCMB_EndH", "015", + "ADPCMB_FreqL", "019", + "ADPCMB_FreqH", "01A", + "ADPCMB_Volume", "01B", + "ADPCM_Flag", "01C", + // FM (Common) + "FM_Test", "021", + "FM_LFOFreq", "022", + "ClockA1", "024", + "ClockA2", "025", + "ClockB", "026", + "FM_Control", "027", + "FM_NoteCtl", "028", + // FM (Channel 1-2) + "FM1_Op1_DT_MULT", "031", + "FM2_Op1_DT_MULT", "032", + "FM1_Op2_DT_MULT", "035", + "FM2_Op2_DT_MULT", "036", + "FM1_Op3_DT_MULT", "039", + "FM2_Op3_DT_MULT", "03A", + "FM1_Op4_DT_MULT", "03D", + "FM2_Op4_DT_MULT", "03E", + "FM1_Op1_TL", "041", + "FM2_Op1_TL", "042", + "FM1_Op2_TL", "045", + "FM2_Op2_TL", "046", + "FM1_Op3_TL", "049", + "FM2_Op3_TL", "04A", + "FM1_Op4_TL", "04D", + "FM2_Op4_TL", "04E", + "FM1_Op1_KS_AR", "051", + "FM2_Op1_KS_AR", "052", + "FM1_Op2_KS_AR", "055", + "FM2_Op2_KS_AR", "056", + "FM1_Op3_KS_AR", "059", + "FM2_Op3_KS_AR", "05A", + "FM1_Op4_KS_AR", "05D", + "FM2_Op4_KS_AR", "05E", + "FM1_Op1_AM_DR", "061", + "FM2_Op1_AM_DR", "062", + "FM1_Op2_AM_DR", "065", + "FM2_Op2_AM_DR", "066", + "FM1_Op3_AM_DR", "069", + "FM2_Op3_AM_DR", "06A", + "FM1_Op4_AM_DR", "06D", + "FM2_Op4_AM_DR", "06E", + "FM1_Op1_SR", "071", + "FM2_Op1_SR", "072", + "FM1_Op2_SR", "075", + "FM2_Op2_SR", "076", + "FM1_Op3_SR", "079", + "FM2_Op3_SR", "07A", + "FM1_Op4_SR", "07D", + "FM2_Op4_SR", "07E", + "FM1_Op1_SL_RR", "081", + "FM2_Op1_SL_RR", "082", + "FM1_Op2_SL_RR", "085", + "FM2_Op2_SL_RR", "086", + "FM1_Op3_SL_RR", "089", + "FM2_Op3_SL_RR", "08A", + "FM1_Op4_SL_RR", "08D", + "FM2_Op4_SL_RR", "08E", + "FM1_Op1_SSG_EG", "091", + "FM2_Op1_SSG_EG", "092", + "FM1_Op2_SSG_EG", "095", + "FM2_Op2_SSG_EG", "096", + "FM1_Op3_SSG_EG", "099", + "FM2_Op3_SSG_EG", "09A", + "FM1_Op4_SSG_EG", "09D", + "FM2_Op4_SSG_EG", "09E", + "FM1_FNum1", "0A1", + "FM2_(Op1)FNum1", "0A2", + "FM1_FNum2", "0A5", + "FM2_(Op1)FNum2", "0A6", + "FM2_Op2_FNum1", "0A8", + "FM2_Op3_FNum1", "0A9", + "FM2_Op4_FNum1", "0AA", + "FM2_Op2_FNum2", "0AC", + "FM2_Op3_FNum2", "0AD", + "FM2_Op4_FNum2", "0AE", + "FM1_FB_ALG", "0B1", + "FM2_FB_ALG", "0B2", + "FM1_Pan_LFO", "0B5", + "FM2_Pan_LFO", "0B6", + // ADPCM-A + "ADPCMA_Control", "100", + "ADPCMA_MVol", "101", + "ADPCMA_Test", "102", + "ADPCMA_Ch1_Vol", "108", + "ADPCMA_Ch2_Vol", "109", + "ADPCMA_Ch3_Vol", "10A", + "ADPCMA_Ch4_Vol", "10B", + "ADPCMA_Ch5_Vol", "10C", + "ADPCMA_Ch6_Vol", "10D", + "ADPCMA_Ch1_StL", "110", + "ADPCMA_Ch2_StL", "111", + "ADPCMA_Ch3_StL", "112", + "ADPCMA_Ch4_StL", "113", + "ADPCMA_Ch5_StL", "114", + "ADPCMA_Ch6_StL", "115", + "ADPCMA_Ch1_StH", "118", + "ADPCMA_Ch2_StH", "119", + "ADPCMA_Ch3_StH", "11A", + "ADPCMA_Ch4_StH", "11B", + "ADPCMA_Ch5_StH", "11C", + "ADPCMA_Ch6_StH", "11D", + "ADPCMA_Ch1_EdL", "120", + "ADPCMA_Ch2_EdL", "121", + "ADPCMA_Ch3_EdL", "122", + "ADPCMA_Ch4_EdL", "123", + "ADPCMA_Ch5_EdL", "124", + "ADPCMA_Ch6_EdL", "125", + "ADPCMA_Ch1_EdH", "128", + "ADPCMA_Ch2_EdH", "129", + "ADPCMA_Ch3_EdH", "12A", + "ADPCMA_Ch4_EdH", "12B", + "ADPCMA_Ch5_EdH", "12C", + "ADPCMA_Ch6_EdH", "12D", + // FM (Channel 3-4) + "FM3_Op1_DT_MULT", "131", + "FM4_Op1_DT_MULT", "132", + "FM3_Op2_DT_MULT", "135", + "FM4_Op2_DT_MULT", "136", + "FM3_Op3_DT_MULT", "139", + "FM4_Op3_DT_MULT", "13A", + "FM3_Op4_DT_MULT", "13D", + "FM4_Op4_DT_MULT", "13E", + "FM3_Op1_TL", "141", + "FM4_Op1_TL", "142", + "FM3_Op2_TL", "145", + "FM4_Op2_TL", "146", + "FM3_Op3_TL", "149", + "FM4_Op3_TL", "14A", + "FM3_Op4_TL", "14D", + "FM4_Op4_TL", "14E", + "FM3_Op1_KS_AR", "151", + "FM4_Op1_KS_AR", "152", + "FM3_Op2_KS_AR", "155", + "FM4_Op2_KS_AR", "156", + "FM3_Op3_KS_AR", "159", + "FM4_Op3_KS_AR", "15A", + "FM3_Op4_KS_AR", "15D", + "FM4_Op4_KS_AR", "15E", + "FM3_Op1_AM_DR", "161", + "FM4_Op1_AM_DR", "162", + "FM3_Op2_AM_DR", "165", + "FM4_Op2_AM_DR", "166", + "FM3_Op3_AM_DR", "169", + "FM4_Op3_AM_DR", "16A", + "FM3_Op4_AM_DR", "16D", + "FM4_Op4_AM_DR", "16E", + "FM3_Op1_SR", "171", + "FM4_Op1_SR", "172", + "FM3_Op2_SR", "175", + "FM4_Op2_SR", "176", + "FM3_Op3_SR", "179", + "FM4_Op3_SR", "17A", + "FM3_Op4_SR", "17D", + "FM4_Op4_SR", "17E", + "FM3_Op1_SL_RR", "181", + "FM4_Op1_SL_RR", "182", + "FM3_Op2_SL_RR", "185", + "FM4_Op2_SL_RR", "186", + "FM3_Op3_SL_RR", "189", + "FM4_Op3_SL_RR", "18A", + "FM3_Op4_SL_RR", "18D", + "FM4_Op4_SL_RR", "18E", + "FM3_Op1_SSG_EG", "191", + "FM4_Op1_SSG_EG", "192", + "FM3_Op2_SSG_EG", "195", + "FM4_Op2_SSG_EG", "196", + "FM3_Op3_SSG_EG", "199", + "FM4_Op3_SSG_EG", "19A", + "FM3_Op4_SSG_EG", "19D", + "FM4_Op4_SSG_EG", "19E", + "FM3_FNum1", "1A1", + "FM4_FNum1", "1A2", + "FM3_FNum2", "1A5", + "FM4_FNum2", "1A6", + "FM3_FB_ALG", "1B1", + "FM4_FB_ALG", "1B2", + "FM3_Pan_LFO", "1B5", + "FM4_Pan_LFO", "1B6", + NULL +}; + +const char** DivPlatformYM2610::getRegisterSheet() { + return regCheatSheetYM2610; +} + const char* DivPlatformYM2610::getEffectName(unsigned char effect) { switch (effect) { case 0x10: @@ -97,15 +309,29 @@ const char* DivPlatformYM2610::getEffectName(unsigned char effect) { case 0x29: return "29xy: Set SSG auto-envelope (x: numerator; y: denominator)"; break; + case 0x30: + return "30xx: Toggle hard envelope reset on new notes"; + break; } return NULL; } +double DivPlatformYM2610::NOTE_OPNB(int ch, int note) { + if (ch>6) { // ADPCM + return NOTE_ADPCMB(note); + } else if (ch>3) { // PSG + return NOTE_PERIODIC(note); + } + // FM + return NOTE_FREQUENCY(note); +} + double DivPlatformYM2610::NOTE_ADPCMB(int note) { - DivInstrument* ins=parent->getIns(chan[13].ins); - if (ins->type!=DIV_INS_AMIGA) return 0; - double off=(double)(parent->getSample(ins->amiga.initSample)->centerRate)/8363.0; - return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + if (chan[13].sample>=0 && chan[13].samplesong.sampleLen) { + double off=(double)(parent->getSample(chan[13].sample)->centerRate)/8363.0; + return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + } + return 0; } void DivPlatformYM2610::acquire(short* bufL, short* bufR, size_t start, size_t len) { @@ -141,114 +367,20 @@ void DivPlatformYM2610::acquire(short* bufL, short* bufR, size_t start, size_t l void DivPlatformYM2610::tick() { // PSG - for (int i=4; i<7; i++) { - chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=MIN(15,chan[i].std.vol)-(15-(chan[i].vol&15)); - if (chan[i].outVol<0) chan[i].outVol=0; - if (isMuted[i]) { - rWrite(0x04+i,0); - } else { - rWrite(0x04+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); - } - } - if (chan[i].std.hadArp) { - if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); - } else { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); - } - } - chan[i].freqChanged=true; - } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { - chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); - chan[i].freqChanged=true; - } - } - if (chan[i].std.hadDuty) { - ayNoiseFreq=31-chan[i].std.duty; - rWrite(0x06,ayNoiseFreq); - } - if (chan[i].std.hadWave) { - chan[i].psgMode=(chan[i].std.wave+1)&7; - if (isMuted[i]) { - rWrite(0x04+i,0); - } else { - rWrite(0x04+i,(chan[i].outVol&15)|((chan[i].psgMode&4)<<2)); - } - } - if (chan[i].std.hadEx2) { - ayEnvMode=chan[i].std.ex2; - rWrite(0x0d,ayEnvMode); - } - if (chan[i].std.hadEx3) { - chan[i].autoEnvNum=chan[i].std.ex3; - chan[i].freqChanged=true; - if (!chan[i].std.willAlg) chan[i].autoEnvDen=1; - } - if (chan[i].std.hadAlg) { - chan[i].autoEnvDen=chan[i].std.alg; - chan[i].freqChanged=true; - if (!chan[i].std.willEx3) chan[i].autoEnvNum=1; - } - if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); - if (chan[i].freq>4095) chan[i].freq=4095; - if (chan[i].keyOn) { - } - if (chan[i].keyOff) { - rWrite(0x04+i,0); - } - rWrite((i-4)<<1,chan[i].freq&0xff); - rWrite(1+((i-4)<<1),chan[i].freq>>8); - if (chan[i].keyOn) chan[i].keyOn=false; - if (chan[i].keyOff) chan[i].keyOff=false; - if (chan[i].freqChanged && chan[i].autoEnvNum>0 && chan[i].autoEnvDen>0) { - ayEnvPeriod=(chan[i].freq*chan[i].autoEnvDen/chan[i].autoEnvNum)>>4; - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - } - chan[i].freqChanged=false; - } - } - - rWrite(0x07, - ~((chan[4].psgMode&1)| - ((chan[5].psgMode&1)<<1)| - ((chan[6].psgMode&1)<<2)| - ((chan[4].psgMode&2)<<2)| - ((chan[5].psgMode&2)<<3)| - ((chan[6].psgMode&2)<<4))); - - if (ayEnvSlide!=0) { - ayEnvSlideLow+=ayEnvSlide; - while (ayEnvSlideLow>7) { - ayEnvSlideLow-=8; - if (ayEnvPeriod<0xffff) { - ayEnvPeriod++; - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - } - } - while (ayEnvSlideLow<-7) { - ayEnvSlideLow+=8; - if (ayEnvPeriod>0) { - ayEnvPeriod--; - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - } - } + ay->tick(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); } + ay->getRegisterWrites().clear(); // FM for (int i=0; i<4; i++) { if (i==1 && extMode) continue; chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; @@ -260,24 +392,24 @@ void DivPlatformYM2610::tick() { } } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp); + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadAlg) { - chan[i].state.alg=chan[i].std.alg; + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); if (!parent->song.algMacroBehavior) for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; @@ -293,74 +425,92 @@ void DivPlatformYM2610::tick() { } } } - if (chan[i].std.hadFb) { - chan[i].state.fb=chan[i].std.fb; + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); } - if (chan[i].std.hadFms) { - chan[i].state.fms=chan[i].std.fms; + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); } - if (chan[i].std.hadAms) { - chan[i].state.ams=chan[i].std.ams; + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); } for (int j=0; j<4; j++) { unsigned short baseAddr=chanOffs[i]|opOffs[j]; DivInstrumentFM::Operator& op=chan[i].state.op[j]; DivMacroInt::IntOp& m=chan[i].std.op[j]; - if (m.hadAm) { - op.am=m.am; + if (m.am.had) { + op.am=m.am.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadAr) { - op.ar=m.ar; + if (m.ar.had) { + op.ar=m.ar.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDr) { - op.dr=m.dr; + if (m.dr.had) { + op.dr=m.dr.val; rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); } - if (m.hadMult) { - op.mult=m.mult; + if (m.mult.had) { + op.mult=m.mult.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadRr) { - op.rr=m.rr; + if (m.rr.had) { + op.rr=m.rr.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadSl) { - op.sl=m.sl; + if (m.sl.had) { + op.sl=m.sl.val; rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); } - if (m.hadTl) { - op.tl=127-m.tl; + if (m.tl.had) { + op.tl=127-m.tl.val; if (isOutput[chan[i].state.alg][j]) { rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); } else { rWrite(baseAddr+ADDR_TL,op.tl); } } - if (m.hadRs) { - op.rs=m.rs; + if (m.rs.had) { + op.rs=m.rs.val; rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); } - if (m.hadDt) { - op.dt=m.dt; + if (m.dt.had) { + op.dt=m.dt.val; rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); } - if (m.hadD2r) { - op.d2r=m.d2r; + if (m.d2r.had) { + op.d2r=m.d2r.val; rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); } - if (m.hadSsg) { - op.ssgEnv=m.ssg; + if (m.ssg.had) { + op.ssgEnv=m.ssg.val; rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); } } if (chan[i].keyOn || chan[i].keyOff) { + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + immWrite(baseAddr+ADDR_SL_RR,0x0f); + immWrite(baseAddr+ADDR_TL,0x7f); + oldWrites[baseAddr+ADDR_SL_RR]=-1; + oldWrites[baseAddr+ADDR_TL]=-1; + //rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + } immWrite(0x28,0x00|konOffs[i]); + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + for (int k=0; k<100; k++) { + immWrite(baseAddr+ADDR_SL_RR,0x0f); + } + } + } chan[i].keyOff=false; } } @@ -369,22 +519,22 @@ void DivPlatformYM2610::tick() { if (chan[13].furnacePCM) { chan[13].std.next(); - if (chan[13].std.hadVol) { - chan[13].outVol=(chan[13].vol*MIN(64,chan[13].std.vol))/64; + if (chan[13].std.vol.had) { + chan[13].outVol=(chan[13].vol*MIN(64,chan[13].std.vol.val))/64; immWrite(0x1b,chan[13].outVol); } - if (chan[13].std.hadArp) { + if (chan[13].std.arp.had) { if (!chan[13].inPorta) { - if (chan[13].std.arpMode) { - chan[13].baseFreq=NOTE_ADPCMB(chan[13].std.arp); + if (chan[13].std.arp.mode) { + chan[13].baseFreq=NOTE_ADPCMB(chan[13].std.arp.val); } else { - chan[13].baseFreq=NOTE_ADPCMB(chan[13].note+(signed char)chan[13].std.arp); + chan[13].baseFreq=NOTE_ADPCMB(chan[13].note+(signed char)chan[13].std.arp.val); } } chan[13].freqChanged=true; } else { - if (chan[13].std.arpMode && chan[13].std.finishedArp) { + if (chan[13].std.arp.mode && chan[13].std.arp.finished) { chan[13].baseFreq=NOTE_ADPCMB(chan[13].note); chan[13].freqChanged=true; } @@ -397,7 +547,7 @@ void DivPlatformYM2610::tick() { chan[13].freqChanged=false; } - for (int i=0; i<512; i++) { + for (int i=16; i<512; i++) { if (pendingWrites[i]!=oldWrites[i]) { immWrite(i,pendingWrites[i]&0xff); oldWrites[i]=pendingWrites[i]; @@ -463,6 +613,10 @@ int DivPlatformYM2610::toFreq(int freq) { } int DivPlatformYM2610::dispatch(DivCommand c) { + if (c.chan>3 && c.chan<7) { + c.chan-=4; + return ay->dispatch(c); + } switch (c.cmd) { case DIV_CMD_NOTE_ON: { if (c.chan>12) { // ADPCM-B @@ -475,26 +629,37 @@ int DivPlatformYM2610::dispatch(DivCommand c) { if (skipRegisterWrites) break; if (chan[c.chan].furnacePCM) { chan[c.chan].std.init(ins); - if (!chan[c.chan].std.willVol) { + if (!chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; immWrite(0x1b,chan[c.chan].outVol); } - DivSample* s=parent->getSample(ins->amiga.initSample); - immWrite(0x12,(s->offB>>8)&0xff); - immWrite(0x13,s->offB>>16); - int end=s->offB+s->lengthB-1; - immWrite(0x14,(end>>8)&0xff); - immWrite(0x15,end>>16); - immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); - immWrite(0x10,0x80); // start - if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].note=c.value; - chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); - chan[c.chan].freqChanged=true; + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + immWrite(0x12,(s->offB>>8)&0xff); + immWrite(0x13,s->offB>>16); + int end=s->offB+s->lengthB-1; + 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 + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + } else { + immWrite(0x10,0x01); // reset + immWrite(0x12,0); + immWrite(0x13,0); + immWrite(0x14,0); + immWrite(0x15,0); + break; } - chan[c.chan].active=true; - chan[c.chan].keyOn=true; } else { + chan[c.chan].sample=-1; chan[c.chan].std.init(NULL); chan[c.chan].outVol=chan[c.chan].vol; if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { @@ -512,7 +677,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,0x80); // start + immWrite(0x10,(s->loopStart>=0)?0x90:0x80); // start/repeat chan[c.chan].baseFreq=(((unsigned int)s->rate)<<16)/(chipClock/144); chan[c.chan].freqChanged=true; } @@ -541,27 +706,11 @@ int DivPlatformYM2610::dispatch(DivCommand c) { DivInstrument* ins=parent->getIns(chan[c.chan].ins); chan[c.chan].std.init(ins); if (c.chan<4) { - if (!chan[c.chan].std.willVol) { + if (!chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } } - if (c.chan>3) { // PSG - if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); - chan[c.chan].freqChanged=true; - chan[c.chan].note=c.value; - } - chan[c.chan].active=true; - chan[c.chan].keyOn=true; - if (isMuted[c.chan]) { - rWrite(0x04+c.chan,0); - } else { - rWrite(0x04+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); - } - break; - } - if (chan[c.chan].insChanged) { chan[c.chan].state=ins->fm; } @@ -595,6 +744,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].portaPause=false; chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; } @@ -625,10 +775,6 @@ int DivPlatformYM2610::dispatch(DivCommand c) { immWrite(0x100,0x80|(1<<(c.chan-7))); break; } - if (c.chan>3) { - chan[c.chan].std.release(); - break; - } chan[c.chan].keyOff=true; chan[c.chan].keyOn=false; chan[c.chan].active=false; @@ -639,7 +785,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { break; case DIV_CMD_VOLUME: { chan[c.chan].vol=c.value; - if (!chan[c.chan].std.hasVol) { + if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (c.chan>12) { // ADPCM-B @@ -650,14 +796,6 @@ int DivPlatformYM2610::dispatch(DivCommand c) { immWrite(0x108+(c.chan-7),isMuted[c.chan]?0:((chan[c.chan].pan<<6)|chan[c.chan].vol)); break; } - if (c.chan>3) { // PSG - if (isMuted[c.chan]) { - rWrite(0x04+c.chan,0); - } else { - if (chan[c.chan].active) rWrite(0x04+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); - } - break; - } for (int i=0; i<4; i++) { unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; @@ -680,16 +818,10 @@ int DivPlatformYM2610::dispatch(DivCommand c) { chan[c.chan].ins=c.value; break; case DIV_CMD_PANNING: { - switch (c.value) { - case 0x01: - chan[c.chan].pan=1; - break; - case 0x10: - chan[c.chan].pan=2; - break; - default: - chan[c.chan].pan=3; - break; + if (c.value==0) { + chan[c.chan].pan=3; + } else { + chan[c.chan].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); } if (c.chan>12) { immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); @@ -699,7 +831,6 @@ int DivPlatformYM2610::dispatch(DivCommand c) { immWrite(0x108+(c.chan-7),isMuted[c.chan]?0:((chan[c.chan].pan<<6)|chan[c.chan].vol)); break; } - if (c.chan>3) break; rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); break; } @@ -709,8 +840,8 @@ int DivPlatformYM2610::dispatch(DivCommand c) { break; } case DIV_CMD_NOTE_PORTA: { - if (c.chan>3) { // PSG - int destFreq=NOTE_PERIODIC(c.value2); + if (c.chan>3) { // PSG, ADPCM-B + int destFreq=NOTE_OPNB(c.chan,c.value2); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -769,11 +900,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) { iface.sampleBank=sampleBank; break; case DIV_CMD_LEGATO: { - if (c.chan>3) { // PSG - chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); - } else { - chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); - } + chan[c.chan].baseFreq=NOTE_OPNB(c.chan,c.value); chan[c.chan].freqChanged=true; break; } @@ -824,58 +951,8 @@ int DivPlatformYM2610::dispatch(DivCommand c) { } break; } - case DIV_CMD_STD_NOISE_MODE: - if (c.chan<4 || c.chan>6) break; - chan[c.chan].psgMode=(c.value+1)&7; - if (isMuted[c.chan]) { - rWrite(0x04+c.chan,0); - } else if (chan[c.chan].active) { - rWrite(0x04+c.chan,(chan[c.chan].outVol&15)|((chan[c.chan].psgMode&4)<<2)); - } - break; - case DIV_CMD_STD_NOISE_FREQ: - if (c.chan<4 || c.chan>6) break; - ayNoiseFreq=31-c.value; - rWrite(0x06,ayNoiseFreq); - break; - case DIV_CMD_AY_ENVELOPE_SET: - if (c.chan<4 || c.chan>6) break; - ayEnvMode=c.value>>4; - rWrite(0x0d,ayEnvMode); - if (c.value&15) { - chan[c.chan].psgMode|=4; - } else { - chan[c.chan].psgMode&=~4; - } - if (isMuted[c.chan]) { - rWrite(0x04+c.chan,0); - } else { - rWrite(0x04+c.chan,(chan[c.chan].vol&15)|((chan[c.chan].psgMode&4)<<2)); - } - break; - case DIV_CMD_AY_ENVELOPE_LOW: - if (c.chan<4 || c.chan>6) break; - ayEnvPeriod&=0xff00; - ayEnvPeriod|=c.value; - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - break; - case DIV_CMD_AY_ENVELOPE_HIGH: - if (c.chan<4 || c.chan>6) break; - ayEnvPeriod&=0xff; - ayEnvPeriod|=c.value<<8; - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - break; - case DIV_CMD_AY_ENVELOPE_SLIDE: - if (c.chan<4 || c.chan>6) break; - ayEnvSlide=c.value; - break; - case DIV_CMD_AY_AUTO_ENVELOPE: - if (c.chan<4 || c.chan>6) break; - chan[c.chan].autoEnvNum=c.value>>4; - chan[c.chan].autoEnvDen=c.value&15; - chan[c.chan].freqChanged=true; + case DIV_CMD_FM_HARD_RESET: + chan[c.chan].hardReset=c.value; break; case DIV_ALWAYS_SET_VOLUME: return 0; @@ -913,11 +990,7 @@ void DivPlatformYM2610::muteChannel(int ch, bool mute) { return; } if (ch>3) { // PSG - if (isMuted[ch]) { - rWrite(0x04+ch,0); - } else { - rWrite(0x04+ch,(chan[ch].outVol&15)|((chan[ch].psgMode&4)<<2)); - } + ay->muteChannel(ch-4,mute); return; } // FM @@ -948,12 +1021,16 @@ void DivPlatformYM2610::forceIns() { chan[i].freqChanged=true; } } - for (int i=4; i<14; i++) { + for (int i=7; i<14; i++) { chan[i].insChanged=true; } - immWrite(0x0b,ayEnvPeriod); - immWrite(0x0c,ayEnvPeriod>>8); - immWrite(0x0d,ayEnvMode); + + ay->forceIns(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); } void* DivPlatformYM2610::getChanState(int ch) { @@ -1004,31 +1081,22 @@ void DivPlatformYM2610::reset() { } lastBusy=60; - dacMode=0; - dacPeriod=0; - dacPos=0; - dacRate=0; - dacSample=-1; sampleBank=0; - ayEnvPeriod=0; - ayEnvMode=0; - ayEnvSlide=0; - ayEnvSlideLow=0; - ayNoiseFreq=0; delay=0; extMode=false; - // AY noise - immWrite(0x06,ayNoiseFreq); - // LFO immWrite(0x22,0x08); // PCM volume immWrite(0x101,0x3f); // A immWrite(0x1b,0xff); // B + + ay->reset(); + ay->getRegisterWrites().clear(); + ay->flushWrites(); } bool DivPlatformYM2610::isStereo() { @@ -1045,12 +1113,16 @@ void DivPlatformYM2610::notifyInsChange(int ins) { chan[i].insChanged=true; } } + ay->notifyInsChange(ins); } void DivPlatformYM2610::notifyInsDeletion(void* ins) { - for (int i=4; i<7; i++) { - chan[i].std.notifyInsDeletion((DivInstrument*)ins); - } + ay->notifyInsDeletion(ins); +} + +void DivPlatformYM2610::setSkipRegisterWrites(bool value) { + DivDispatch::setSkipRegisterWrites(value); + ay->setSkipRegisterWrites(value); } int DivPlatformYM2610::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { @@ -1065,11 +1137,17 @@ int DivPlatformYM2610::init(DivEngine* p, int channels, int sugRate, unsigned in iface.parent=parent; iface.sampleBank=0; fm=new ymfm::ym2610(iface); + // YM2149, 2MHz + ay=new DivPlatformAY8910; + ay->init(p,3,sugRate,35); + ay->toggleRegisterDump(true); reset(); return 14; } void DivPlatformYM2610::quit() { + ay->quit(); + delete ay; delete fm; } diff --git a/src/engine/platform/ym2610.h b/src/engine/platform/ym2610.h index 317870f69..2fd525b08 100644 --- a/src/engine/platform/ym2610.h +++ b/src/engine/platform/ym2610.h @@ -22,6 +22,7 @@ #include "../dispatch.h" #include "../macroInt.h" #include +#include "ay.h" #include "sound/ymfm/ymfm_opn.h" class DivYM2610Interface: public ymfm::ymfm_interface { @@ -35,14 +36,19 @@ class DivYM2610Interface: public ymfm::ymfm_interface { class DivPlatformYM2610: public DivDispatch { protected: + const unsigned short chanOffs[4]={ + 0x01, 0x02, 0x101, 0x102 + }; + struct Channel { DivInstrumentFM state; unsigned char freqH, freqL; int freq, baseFreq, pitch, note; unsigned char ins, psgMode, autoEnvNum, autoEnvDen; signed char konCycles; - bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; int vol, outVol; + int sample; unsigned char pan; DivMacroInt std; Channel(): @@ -64,8 +70,10 @@ class DivPlatformYM2610: public DivDispatch { portaPause(false), inPorta(false), furnacePCM(false), + hardReset(false), vol(0), outVol(15), + sample(-1), pan(3) {} }; Channel chan[14]; @@ -80,15 +88,11 @@ class DivPlatformYM2610: public DivDispatch { ymfm::ym2610* fm; ymfm::ym2610::output_data fmout; DivYM2610Interface iface; + + DivPlatformAY8910* ay; unsigned char regPool[512]; unsigned char lastBusy; - bool dacMode; - int dacPeriod; - int dacRate; - int dacPos; - int dacSample; - int ayNoiseFreq; unsigned char sampleBank; int delay; @@ -97,13 +101,10 @@ class DivPlatformYM2610: public DivDispatch { short oldWrites[512]; short pendingWrites[512]; - unsigned char ayEnvMode; - unsigned short ayEnvPeriod; - short ayEnvSlideLow; - short ayEnvSlide; int octave(int freq); int toFreq(int freq); + double NOTE_OPNB(int ch, int note); double NOTE_ADPCMB(int note); friend void putDispatchChan(void*,int,int); @@ -121,8 +122,10 @@ class DivPlatformYM2610: public DivDispatch { bool keyOffAffectsArp(int ch); void notifyInsChange(int ins); void notifyInsDeletion(void* ins); + void setSkipRegisterWrites(bool val); void poke(unsigned int addr, unsigned short val); void poke(std::vector& wlist); + const char** getRegisterSheet(); const char* getEffectName(unsigned char effect); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp new file mode 100644 index 000000000..c51f41198 --- /dev/null +++ b/src/engine/platform/ym2610b.cpp @@ -0,0 +1,1218 @@ +/** + * 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 "ym2610b.h" +#include "../engine.h" +#include +#include + +#include "ym2610shared.h" + +#include "fmshared_OPN.h" + +static unsigned char konOffs[6]={ + 0, 1, 2, 4, 5, 6 +}; + +#define CHIP_DIVIDER 32 + +const char* regCheatSheetYM2610B[]={ + // SSG + "SSG_FreqL_A", "000", + "SSG_FreqH_A", "001", + "SSG_FreqL_B", "002", + "SSG_FreqH_B", "003", + "SSG_FreqL_C", "004", + "SSG_FreqH_C", "005", + "SSG_FreqNoise", "006", + "SSG_Enable", "007", + "SSG_Volume_A", "008", + "SSG_Volume_B", "009", + "SSG_Volume_C", "00A", + "SSG_FreqL_Env", "00B", + "SSG_FreqH_Env", "00C", + "SSG_Control_Env", "00D", + // ADPCM-B + "ADPCMB_Control", "010", + "ADPCMB_L_R", "011", + "ADPCMB_StartL", "012", + "ADPCMB_StartH", "013", + "ADPCMB_EndL", "014", + "ADPCMB_EndH", "015", + "ADPCMB_FreqL", "019", + "ADPCMB_FreqH", "01A", + "ADPCMB_Volume", "01B", + "ADPCM_Flag", "01C", + // FM (Common) + "FM_Test", "021", + "FM_LFOFreq", "022", + "ClockA1", "024", + "ClockA2", "025", + "ClockB", "026", + "FM_Control", "027", + "FM_NoteCtl", "028", + // FM (Channel 1-3) + "FM1_Op1_DT_MULT", "030", + "FM2_Op1_DT_MULT", "031", + "FM3_Op1_DT_MULT", "032", + "FM1_Op2_DT_MULT", "034", + "FM2_Op2_DT_MULT", "035", + "FM3_Op2_DT_MULT", "036", + "FM1_Op3_DT_MULT", "038", + "FM2_Op3_DT_MULT", "039", + "FM3_Op3_DT_MULT", "03A", + "FM1_Op4_DT_MULT", "03C", + "FM2_Op4_DT_MULT", "03D", + "FM3_Op4_DT_MULT", "03E", + "FM1_Op1_TL", "040", + "FM2_Op1_TL", "041", + "FM3_Op1_TL", "042", + "FM1_Op2_TL", "044", + "FM2_Op2_TL", "045", + "FM3_Op2_TL", "046", + "FM1_Op3_TL", "048", + "FM2_Op3_TL", "049", + "FM3_Op3_TL", "04A", + "FM1_Op4_TL", "04C", + "FM2_Op4_TL", "04D", + "FM3_Op4_TL", "04E", + "FM1_Op1_KS_AR", "050", + "FM2_Op1_KS_AR", "051", + "FM3_Op1_KS_AR", "052", + "FM1_Op2_KS_AR", "054", + "FM2_Op2_KS_AR", "055", + "FM3_Op2_KS_AR", "056", + "FM1_Op3_KS_AR", "058", + "FM2_Op3_KS_AR", "059", + "FM3_Op3_KS_AR", "05A", + "FM1_Op4_KS_AR", "05C", + "FM2_Op4_KS_AR", "05D", + "FM3_Op4_KS_AR", "05E", + "FM1_Op1_AM_DR", "060", + "FM2_Op1_AM_DR", "061", + "FM3_Op1_AM_DR", "062", + "FM1_Op2_AM_DR", "064", + "FM2_Op2_AM_DR", "065", + "FM3_Op2_AM_DR", "066", + "FM1_Op3_AM_DR", "068", + "FM2_Op3_AM_DR", "069", + "FM3_Op3_AM_DR", "06A", + "FM1_Op4_AM_DR", "06C", + "FM2_Op4_AM_DR", "06D", + "FM3_Op4_AM_DR", "06E", + "FM1_Op1_SR", "070", + "FM2_Op1_SR", "071", + "FM3_Op1_SR", "072", + "FM1_Op2_SR", "074", + "FM2_Op2_SR", "075", + "FM3_Op2_SR", "076", + "FM1_Op3_SR", "078", + "FM2_Op3_SR", "079", + "FM3_Op3_SR", "07A", + "FM1_Op4_SR", "07C", + "FM2_Op4_SR", "07D", + "FM3_Op4_SR", "07E", + "FM1_Op1_SL_RR", "080", + "FM2_Op1_SL_RR", "081", + "FM3_Op1_SL_RR", "082", + "FM1_Op2_SL_RR", "084", + "FM2_Op2_SL_RR", "085", + "FM3_Op2_SL_RR", "086", + "FM1_Op3_SL_RR", "088", + "FM2_Op3_SL_RR", "089", + "FM3_Op3_SL_RR", "08A", + "FM1_Op4_SL_RR", "08C", + "FM2_Op4_SL_RR", "08D", + "FM3_Op4_SL_RR", "08E", + "FM1_Op1_SSG_EG", "090", + "FM2_Op1_SSG_EG", "091", + "FM3_Op1_SSG_EG", "092", + "FM1_Op2_SSG_EG", "094", + "FM2_Op2_SSG_EG", "095", + "FM3_Op2_SSG_EG", "096", + "FM1_Op3_SSG_EG", "098", + "FM2_Op3_SSG_EG", "099", + "FM3_Op3_SSG_EG", "09A", + "FM1_Op4_SSG_EG", "09C", + "FM2_Op4_SSG_EG", "09D", + "FM3_Op4_SSG_EG", "09E", + "FM1_FNum1", "0A0", + "FM2_FNum1", "0A1", + "FM3_(Op1)FNum1", "0A2", + "FM1_FNum2", "0A4", + "FM2_FNum2", "0A5", + "FM3_(Op1)FNum2", "0A6", + "FM3_Op2_FNum1", "0A8", + "FM3_Op3_FNum1", "0A9", + "FM3_Op4_FNum1", "0AA", + "FM3_Op2_FNum2", "0AC", + "FM3_Op3_FNum2", "0AD", + "FM3_Op4_FNum2", "0AE", + "FM1_FB_ALG", "0B0", + "FM2_FB_ALG", "0B1", + "FM3_FB_ALG", "0B2", + "FM1_Pan_LFO", "0B4", + "FM2_Pan_LFO", "0B5", + "FM3_Pan_LFO", "0B6", + // ADPCM-A + "ADPCMA_Control", "100", + "ADPCMA_MVol", "101", + "ADPCMA_Test", "102", + "ADPCMA_Ch1_Vol", "108", + "ADPCMA_Ch2_Vol", "109", + "ADPCMA_Ch3_Vol", "10A", + "ADPCMA_Ch4_Vol", "10B", + "ADPCMA_Ch5_Vol", "10C", + "ADPCMA_Ch6_Vol", "10D", + "ADPCMA_Ch1_StL", "110", + "ADPCMA_Ch2_StL", "111", + "ADPCMA_Ch3_StL", "112", + "ADPCMA_Ch4_StL", "113", + "ADPCMA_Ch5_StL", "114", + "ADPCMA_Ch6_StL", "115", + "ADPCMA_Ch1_StH", "118", + "ADPCMA_Ch2_StH", "119", + "ADPCMA_Ch3_StH", "11A", + "ADPCMA_Ch4_StH", "11B", + "ADPCMA_Ch5_StH", "11C", + "ADPCMA_Ch6_StH", "11D", + "ADPCMA_Ch1_EdL", "120", + "ADPCMA_Ch2_EdL", "121", + "ADPCMA_Ch3_EdL", "122", + "ADPCMA_Ch4_EdL", "123", + "ADPCMA_Ch5_EdL", "124", + "ADPCMA_Ch6_EdL", "125", + "ADPCMA_Ch1_EdH", "128", + "ADPCMA_Ch2_EdH", "129", + "ADPCMA_Ch3_EdH", "12A", + "ADPCMA_Ch4_EdH", "12B", + "ADPCMA_Ch5_EdH", "12C", + "ADPCMA_Ch6_EdH", "12D", + // FM (Channel 4-6) + "FM4_Op1_DT_MULT", "130", + "FM5_Op1_DT_MULT", "131", + "FM6_Op1_DT_MULT", "132", + "FM4_Op2_DT_MULT", "134", + "FM5_Op2_DT_MULT", "135", + "FM6_Op2_DT_MULT", "136", + "FM4_Op3_DT_MULT", "138", + "FM5_Op3_DT_MULT", "139", + "FM6_Op3_DT_MULT", "13A", + "FM4_Op4_DT_MULT", "13C", + "FM5_Op4_DT_MULT", "13D", + "FM6_Op4_DT_MULT", "13E", + "FM4_Op1_TL", "140", + "FM5_Op1_TL", "141", + "FM6_Op1_TL", "142", + "FM4_Op2_TL", "144", + "FM5_Op2_TL", "145", + "FM6_Op2_TL", "146", + "FM4_Op3_TL", "148", + "FM5_Op3_TL", "149", + "FM6_Op3_TL", "14A", + "FM4_Op4_TL", "14C", + "FM5_Op4_TL", "14D", + "FM6_Op4_TL", "14E", + "FM4_Op1_KS_AR", "150", + "FM5_Op1_KS_AR", "151", + "FM6_Op1_KS_AR", "152", + "FM4_Op2_KS_AR", "154", + "FM5_Op2_KS_AR", "155", + "FM6_Op2_KS_AR", "156", + "FM4_Op3_KS_AR", "158", + "FM5_Op3_KS_AR", "159", + "FM6_Op3_KS_AR", "15A", + "FM4_Op4_KS_AR", "15C", + "FM5_Op4_KS_AR", "15D", + "FM6_Op4_KS_AR", "15E", + "FM4_Op1_AM_DR", "160", + "FM5_Op1_AM_DR", "161", + "FM6_Op1_AM_DR", "162", + "FM4_Op2_AM_DR", "164", + "FM5_Op2_AM_DR", "165", + "FM6_Op2_AM_DR", "166", + "FM4_Op3_AM_DR", "168", + "FM5_Op3_AM_DR", "169", + "FM6_Op3_AM_DR", "16A", + "FM4_Op4_AM_DR", "16C", + "FM5_Op4_AM_DR", "16D", + "FM6_Op4_AM_DR", "16E", + "FM4_Op1_SR", "170", + "FM5_Op1_SR", "171", + "FM6_Op1_SR", "172", + "FM4_Op2_SR", "174", + "FM5_Op2_SR", "175", + "FM6_Op2_SR", "176", + "FM4_Op3_SR", "178", + "FM5_Op3_SR", "179", + "FM6_Op3_SR", "17A", + "FM4_Op4_SR", "17C", + "FM5_Op4_SR", "17D", + "FM6_Op4_SR", "17E", + "FM4_Op1_SL_RR", "180", + "FM5_Op1_SL_RR", "181", + "FM6_Op1_SL_RR", "182", + "FM4_Op2_SL_RR", "184", + "FM5_Op2_SL_RR", "185", + "FM6_Op2_SL_RR", "186", + "FM4_Op3_SL_RR", "188", + "FM5_Op3_SL_RR", "189", + "FM6_Op3_SL_RR", "18A", + "FM4_Op4_SL_RR", "18C", + "FM5_Op4_SL_RR", "18D", + "FM6_Op4_SL_RR", "18E", + "FM4_Op1_SSG_EG", "190", + "FM5_Op1_SSG_EG", "191", + "FM6_Op1_SSG_EG", "192", + "FM4_Op2_SSG_EG", "194", + "FM5_Op2_SSG_EG", "195", + "FM6_Op2_SSG_EG", "196", + "FM4_Op3_SSG_EG", "198", + "FM5_Op3_SSG_EG", "199", + "FM6_Op3_SSG_EG", "19A", + "FM4_Op4_SSG_EG", "19C", + "FM5_Op4_SSG_EG", "19D", + "FM6_Op4_SSG_EG", "19E", + "FM4_FNum1", "1A0", + "FM5_FNum1", "1A1", + "FM6_FNum1", "1A2", + "FM4_FNum2", "1A4", + "FM5_FNum2", "1A5", + "FM6_FNum2", "1A6", + "FM4_FB_ALG", "1B0", + "FM5_FB_ALG", "1B1", + "FM6_FB_ALG", "1B2", + "FM4_Pan_LFO", "1B4", + "FM5_Pan_LFO", "1B5", + "FM6_Pan_LFO", "1B6", + NULL +}; + +const char** DivPlatformYM2610B::getRegisterSheet() { + return regCheatSheetYM2610B; +} + +const char* DivPlatformYM2610B::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xy: Setup LFO (x: enable; y: speed)"; + break; + case 0x11: + return "11xx: Set feedback (0 to 7)"; + break; + case 0x12: + return "12xx: Set level of operator 1 (0 highest, 7F lowest)"; + break; + case 0x13: + return "13xx: Set level of operator 2 (0 highest, 7F lowest)"; + break; + case 0x14: + return "14xx: Set level of operator 3 (0 highest, 7F lowest)"; + break; + case 0x15: + return "15xx: Set level of operator 4 (0 highest, 7F lowest)"; + break; + case 0x16: + return "16xy: Set operator multiplier (x: operator from 1 to 4; y: multiplier)"; + break; + case 0x18: + return "18xx: Toggle extended channel 3 mode"; + break; + case 0x19: + return "19xx: Set attack of all operators (0 to 1F)"; + break; + case 0x1a: + return "1Axx: Set attack of operator 1 (0 to 1F)"; + break; + case 0x1b: + return "1Bxx: Set attack of operator 2 (0 to 1F)"; + break; + case 0x1c: + return "1Cxx: Set attack of operator 3 (0 to 1F)"; + break; + case 0x1d: + return "1Dxx: Set attack of operator 4 (0 to 1F)"; + break; + case 0x20: + return "20xx: Set SSG channel mode (bit 0: square; bit 1: noise; bit 2: envelope)"; + break; + case 0x21: + return "21xx: Set SSG noise frequency (0 to 1F)"; + break; + case 0x22: + return "22xy: Set SSG envelope mode (x: shape, y: enable for this channel)"; + break; + case 0x23: + return "23xx: Set SSG envelope period low byte"; + break; + case 0x24: + return "24xx: Set SSG envelope period high byte"; + break; + case 0x25: + return "25xx: SSG envelope slide up"; + break; + case 0x26: + return "26xx: SSG envelope slide down"; + break; + case 0x29: + return "29xy: Set SSG auto-envelope (x: numerator; y: denominator)"; + break; + case 0x30: + return "30xx: Toggle hard envelope reset on new notes"; + break; + } + return NULL; +} + +double DivPlatformYM2610B::NOTE_OPNB(int ch, int note) { + if (ch>8) { // ADPCM-B + return NOTE_ADPCMB(note); + } else if (ch>5) { // PSG + return NOTE_PERIODIC(note); + } + // FM + return NOTE_FREQUENCY(note); +} + +double DivPlatformYM2610B::NOTE_ADPCMB(int note) { + if (chan[15].sample>=0 && chan[15].samplesong.sampleLen) { + double off=(double)(parent->getSample(chan[15].sample)->centerRate)/8363.0; + return off*parent->calcBaseFreq((double)chipClock/144,65535,note,false); + } + return 0; +} + +void DivPlatformYM2610B::acquire(short* bufL, short* bufR, size_t start, size_t len) { + static int os[2]; + + for (size_t h=start; hwrite(0x0+((w.addr>>8)<<1),w.addr); + fm->write(0x1+((w.addr>>8)<<1),w.val); + regPool[w.addr&0x1ff]=w.val; + writes.pop(); + delay=4; + } + } + + fm->generate(&fmout); + + os[0]=fmout.data[0]+(fmout.data[2]>>1); + if (os[0]<-32768) os[0]=-32768; + if (os[0]>32767) os[0]=32767; + + os[1]=fmout.data[1]+(fmout.data[2]>>1); + if (os[1]<-32768) os[1]=-32768; + if (os[1]>32767) os[1]=32767; + + bufL[h]=os[0]; + bufR[h]=os[1]; + } +} + +void DivPlatformYM2610B::tick() { + // PSG + ay->tick(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); + + // FM + for (int i=0; i<6; i++) { + if (i==2 && extMode) continue; + chan[i].std.next(); + + if (chan[i].std.vol.had) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol.val))/127; + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); + chan[i].freqChanged=true; + } + } + + if (chan[i].std.alg.had) { + chan[i].state.alg=chan[i].std.alg.val; + rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + if (!parent->song.algMacroBehavior) for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + } + if (chan[i].std.fb.had) { + chan[i].state.fb=chan[i].std.fb.val; + rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + } + if (chan[i].std.fms.had) { + chan[i].state.fms=chan[i].std.fms.val; + rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + } + if (chan[i].std.ams.had) { + chan[i].state.ams=chan[i].std.ams.val; + rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + } + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + DivMacroInt::IntOp& m=chan[i].std.op[j]; + if (m.am.had) { + op.am=m.am.val; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.ar.had) { + op.ar=m.ar.val; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + if (m.dr.had) { + op.dr=m.dr.val; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.mult.had) { + op.mult=m.mult.val; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.rr.had) { + op.rr=m.rr.val; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.sl.had) { + op.sl=m.sl.val; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.tl.had) { + op.tl=127-m.tl.val; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + if (m.rs.had) { + op.rs=m.rs.val; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + if (m.dt.had) { + op.dt=m.dt.val; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.d2r.had) { + op.d2r=m.d2r.val; + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + } + if (m.ssg.had) { + op.ssgEnv=m.ssg.val; + rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); + } + } + + if (chan[i].keyOn || chan[i].keyOff) { + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + immWrite(baseAddr+ADDR_SL_RR,0x0f); + immWrite(baseAddr+ADDR_TL,0x7f); + oldWrites[baseAddr+ADDR_SL_RR]=-1; + oldWrites[baseAddr+ADDR_TL]=-1; + //rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + } + immWrite(0x28,0x00|konOffs[i]); + if (chan[i].hardReset && chan[i].keyOn) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + for (int k=0; k<100; k++) { + immWrite(baseAddr+ADDR_SL_RR,0x0f); + } + } + } + chan[i].keyOff=false; + } + } + // ADPCM-B + if (chan[15].furnacePCM) { + chan[15].std.next(); + + if (chan[15].std.vol.had) { + chan[15].outVol=(chan[15].vol*MIN(64,chan[15].std.vol.val))/64; + immWrite(0x1b,chan[15].outVol); + } + + if (chan[15].std.arp.had) { + if (!chan[15].inPorta) { + if (chan[15].std.arp.mode) { + chan[15].baseFreq=NOTE_ADPCMB(chan[15].std.arp.val); + } else { + chan[15].baseFreq=NOTE_ADPCMB(chan[15].note+(signed char)chan[15].std.arp.val); + } + } + chan[15].freqChanged=true; + } else { + if (chan[15].std.arp.mode && chan[15].std.arp.finished) { + chan[15].baseFreq=NOTE_ADPCMB(chan[15].note); + chan[15].freqChanged=true; + } + } + } + if (chan[15].freqChanged) { + chan[15].freq=parent->calcFreq(chan[15].baseFreq,chan[15].pitch,false,4); + immWrite(0x19,chan[15].freq&0xff); + immWrite(0x1a,(chan[15].freq>>8)&0xff); + chan[15].freqChanged=false; + } + + for (int i=16; i<512; i++) { + if (pendingWrites[i]!=oldWrites[i]) { + immWrite(i,pendingWrites[i]&0xff); + oldWrites[i]=pendingWrites[i]; + } + } + + for (int i=0; i<6; i++) { + if (i==2 && extMode) continue; + if (chan[i].freqChanged) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); + if (chan[i].freq>262143) chan[i].freq=262143; + int freqt=toFreq(chan[i].freq); + immWrite(chanOffs[i]+ADDR_FREQH,freqt>>8); + immWrite(chanOffs[i]+ADDR_FREQ,freqt&0xff); + chan[i].freqChanged=false; + } + if (chan[i].keyOn) { + immWrite(0x28,0xf0|konOffs[i]); + chan[i].keyOn=false; + } + } +} + +int DivPlatformYM2610B::octave(int freq) { + if (freq>=622.0f*128) { + return 128; + } else if (freq>=622.0f*64) { + return 64; + } else if (freq>=622.0f*32) { + return 32; + } else if (freq>=622.0f*16) { + return 16; + } else if (freq>=622.0f*8) { + return 8; + } else if (freq>=622.0f*4) { + return 4; + } else if (freq>=622.0f*2) { + return 2; + } else { + return 1; + } + return 1; +} + +int DivPlatformYM2610B::toFreq(int freq) { + if (freq>=622.0f*128) { + return 0x3800|((freq>>7)&0x7ff); + } else if (freq>=622.0f*64) { + return 0x3000|((freq>>6)&0x7ff); + } else if (freq>=622.0f*32) { + return 0x2800|((freq>>5)&0x7ff); + } else if (freq>=622.0f*16) { + return 0x2000|((freq>>4)&0x7ff); + } else if (freq>=622.0f*8) { + return 0x1800|((freq>>3)&0x7ff); + } else if (freq>=622.0f*4) { + return 0x1000|((freq>>2)&0x7ff); + } else if (freq>=622.0f*2) { + return 0x800|((freq>>1)&0x7ff); + } else { + return freq&0x7ff; + } +} + +int DivPlatformYM2610B::dispatch(DivCommand c) { + if (c.chan>5 && c.chan<9) { + c.chan-=6; + return ay->dispatch(c); + } + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + if (c.chan>14) { // ADPCM-B + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].furnacePCM=true; + } else { + chan[c.chan].furnacePCM=false; + } + if (skipRegisterWrites) break; + if (chan[c.chan].furnacePCM) { + chan[c.chan].std.init(ins); + if (!chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + immWrite(0x1b,chan[c.chan].outVol); + } + chan[c.chan].sample=ins->amiga.initSample; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + immWrite(0x12,(s->offB>>8)&0xff); + immWrite(0x13,s->offB>>16); + int end=s->offB+s->lengthB-1; + 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 + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + chan[c.chan].baseFreq=NOTE_ADPCMB(chan[c.chan].note); + chan[c.chan].freqChanged=true; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + } else { + immWrite(0x10,0x01); // reset + immWrite(0x12,0); + immWrite(0x13,0); + immWrite(0x14,0); + immWrite(0x15,0); + break; + } + } else { + chan[c.chan].sample=-1; + chan[c.chan].std.init(NULL); + chan[c.chan].outVol=chan[c.chan].vol; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + immWrite(0x10,0x01); // reset + immWrite(0x12,0); + immWrite(0x13,0); + immWrite(0x14,0); + immWrite(0x15,0); + break; + } + DivSample* s=parent->getSample(12*sampleBank+c.value%12); + immWrite(0x12,(s->offB>>8)&0xff); + immWrite(0x13,s->offB>>16); + int end=s->offB+s->lengthB-1; + 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 + chan[c.chan].baseFreq=(((unsigned int)s->rate)<<16)/(chipClock/144); + chan[c.chan].freqChanged=true; + } + break; + } + if (c.chan>8) { // ADPCM-A + if (skipRegisterWrites) break; + if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + immWrite(0x100,0x80|(1<<(c.chan-9))); + immWrite(0x110+c.chan-9,0); + immWrite(0x118+c.chan-9,0); + immWrite(0x120+c.chan-9,0); + immWrite(0x128+c.chan-9,0); + break; + } + DivSample* s=parent->getSample(12*sampleBank+c.value%12); + immWrite(0x110+c.chan-9,(s->offA>>8)&0xff); + immWrite(0x118+c.chan-9,s->offA>>16); + int end=s->offA+s->lengthA-1; + immWrite(0x120+c.chan-9,(end>>8)&0xff); + immWrite(0x128+c.chan-9,end>>16); + immWrite(0x108+(c.chan-9),isMuted[c.chan]?0:((chan[c.chan].pan<<6)|chan[c.chan].vol)); + immWrite(0x100,0x00|(1<<(c.chan-9))); + break; + } + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + chan[c.chan].std.init(ins); + if (c.chan<6) { + if (!chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + } + + if (chan[c.chan].insChanged) { + chan[c.chan].state=ins->fm; + } + + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + if (isOutput[chan[c.chan].state.alg][i]) { + if (!chan[c.chan].active || chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } + } else { + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); + } + } + if (chan[c.chan].insChanged) { + rWrite(chanOffs[c.chan]+ADDR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); + } + chan[c.chan].insChanged=false; + + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].portaPause=false; + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].keyOn=true; + chan[c.chan].active=true; + break; + } + case DIV_CMD_NOTE_OFF: + if (c.chan>14) { + immWrite(0x10,0x01); // reset + break; + } + if (c.chan>8) { + immWrite(0x100,0x80|(1<<(c.chan-9))); + break; + } + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + if (c.chan>14) { + immWrite(0x10,0x01); // reset + break; + } + if (c.chan>8) { + immWrite(0x100,0x80|(1<<(c.chan-9))); + break; + } + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + chan[c.chan].std.release(); + break; + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_VOLUME: { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + if (c.chan>14) { // ADPCM-B + immWrite(0x1b,chan[c.chan].outVol); + break; + } + if (c.chan>8) { // ADPCM-A + immWrite(0x108+(c.chan-9),isMuted[c.chan]?0:((chan[c.chan].pan<<6)|chan[c.chan].vol)); + break; + } + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + if (isOutput[chan[c.chan].state.alg][i]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + break; + } + case DIV_CMD_GET_VOLUME: { + return chan[c.chan].vol; + break; + } + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].insChanged=true; + } + chan[c.chan].ins=c.value; + break; + case DIV_CMD_PANNING: { + if (c.value==0) { + chan[c.chan].pan=3; + } else { + chan[c.chan].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); + } + if (c.chan>14) { + immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6)); + break; + } + if (c.chan>8) { + immWrite(0x108+(c.chan-9),isMuted[c.chan]?0:((chan[c.chan].pan<<6)|chan[c.chan].vol)); + break; + } + rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); + break; + } + case DIV_CMD_PITCH: { + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + if (c.chan>5) { // PSG, ADPCM-B + int destFreq=NOTE_OPNB(c.chan,c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + + int destFreq=NOTE_FREQUENCY(c.value2); + int newFreq; + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + newFreq=chan[c.chan].baseFreq+c.value*octave(chan[c.chan].baseFreq); + if (newFreq>=destFreq) { + newFreq=destFreq; + return2=true; + } + } else { + newFreq=chan[c.chan].baseFreq-c.value*octave(chan[c.chan].baseFreq); + if (newFreq<=destFreq) { + newFreq=destFreq; + return2=true; + } + } + if (!chan[c.chan].portaPause) { + if (octave(chan[c.chan].baseFreq)!=octave(newFreq)) { + chan[c.chan].portaPause=true; + break; + } + } + chan[c.chan].baseFreq=newFreq; + chan[c.chan].portaPause=false; + chan[c.chan].freqChanged=true; + if (return2) return 2; + break; + } + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + iface.sampleBank=sampleBank; + break; + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_OPNB(c.chan,c.value); + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_FM_LFO: { + rWrite(0x22,(c.value&7)|((c.value>>4)<<3)); + break; + } + case DIV_CMD_FM_FB: { + if (c.chan>5) break; + chan[c.chan].state.fb=c.value&7; + rWrite(chanOffs[c.chan]+ADDR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + break; + } + case DIV_CMD_FM_MULT: { + if (c.chan>5) break; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.mult=c.value2&15; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + break; + } + case DIV_CMD_FM_TL: { + if (c.chan>5) break; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.tl=c.value2; + if (isOutput[chan[c.chan].state.alg][c.value]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + break; + } + case DIV_CMD_FM_AR: { + if (c.chan>5) break; + if (c.value<0) { + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + } else { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + break; + } + case DIV_CMD_FM_HARD_RESET: + chan[c.chan].hardReset=c.value; + break; + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_GET_VOLMAX: + if (c.chan>14) return 255; + if (c.chan>8) return 31; + if (c.chan>5) return 15; + return 127; + break; + case DIV_CMD_PRE_PORTA: + if (c.chan>5) { + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_PRE_NOTE: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +void DivPlatformYM2610B::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (ch>14) { // ADPCM-B + immWrite(0x11,isMuted[ch]?0:(chan[ch].pan<<6)); + } + if (ch>8) { // ADPCM-A + immWrite(0x108+(ch-9),isMuted[ch]?0:((chan[ch].pan<<6)|chan[ch].vol)); + return; + } + if (ch>5) { // PSG + ay->muteChannel(ch-6,mute); + return; + } + // FM + rWrite(chanOffs[ch]+ADDR_LRAF,(isMuted[ch]?0:(chan[ch].pan<<6))|(chan[ch].state.fms&7)|((chan[ch].state.ams&3)<<4)); +} + +void DivPlatformYM2610B::forceIns() { + for (int i=0; i<6; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + 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)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + for (int i=9; i<16; i++) { + chan[i].insChanged=true; + } + + ay->forceIns(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); +} + +void* DivPlatformYM2610B::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformYM2610B::getRegisterPool() { + return regPool; +} + +int DivPlatformYM2610B::getRegisterPoolSize() { + return 512; +} + +void DivPlatformYM2610B::poke(unsigned int addr, unsigned short val) { + immWrite(addr,val); +} + +void DivPlatformYM2610B::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) immWrite(i.addr,i.val); +} + +void DivPlatformYM2610B::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,512); + if (dumpWrites) { + addWrite(0xffffffff,0); + } + fm->reset(); + for (int i=0; i<16; i++) { + chan[i]=DivPlatformYM2610B::Channel(); + } + for (int i=0; i<6; i++) { + chan[i].vol=0x7f; + chan[i].outVol=0x7f; + } + for (int i=6; i<9; i++) { + chan[i].vol=0x0f; + } + for (int i=9; i<15; i++) { + chan[i].vol=0x1f; + } + chan[15].vol=0xff; + + for (int i=0; i<512; i++) { + oldWrites[i]=-1; + pendingWrites[i]=-1; + } + + lastBusy=60; + sampleBank=0; + + delay=0; + + extMode=false; + + // LFO + immWrite(0x22,0x08); + + // PCM volume + immWrite(0x101,0x3f); // A + immWrite(0x1b,0xff); // B + + ay->reset(); + ay->getRegisterWrites().clear(); + ay->flushWrites(); +} + +bool DivPlatformYM2610B::isStereo() { + return true; +} + +bool DivPlatformYM2610B::keyOffAffectsArp(int ch) { + return (ch>5); +} + +void DivPlatformYM2610B::notifyInsChange(int ins) { + for (int i=0; i<16; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } + ay->notifyInsChange(ins); +} + +void DivPlatformYM2610B::notifyInsDeletion(void* ins) { + ay->notifyInsDeletion(ins); +} + +void DivPlatformYM2610B::setSkipRegisterWrites(bool value) { + DivDispatch::setSkipRegisterWrites(value); + ay->setSkipRegisterWrites(value); +} + +int DivPlatformYM2610B::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<16; i++) { + isMuted[i]=false; + } + chipClock=8000000; + rate=chipClock/16; + iface.parent=parent; + iface.sampleBank=0; + fm=new ymfm::ym2610b(iface); + // YM2149, 2MHz + ay=new DivPlatformAY8910; + ay->init(p,3,sugRate,35); + ay->toggleRegisterDump(true); + reset(); + return 16; +} + +void DivPlatformYM2610B::quit() { + ay->quit(); + delete ay; + delete fm; +} + +DivPlatformYM2610B::~DivPlatformYM2610B() { +} diff --git a/src/engine/platform/ym2610b.h b/src/engine/platform/ym2610b.h new file mode 100644 index 000000000..d6b616c50 --- /dev/null +++ b/src/engine/platform/ym2610b.h @@ -0,0 +1,125 @@ +/** + * 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 _YM2610B_H +#define _YM2610B_H +#include "../dispatch.h" +#include "../macroInt.h" +#include +#include "sound/ymfm/ymfm_opn.h" + +#include "ym2610.h" + +class DivPlatformYM2610B: public DivDispatch { + protected: + const unsigned short chanOffs[6]={ + 0x00, 0x01, 0x02, 0x100, 0x101, 0x102 + }; + + struct Channel { + DivInstrumentFM state; + unsigned char freqH, freqL; + int freq, baseFreq, pitch, note; + unsigned char ins, psgMode, autoEnvNum, autoEnvDen; + signed char konCycles; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, inPorta, furnacePCM, hardReset; + int vol, outVol; + int sample; + unsigned char pan; + DivMacroInt std; + Channel(): + freqH(0), + freqL(0), + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + psgMode(1), + autoEnvNum(0), + autoEnvDen(0), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + portaPause(false), + inPorta(false), + furnacePCM(false), + hardReset(false), + vol(0), + outVol(15), + sample(-1), + pan(3) {} + }; + Channel chan[16]; + bool isMuted[16]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + bool addrOrVal; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} + }; + std::queue writes; + ymfm::ym2610b* fm; + ymfm::ym2610b::output_data fmout; + DivYM2610Interface iface; + unsigned char regPool[512]; + unsigned char lastBusy; + + DivPlatformAY8910* ay; + unsigned char sampleBank; + + int delay; + + bool extMode; + + short oldWrites[512]; + short pendingWrites[512]; + + int octave(int freq); + int toFreq(int freq); + double NOTE_OPNB(int ch, int note); + double NOTE_ADPCMB(int note); + 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); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void notifyInsChange(int ins); + void notifyInsDeletion(void* ins); + void setSkipRegisterWrites(bool val); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformYM2610B(); +}; +#endif diff --git a/src/engine/platform/ym2610bext.cpp b/src/engine/platform/ym2610bext.cpp new file mode 100644 index 000000000..03e9f8699 --- /dev/null +++ b/src/engine/platform/ym2610bext.cpp @@ -0,0 +1,387 @@ +/** + * 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 "ym2610bext.h" +#include "../engine.h" +#include + +#include "ym2610shared.h" +#include "fmshared_OPN.h" + +int DivPlatformYM2610BExt::dispatch(DivCommand c) { + if (c.chan<2) { + return DivPlatformYM2610B::dispatch(c); + } + if (c.chan>5) { + c.chan-=3; + return DivPlatformYM2610B::dispatch(c); + } + int ch=c.chan-2; + int ordch=orderedOps[ch]; + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(opChan[ch].ins); + + unsigned short baseAddr=chanOffs[2]|opOffs[ordch]; + DivInstrumentFM::Operator op=ins->fm.op[ordch]; + // TODO: how does this work?! + if (isOpMuted[ch]) { + rWrite(baseAddr+0x40,127); + } else { + if (opChan[ch].insChanged) { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[ch].vol&0x7f))/127)); + } + } + if (opChan[ch].insChanged) { + rWrite(baseAddr+0x30,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+0x50,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+0x60,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+0x70,op.d2r&31); + rWrite(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+0x90,op.ssgEnv&15); + } + if (opChan[ch].insChanged) { // TODO how does this work? + rWrite(chanOffs[2]+0xb0,(ins->fm.alg&7)|(ins->fm.fb<<3)); + rWrite(chanOffs[2]+0xb4,(opChan[ch].pan<<6)|(ins->fm.fms&7)|((ins->fm.ams&3)<<4)); + } + opChan[ch].insChanged=false; + + if (c.value!=DIV_NOTE_NULL) { + opChan[ch].baseFreq=NOTE_FREQUENCY(c.value); + opChan[ch].portaPause=false; + opChan[ch].freqChanged=true; + } + opChan[ch].keyOn=true; + opChan[ch].active=true; + break; + } + case DIV_CMD_NOTE_OFF: + opChan[ch].keyOff=true; + opChan[ch].keyOn=false; + opChan[ch].active=false; + break; + case DIV_CMD_VOLUME: { + opChan[ch].vol=c.value; + DivInstrument* ins=parent->getIns(opChan[ch].ins); + unsigned short baseAddr=chanOffs[2]|opOffs[ordch]; + DivInstrumentFM::Operator op=ins->fm.op[ordch]; + if (isOpMuted[ch]) { + rWrite(baseAddr+0x40,127); + } else { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[ch].vol&0x7f))/127)); + } + break; + } + case DIV_CMD_GET_VOLUME: { + return opChan[ch].vol; + break; + } + case DIV_CMD_INSTRUMENT: + if (opChan[ch].ins!=c.value || c.value2==1) { + opChan[ch].insChanged=true; + } + opChan[ch].ins=c.value; + break; + case DIV_CMD_PANNING: { + if (c.value==0) { + opChan[ch].pan=3; + } else { + opChan[ch].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); + } + DivInstrument* ins=parent->getIns(opChan[ch].ins); + if (parent->song.sharedExtStat) { + for (int i=0; i<4; i++) { + if (ch==i) continue; + opChan[i].pan=opChan[ch].pan; + } + } + rWrite(chanOffs[2]+0xb4,(opChan[ch].pan<<6)|(ins->fm.fms&7)|((ins->fm.ams&3)<<4)); + break; + } + case DIV_CMD_PITCH: { + opChan[ch].pitch=c.value; + opChan[ch].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + int newFreq; + bool return2=false; + if (destFreq>opChan[ch].baseFreq) { + newFreq=opChan[ch].baseFreq+c.value*octave(opChan[ch].baseFreq); + if (newFreq>=destFreq) { + newFreq=destFreq; + return2=true; + } + } else { + newFreq=opChan[ch].baseFreq-c.value*octave(opChan[ch].baseFreq); + if (newFreq<=destFreq) { + newFreq=destFreq; + return2=true; + } + } + if (!opChan[ch].portaPause) { + if (octave(opChan[ch].baseFreq)!=octave(newFreq)) { + opChan[ch].portaPause=true; + break; + } + } + opChan[ch].baseFreq=newFreq; + opChan[ch].portaPause=false; + opChan[ch].freqChanged=true; + if (return2) return 2; + break; + } + case DIV_CMD_LEGATO: { + opChan[ch].baseFreq=NOTE_FREQUENCY(c.value); + opChan[ch].freqChanged=true; + break; + } + case DIV_CMD_FM_LFO: { + rWrite(0x22,(c.value&7)|((c.value>>4)<<3)); + break; + } + case DIV_CMD_FM_MULT: { // TODO + unsigned short baseAddr=chanOffs[2]|opOffs[orderedOps[c.value]]; + DivInstrument* ins=parent->getIns(opChan[ch].ins); + DivInstrumentFM::Operator op=ins->fm.op[orderedOps[c.value]]; + rWrite(baseAddr+0x30,(c.value2&15)|(dtTable[op.dt&7]<<4)); + break; + } + case DIV_CMD_FM_TL: { // TODO + unsigned short baseAddr=chanOffs[2]|opOffs[orderedOps[c.value]]; + DivInstrument* ins=parent->getIns(opChan[ch].ins); + if (isOutput[ins->fm.alg][c.value]) { + rWrite(baseAddr+0x40,127-(((127-c.value2)*(opChan[ch].vol&0x7f))/127)); + } else { + rWrite(baseAddr+0x40,c.value2); + } + break; + } + case DIV_CMD_FM_AR: { + DivInstrument* ins=parent->getIns(opChan[ch].ins); + if (c.value<0) { + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator op=ins->fm.op[i]; + unsigned short baseAddr=chanOffs[2]|opOffs[i]; + rWrite(baseAddr+0x50,(c.value2&31)|(op.rs<<6)); + } + } else { + DivInstrumentFM::Operator op=ins->fm.op[orderedOps[c.value]]; + unsigned short baseAddr=chanOffs[2]|opOffs[orderedOps[c.value]]; + rWrite(baseAddr+0x50,(c.value2&31)|(op.rs<<6)); + } + break; + } + case DIV_CMD_GET_VOLMAX: + return 127; + break; + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_PRE_PORTA: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +static int opChanOffsL[4]={ + 0xa9, 0xaa, 0xa8, 0xa2 +}; + +static int opChanOffsH[4]={ + 0xad, 0xae, 0xac, 0xa6 +}; + +void DivPlatformYM2610BExt::tick() { + if (extMode) { + bool writeSomething=false; + unsigned char writeMask=2; + for (int i=0; i<4; i++) { + writeMask|=opChan[i].active<<(4+i); + if (opChan[i].keyOn || opChan[i].keyOff) { + writeSomething=true; + writeMask&=~(1<<(4+i)); + opChan[i].keyOff=false; + } + } + if (writeSomething) { + immWrite(0x28,writeMask); + } + } + + DivPlatformYM2610B::tick(); + + bool writeNoteOn=false; + unsigned char writeMask=2; + if (extMode) for (int i=0; i<4; i++) { + if (opChan[i].freqChanged) { + opChan[i].freq=parent->calcFreq(opChan[i].baseFreq,opChan[i].pitch); + if (opChan[i].freq>262143) opChan[i].freq=262143; + int freqt=toFreq(opChan[i].freq); + opChan[i].freqH=freqt>>8; + opChan[i].freqL=freqt&0xff; + immWrite(opChanOffsH[i],opChan[i].freqH); + immWrite(opChanOffsL[i],opChan[i].freqL); + opChan[i].freqChanged=false; + } + writeMask|=opChan[i].active<<(4+i); + if (opChan[i].keyOn) { + writeNoteOn=true; + writeMask|=1<<(4+i); + opChan[i].keyOn=false; + } + } + if (writeNoteOn) { + immWrite(0x28,writeMask); + } +} + +void DivPlatformYM2610BExt::muteChannel(int ch, bool mute) { + if (ch<2) { + DivPlatformYM2610B::muteChannel(ch,mute); + return; + } + if (ch>5) { + DivPlatformYM2610B::muteChannel(ch-3,mute); + return; + } + isOpMuted[ch-2]=mute; + + int ordch=orderedOps[ch-2]; + DivInstrument* ins=parent->getIns(opChan[ch-2].ins); + unsigned short baseAddr=chanOffs[2]|opOffs[ordch]; + DivInstrumentFM::Operator op=ins->fm.op[ordch]; + if (isOpMuted[ch-2]) { + rWrite(baseAddr+0x40,127); + } else if (isOutput[ins->fm.alg][ordch]) { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[ch-2].vol&0x7f))/127)); + } else { + rWrite(baseAddr+0x40,op.tl); + } +} + +void DivPlatformYM2610BExt::forceIns() { + for (int i=0; i<6; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (i==2) { // extended channel + if (isOpMuted[j]) { + rWrite(baseAddr+0x40,127); + } else if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[j].vol&0x7f))/127)); + } else { + rWrite(baseAddr+0x40,op.tl); + } + } else { + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + 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)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + for (int i=6; i<16; i++) { + chan[i].insChanged=true; + } + ay->forceIns(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); + for (int i=0; i<4; i++) { + opChan[i].insChanged=true; + if (opChan[i].active) { + opChan[i].keyOn=true; + opChan[i].freqChanged=true; + } + } +} + + +void* DivPlatformYM2610BExt::getChanState(int ch) { + if (ch>=6) return &chan[ch-3]; + if (ch>=2) return &opChan[ch-2]; + return &chan[ch]; +} + +void DivPlatformYM2610BExt::reset() { + DivPlatformYM2610B::reset(); + + for (int i=0; i<4; i++) { + opChan[i]=DivPlatformYM2610BExt::OpChannel(); + opChan[i].vol=127; + } + + // channel 2 mode + immWrite(0x27,0x40); + extMode=true; +} + +bool DivPlatformYM2610BExt::keyOffAffectsArp(int ch) { + return (ch>8); +} + +void DivPlatformYM2610BExt::notifyInsChange(int ins) { + DivPlatformYM2610B::notifyInsChange(ins); + for (int i=0; i<4; i++) { + if (opChan[i].ins==ins) { + opChan[i].insChanged=true; + } + } +} + +int DivPlatformYM2610BExt::init(DivEngine* parent, int channels, int sugRate, unsigned int flags) { + DivPlatformYM2610B::init(parent,channels,sugRate,flags); + for (int i=0; i<4; i++) { + isOpMuted[i]=false; + } + + reset(); + return 19; +} + +void DivPlatformYM2610BExt::quit() { + DivPlatformYM2610B::quit(); +} + +DivPlatformYM2610BExt::~DivPlatformYM2610BExt() { +} diff --git a/src/engine/platform/ym2610bext.h b/src/engine/platform/ym2610bext.h new file mode 100644 index 000000000..25ca59196 --- /dev/null +++ b/src/engine/platform/ym2610bext.h @@ -0,0 +1,51 @@ +/** + * 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 "../dispatch.h" + +#include "ym2610b.h" + +class DivPlatformYM2610BExt: public DivPlatformYM2610B { + struct OpChannel { + DivMacroInt std; + unsigned char freqH, freqL; + int freq, baseFreq, pitch; + unsigned char ins; + signed char konCycles; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause; + int vol; + unsigned char pan; + OpChannel(): freqH(0), freqL(0), freq(0), baseFreq(0), pitch(0), ins(-1), active(false), insChanged(true), freqChanged(false), keyOn(false), keyOff(false), portaPause(false), vol(0), pan(3) {} + }; + OpChannel opChan[4]; + bool isOpMuted[4]; + friend void putDispatchChan(void*,int,int); + public: + int dispatch(DivCommand c); + void* getChanState(int chan); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + void notifyInsChange(int ins); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformYM2610BExt(); +}; diff --git a/src/engine/platform/ym2610ext.cpp b/src/engine/platform/ym2610ext.cpp index 000ab69da..40294733d 100644 --- a/src/engine/platform/ym2610ext.cpp +++ b/src/engine/platform/ym2610ext.cpp @@ -22,6 +22,7 @@ #include #include "ym2610shared.h" +#include "fmshared_OPN.h" int DivPlatformYM2610Ext::dispatch(DivCommand c) { if (c.chan<1) { @@ -63,6 +64,7 @@ int DivPlatformYM2610Ext::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { opChan[ch].baseFreq=NOTE_FREQUENCY(c.value); + opChan[ch].portaPause=false; opChan[ch].freqChanged=true; } opChan[ch].keyOn=true; @@ -97,19 +99,18 @@ int DivPlatformYM2610Ext::dispatch(DivCommand c) { opChan[ch].ins=c.value; break; case DIV_CMD_PANNING: { - switch (c.value) { - case 0x01: - opChan[ch].pan=1; - break; - case 0x10: - opChan[ch].pan=2; - break; - default: - opChan[ch].pan=3; - break; + if (c.value==0) { + opChan[ch].pan=3; + } else { + opChan[ch].pan=((c.value&15)>0)|(((c.value>>4)>0)<<1); } DivInstrument* ins=parent->getIns(opChan[ch].ins); - // TODO: ??? + if (parent->song.sharedExtStat) { + for (int i=0; i<4; i++) { + if (ch==i) continue; + opChan[i].pan=opChan[ch].pan; + } + } rWrite(chanOffs[1]+0xb4,(opChan[ch].pan<<6)|(ins->fm.fms&7)|((ins->fm.ams&3)<<4)); break; } @@ -147,15 +148,15 @@ int DivPlatformYM2610Ext::dispatch(DivCommand c) { if (return2) return 2; break; } - case DIV_CMD_SAMPLE_MODE: { - // ignored on extended channel 2 mode. - break; - } case DIV_CMD_LEGATO: { opChan[ch].baseFreq=NOTE_FREQUENCY(c.value); opChan[ch].freqChanged=true; break; } + case DIV_CMD_FM_LFO: { + rWrite(0x22,(c.value&7)|((c.value>>4)<<3)); + break; + } case DIV_CMD_FM_MULT: { // TODO unsigned short baseAddr=chanOffs[1]|opOffs[orderedOps[c.value]]; DivInstrument* ins=parent->getIns(opChan[ch].ins); @@ -280,7 +281,52 @@ void DivPlatformYM2610Ext::muteChannel(int ch, bool mute) { } void DivPlatformYM2610Ext::forceIns() { - DivPlatformYM2610::forceIns(); + for (int i=0; i<4; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (i==1) { // extended channel + if (isOpMuted[j]) { + rWrite(baseAddr+0x40,127); + } else if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+0x40,127-(((127-op.tl)*(opChan[j].vol&0x7f))/127)); + } else { + rWrite(baseAddr+0x40,op.tl); + } + } else { + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + 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)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + for (int i=4; i<14; i++) { + chan[i].insChanged=true; + } + ay->forceIns(); + ay->flushWrites(); + for (DivRegWrite& i: ay->getRegisterWrites()) { + immWrite(i.addr&15,i.val); + } + ay->getRegisterWrites().clear(); for (int i=0; i<4; i++) { opChan[i].insChanged=true; if (opChan[i].active) { @@ -338,4 +384,4 @@ void DivPlatformYM2610Ext::quit() { } DivPlatformYM2610Ext::~DivPlatformYM2610Ext() { -} \ No newline at end of file +} diff --git a/src/engine/platform/ym2610shared.h b/src/engine/platform/ym2610shared.h index b548a0ec1..8d8847c2e 100644 --- a/src/engine/platform/ym2610shared.h +++ b/src/engine/platform/ym2610shared.h @@ -17,9 +17,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -static unsigned short chanOffs[4]={ - 0x01, 0x02, 0x101, 0x102 -}; static unsigned short opOffs[4]={ 0x00, 0x04, 0x08, 0x0c }; @@ -35,7 +32,7 @@ static bool isOutput[8][4]={ {true ,true ,true ,true}, }; static unsigned char dtTable[8]={ - 7,6,5,0,1,2,3,0 + 7,6,5,0,1,2,3,4 }; static int orderedOps[4]={ @@ -45,4 +42,4 @@ static int orderedOps[4]={ #define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} #define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } -#define CHIP_FREQBASE 9440540 \ No newline at end of file +#define CHIP_FREQBASE 9440540 diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 656bc35c2..97047b64d 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -17,9 +17,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "blip_buf.h" -#include "song.h" -#include "wavetable.h" #define _USE_MATH_DEFINES #include "dispatch.h" #include "engine.h" @@ -42,6 +39,7 @@ const char* notes[12]={ "C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-" }; +// update this when adding new commands. const char* cmdName[DIV_CMD_MAX]={ "NOTE_ON", "NOTE_OFF", @@ -61,7 +59,9 @@ const char* cmdName[DIV_CMD_MAX]={ "SAMPLE_MODE", "SAMPLE_FREQ", "SAMPLE_BANK", + "SAMPLE_POS", + "FM_HARD_RESET", "FM_LFO", "FM_LFO_WAVE", "FM_TL", @@ -107,8 +107,20 @@ const char* cmdName[DIV_CMD_MAX]={ "AY_NOISE_MASK_AND", "AY_NOISE_MASK_OR", "AY_AUTO_ENVELOPE", + "AY_IO_WRITE", + "AY_AUTO_PWM", + + "FDS_MOD_DEPTH", + "FDS_MOD_HIGH", + "FDS_MOD_LOW", + "FDS_MOD_POS", + "FDS_MOD_WAVE", "SAA_ENVELOPE", + + "AMIGA_FILTER", + "AMIGA_AM", + "AMIGA_PM", "LYNX_LFSR_LOAD", @@ -116,6 +128,28 @@ const char* cmdName[DIV_CMD_MAX]={ "QSOUND_ECHO_DELAY", "QSOUND_ECHO_LEVEL", + "X1_010_ENVELOPE_SHAPE", + "X1_010_ENVELOPE_ENABLE", + "X1_010_ENVELOPE_MODE", + "X1_010_ENVELOPE_PERIOD", + "X1_010_ENVELOPE_SLIDE", + "X1_010_AUTO_ENVELOPE", + + "WS_SWEEP_TIME", + "WS_SWEEP_AMOUNT", + + "N163_WAVE_POSITION", + "N163_WAVE_LENGTH", + "N163_WAVE_MODE", + "N163_WAVE_LOAD", + "N163_WAVE_LOADPOS", + "N163_WAVE_LOADLEN", + "N163_CHANNEL_LIMIT", + "N163_GLOBAL_WAVE_LOAD", + "N163_GLOBAL_WAVE_LOADPOS", + "N163_GLOBAL_WAVE_LOADLEN", + "N163_GLOBAL_WAVE_LOADMODE", + "ALWAYS_SET_VOLUME" }; @@ -142,7 +176,57 @@ int DivEngine::dispatchCmd(DivCommand c) { if (cmdStreamEnabled && cmdStream.size()<2000) { cmdStream.push_back(c); } + + if (output) if (!skipping && output->midiOut!=NULL) { + if (output->midiOut->isDeviceOpen()) { + int scaledVol=(chan[c.chan].volume*127)/MAX(1,chan[c.chan].volMax); + if (scaledVol<0) scaledVol=0; + if (scaledVol>127) scaledVol=127; + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + case DIV_CMD_LEGATO: + if (chan[c.chan].curMidiNote>=0) { + output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); + } + if (c.value!=DIV_NOTE_NULL) chan[c.chan].curMidiNote=c.value+12; + output->midiOut->send(TAMidiMessage(0x90|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); + break; + case DIV_CMD_NOTE_OFF: + case DIV_CMD_NOTE_OFF_ENV: + if (chan[c.chan].curMidiNote>=0) { + output->midiOut->send(TAMidiMessage(0x80|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); + } + chan[c.chan].curMidiNote=-1; + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].lastIns!=c.value) { + output->midiOut->send(TAMidiMessage(0xc0|(c.chan&15),c.value,0)); + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].curMidiNote>=0 && chan[c.chan].midiAftertouch) { + chan[c.chan].midiAftertouch=false; + output->midiOut->send(TAMidiMessage(0xa0|(c.chan&15),chan[c.chan].curMidiNote,scaledVol)); + } + break; + case DIV_CMD_PITCH: { + int pitchBend=8192+(c.value<<5); + if (pitchBend<0) pitchBend=0; + if (pitchBend>16383) pitchBend=16383; + if (pitchBend!=chan[c.chan].midiPitch) { + chan[c.chan].midiPitch=pitchBend; + output->midiOut->send(TAMidiMessage(0xe0|(c.chan&15),pitchBend&0x7f,pitchBend>>7)); + } + break; + } + default: + break; + } + } + } + c.chan=dispatchChanOfChan[c.dis]; + return disCont[dispatchOfChan[c.dis]].dispatch->dispatch(c); } @@ -157,6 +241,23 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe case 0x20: // SN noise mode dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); break; + case 0x30: // toggle hard-reset + dispatchCmd(DivCommand(DIV_CMD_FM_HARD_RESET,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_YM2151: + case DIV_SYSTEM_YM2610: + case DIV_SYSTEM_YM2610_EXT: + case DIV_SYSTEM_YM2610B: + case DIV_SYSTEM_YM2610B_EXT: + case DIV_SYSTEM_OPZ: + switch (effect) { + case 0x30: // toggle hard-reset + dispatchCmd(DivCommand(DIV_CMD_FM_HARD_RESET,ch,effectVal)); + break; default: return false; } @@ -210,6 +311,7 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe } break; case DIV_SYSTEM_NES: + case DIV_SYSTEM_MMC5: switch (effect) { case 0x12: // duty or noise mode dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); @@ -224,13 +326,102 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe return false; } break; - case DIV_SYSTEM_LYNX: - if (effect>=0x30 && effect<0x40) { - int value = ((int)(effect&0x0f)<<8)|effectVal; - dispatchCmd(DivCommand(DIV_CMD_LYNX_LFSR_LOAD,ch,value)); - break; + case DIV_SYSTEM_FDS: + switch (effect) { + case 0x10: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x11: // modulation depth + dispatchCmd(DivCommand(DIV_CMD_FDS_MOD_DEPTH,ch,effectVal)); + break; + case 0x12: // modulation enable/high + dispatchCmd(DivCommand(DIV_CMD_FDS_MOD_HIGH,ch,effectVal)); + break; + case 0x13: // modulation low + dispatchCmd(DivCommand(DIV_CMD_FDS_MOD_LOW,ch,effectVal)); + break; + case 0x14: // modulation pos + dispatchCmd(DivCommand(DIV_CMD_FDS_MOD_POS,ch,effectVal)); + break; + case 0x15: // modulation wave + dispatchCmd(DivCommand(DIV_CMD_FDS_MOD_WAVE,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_OPL_DRUMS: + case DIV_SYSTEM_OPL2_DRUMS: + case DIV_SYSTEM_OPL3_DRUMS: + switch (effect) { + case 0x18: // drum mode toggle + dispatchCmd(DivCommand(DIV_CMD_FM_EXTCH,ch,effectVal)); + break; + case 0x30: // toggle hard-reset + dispatchCmd(DivCommand(DIV_CMD_FM_HARD_RESET,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_VRC7: + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL3: + switch (effect) { + case 0x30: // toggle hard-reset + dispatchCmd(DivCommand(DIV_CMD_FM_HARD_RESET,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_N163: + switch (effect) { + case 0x10: // select instrument waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x11: // select instrument waveform position in RAM + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_POSITION,ch,effectVal)); + break; + case 0x12: // select instrument waveform length in RAM + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_LENGTH,ch,effectVal)); + break; + case 0x13: // change instrument waveform update mode + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_MODE,ch,effectVal)); + break; + case 0x14: // select waveform for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_LOAD,ch,effectVal)); + break; + case 0x15: // select waveform position for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_LOADPOS,ch,effectVal)); + break; + case 0x16: // select waveform length for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_LOADLEN,ch,effectVal)); + break; + case 0x17: // change waveform load mode + dispatchCmd(DivCommand(DIV_CMD_N163_WAVE_LOADMODE,ch,effectVal)); + break; + case 0x18: // change channel limits + dispatchCmd(DivCommand(DIV_CMD_N163_CHANNEL_LIMIT,ch,effectVal)); + break; + case 0x20: // (global) select waveform for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_GLOBAL_WAVE_LOAD,ch,effectVal)); + break; + case 0x21: // (global) select waveform position for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_GLOBAL_WAVE_LOADPOS,ch,effectVal)); + break; + case 0x22: // (global) select waveform length for load to RAM + dispatchCmd(DivCommand(DIV_CMD_N163_GLOBAL_WAVE_LOADLEN,ch,effectVal)); + break; + case 0x23: // (global) change waveform load mode + dispatchCmd(DivCommand(DIV_CMD_N163_GLOBAL_WAVE_LOADMODE,ch,effectVal)); + break; + default: + return false; } - return false; break; case DIV_SYSTEM_QSOUND: switch (effect) { @@ -249,13 +440,85 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe break; } break; + case DIV_SYSTEM_X1_010: + switch (effect) { + case 0x10: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x11: // select envelope shape + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SHAPE,ch,effectVal)); + break; + case 0x17: // PCM enable + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0))); + break; + default: + return false; + } + break; + case DIV_SYSTEM_SWAN: + switch (effect) { + case 0x10: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x11: // noise mode + dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); + break; + case 0x12: // sweep period + dispatchCmd(DivCommand(DIV_CMD_WS_SWEEP_TIME,ch,effectVal)); + break; + case 0x13: // sweep amount + dispatchCmd(DivCommand(DIV_CMD_WS_SWEEP_AMOUNT,ch,effectVal)); + break; + case 0x17: // PCM enable + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0))); + break; + default: + return false; + } + break; + case DIV_SYSTEM_VERA: + switch (effect) { + case 0x20: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + case 0x22: // duty + dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_BUBSYS_WSG: + case DIV_SYSTEM_PET: + case DIV_SYSTEM_VIC20: + switch (effect) { + case 0x10: // select waveform + dispatchCmd(DivCommand(DIV_CMD_WAVE,ch,effectVal)); + break; + default: + return false; + } + break; + case DIV_SYSTEM_VRC6: + switch (effect) { + case 0x12: // duty or noise mode + dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); + break; + case 0x17: // PCM enable + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0))); + break; + default: + return false; + } + break; default: return false; } return true; } -#define IS_YM2610 (sysOfChan[ch]==DIV_SYSTEM_YM2610 || sysOfChan[ch]==DIV_SYSTEM_YM2610_EXT || sysOfChan[ch]==DIV_SYSTEM_YM2610_FULL || sysOfChan[ch]==DIV_SYSTEM_YM2610_FULL_EXT) +#define IS_YM2610 (sysOfChan[ch]==DIV_SYSTEM_YM2610 || sysOfChan[ch]==DIV_SYSTEM_YM2610_EXT || sysOfChan[ch]==DIV_SYSTEM_YM2610_FULL || sysOfChan[ch]==DIV_SYSTEM_YM2610_FULL_EXT || sysOfChan[ch]==DIV_SYSTEM_YM2610B || sysOfChan[ch]==DIV_SYSTEM_YM2610B_EXT) +#define IS_OPM_LIKE (sysOfChan[ch]==DIV_SYSTEM_YM2151 || sysOfChan[ch]==DIV_SYSTEM_OPZ) bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char effectVal) { switch (sysOfChan[ch]) { @@ -266,9 +529,12 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL: case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B: + case DIV_SYSTEM_YM2610B_EXT: + case DIV_SYSTEM_OPZ: switch (effect) { case 0x10: // LFO or noise mode - if (sysOfChan[ch]==DIV_SYSTEM_YM2151) { + if (IS_OPM_LIKE) { dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_FREQ,ch,effectVal)); } else { dispatchCmd(DivCommand(DIV_CMD_FM_LFO,ch,effectVal)); @@ -295,12 +561,12 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char } break; case 0x17: // arcade LFO - if (sysOfChan[ch]==DIV_SYSTEM_YM2151) { + if (IS_OPM_LIKE) { dispatchCmd(DivCommand(DIV_CMD_FM_LFO,ch,effectVal)); } break; case 0x18: // EXT or LFO waveform - if (sysOfChan[ch]==DIV_SYSTEM_YM2151) { + if (IS_OPM_LIKE) { dispatchCmd(DivCommand(DIV_CMD_FM_LFO_WAVE,ch,effectVal)); } else { dispatchCmd(DivCommand(DIV_CMD_FM_EXTCH,ch,effectVal)); @@ -389,8 +655,6 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char dispatchCmd(DivCommand(DIV_CMD_FM_MULT,ch,(effectVal>>4)-1,effectVal&15)); } break; - case 0x18: // drum mode toggle - break; case 0x19: // AR global dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,-1,effectVal&31)); break; @@ -404,6 +668,58 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char return false; } break; + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL3: + case DIV_SYSTEM_OPL_DRUMS: + case DIV_SYSTEM_OPL2_DRUMS: + case DIV_SYSTEM_OPL3_DRUMS: + switch (effect) { + case 0x10: // DAM + dispatchCmd(DivCommand(DIV_CMD_FM_LFO,ch,effectVal&1)); + break; + case 0x11: // FB + dispatchCmd(DivCommand(DIV_CMD_FM_FB,ch,effectVal&7)); + break; + case 0x12: // TL op1 + dispatchCmd(DivCommand(DIV_CMD_FM_TL,ch,0,effectVal&0x3f)); + break; + case 0x13: // TL op2 + dispatchCmd(DivCommand(DIV_CMD_FM_TL,ch,1,effectVal&0x3f)); + break; + case 0x14: // TL op3 + dispatchCmd(DivCommand(DIV_CMD_FM_TL,ch,2,effectVal&0x3f)); + break; + case 0x15: // TL op4 + dispatchCmd(DivCommand(DIV_CMD_FM_TL,ch,3,effectVal&0x3f)); + break; + case 0x16: // MULT + if ((effectVal>>4)>0 && (effectVal>>4)<5) { + dispatchCmd(DivCommand(DIV_CMD_FM_MULT,ch,(effectVal>>4)-1,effectVal&15)); + } + break; + case 0x17: // DVB + dispatchCmd(DivCommand(DIV_CMD_FM_LFO,ch,2+(effectVal&1))); + break; + case 0x19: // AR global + dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,-1,effectVal&15)); + break; + case 0x1a: // AR op1 + dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,0,effectVal&15)); + break; + case 0x1b: // AR op2 + dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,1,effectVal&15)); + break; + case 0x1c: // AR op3 + dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,2,effectVal&15)); + break; + case 0x1d: // AR op4 + dispatchCmd(DivCommand(DIV_CMD_FM_AR,ch,3,effectVal&15)); + break; + default: + return false; + } + break; case DIV_SYSTEM_C64_6581: case DIV_SYSTEM_C64_8580: switch (effect) { case 0x10: // select waveform @@ -486,6 +802,15 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char case 0x29: // auto-envelope dispatchCmd(DivCommand(DIV_CMD_AY_AUTO_ENVELOPE,ch,effectVal)); break; + case 0x2d: // TEST + dispatchCmd(DivCommand(DIV_CMD_AY_IO_WRITE,ch,255,effectVal)); + break; + case 0x2e: // I/O port A + dispatchCmd(DivCommand(DIV_CMD_AY_IO_WRITE,ch,0,effectVal)); + break; + case 0x2f: // I/O port B + dispatchCmd(DivCommand(DIV_CMD_AY_IO_WRITE,ch,1,effectVal)); + break; default: return false; } @@ -514,6 +839,21 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char return false; } break; + case DIV_SYSTEM_AMIGA: + switch (effect) { + case 0x10: // toggle filter + dispatchCmd(DivCommand(DIV_CMD_AMIGA_FILTER,ch,effectVal)); + break; + case 0x11: // toggle AM + dispatchCmd(DivCommand(DIV_CMD_AMIGA_AM,ch,effectVal)); + break; + case 0x12: // toggle PM + dispatchCmd(DivCommand(DIV_CMD_AMIGA_PM,ch,effectVal)); + break; + default: + return false; + } + break; case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: switch (effect) { @@ -524,6 +864,38 @@ bool DivEngine::perSystemPostEffect(int ch, unsigned char effect, unsigned char return false; } break; + case DIV_SYSTEM_LYNX: + if (effect>=0x30 && effect<0x40) { + int value = ((int)(effect&0x0f)<<8)|effectVal; + dispatchCmd(DivCommand(DIV_CMD_LYNX_LFSR_LOAD,ch,value)); + break; + } + return false; + break; + case DIV_SYSTEM_X1_010: + switch (effect) { + case 0x20: // PCM frequency + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_FREQ,ch,effectVal)); + break; + case 0x22: // envelope mode + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_MODE,ch,effectVal)); + break; + case 0x23: // envelope period + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_PERIOD,ch,effectVal)); + break; + case 0x25: // envelope slide up + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SLIDE,ch,effectVal)); + break; + case 0x26: // envelope slide down + dispatchCmd(DivCommand(DIV_CMD_X1_010_ENVELOPE_SLIDE,ch,-effectVal)); + break; + case 0x29: // auto-envelope + dispatchCmd(DivCommand(DIV_CMD_X1_010_AUTO_ENVELOPE,ch,effectVal)); + break; + default: + return false; + } + break; default: return false; } @@ -560,8 +932,13 @@ void DivEngine::processRow(int i, bool afterDelay) { if (chan[i].delayLocked) return; // 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]) { + chan[i].lastIns=pat->data[whatRow][2]; + insChanged=true; + } } // note if (pat->data[whatRow][0]==100) { // note off @@ -577,10 +954,10 @@ void DivEngine::processRow(int i, bool afterDelay) { if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; - if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { + /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; - } + }*/ } chan[i].scheduledSlideReset=true; } @@ -598,10 +975,10 @@ void DivEngine::processRow(int i, bool afterDelay) { if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; - if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { + /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; - } + }*/ } chan[i].scheduledSlideReset=true; } @@ -625,6 +1002,9 @@ void DivEngine::processRow(int i, bool afterDelay) { // volume if (pat->data[whatRow][3]!=-1) { if (dispatchCmd(DivCommand(DIV_ALWAYS_SET_VOLUME,i)) || (MIN(chan[i].volMax,chan[i].volume)>>8)!=pat->data[whatRow][3]) { + if (pat->data[whatRow][0]==0 && pat->data[whatRow][1]==0) { + chan[i].midiAftertouch=true; + } chan[i].volume=pat->data[whatRow][3]<<8; dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } @@ -633,6 +1013,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].retrigSpeed=0; short lastSlide=-1; + bool calledPorta=false; // effects for (int j=0; j>4; 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 + // TODO + // this effect is really weird. i thought it would alter the tremolo depth but turns out it's completely different + // this is how it works: + // - 07xy enables tremolo + // - when enabled, a "low" boundary is calculated based on the current volume + // - then a volume slide down starts to the low boundary, and then when this is reached a volume slide up begins + // - this process repeats until 0700 or 0Axy are found + // - note that a volume value does not stop tremolo - instead it glitches this whole thing up + break; case 0x0a: // volume ramp + // TODO: non-0x-or-x0 value should be treated as 00 if (effectVal!=0) { if ((effectVal&15)!=0) { chan[i].volSpeed=-(effectVal&15)*64; @@ -743,6 +1136,9 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x00: // arpeggio chan[i].arp=effectVal; + if (chan[i].arp==0 && song.arp0Reset) { + chan[i].resetArp=true; + } break; case 0x0c: // retrigger if (effectVal!=0) { @@ -750,14 +1146,17 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].retrigTick=0; } break; - case 0xc0: case 0xc1: case 0xc2: case 0xc3: // set Hz - divider=((effect&0x3)<<8)|effectVal; - if (divider<10) divider=10; - cycles=((int)(got.rate)<0) { @@ -769,7 +1168,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].portaSpeed=(effectVal>>4)*4; chan[i].portaStop=true; chan[i].nowYouCanStop=false; - chan[i].stopOnOff=true; + chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! chan[i].scheduledSlideReset=false; if ((effectVal&15)!=0) { chan[i].inPorta=true; @@ -785,7 +1184,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].portaSpeed=(effectVal>>4)*4; chan[i].portaStop=true; chan[i].nowYouCanStop=false; - chan[i].stopOnOff=true; + chan[i].stopOnOff=song.stopPortaOnNoteOff; // what?! chan[i].scheduledSlideReset=false; if ((effectVal&15)!=0) { chan[i].inPorta=true; @@ -809,7 +1208,7 @@ void DivEngine::processRow(int i, bool afterDelay) { if (chan[i].pitch<-128) chan[i].pitch=-128; if (chan[i].pitch>127) chan[i].pitch=127; } - chan[i].pitch+=globalPitch; + //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))); break; case 0xea: // legato mode @@ -831,6 +1230,58 @@ void DivEngine::processRow(int i, bool afterDelay) { case 0xef: // global pitch globalPitch+=(signed char)(effectVal-0x80); break; + case 0xf0: // set Hz by tempo + divider=(double)effectVal*2.0/5.0; + if (divider<10) divider=10; + cycles=got.rate*pow(2,MASTER_CLOCK_PREC)/divider; + clockDrift=0; + break; + case 0xf1: // single pitch ramp up + case 0xf2: // single pitch ramp down + if (effect==0xf1) { + chan[i].portaNote=song.limitSlides?0x60:255; + } else { + chan[i].portaNote=song.limitSlides?disCont[dispatchOfChan[i]].dispatch->getPortaFloor(dispatchChanOfChan[i]):-60; + } + chan[i].portaSpeed=effectVal; + chan[i].portaStop=true; + chan[i].nowYouCanStop=false; + chan[i].stopOnOff=false; + chan[i].scheduledSlideReset=false; + chan[i].inPorta=false; + if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,true,0)); + dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed,chan[i].portaNote)); + chan[i].portaNote=-1; + chan[i].portaSpeed=-1; + chan[i].inPorta=false; + if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); + break; + case 0xf3: // fine volume ramp up + chan[i].volSpeed=effectVal; + break; + case 0xf4: // fine volume ramp down + chan[i].volSpeed=-effectVal; + 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)); + 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)); + break; + case 0xfa: // fast volume ramp + if (effectVal!=0) { + if ((effectVal&15)!=0) { + chan[i].volSpeed=-(effectVal&15)*256; + } else { + chan[i].volSpeed=(effectVal>>4)*256; + } + } else { + chan[i].volSpeed=0; + } + break; + case 0xff: // stop song freelance=false; playing=false; @@ -844,8 +1295,14 @@ void DivEngine::processRow(int i, bool afterDelay) { } } + if (insChanged && (chan[i].inPorta || calledPorta) && song.newInsTriggersInPorta) { + dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); + } + if (chan[i].doNote) { - chan[i].vibratoPos=0; + if (!song.continuousVibrato) { + chan[i].vibratoPos=0; + } 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)); @@ -957,39 +1414,75 @@ void DivEngine::nextRow() { if (haltOn==DIV_HALT_PATTERN) halted=true; } - if (speedAB) { - ticks=speed2*(song.timeBase+1); - nextSpeed=speed1; + if (song.brokenSpeedSel) { + if ((song.patLen&1) && curOrder&1) { + ticks=((curRow&1)?speed2:speed1)*(song.timeBase+1); + nextSpeed=(curRow&1)?speed1:speed2; + } else { + ticks=((curRow&1)?speed1:speed2)*(song.timeBase+1); + nextSpeed=(curRow&1)?speed2:speed1; + } } else { - ticks=speed1*(song.timeBase+1); - nextSpeed=speed2; + if (speedAB) { + ticks=speed2*(song.timeBase+1); + nextSpeed=speed1; + } else { + ticks=speed1*(song.timeBase+1); + nextSpeed=speed2; + } + speedAB=!speedAB; } - speedAB=!speedAB; // post row details for (int i=0; idata[curRow][0]==0 && pat->data[curRow][1]==0)) { - if (pat->data[curRow][0]!=100) { - if (!chan[i].legato) dispatchCmd(DivCommand(DIV_CMD_PRE_NOTE,i,ticks)); + 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 (song.oneTickCut) { + bool doPrepareCut=true; + + for (int j=0; jdata[curRow][4+(j<<1)]==0x03) { + doPrepareCut=false; + break; + } + if (pat->data[curRow][4+(j<<1)]==0xea) { + if (pat->data[curRow][5+(j<<1)]>0) { + doPrepareCut=false; + break; + } + } + } + if (doPrepareCut) chan[i].cut=ticks; + } + } } } } if (haltOn==DIV_HALT_ROW) halted=true; + firstTick=true; } bool DivEngine::nextTick(bool noAccum) { bool ret=false; if (divider<10) divider=10; - cycles=((int)(got.rate)<=divider) { clockDrift-=divider; cycles++; } + // MIDI clock + if (output) if (!skipping && output->midiOut!=NULL) { + output->midiOut->send(TAMidiMessage(TA_MIDI_CLOCK,0,0)); + } + while (!pendingNotes.empty()) { DivNoteEvent& note=pendingNotes.front(); if (note.on) { @@ -1029,23 +1522,25 @@ bool DivEngine::nextTick(bool noAccum) { keyHit[i]=true; } } - if (chan[i].volSpeed!=0) { - chan[i].volume=(chan[i].volume&0xff)|(dispatchCmd(DivCommand(DIV_CMD_GET_VOLUME,i))<<8); - chan[i].volume+=chan[i].volSpeed; - if (chan[i].volume>chan[i].volMax) { - chan[i].volume=chan[i].volMax; - chan[i].volSpeed=0; - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); - } else if (chan[i].volume<0) { - chan[i].volSpeed=0; - if (song.legacyVolumeSlides) { - chan[i].volume=chan[i].volMax+1; + if (!song.noSlidesOnFirstTick || !firstTick) { + if (chan[i].volSpeed!=0) { + chan[i].volume=(chan[i].volume&0xff)|(dispatchCmd(DivCommand(DIV_CMD_GET_VOLUME,i))<<8); + chan[i].volume+=chan[i].volSpeed; + if (chan[i].volume>chan[i].volMax) { + chan[i].volume=chan[i].volMax; + chan[i].volSpeed=0; + dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + } else if (chan[i].volume<0) { + chan[i].volSpeed=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)); } else { - chan[i].volume=0; + dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); - } else { - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } } if (chan[i].vibratoDepth>0) { @@ -1063,13 +1558,15 @@ bool DivEngine::nextTick(bool noAccum) { break; } } - if ((chan[i].keyOn || chan[i].keyOff) && chan[i].portaSpeed>0) { - if (dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed,chan[i].portaNote))==2 && chan[i].portaStop && song.targetResetsSlides) { - chan[i].portaSpeed=0; - 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)); + if (!song.noSlidesOnFirstTick || !firstTick) { + if ((chan[i].keyOn || chan[i].keyOff) && chan[i].portaSpeed>0) { + if (dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed,chan[i].portaNote))==2 && chan[i].portaStop && song.targetResetsSlides) { + chan[i].portaSpeed=0; + 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)); + } } } if (chan[i].cut>0) { @@ -1087,10 +1584,10 @@ bool DivEngine::nextTick(bool noAccum) { if (disCont[dispatchOfChan[i]].dispatch->keyOffAffectsPorta(dispatchChanOfChan[i])) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; - if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { + /*if (i==2 && sysOfChan[i]==DIV_SYSTEM_SMS) { chan[i+1].portaNote=-1; chan[i+1].portaSpeed=-1; - } + }*/ } dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); chan[i].scheduledSlideReset=true; @@ -1098,6 +1595,13 @@ bool DivEngine::nextTick(bool noAccum) { dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF,i)); } } + if (chan[i].resetArp) { + dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + chan[i].resetArp=false; + } + if (song.rowResetsArpPos && firstTick) { + chan[i].arpStage=-1; + } if (chan[i].arp!=0 && !chan[i].arpYield && chan[i].portaSpeed<1) { if (--chan[i].arpTicks<1) { chan[i].arpTicks=song.arpLen; @@ -1121,6 +1625,8 @@ bool DivEngine::nextTick(bool noAccum) { } } + firstTick=false; + // system tick for (int i=0; itick(); @@ -1152,9 +1658,65 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi memset(out[1],0,size*sizeof(float)); } - isBusy.lock(); + if (softLocked) { + if (!isBusy.try_lock()) { + logV("audio is soft-locked (%d)",softLockCount++); + return; + } + } else { + isBusy.lock(); + } got.bufsize=size; + + // process MIDI events (TODO: everything) + if (output) if (output->midiIn) while (!output->midiIn->queue.empty()) { + TAMidiMessage& msg=output->midiIn->queue.front(); + int ins=-1; + if ((ins=midiCallback(msg))!=-2) { + int chan=msg.type&15; + switch (msg.type&0xf0) { + case TA_MIDI_NOTE_OFF: { + if (chan<0 || chan>=chans) break; + if (midiIsDirect) { + pendingNotes.push(DivNoteEvent(chan,-1,-1,-1,false)); + } else { + autoNoteOff(msg.type&15,msg.data[0]-12,msg.data[1]); + } + if (!playing) { + reset(); + freelance=true; + playing=true; + } + break; + } + case TA_MIDI_NOTE_ON: { + if (chan<0 || chan>=chans) break; + if (msg.data[1]==0) { + if (midiIsDirect) { + pendingNotes.push(DivNoteEvent(chan,-1,-1,-1,false)); + } else { + autoNoteOff(msg.type&15,msg.data[0]-12,msg.data[1]); + } + } else { + if (midiIsDirect) { + pendingNotes.push(DivNoteEvent(chan,ins,msg.data[0]-12,msg.data[1],true)); + } else { + autoNoteOn(msg.type&15,ins,msg.data[0]-12,msg.data[1]); + } + } + break; + } + case TA_MIDI_PROGRAM: { + // TODO: change instrument event thingy + break; + } + } + } + logD("%.2x",msg.type); + output->midiIn->queue.pop(); + } + // process audio if (out!=NULL && ((sPreview.sample>=0 && sPreview.sample<(int)song.sample.size()) || (sPreview.wave>=0 && sPreview.wave<(int)song.wave.size()))) { unsigned int samp_bbOff=0; unsigned int prevAvail=blip_samples_avail(samp_bb); @@ -1217,8 +1779,11 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi if (!playing) { if (out!=NULL) { - memcpy(oscBuf[0],out[0],size*sizeof(float)); - memcpy(oscBuf[1],out[1],size*sizeof(float)); + for (unsigned int i=0; i=32768) oscWritePos=0; + } oscSize=size; } isBusy.unlock(); @@ -1280,7 +1845,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi if (remainingLoops>0) { remainingLoops--; if (!remainingLoops) { - logI("end of song!\n"); + logI("end of song!"); remainingLoops=-1; playing=false; freelance=false; @@ -1316,9 +1881,9 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi return; } - logD("attempts: %d\n",attempts); + //logD("attempts: %d",attempts); if (attempts>=100) { - logE("hang detected! stopping! at %d seconds %d micro\n",totalSeconds,totalTicks); + logE("hang detected! stopping! at %d seconds %d micro",totalSeconds,totalTicks); freelance=false; playing=false; extValuePresent=false; @@ -1332,6 +1897,8 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi for (int i=0; igetPostAmp(); + volR*=disCont[i].dispatch->getPostAmp(); if (disCont[i].dispatch->isStereo()) { for (size_t j=0; j=1) metroPos--; } - memcpy(oscBuf[0],out[0],size*sizeof(float)); - memcpy(oscBuf[1],out[1],size*sizeof(float)); + for (unsigned int i=0; i=32768) oscWritePos=0; + } oscSize=size; if (forceMono) { diff --git a/src/engine/safeReader.cpp b/src/engine/safeReader.cpp index df3936655..7fa66e8b3 100644 --- a/src/engine/safeReader.cpp +++ b/src/engine/safeReader.cpp @@ -30,7 +30,7 @@ bool SafeReader::seek(ssize_t where, int whence) { curSeek=where; break; case SEEK_CUR: { - ssize_t finalSeek=len+where; + ssize_t finalSeek=curSeek+where; if (finalSeek<0) return false; if (finalSeek>(ssize_t)len) return false; curSeek=finalSeek; @@ -57,7 +57,7 @@ size_t SafeReader::size() { int SafeReader::read(void* where, size_t count) { #ifdef READ_DEBUG - logD("SR: reading %d bytes at %x\n",count,curSeek); + logD("SR: reading %d bytes at %x",count,curSeek); #endif if (count==0) return 0; if (curSeek+count>len) throw EndOfFileException(this,len); @@ -68,53 +68,53 @@ int SafeReader::read(void* where, size_t count) { signed char SafeReader::readC() { #ifdef READ_DEBUG - logD("SR: reading char %x:\n",curSeek); + logD("SR: reading char %x:",curSeek); #endif if (curSeek+1>len) throw EndOfFileException(this,len); #ifdef READ_DEBUG - logD("SR: %.2x\n",buf[curSeek]); + logD("SR: %.2x",buf[curSeek]); #endif return (signed char)buf[curSeek++]; } short SafeReader::readS() { #ifdef READ_DEBUG - logD("SR: reading short %x:\n",curSeek); + logD("SR: reading short %x:",curSeek); #endif if (curSeek+2>len) throw EndOfFileException(this,len); short ret=*(short*)(&buf[curSeek]); #ifdef READ_DEBUG - logD("SR: %.4x\n",ret); + logD("SR: %.4x",ret); #endif curSeek+=2; return ret; } short SafeReader::readS_BE() { - if (curSeek+1>len) throw EndOfFileException(this,len); + if (curSeek+2>len) throw EndOfFileException(this,len); short ret=*(short*)(&buf[curSeek]); curSeek+=2; - return (ret>>8)|((ret&0xff)<<8); + return ((ret>>8)&0xff)|(ret<<8); } int SafeReader::readI() { #ifdef READ_DEBUG - logD("SR: reading int %x:\n",curSeek); + logD("SR: reading int %x:",curSeek); #endif if (curSeek+4>len) throw EndOfFileException(this,len); int ret=*(int*)(&buf[curSeek]); curSeek+=4; #ifdef READ_DEBUG - logD("SR: %.8x\n",ret); + logD("SR: %.8x",ret); #endif return ret; } int SafeReader::readI_BE() { if (curSeek+4>len) throw EndOfFileException(this,len); - int ret=*(int*)(&buf[curSeek]); + unsigned int ret=*(unsigned int*)(&buf[curSeek]); curSeek+=4; - return (ret>>24)|((ret&0xff0000)>>8)|((ret&0xff00)<<8)|((ret&0xff)<<24); + return (int)((ret>>24)|((ret&0xff0000)>>8)|((ret&0xff00)<<8)|((ret&0xff)<<24)); } int64_t SafeReader::readL() { @@ -141,7 +141,7 @@ double SafeReader::readD() { String SafeReader::readString(size_t stlen) { String ret; #ifdef READ_DEBUG - logD("SR: reading string len %d at %x\n",stlen,curSeek); + logD("SR: reading string len %d at %x",stlen,curSeek); #endif size_t curPos=0; while (curPos #include "../ta-utils.h" +enum Endianness { + LittleEndian=0, + BigEndian +}; + class SafeReader; struct EndOfFileException { diff --git a/src/engine/safeWriter.cpp b/src/engine/safeWriter.cpp index 0e7a118a2..f29800a4a 100644 --- a/src/engine/safeWriter.cpp +++ b/src/engine/safeWriter.cpp @@ -80,6 +80,10 @@ int SafeWriter::writeC(signed char val) { int SafeWriter::writeS(short val) { return write(&val,2); } +int SafeWriter::writeS_BE(short val) { + unsigned char bytes[2]{(unsigned char)((val>>8)&0xff), (unsigned char)(val&0xff)}; + return write(bytes,2); +} int SafeWriter::writeI(int val) { return write(&val,4); diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index ec6313fff..93e3ece6d 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -19,9 +19,10 @@ #include "sample.h" #include "../ta-log.h" +#include #include #include -#include +#include "filter.h" extern "C" { #include "../../extern/adpcm/bs_codec.h" @@ -31,6 +32,10 @@ extern "C" { #include "../../extern/adpcm/ymz_codec.h" } +DivSampleHistory::~DivSampleHistory() { + if (data!=NULL) delete[] data; +} + bool DivSample::save(const char* path) { SNDFILE* f; SF_INFO si; @@ -45,7 +50,7 @@ bool DivSample::save(const char* path) { f=sf_open(path,SFM_WRITE,&si); if (f==NULL) { - logE("could not open wave file for saving! %s\n",sf_error_number(sf_error(f))); + logE("could not open wave file for saving! %s",sf_error_number(sf_error(f))); return false; } @@ -115,8 +120,9 @@ bool DivSample::initInternal(unsigned char d, int count) { case 8: // 8-bit if (data8!=NULL) delete[] data8; length8=count; - data8=new signed char[length8]; - memset(data8,0,length8); + // for padding X1-010 sample + data8=new signed char[(count+4095)&(~0xfff)]; + memset(data8,0,(count+4095)&(~0xfff)); break; case 9: // BRR if (dataBRR!=NULL) delete[] dataBRR; @@ -148,6 +154,488 @@ bool DivSample::init(unsigned int count) { return true; } +bool DivSample::resize(unsigned int count) { + if (depth==8) { + if (data8!=NULL) { + signed char* oldData8=data8; + data8=NULL; + initInternal(8,count); + memcpy(data8,oldData8,MIN(count,samples)); + delete[] oldData8; + } else { + initInternal(8,count); + } + samples=count; + return true; + } else if (depth==16) { + if (data16!=NULL) { + short* oldData16=data16; + data16=NULL; + initInternal(16,count); + memcpy(data16,oldData16,sizeof(short)*MIN(count,samples)); + delete[] oldData16; + } else { + initInternal(16,count); + } + samples=count; + return true; + } + return false; +} + +bool DivSample::strip(unsigned int begin, unsigned int end) { + if (begin>samples) begin=samples; + if (end>samples) end=samples; + int count=samples-(end-begin); + if (count<=0) return resize(0); + if (depth==8) { + if (data8!=NULL) { + signed char* oldData8=data8; + data8=NULL; + initInternal(8,count); + if (begin>0) { + memcpy(data8,oldData8,begin); + } + if (samples-end>0) { + memcpy(data8+begin,oldData8+end,samples-end); + } + delete[] oldData8; + } else { + // do nothing + return true; + } + samples=count; + return true; + } else if (depth==16) { + if (data16!=NULL) { + short* oldData16=data16; + data16=NULL; + initInternal(16,count); + if (begin>0) { + memcpy(data16,oldData16,sizeof(short)*begin); + } + if (samples-end>0) { + memcpy(&(data16[begin]),&(oldData16[end]),sizeof(short)*(samples-end)); + } + delete[] oldData16; + } else { + // do nothing + return true; + } + samples=count; + return true; + } + return false; +} + +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 (data8!=NULL) { + signed char* oldData8=data8; + data8=NULL; + initInternal(8,count); + memcpy(data8,oldData8+begin,count); + delete[] oldData8; + } else { + // do nothing + return true; + } + samples=count; + return true; + } else if (depth==16) { + if (data16!=NULL) { + short* oldData16=data16; + data16=NULL; + initInternal(16,count); + memcpy(data16,&(oldData16[begin]),sizeof(short)*count); + delete[] oldData16; + } else { + // do nothing + return true; + } + samples=count; + return true; + } + return false; +} + +bool DivSample::insert(unsigned int pos, unsigned int length) { + unsigned int count=samples+length; + if (depth==8) { + if (data8!=NULL) { + signed char* oldData8=data8; + data8=NULL; + initInternal(8,count); + if (pos>0) { + memcpy(data8,oldData8,pos); + } + if (count-pos-length>0) { + memcpy(data8+pos+length,oldData8+pos,count-pos-length); + } + delete[] oldData8; + } else { + initInternal(8,count); + } + samples=count; + return true; + } else if (depth==16) { + if (data16!=NULL) { + short* oldData16=data16; + data16=NULL; + initInternal(16,count); + if (pos>0) { + memcpy(data16,oldData16,sizeof(short)*pos); + } + if (count-pos-length>0) { + memcpy(&(data16[pos+length]),&(oldData16[pos]),sizeof(short)*(count-pos-length)); + } + delete[] oldData16; + } else { + initInternal(16,count); + } + samples=count; + return true; + } + return false; +} + +#define RESAMPLE_BEGIN \ + if (samples<1) return true; \ + int finalCount=(double)samples*(r/(double)rate); \ + signed char* oldData8=data8; \ + short* oldData16=data16; \ + if (depth==16) { \ + if (data16!=NULL) { \ + data16=NULL; \ + initInternal(16,finalCount); \ + } \ + } else if (depth==8) { \ + if (data8!=NULL) { \ + data8=NULL; \ + initInternal(8,finalCount); \ + } \ + } else { \ + return false; \ + } + +#define RESAMPLE_END \ + if (loopStart>=0) loopStart=(double)loopStart*(r/(double)rate); \ + rate=r; \ + samples=finalCount; \ + if (depth==16) { \ + delete[] oldData16; \ + } else if (depth==8) { \ + delete[] oldData8; \ + } + +bool DivSample::resampleNone(double r) { + RESAMPLE_BEGIN; + + if (depth==16) { + for (int i=0; i=samples) { + data16[i]=0; + } else { + data16[i]=oldData16[pos]; + } + } + } else if (depth==8) { + for (int i=0; i=samples) { + data8[i]=0; + } else { + data8[i]=oldData8[pos]; + } + } + } + + RESAMPLE_END; + return true; +} + +bool DivSample::resampleLinear(double r) { + RESAMPLE_BEGIN; + + double posFrac=0; + unsigned int posInt=0; + double factor=(double)rate/r; + + if (depth==16) { + 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]; + + data16[i]=s1+(float)(s2-s1)*posFrac; + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + } + } + } else if (depth==8) { + 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]; + + data8[i]=s1+(float)(s2-s1)*posFrac; + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + } + } + } + + RESAMPLE_END; + return true; +} + +bool DivSample::resampleCubic(double r) { + RESAMPLE_BEGIN; + + double posFrac=0; + unsigned int posInt=0; + double factor=(double)rate/r; + float* cubicTable=DivFilterTables::getCubicTable(); + + if (depth==16) { + for (int i=0; i=samples)?0:oldData16[posInt]; + float s2=(posInt+1>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData16[loopStart]:0):oldData16[posInt+1]; + float s3=(posInt+2>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData16[loopStart]:0):oldData16[posInt+2]; + + float result=s0*t[0]+s1*t[1]+s2*t[2]+s3*t[3]; + if (result<-32768) result=-32768; + if (result>32767) result=32767; + data16[i]=result; + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + } + } + } else if (depth==8) { + for (int i=0; i=samples)?0:oldData8[posInt]; + float s2=(posInt+1>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData8[loopStart]:0):oldData8[posInt+1]; + float s3=(posInt+2>=samples)?((loopStart>=0 && loopStart<(int)samples)?oldData8[loopStart]:0):oldData8[posInt+2]; + + float result=s0*t[0]+s1*t[1]+s2*t[2]+s3*t[3]; + if (result<-128) result=-128; + if (result>127) result=127; + data8[i]=result; + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + } + } + } + + RESAMPLE_END; + return true; +} + +bool DivSample::resampleBlep(double r) { + RESAMPLE_BEGIN; + + double posFrac=0; + unsigned int posInt=0; + double factor=r/(double)rate; + float* sincITable=DivFilterTables::getSincIntegralTable(); + float s[16]; + + memset(s,0,16*sizeof(float)); + + if (depth==16) { + memset(data16,0,finalCount*sizeof(short)); + for (int i=0; i32767) result=32767; + data16[i]=result; + } + + posFrac+=1.0; + while (posFrac>=1.0) { + unsigned int n=((unsigned int)(posFrac*8192.0))&8191; + posFrac-=factor; + posInt++; + + float* t1=&sincITable[(8191-n)<<3]; + float* t2=&sincITable[n<<3]; + float delta=oldData16[posInt]-oldData16[posInt-1]; + + for (int j=0; j<8; j++) { + if (i-j>0) { + float result=data16[i-j]+t1[j]*-delta; + if (result<-32768) result=-32768; + if (result>32767) result=32767; + data16[i-j]=result; + } + if (i+j+132767) result=32767; + data16[i+j+1]=result; + } + } + } + } + } else if (depth==8) { + memset(data8,0,finalCount); + for (int i=0; i127) result=127; + data8[i]=result; + } + + posFrac+=1.0; + while (posFrac>=1.0) { + unsigned int n=((unsigned int)(posFrac*8192.0))&8191; + posFrac-=factor; + posInt++; + + float* t1=&sincITable[(8191-n)<<3]; + float* t2=&sincITable[n<<3]; + float delta=oldData8[posInt]-oldData8[posInt-1]; + + for (int j=0; j<8; j++) { + if (i-j>0) { + float result=data8[i-j]+t1[j]*-delta; + if (result<-128) result=-128; + if (result>127) result=127; + data8[i-j]=result; + } + if (i+j+1127) result=127; + data8[i+j+1]=result; + } + } + } + } + } + + RESAMPLE_END; + return true; +} + +bool DivSample::resampleSinc(double r) { + RESAMPLE_BEGIN; + + double posFrac=0; + unsigned int posInt=0; + double factor=(double)rate/r; + float* sincTable=DivFilterTables::getSincTable(); + float s[16]; + + memset(s,0,16*sizeof(float)); + + if (depth==16) { + for (int i=0; i32767) result=32767; + if (i>=8) { + data16[i-8]=result; + } + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + + for (int j=0; j<15; j++) s[j]=s[j+1]; + s[15]=(posInt>=samples)?0:oldData16[posInt]; + } + } + } else if (depth==8) { + for (int i=0; i32767) result=32767; + if (i>=8) { + data8[i-8]=result; + } + + posFrac+=factor; + while (posFrac>=1.0) { + posFrac-=1.0; + posInt++; + + for (int j=0; j<15; j++) s[j]=s[j+1]; + s[15]=(posInt>=samples)?0:oldData8[posInt]; + } + } + } + + RESAMPLE_END; + return true; +} + +bool DivSample::resample(double r, int filter) { + if (depth!=8 && depth!=16) return false; + switch (filter) { + case DIV_RESAMPLE_NONE: + return resampleNone(r); + break; + case DIV_RESAMPLE_LINEAR: + return resampleLinear(r); + break; + case DIV_RESAMPLE_CUBIC: + return resampleCubic(r); + break; + case DIV_RESAMPLE_BLEP: + return resampleBlep(r); + break; + case DIV_RESAMPLE_SINC: + return resampleSinc(r); + break; + case DIV_RESAMPLE_BEST: + if (r>rate) { + return resampleSinc(r); + } else { + return resampleBlep(r); + } + break; + } + return false; +} + void DivSample::render() { // step 1: convert to 16-bit if needed if (depth!=16) { @@ -161,7 +649,7 @@ void DivSample::render() { case 1: { // DPCM int accum=0; for (unsigned int i=0; i>3]>>(i&7))&1)?1:-1; + accum+=((dataDPCM[i>>3]>>(i&7))&1)?1:-1; if (accum>63) accum=63; if (accum<-64) accum=-64; data16[i]=accum*512; @@ -302,7 +790,92 @@ unsigned int DivSample::getCurBufLen() { return 0; } +DivSampleHistory* DivSample::prepareUndo(bool data, bool doNotPush) { + DivSampleHistory* h; + if (data) { + unsigned char* duplicate; + if (getCurBuf()==NULL) { + duplicate=NULL; + } else { + duplicate=new unsigned char[getCurBufLen()]; + memcpy(duplicate,getCurBuf(),getCurBufLen()); + } + h=new DivSampleHistory(duplicate,getCurBufLen(),samples,depth,rate,centerRate,loopStart); + } else { + h=new DivSampleHistory(depth,rate,centerRate,loopStart); + } + if (!doNotPush) { + while (!redoHist.empty()) { + DivSampleHistory* h=redoHist.back(); + delete h; + redoHist.pop_back(); + } + if (undoHist.size()>100) undoHist.pop_front(); + undoHist.push_back(h); + } + return h; +} + +#define applyHistory \ + depth=h->depth; \ + if (h->hasSample) { \ + initInternal(h->depth,h->samples); \ + samples=h->samples; \ +\ + if (h->length!=getCurBufLen()) logW("undo buffer length not equal to current buffer length! %d != %d",h->length,getCurBufLen()); \ +\ + void* buf=getCurBuf(); \ +\ + if (buf!=NULL && h->data!=NULL) { \ + memcpy(buf,h->data,h->length); \ + } \ + } \ + rate=h->rate; \ + centerRate=h->centerRate; \ + loopStart=h->loopStart; + + +int DivSample::undo() { + if (undoHist.empty()) return 0; + DivSampleHistory* h=undoHist.back(); + DivSampleHistory* redo=prepareUndo(h->hasSample,true); + + int ret=h->hasSample?2:1; + + applyHistory; + + redoHist.push_back(redo); + delete h; + undoHist.pop_back(); + return ret; +} + +int DivSample::redo() { + if (redoHist.empty()) return 0; + DivSampleHistory* h=redoHist.back(); + DivSampleHistory* undo=prepareUndo(h->hasSample,true); + + int ret=h->hasSample?2:1; + + applyHistory; + + undoHist.push_back(undo); + delete h; + redoHist.pop_back(); + return ret; +} + DivSample::~DivSample() { + while (!undoHist.empty()) { + DivSampleHistory* h=undoHist.back(); + delete h; + undoHist.pop_back(); + } + while (!redoHist.empty()) { + DivSampleHistory* h=redoHist.back(); + delete h; + redoHist.pop_back(); + } if (data8) delete[] data8; if (data16) delete[] data16; if (data1) delete[] data1; diff --git a/src/engine/sample.h b/src/engine/sample.h index 2915f2cef..1a9e53d32 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -18,6 +18,43 @@ */ #include "../ta-utils.h" +#include + +enum DivResampleFilters { + DIV_RESAMPLE_NONE=0, + DIV_RESAMPLE_LINEAR, + DIV_RESAMPLE_CUBIC, + DIV_RESAMPLE_BLEP, + DIV_RESAMPLE_SINC, + DIV_RESAMPLE_BEST +}; + +struct DivSampleHistory { + unsigned char* data; + unsigned int length, samples; + unsigned char depth; + int rate, centerRate, loopStart; + bool hasSample; + DivSampleHistory(void* d, unsigned int l, unsigned int s, unsigned char de, int r, int cr, int ls): + data((unsigned char*)d), + length(l), + samples(s), + depth(de), + rate(r), + centerRate(cr), + loopStart(ls), + hasSample(true) {} + DivSampleHistory(unsigned char de, int r, int cr, int ls): + data(NULL), + length(0), + samples(0), + depth(de), + rate(r), + centerRate(cr), + loopStart(ls), + hasSample(false) {} + ~DivSampleHistory(); +}; struct DivSample { String name; @@ -49,16 +86,127 @@ struct DivSample { unsigned int length8, length16, length1, lengthDPCM, lengthQSoundA, lengthA, lengthB, lengthX68, lengthBRR, lengthVOX; unsigned int off8, off16, off1, offDPCM, offQSoundA, offA, offB, offX68, offBRR, offVOX; - unsigned int offSegaPCM, offQSound; + unsigned int offSegaPCM, offQSound, offX1_010; unsigned int samples; + std::deque undoHist; + std::deque redoHist; + + /** + * @warning DO NOT USE - internal functions + */ + bool resampleNone(double rate); + bool resampleLinear(double rate); + bool resampleCubic(double rate); + bool resampleBlep(double rate); + bool resampleSinc(double rate); + + /** + * save this sample to a file. + * @param path a path. + * @return whether saving succeeded or not. + */ bool save(const char* path); + + /** + * @warning DO NOT USE - internal function + * initialize sample data. + * @param d sample type. + * @param count number of samples. + * @return whether it was successful. + */ bool initInternal(unsigned char d, int count); + + /** + * initialize sample data. make sure you have set `depth` before doing so. + * @param count number of samples. + * @return whether it was successful. + */ bool init(unsigned int count); + + /** + * resize sample data. make sure the sample has been initialized before doing so. + * @warning do not attempt to resize a sample outside of a synchronized block! + * @param count number of samples. + * @return whether it was successful. + */ + bool resize(unsigned int count); + + /** + * remove part of the sample data. + * @warning do not attempt to strip a sample outside of a synchronized block! + * @param start the beginning. + * @param end the end. + * @return whether it was successful. + */ + bool strip(unsigned int begin, unsigned int end); + + /** + * clip the sample data to specified boundaries. + * @warning do not attempt to trim a sample outside of a synchronized block! + * @param start the beginning. + * @param end the end. + * @return whether it was successful. + */ + bool trim(unsigned int begin, unsigned int end); + + /** + * insert silence at specified position. + * @warning do not attempt to do this outside of a synchronized block! + * @param pos the beginning. + * @param length how many samples to insert. + * @return whether it was successful. + */ + bool insert(unsigned int pos, unsigned int length); + + /** + * change the sample rate. + * @warning do not attempt to resample outside of a synchronized block! + * @param rate number of samples. + * @param filter the interpolation filter. + * @return whether it was successful. + */ + bool resample(double rate, int filter); + + /** + * initialize the rest of sample formats for this sample. + */ void render(); + + /** + * get the sample data for the current depth. + * @return the sample data, or NULL if not created. + */ void* getCurBuf(); + + /** + * get the sample data length for the current depth. + * @return the sample data length. + */ unsigned int getCurBufLen(); + + /** + * prepare an undo step for this sample. + * @param data whether to include sample data. + * @param doNotPush if this is true, don't push the DivSampleHistory to the undo history. + * @return the undo step. + */ + DivSampleHistory* prepareUndo(bool data, bool doNotPush=false); + + /** + * undo. you may need to call DivEngine::renderSamples afterwards. + * @warning do not attempt to undo outside of a synchronized block! + * @return 0 on failure; 1 on success and 2 on success (data changed). + */ + int undo(); + + /** + * redo. you may need to call DivEngine::renderSamples afterwards. + * @warning do not attempt to redo outside of a synchronized block! + * @return 0 on failure; 1 on success and 2 on success (data changed). + */ + int redo(); DivSample(): name(""), rate(32000), diff --git a/src/engine/song.h b/src/engine/song.h index 85752288e..75e5a03e9 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -42,6 +42,7 @@ enum DivSystem { DIV_SYSTEM_PCE, DIV_SYSTEM_NES, DIV_SYSTEM_NES_VRC7, // ** COMPOUND SYSTEM - DO NOT USE! ** + DIV_SYSTEM_NES_FDS, // ** COMPOUND SYSTEM - DO NOT USE! ** DIV_SYSTEM_C64_6581, DIV_SYSTEM_C64_8580, DIV_SYSTEM_ARCADE, // ** COMPOUND SYSTEM - DO NOT USE! ** @@ -90,13 +91,19 @@ enum DivSystem { DIV_SYSTEM_OPLL_DRUMS, DIV_SYSTEM_LYNX, DIV_SYSTEM_QSOUND, - DIV_SYSTEM_SEGAPCM_COMPAT + DIV_SYSTEM_VERA, + DIV_SYSTEM_YM2610B_EXT, + DIV_SYSTEM_SEGAPCM_COMPAT, + DIV_SYSTEM_X1_010, + DIV_SYSTEM_BUBSYS_WSG }; struct DivSong { // version number used for saving the song. // Furnace will save using the latest possible version, // known version numbers: + // - 26: v1.1.3 + // - changes height of FDS wave to 6-bit (it was 4-bit before) // - 25: v1.1 // - adds pattern names (in a rather odd way) // - introduces SMS+OPLL system @@ -136,12 +143,15 @@ struct DivSong { // - 9: v3.9 // - introduces Genesis system // - introduces system number + // - patterns now stored in current known format // - 7: ??? - // - 5: BETA 3 (?) + // - 5: BETA 3 // - adds arpeggio tick - // - 3: BETA 2 + // - 4: BETA 2 + // - 3: BETA 1 // - possibly the first version that could save // - basic format, no system number, 16 instruments, one speed, YMU759-only + // - patterns were stored in a different format (chars instead of shorts) // - if somebody manages to find a version 2 or even 1 module, please tell me as it will be worth more than a luxury vehicle unsigned short version; bool isDMF; @@ -193,6 +203,8 @@ struct DivSong { // - 6: 0.89MHz (Sunsoft 5B) // - 7: 1.67MHz // - 8: 0.83MHz (Sunsoft 5B on PAL) + // - 9: 1.10MHz (Gamate/VIC-20 PAL) + // - 10: 2.097152MHz (Game Boy) // - bit 4-5: chip type (ignored on AY8930) // - 0: AY-3-8910 or similar // - 1: YM2149 @@ -214,12 +226,37 @@ struct DivSong { // - 1: Amiga 1200 // - bit 8-14: stereo separation // - 0 is 0% while 127 is 100% + // - PC Speaker: + // - bit 0-1: speaker type + // - 0: unfiltered + // - 1: cone + // - 2: piezo + // - 3: real (TODO) // - QSound: // - bit 12-20: echo feedback // - Valid values are 0-255 // - bit 0-11: echo delay length // - Valid values are 0-2725 // - 0 is max length, 2725 is min length + // - OPLL: + // - bit 0-3: clock rate + // - 0: NTSC (3.58MHz) + // - 1: PAL (3.55MHz) + // - 2: Other (4MHz) + // - 3: half NTSC (1.79MHz) + // - bit 4-7: patch set + // - 0: YM2413 + // - 1: YMF281 + // - 2: YM2423 + // - 3: VRC7 + // - 4: custom (TODO) + // - X1-010: + // - bit 0-3: clock rate + // - 0: 16MHz (Seta 1) + // - 1: 16.67MHz (Seta 2) + // - bit 4: stereo + // - 0: mono + // - 1: stereo unsigned int systemFlags[32]; // song information @@ -240,7 +277,8 @@ struct DivSong { unsigned char timeBase, speed1, speed2, arpLen; bool pal; bool customTempo; - int hz, patLen, ordersLen, insLen, waveLen, sampleLen; + float hz; + int patLen, ordersLen, insLen, waveLen, sampleLen; float masterVol; float tuning; @@ -263,6 +301,19 @@ struct DivSong { bool algMacroBehavior; bool brokenShortcutSlides; bool ignoreDuplicateSlides; + bool stopPortaOnNoteOff; + bool continuousVibrato; + bool brokenDACMode; + bool oneTickCut; + bool newInsTriggersInPorta; + bool arp0Reset; + bool brokenSpeedSel; + bool noSlidesOnFirstTick; + bool rowResetsArpPos; + bool ignoreJumpAtEnd; + bool buggyPortaAfterSlide; + bool gbInsAffectsEnvelope; + bool sharedExtStat; DivOrders orders; std::vector ins; @@ -277,6 +328,10 @@ struct DivSong { DivWavetable nullWave; DivSample nullSample; + /** + * unloads the song, freeing all memory associated with it. + * use before destroying the object. + */ void unload(); DivSong(): @@ -304,7 +359,7 @@ struct DivSong { arpLen(1), pal(true), customTempo(false), - hz(60), + hz(60.0), patLen(64), ordersLen(1), insLen(0), @@ -325,7 +380,20 @@ struct DivSong { arpNonPorta(false), algMacroBehavior(false), brokenShortcutSlides(false), - ignoreDuplicateSlides(false) { + ignoreDuplicateSlides(false), + stopPortaOnNoteOff(false), + continuousVibrato(false), + brokenDACMode(false), + oneTickCut(false), + newInsTriggersInPorta(true), + arp0Reset(true), + brokenSpeedSel(false), + noSlidesOnFirstTick(false), + rowResetsArpPos(false), + ignoreJumpAtEnd(false), + buggyPortaAfterSlide(false), + gbInsAffectsEnvelope(true), + sharedExtStat(true) { for (int i=0; i<32; i++) { system[i]=DIV_SYSTEM_NULL; systemVol[i]=64; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 54589e37d..084504fe1 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -20,7 +20,7 @@ #include "engine.h" #include "song.h" -DivSystem DivEngine::systemFromFile(unsigned char val) { +DivSystem DivEngine::systemFromFileFur(unsigned char val) { switch (val) { case 0x01: return DIV_SYSTEM_YMU759; @@ -50,7 +50,6 @@ DivSystem DivEngine::systemFromFile(unsigned char val) { return DIV_SYSTEM_C64_6581; case 0x49: return DIV_SYSTEM_YM2610_EXT; - // Furnace-specific systems case 0x80: return DIV_SYSTEM_AY8910; case 0x81: @@ -135,13 +134,21 @@ DivSystem DivEngine::systemFromFile(unsigned char val) { return DIV_SYSTEM_LYNX; case 0xa9: return DIV_SYSTEM_SEGAPCM_COMPAT; + case 0xac: + return DIV_SYSTEM_VERA; + case 0xad: + return DIV_SYSTEM_BUBSYS_WSG; + case 0xb0: + return DIV_SYSTEM_X1_010; + case 0xde: + return DIV_SYSTEM_YM2610B_EXT; case 0xe0: return DIV_SYSTEM_QSOUND; } return DIV_SYSTEM_NULL; } -unsigned char DivEngine::systemToFile(DivSystem val) { +unsigned char DivEngine::systemToFileFur(DivSystem val) { switch (val) { case DIV_SYSTEM_YMU759: return 0x01; @@ -167,11 +174,12 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0x43; case DIV_SYSTEM_NES_VRC7: return 0x46; + case DIV_SYSTEM_NES_FDS: + return 0; // unsupported case DIV_SYSTEM_C64_6581: return 0x47; case DIV_SYSTEM_YM2610_EXT: return 0x49; - // Furnace-specific systems case DIV_SYSTEM_AY8910: return 0x80; case DIV_SYSTEM_AMIGA: @@ -220,7 +228,7 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0x96; case DIV_SYSTEM_SAA1099: return 0x97; - case DIV_SYSTEM_OPZ: + case DIV_SYSTEM_OPZ: return 0x98; case DIV_SYSTEM_POKEMINI: return 0x99; @@ -256,6 +264,14 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0xa8; case DIV_SYSTEM_SEGAPCM_COMPAT: return 0xa9; + case DIV_SYSTEM_VERA: + return 0xac; + case DIV_SYSTEM_BUBSYS_WSG: + return 0xad; + case DIV_SYSTEM_X1_010: + return 0xb0; + case DIV_SYSTEM_YM2610B_EXT: + return 0xde; case DIV_SYSTEM_QSOUND: return 0xe0; @@ -265,6 +281,80 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0; } +DivSystem DivEngine::systemFromFileDMF(unsigned char val) { + switch (val) { + case 0x01: + return DIV_SYSTEM_YMU759; + case 0x02: + return DIV_SYSTEM_GENESIS; + case 0x03: + return DIV_SYSTEM_SMS; + case 0x04: + return DIV_SYSTEM_GB; + case 0x05: + return DIV_SYSTEM_PCE; + case 0x06: + return DIV_SYSTEM_NES; + case 0x07: + return DIV_SYSTEM_C64_8580; + case 0x08: + return DIV_SYSTEM_ARCADE; + case 0x09: + return DIV_SYSTEM_YM2610; + case 0x42: + return DIV_SYSTEM_GENESIS_EXT; + case 0x43: + return DIV_SYSTEM_SMS_OPLL; + case 0x46: + return DIV_SYSTEM_NES_VRC7; + case 0x47: + return DIV_SYSTEM_C64_6581; + case 0x49: + return DIV_SYSTEM_YM2610_EXT; + case 0x86: + return DIV_SYSTEM_NES_FDS; + } + return DIV_SYSTEM_NULL; +} + +unsigned char DivEngine::systemToFileDMF(DivSystem val) { + switch (val) { + case DIV_SYSTEM_YMU759: + return 0x01; + case DIV_SYSTEM_GENESIS: + return 0x02; + case DIV_SYSTEM_SMS: + return 0x03; + case DIV_SYSTEM_GB: + return 0x04; + case DIV_SYSTEM_PCE: + return 0x05; + case DIV_SYSTEM_NES: + return 0x06; + case DIV_SYSTEM_C64_8580: + return 0x07; + case DIV_SYSTEM_ARCADE: + return 0x08; + case DIV_SYSTEM_YM2610: + return 0x09; + case DIV_SYSTEM_GENESIS_EXT: + return 0x42; + case DIV_SYSTEM_SMS_OPLL: + return 0x43; + case DIV_SYSTEM_NES_VRC7: + return 0x46; + case DIV_SYSTEM_NES_FDS: + return 0x86; + case DIV_SYSTEM_C64_6581: + return 0x47; + case DIV_SYSTEM_YM2610_EXT: + return 0x49; + default: + return 0; + } + return 0; +} + int DivEngine::getChannelCount(DivSystem sys) { switch (sys) { case DIV_SYSTEM_NULL: @@ -290,9 +380,10 @@ int DivEngine::getChannelCount(DivSystem sys) { return 13; case DIV_SYSTEM_NES_VRC7: return 11; + case DIV_SYSTEM_NES_FDS: + return 6; case DIV_SYSTEM_YM2610_EXT: return 16; - // Furnace-specific systems case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: return 3; @@ -331,7 +422,7 @@ int DivEngine::getChannelCount(DivSystem sys) { case DIV_SYSTEM_OPL3: return 18; case DIV_SYSTEM_MULTIPCM: - return 24; + return 28; case DIV_SYSTEM_PCSPKR: return 1; case DIV_SYSTEM_POKEY: @@ -347,6 +438,7 @@ int DivEngine::getChannelCount(DivSystem sys) { case DIV_SYSTEM_POKEMINI: return 1; case DIV_SYSTEM_SEGAPCM: + case DIV_SYSTEM_X1_010: return 16; case DIV_SYSTEM_VBOY: return 6; @@ -376,8 +468,13 @@ int DivEngine::getChannelCount(DivSystem sys) { return 4; case DIV_SYSTEM_SEGAPCM_COMPAT: return 5; + case DIV_SYSTEM_YM2610B_EXT: case DIV_SYSTEM_QSOUND: return 19; + case DIV_SYSTEM_VERA: + return 17; + case DIV_SYSTEM_BUBSYS_WSG: + return 2; } return 0; } @@ -386,6 +483,7 @@ int DivEngine::getTotalChannelCount() { return chans; } +// TODO: rewrite this function (again). it's an unreliable mess. const char* DivEngine::getSongSystemName() { switch (song.systemLen) { case 0: @@ -403,10 +501,6 @@ const char* DivEngine::getSongSystemName() { return "Vectrex"; case 5: // AY-3-8910, 1MHz return "Amstrad CPC"; - case 6: // AY-3-8910, 0.somethingMhz - return "Intellivision"; - case 8: // AY-3-8910, 0.somethingMhz - return "Intellivision (PAL)"; case 0x10: // YM2149, 1.79MHz return "MSX"; @@ -416,7 +510,12 @@ const char* DivEngine::getSongSystemName() { return "Sunsoft 5B standalone"; case 0x28: // 5B PAL return "Sunsoft 5B standalone (PAL)"; - + + case 0x30: // AY-3-8914, 1.79MHz + return "Intellivision"; + case 0x33: // AY-3-8914, 2MHz + return "Intellivision (PAL)"; + default: if ((song.systemFlags[0]&0x30)==0x00) { return "AY-3-8910"; @@ -424,6 +523,8 @@ const char* DivEngine::getSongSystemName() { return "Yamaha YM2149"; } else if ((song.systemFlags[0]&0x30)==0x20) { return "Overclocked Sunsoft 5B"; + } else if ((song.systemFlags[0]&0x30)==0x30) { + return "Intellivision"; } } } else if (song.system[0]==DIV_SYSTEM_SMS) { @@ -458,6 +559,18 @@ const char* DivEngine::getSongSystemName() { return "Sega Genesis Extended Channel 3"; } + if (song.system[0]==DIV_SYSTEM_OPLL && song.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) { + return "NTSC-J Sega Master System + drums"; + } + if (song.system[0]==DIV_SYSTEM_OPLL && song.system[1]==DIV_SYSTEM_AY8910) { + return "MSX-MUSIC"; + } + if (song.system[0]==DIV_SYSTEM_OPLL_DRUMS && song.system[1]==DIV_SYSTEM_AY8910) { + return "MSX-MUSIC + drums"; + } if (song.system[0]==DIV_SYSTEM_C64_6581 && song.system[1]==DIV_SYSTEM_C64_6581) { return "Commodore 64 with dual 6581"; } @@ -505,8 +618,15 @@ const char* DivEngine::getSongSystemName() { if (song.system[0]==DIV_SYSTEM_AY8910 && song.system[1]==DIV_SYSTEM_AY8910) { return "Bally Midway MCR"; } + + if (song.system[0]==DIV_SYSTEM_YM2151 && song.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) { + return "Konami Bubble System"; + } break; } return "multi-system"; @@ -532,6 +652,8 @@ const char* DivEngine::getSystemName(DivSystem sys) { return "NES"; case DIV_SYSTEM_NES_VRC7: return "NES + Konami VRC7"; + case DIV_SYSTEM_NES_FDS: + return "Famicom Disk System"; case DIV_SYSTEM_C64_6581: return "Commodore 64 with 6581"; case DIV_SYSTEM_C64_8580: @@ -544,7 +666,6 @@ const char* DivEngine::getSystemName(DivSystem sys) { return "Neo Geo CD"; case DIV_SYSTEM_YM2610_EXT: return "Neo Geo CD Extended Channel 2"; - // Furnace-specific systems case DIV_SYSTEM_YM2610_FULL: return "Neo Geo"; case DIV_SYSTEM_YM2610_FULL_EXT: @@ -629,8 +750,16 @@ const char* DivEngine::getSystemName(DivSystem sys) { return "Atari Lynx"; case DIV_SYSTEM_SEGAPCM_COMPAT: return "SegaPCM (compatible 5-channel mode)"; + case DIV_SYSTEM_YM2610B_EXT: + return "Taito Arcade Extended Channel 3"; case DIV_SYSTEM_QSOUND: return "Capcom QSound"; + case DIV_SYSTEM_VERA: + return "VERA"; + case DIV_SYSTEM_X1_010: + return "Seta/Allumer X1-010"; + case DIV_SYSTEM_BUBSYS_WSG: + return "Konami Bubble System WSG"; } return "Unknown"; } @@ -655,6 +784,8 @@ const char* DivEngine::getSystemChips(DivSystem sys) { return "Ricoh 2A03"; case DIV_SYSTEM_NES_VRC7: return "Ricoh 2A03 + Konami VRC7"; + case DIV_SYSTEM_NES_FDS: + return "Ricoh 2A03 + Famicom Disk System"; case DIV_SYSTEM_C64_6581: return "SID 6581"; case DIV_SYSTEM_C64_8580: @@ -667,15 +798,14 @@ const char* DivEngine::getSystemChips(DivSystem sys) { return "Yamaha YM2610 no ADPCM-B"; case DIV_SYSTEM_YM2610_EXT: return "Yamaha YM2610 no ADPCM-B (extended channel 2)"; - // Furnace-specific systems case DIV_SYSTEM_AY8910: return "AY-3-8910"; case DIV_SYSTEM_AMIGA: return "MOS 8364 Paula"; case DIV_SYSTEM_YM2151: - return "Yamaha YM2151 standalone"; + return "Yamaha YM2151"; case DIV_SYSTEM_YM2612: - return "Yamaha YM2612 standalone"; + return "Yamaha YM2612"; case DIV_SYSTEM_TIA: return "Atari TIA"; case DIV_SYSTEM_VIC20: @@ -752,8 +882,16 @@ const char* DivEngine::getSystemChips(DivSystem sys) { return "Mikey"; case DIV_SYSTEM_SEGAPCM_COMPAT: return "SegaPCM (compatible 5-channel mode)"; + case DIV_SYSTEM_YM2610B_EXT: + return "Yamaha YM2610B Extended Channel 3"; case DIV_SYSTEM_QSOUND: return "Capcom DL-1425"; + case DIV_SYSTEM_VERA: + return "VERA"; + case DIV_SYSTEM_X1_010: + return "Seta/Allumer X1-010"; + case DIV_SYSTEM_BUBSYS_WSG: + return "Konami Bubble System WSG"; } return "Unknown"; } @@ -791,7 +929,6 @@ const char* DivEngine::getSystemNameJ(DivSystem sys) { return "業務用ネオジオ"; case DIV_SYSTEM_YM2610_FULL_EXT: return "業務用ネオジオ"; - // Furnace-specific systems case DIV_SYSTEM_AY8910: return ""; case DIV_SYSTEM_AMIGA: @@ -822,6 +959,8 @@ bool DivEngine::isFMSystem(DivSystem sys) { sys==DIV_SYSTEM_YM2610_EXT || sys==DIV_SYSTEM_YM2610_FULL || sys==DIV_SYSTEM_YM2610_FULL_EXT || + sys==DIV_SYSTEM_YM2610B || + sys==DIV_SYSTEM_YM2610B_EXT || sys==DIV_SYSTEM_YMU759 || sys==DIV_SYSTEM_YM2151 || sys==DIV_SYSTEM_YM2612); @@ -834,7 +973,7 @@ bool DivEngine::isSTDSystem(DivSystem sys) { sys!=DIV_SYSTEM_YM2151); } -const char* chanNames[37][24]={ +const char* chanNames[40][32]={ {"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", "PCM"}, // YMU759/SegaPCM {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis {"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis (extended channel 3) @@ -847,7 +986,7 @@ const char* chanNames[37][24]={ {"FM 1", "FM 2", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 {"FM 1", "FM 2 OP1", "FM 2 OP2", "FM 2 OP3", "FM 2 OP4", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 (extended channel 2) {"PSG 1", "PSG 2", "PSG 3"}, // AY-3-8910 - {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Swan/Lynx + {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Lynx {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8"}, // YM2151/YM2414 {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6"}, // YM2612 {"Channel 1", "Channel 2"}, // TIA @@ -862,19 +1001,22 @@ const char* chanNames[37][24]={ {"Pulse 1", "Pulse 2", "PCM"}, // MMC5 {"FM 1", "FM 2", "FM 3", "PSG 1", "PSG 2", "PSG 3"}, // OPN {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Kick", "Snare", "Top", "HiHat", "Tom", "Rim", "ADPCM"}, // PC-98 - {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "FM 10", "FM 11", "FM 12", "FM 13", "FM 14", "FM 15", "FM 16", "FM 17", "FM 18"}, // OPL3 - {"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"}, // MultiPCM + {"4OP 1", "FM 2", "4OP 3", "FM 4", "4OP 5", "FM 6", "4OP 7", "FM 8", "4OP 9", "FM 10", "4OP 11", "FM 12", "FM 13", "FM 14", "FM 15", "FM 16", "FM 17", "FM 18"}, // OPL3 + {"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"}, // MultiPCM {"Square"}, // PC Speaker/Pokémon Mini {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Noise"}, // Virtual Boy/SCC {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610B {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPLL/OPL/OPL2 drums - {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8", "FM 9", "FM 10", "FM 11", "FM 12", "FM 13", "FM 14", "FM 15", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 drums - {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6"}, // OPL3 4-op - {"FM 1", "FM 2", "FM 3", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 4-op + drums + {"4OP 1", "FM 2", "4OP 3", "FM 4", "4OP 5", "FM 6", "4OP 7", "FM 8", "4OP 9", "FM 10", "4OP 11", "FM 12", "FM 13", "FM 14", "FM 15", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 drums + {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6"}, // OPL3 4-op (UNUSED) + {"FM 1", "FM 2", "FM 3", "4OP 1", "4OP 2", "4OP 3", "4OP 4", "4OP 5", "4OP 6", "Kick", "Snare", "Tom", "Top", "HiHat"}, // OPL3 4-op + drums (UNUSED) {"PCM 1", "PCM 2", "PCM 3", "PCM 4", "PCM 5", "PCM 6", "PCM 7", "PCM 8", "PCM 9", "PCM 10", "PCM 11", "PCM 12", "PCM 13", "PCM 14", "PCM 15", "PCM 16", "ADPCM 1", "ADPCM 2", "ADPCM 3"}, // QSound + {"FM 1", "FM 2", "FM 3 OP1", "FM 3 OP2", "FM 3 OP3", "FM 3 OP4", "FM 4", "FM 5", "FM 6", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610B (extended channel 3) + {"Wave", "Wave/PCM", "Wave", "Wave/Noise"}, // Swan + {"PSG 1", "PSG 2", "PSG 3", "PSG 4", "PSG 5", "PSG 6", "PSG 7", "PSG 8", "PSG 9", "PSG 10", "PSG 11", "PSG 12", "PSG 13", "PSG 14", "PSG 15", "PSG 16", "PCM"}, // VERA }; -const char* chanShortNames[37][24]={ +const char* chanShortNames[38][32]={ {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "PCM"}, // YMU759 {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "NO"}, // Genesis {"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "S4"}, // Genesis (extended channel 3) @@ -903,18 +1045,19 @@ const char* chanShortNames[37][24]={ {"F1", "F2", "F3", "S1", "S2", "S3"}, // OPN {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "BD", "SD", "TP", "HH", "TM", "RM", "P"}, // PC-98 {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18"}, // OPL3 - {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24"}, // MultiPCM + {"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"}, // MultiPCM {"SQ"}, // PC Speaker/Pokémon Mini {"CH1", "CH2", "CH3", "CH4", "CH5", "NO"}, // Virtual Boy/SCC {"F1", "F2", "F3", "F4", "F5", "F6", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610B {"F1", "F2", "F3", "F4", "F5", "F6", "BD", "SD", "TM", "TP", "HH"}, // OPLL/OPL/OPL2 drums {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "BD", "SD", "TM", "TP", "HH"}, // OPL3 drums - {"F1", "F2", "F3", "F4", "F5", "F6", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6"}, // OPL3 4-op - {"F1", "F2", "F3", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "BD", "SD", "TM", "TP", "HH"}, // OPL3 4-op + drums + {"F1", "F2", "F3", "F4", "F5", "F6", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6"}, // OPL3 4-op (UNUSED) + {"F1", "F2", "F3", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "BD", "SD", "TM", "TP", "HH"}, // OPL3 4-op + drums (UNUSED) {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "A1", "A2", "A3"}, // QSound + {"F1", "F2", "O1", "O2", "O3", "O4", "F4", "F5", "F6", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610B (extended channel 3) }; -const int chanTypes[37][24]={ +const int chanTypes[41][32]={ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}, // YMU759 {0, 0, 0, 0, 0, 0, 1, 1, 1, 2}, // Genesis {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2}, // Genesis (extended channel 3) @@ -942,19 +1085,23 @@ const int chanTypes[37][24]={ {1, 1, 4}, // MMC5 {0, 0, 0, 1, 1, 1}, // OPN {0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 4}, // PC-98 - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 - {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, // MultiPCM/QSound + {5, 0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 0, 0, 0, 0, 0, 0, 0}, // OPL3 + {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}, // MultiPCM/QSound {1}, // PC Speaker/Pokémon Mini {3, 3, 3, 3, 3, 2}, // Virtual Boy/SCC {0, 0, 0, 0, 0, 0, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4}, // YM2610B {0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPLL/OPL/OPL2 drums - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op - {0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums - {3, 3, 3, 3}, //Lynx + {5, 0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op (UNUSED) + {0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums (UNUSED) + {3, 3, 3, 3}, // Lynx + {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 4, 4, 4, 4, 4, 4, 4}, // YM2610B (extended channel 3) + {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4}, // VERA + {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}, // X1-010 + {3, 4, 3, 2}, // Swan }; -const DivInstrumentType chanPrefType[43][24]={ +const DivInstrumentType chanPrefType[47][28]={ {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM}, // YMU759 {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis (extended channel 3) @@ -983,7 +1130,7 @@ const DivInstrumentType chanPrefType[43][24]={ {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY}, // OPN {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // PC-98 {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_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}, // OPL/OPL2/OPL3 - {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // MultiPCM/QSound + {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // MultiPCM/QSound {DIV_INS_STD}, // PC Speaker/Pokémon Mini {DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY}, // Virtual Boy {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // YM2610B @@ -998,6 +1145,10 @@ const DivInstrumentType chanPrefType[43][24]={ {DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN}, // Swan {DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ}, // Z {DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY}, // Lynx + {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_AY, DIV_INS_AY, DIV_INS_AY, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, // YM2610B (extended channel 3) + {DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_VERA, DIV_INS_AMIGA}, // VERA + {DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010, DIV_INS_X1_010}, // X1-010 + {DIV_INS_SCC, DIV_INS_SCC}, // Bubble System WSG }; const char* DivEngine::getChannelName(int chan) { @@ -1019,6 +1170,7 @@ const char* DivEngine::getChannelName(int chan) { break; case DIV_SYSTEM_SMS_OPLL: // this is flattened to SMS + OPLL. case DIV_SYSTEM_NES_VRC7: // this is flattened to NES + VRC7. + case DIV_SYSTEM_NES_FDS: // this is flattened to NES + FDS. return "??"; break; case DIV_SYSTEM_GB: @@ -1026,6 +1178,7 @@ const char* DivEngine::getChannelName(int chan) { break; case DIV_SYSTEM_PCE: case DIV_SYSTEM_SFX_BEEPER: + case DIV_SYSTEM_BUBSYS_WSG: return chanNames[5][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_NES: @@ -1051,10 +1204,12 @@ const char* DivEngine::getChannelName(int chan) { break; case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: - case DIV_SYSTEM_SWAN: case DIV_SYSTEM_LYNX: return chanNames[12][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_SWAN: + return chanNames[38][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_YM2151: return chanNames[13][dispatchChanOfChan[chan]]; break; @@ -1102,6 +1257,7 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_MULTIPCM: case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: + case DIV_SYSTEM_X1_010: return chanNames[28][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_PCSPKR: @@ -1115,6 +1271,9 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_YM2610B: return chanNames[31][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_YM2610B_EXT: + return chanNames[37][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPL_DRUMS: case DIV_SYSTEM_OPL2_DRUMS: @@ -1132,6 +1291,9 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_QSOUND: return chanNames[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanNames[39][dispatchChanOfChan[chan]]; + break; } return "??"; } @@ -1155,6 +1317,7 @@ const char* DivEngine::getChannelShortName(int chan) { break; case DIV_SYSTEM_SMS_OPLL: // this is flattened to SMS + OPLL. case DIV_SYSTEM_NES_VRC7: // this is flattened to NES + VRC7. + case DIV_SYSTEM_NES_FDS: // this is flattened to NES + FDS. return "??"; break; case DIV_SYSTEM_GB: @@ -1162,6 +1325,7 @@ const char* DivEngine::getChannelShortName(int chan) { break; case DIV_SYSTEM_PCE: case DIV_SYSTEM_SFX_BEEPER: + case DIV_SYSTEM_BUBSYS_WSG: return chanShortNames[5][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_NES: @@ -1238,6 +1402,7 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_MULTIPCM: case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: + case DIV_SYSTEM_X1_010: return chanShortNames[28][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_PCSPKR: @@ -1251,6 +1416,9 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_YM2610B: return chanShortNames[31][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_YM2610B_EXT: + return chanShortNames[37][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPL_DRUMS: case DIV_SYSTEM_OPL2_DRUMS: @@ -1268,6 +1436,9 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_QSOUND: return chanShortNames[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanShortNames[0][dispatchChanOfChan[chan]]; + break; } return "??"; } @@ -1289,6 +1460,7 @@ int DivEngine::getChannelType(int chan) { break; case DIV_SYSTEM_SMS_OPLL: // this is flattened to SMS + OPLL. case DIV_SYSTEM_NES_VRC7: // this is flattened to NES + VRC7. + case DIV_SYSTEM_NES_FDS: // this is flattened to NES + FDS. return 0; break; case DIV_SYSTEM_GB: @@ -1296,6 +1468,7 @@ int DivEngine::getChannelType(int chan) { break; case DIV_SYSTEM_PCE: case DIV_SYSTEM_SFX_BEEPER: + case DIV_SYSTEM_BUBSYS_WSG: return chanTypes[5][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_NES: @@ -1321,7 +1494,6 @@ int DivEngine::getChannelType(int chan) { break; case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: - case DIV_SYSTEM_SWAN: return chanTypes[12][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_YM2151: @@ -1385,6 +1557,9 @@ int DivEngine::getChannelType(int chan) { case DIV_SYSTEM_YM2610B: return chanTypes[31][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_YM2610B_EXT: + return chanTypes[37][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_OPL_DRUMS: case DIV_SYSTEM_OPL2_DRUMS: @@ -1402,6 +1577,15 @@ int DivEngine::getChannelType(int chan) { case DIV_SYSTEM_LYNX: return chanTypes[36][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanTypes[38][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_X1_010: + return chanTypes[39][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_SWAN: + return chanTypes[40][dispatchChanOfChan[chan]]; + break; } return 1; } @@ -1425,6 +1609,9 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) { case DIV_SYSTEM_NES_VRC7: // this is flattened to NES + VRC7. return DIV_INS_OPLL; break; + case DIV_SYSTEM_NES_FDS: // this is flattened to NES + FDS. + return DIV_INS_STD; + break; case DIV_SYSTEM_GB: return chanPrefType[4][dispatchChanOfChan[chan]]; break; @@ -1519,6 +1706,9 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) { case DIV_SYSTEM_YM2610B: return chanPrefType[31][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_YM2610B_EXT: + return chanPrefType[43][dispatchChanOfChan[chan]]; + break; case DIV_SYSTEM_OPLL_DRUMS: return chanPrefType[32][dispatchChanOfChan[chan]]; break; @@ -1549,30 +1739,58 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) { case DIV_SYSTEM_LYNX: return chanPrefType[42][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_VERA: + return chanPrefType[44][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_X1_010: + return chanPrefType[45][dispatchChanOfChan[chan]]; + break; + case DIV_SYSTEM_BUBSYS_WSG: + return chanPrefType[46][dispatchChanOfChan[chan]]; + break; } return DIV_INS_FM; } -bool DivEngine::isVGMExportable(DivSystem which) { +int DivEngine::minVGMVersion(DivSystem which) { switch (which) { + case DIV_SYSTEM_YM2612: + case DIV_SYSTEM_YM2612_EXT: case DIV_SYSTEM_SMS: + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_VRC7: + case DIV_SYSTEM_YM2151: + return 0x150; // due to usage of data blocks + case DIV_SYSTEM_SEGAPCM: + case DIV_SYSTEM_SEGAPCM_COMPAT: + case DIV_SYSTEM_YM2610: + case DIV_SYSTEM_YM2610_EXT: + case DIV_SYSTEM_YM2610_FULL: + case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B: + case DIV_SYSTEM_YM2610B_EXT: + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL_DRUMS: + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL2_DRUMS: + case DIV_SYSTEM_OPL3: + case DIV_SYSTEM_OPL3_DRUMS: + case DIV_SYSTEM_AY8910: + case DIV_SYSTEM_AY8930: + return 0x151; case DIV_SYSTEM_GB: case DIV_SYSTEM_PCE: case DIV_SYSTEM_NES: - case DIV_SYSTEM_YM2151: - case DIV_SYSTEM_YM2612: - case DIV_SYSTEM_YM2612_EXT: - case DIV_SYSTEM_YM2610: - case DIV_SYSTEM_YM2610_EXT: - case DIV_SYSTEM_AY8910: - case DIV_SYSTEM_AY8930: - case DIV_SYSTEM_SAA1099: + case DIV_SYSTEM_FDS: case DIV_SYSTEM_QSOUND: - case DIV_SYSTEM_SEGAPCM: - case DIV_SYSTEM_SEGAPCM_COMPAT: - return true; + return 0x161; + case DIV_SYSTEM_SAA1099: + case DIV_SYSTEM_X1_010: + case DIV_SYSTEM_SWAN: + return 0x171; default: - return false; + return 0; } - return false; + return 0; } diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 9424325c5..b20813489 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -25,119 +25,123 @@ constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0; void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write, int streamOff, double* loopTimer, double* loopFreq, int* loopSample, bool isSecond) { + unsigned char baseAddr1=isSecond?0xa0:0x50; + unsigned char baseAddr2=isSecond?0x80:0; + unsigned short baseAddr2S=isSecond?0x8000:0; + unsigned char smsAddr=isSecond?0x30:0x50; if (write.addr==0xffffffff) { // Furnace fake reset switch (sys) { case DIV_SYSTEM_YM2612: case DIV_SYSTEM_YM2612_EXT: for (int i=0; i<3; i++) { // set SL and RR to highest - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x80+i); w->writeC(0xff); - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x84+i); w->writeC(0xff); - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x88+i); w->writeC(0xff); - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x8c+i); w->writeC(0xff); - w->writeC(isSecond?0xa3:0x53); + w->writeC(3|baseAddr1); w->writeC(0x80+i); w->writeC(0xff); - w->writeC(isSecond?0xa3:0x53); + w->writeC(3|baseAddr1); w->writeC(0x84+i); w->writeC(0xff); - w->writeC(isSecond?0xa3:0x53); + w->writeC(3|baseAddr1); w->writeC(0x88+i); w->writeC(0xff); - w->writeC(isSecond?0xa3:0x53); + w->writeC(3|baseAddr1); w->writeC(0x8c+i); w->writeC(0xff); } for (int i=0; i<3; i++) { // note off - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x28); w->writeC(i); - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(0x28); w->writeC(4+i); } - w->writeC(isSecond?0xa2:0x52); // disable DAC + w->writeC(2|baseAddr1); // disable DAC w->writeC(0x2b); w->writeC(0); break; case DIV_SYSTEM_SMS: for (int i=0; i<4; i++) { - w->writeC(isSecond?0x30:0x50); + w->writeC(smsAddr); w->writeC(0x90|(i<<5)|15); } break; case DIV_SYSTEM_GB: // square 1 w->writeC(0xb3); - w->writeC(isSecond?0x82:2); + w->writeC(2|baseAddr2); w->writeC(0); w->writeC(0xb3); - w->writeC(isSecond?0x84:4); + w->writeC(4|baseAddr2); w->writeC(0x80); // square 2 w->writeC(0xb3); - w->writeC(isSecond?0x87:7); + w->writeC(7|baseAddr2); w->writeC(0); w->writeC(0xb3); - w->writeC(isSecond?0x89:9); + w->writeC(9|baseAddr2); w->writeC(0x80); // wave w->writeC(0xb3); - w->writeC(isSecond?0x8c:0x0c); + w->writeC(0x0c|baseAddr2); w->writeC(0); w->writeC(0xb3); - w->writeC(isSecond?0x8e:0x0e); + w->writeC(0x0e|baseAddr2); w->writeC(0x80); // noise w->writeC(0xb3); - w->writeC(isSecond?0x91:0x11); + w->writeC(0x11|baseAddr2); w->writeC(0); w->writeC(0xb3); - w->writeC(isSecond?0x93:0x13); + w->writeC(0x13|baseAddr2); w->writeC(0x80); break; case DIV_SYSTEM_PCE: for (int i=0; i<6; i++) { w->writeC(0xb9); - w->writeC(isSecond?0x80:0); + w->writeC(0|baseAddr2); w->writeC(i); w->writeC(0xb9); - w->writeC(isSecond?0x84:4); + w->writeC(4|baseAddr2); w->writeC(0); } break; case DIV_SYSTEM_NES: w->writeC(0xb4); - w->writeC(isSecond?0x95:0x15); + w->writeC(0x15|baseAddr2); w->writeC(0); break; case DIV_SYSTEM_YM2151: for (int i=0; i<8; i++) { - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(0xe0+i); w->writeC(0xff); - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(0xe8+i); w->writeC(0xff); - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(0xf0+i); w->writeC(0xff); - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(0xf8+i); w->writeC(0xff); - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(0x08); w->writeC(i); } @@ -146,111 +150,135 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write case DIV_SYSTEM_SEGAPCM_COMPAT: for (int i=0; i<16; i++) { w->writeC(0xc0); - w->writeS((isSecond?0x8086:0x86)+(i<<3)); + w->writeS((0x86|baseAddr2S)+(i<<3)); w->writeC(3); } break; + case DIV_SYSTEM_X1_010: + for (int i=0; i<16; i++) { + w->writeC(0xc8); + w->writeS_BE(baseAddr2S+(i<<3)); + w->writeC(0); + } + break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: + case DIV_SYSTEM_YM2610B: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B_EXT: for (int i=0; i<2; i++) { // set SL and RR to highest - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x81+i); w->writeC(0xff); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x85+i); w->writeC(0xff); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x89+i); w->writeC(0xff); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x8d+i); w->writeC(0xff); - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(0x81+i); w->writeC(0xff); - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(0x85+i); w->writeC(0xff); - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(0x89+i); w->writeC(0xff); - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(0x8d+i); w->writeC(0xff); } for (int i=0; i<2; i++) { // note off - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x28); w->writeC(1+i); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(0x28); w->writeC(5+i); } // reset AY - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(7); w->writeC(0x3f); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(8); w->writeC(0); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(9); w->writeC(0); - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(10); w->writeC(0); // reset sample - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(0); w->writeC(0xbf); break; + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_VRC7: + for (int i=0; i<9; i++) { + w->writeC(1|baseAddr1); + w->writeC(0x20+i); + w->writeC(0); + w->writeC(1|baseAddr1); + w->writeC(0x30+i); + w->writeC(0); + w->writeC(1|baseAddr1); + w->writeC(0x10+i); + w->writeC(0); + } + break; case DIV_SYSTEM_AY8910: w->writeC(0xa0); - w->writeC(isSecond?0x87:7); + w->writeC(7|baseAddr2); w->writeC(0x3f); w->writeC(0xa0); - w->writeC(isSecond?0x88:8); + w->writeC(8|baseAddr2); w->writeC(0); w->writeC(0xa0); - w->writeC(isSecond?0x89:9); + w->writeC(9|baseAddr2); w->writeC(0); w->writeC(0xa0); - w->writeC(isSecond?0x8a:10); + w->writeC(10|baseAddr2); w->writeC(0); break; case DIV_SYSTEM_AY8930: w->writeC(0xa0); - w->writeC(isSecond?0x8d:0x0d); + w->writeC(0x0d|baseAddr2); w->writeC(0); w->writeC(0xa0); - w->writeC(isSecond?0x8d:0x0d); + w->writeC(0x0d|baseAddr2); w->writeC(0xa0); break; case DIV_SYSTEM_SAA1099: w->writeC(0xbd); - w->writeC(isSecond?0x9c:0x1c); + w->writeC(0x1c|baseAddr2); w->writeC(0x02); w->writeC(0xbd); - w->writeC(isSecond?0x94:0x14); + w->writeC(0x14|baseAddr2); w->writeC(0); w->writeC(0xbd); - w->writeC(isSecond?0x95:0x15); + w->writeC(0x15|baseAddr2); w->writeC(0); for (int i=0; i<6; i++) { w->writeC(0xbd); - w->writeC((isSecond?0x80:0)+i); + w->writeC((0|baseAddr2)+i); w->writeC(0); } break; @@ -289,13 +317,105 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(0xd6+i); } break; + // TODO: it's 3:35am + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL_DRUMS: + // disable envelope + for (int i=0; i<6; i++) { + w->writeC(0x0b|baseAddr1); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0x0b|baseAddr1); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0x0b|baseAddr1); + w->writeC(0x90+i); + w->writeC(0x0f); + } + // key off + freq reset + for (int i=0; i<9; i++) { + w->writeC(0x0b|baseAddr1); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0x0b|baseAddr1); + w->writeC(0xb0+i); + w->writeC(0); + } + break; + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL2_DRUMS: + // disable envelope + for (int i=0; i<6; i++) { + w->writeC(0x0a|baseAddr1); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0x0a|baseAddr1); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0x0a|baseAddr1); + w->writeC(0x90+i); + w->writeC(0x0f); + } + // key off + freq reset + for (int i=0; i<9; i++) { + w->writeC(0x0a|baseAddr1); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0x0a|baseAddr1); + w->writeC(0xb0+i); + w->writeC(0); + } + break; + case DIV_SYSTEM_OPL3: + case DIV_SYSTEM_OPL3_DRUMS: + // disable envelope + for (int i=0; i<6; i++) { + w->writeC(0x0e|baseAddr1); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0x0e|baseAddr1); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0x0e|baseAddr1); + w->writeC(0x90+i); + w->writeC(0x0f); + w->writeC(0x0f|baseAddr1); + w->writeC(0x80+i); + w->writeC(0x0f); + w->writeC(0x0f|baseAddr1); + w->writeC(0x88+i); + w->writeC(0x0f); + w->writeC(0x0f|baseAddr1); + w->writeC(0x90+i); + w->writeC(0x0f); + } + // key off + freq reset + for (int i=0; i<9; i++) { + w->writeC(0x0e|baseAddr1); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0x0e|baseAddr1); + w->writeC(0xb0+i); + w->writeC(0); + w->writeC(0x0f|baseAddr1); + w->writeC(0xa0+i); + w->writeC(0); + w->writeC(0x0f|baseAddr1); + w->writeC(0xb0+i); + w->writeC(0); + } + // reset 4-op + w->writeC(0x0f|baseAddr1); + w->writeC(0x04); + w->writeC(0x00); + break; default: break; } } if (write.addr>=0xffff0000) { // Furnace special command unsigned char streamID=streamOff+((write.addr&0xff00)>>8); - logD("writing stream command %x:%x with stream ID %d\n",write.addr,write.val,streamID); + logD("writing stream command %x:%x with stream ID %d",write.addr,write.val,streamID); switch (write.addr&0xff) { case 0: // play sample if (write.val>8) { case 0: // port 0 - w->writeC(isSecond?0xa2:0x52); + w->writeC(2|baseAddr1); w->writeC(write.addr&0xff); w->writeC(write.val); break; case 1: // port 1 - w->writeC(isSecond?0xa3:0x53); + w->writeC(3|baseAddr1); w->writeC(write.addr&0xff); w->writeC(write.val); break; case 2: // PSG - w->writeC(isSecond?0x30:0x50); + w->writeC(smsAddr); w->writeC(write.val); break; } break; case DIV_SYSTEM_SMS: - w->writeC(isSecond?0x30:0x50); + w->writeC(smsAddr); w->writeC(write.val); break; case DIV_SYSTEM_GB: w->writeC(0xb3); - w->writeC((isSecond?0x80:0)|((write.addr-16)&0xff)); + w->writeC(baseAddr2|((write.addr-16)&0xff)); w->writeC(write.val); break; case DIV_SYSTEM_PCE: w->writeC(0xb9); - w->writeC((isSecond?0x80:0)|(write.addr&0xff)); + w->writeC(baseAddr2|(write.addr&0xff)); w->writeC(write.val); break; case DIV_SYSTEM_NES: w->writeC(0xb4); - w->writeC((isSecond?0x80:0)|(write.addr&0xff)); + w->writeC(baseAddr2|(write.addr&0xff)); + w->writeC(write.val); + break; + case DIV_SYSTEM_FDS: // yeah + w->writeC(0xb4); + if ((write.addr&0xff)==0x23) { + w->writeC(baseAddr2|0x3f); + } else if ((write.addr&0xff)>=0x80) { + w->writeC(baseAddr2|(0x20+(write.addr&0x7f))); + } else { + w->writeC(baseAddr2|(write.addr&0xff)); + } + w->writeC(write.val); break; case DIV_SYSTEM_YM2151: - w->writeC(isSecond?0xa4:0x54); + w->writeC(4|baseAddr1); w->writeC(write.addr&0xff); w->writeC(write.val); break; case DIV_SYSTEM_SEGAPCM: case DIV_SYSTEM_SEGAPCM_COMPAT: w->writeC(0xc0); - w->writeS((isSecond?0x8000:0)|(write.addr&0xffff)); + w->writeS(baseAddr2S|(write.addr&0xffff)); + w->writeC(write.val); + break; + case DIV_SYSTEM_X1_010: + w->writeC(0xc8); + w->writeS_BE(baseAddr2S|(write.addr&0x1fff)); w->writeC(write.val); break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: + case DIV_SYSTEM_YM2610B: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B_EXT: switch (write.addr>>8) { case 0: // port 0 - w->writeC(isSecond?0xa8:0x58); + w->writeC(8|baseAddr1); w->writeC(write.addr&0xff); w->writeC(write.val); break; case 1: // port 1 - w->writeC(isSecond?0xa9:0x59); + w->writeC(9|baseAddr1); w->writeC(write.addr&0xff); w->writeC(write.val); break; } break; + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_VRC7: + w->writeC(1|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: w->writeC(0xa0); - w->writeC((isSecond?0x80:0)|(write.addr&0xff)); + w->writeC(baseAddr2|(write.addr&0xff)); w->writeC(write.val); break; case DIV_SYSTEM_SAA1099: w->writeC(0xbd); - w->writeC((isSecond?0x80:0)|(write.addr&0xff)); + w->writeC(baseAddr2|(write.addr&0xff)); w->writeC(write.val); break; case DIV_SYSTEM_LYNX: @@ -413,17 +559,60 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(write.val&0xff); w->writeC(write.addr&0xff); break; + case DIV_SYSTEM_SWAN: + if ((write.addr&0x7f)<0x40) { + w->writeC(0xbc); + w->writeC(baseAddr2|(write.addr&0x3f)); + w->writeC(write.val&0xff); + } else { + // (Wave) RAM write + w->writeC(0xc6); + w->writeS_BE(baseAddr2S|(write.addr&0x3f)); + w->writeC(write.val&0xff); + } + break; + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL_DRUMS: + w->writeC(0x0b|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL2_DRUMS: + w->writeC(0x0a|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; + case DIV_SYSTEM_OPL3: + case DIV_SYSTEM_OPL3_DRUMS: + switch (write.addr>>8) { + case 0: // port 0 + w->writeC(0x0e|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; + case 1: // port 1 + w->writeC(0x0f|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val); + break; + } + break; default: - logW("write not handled!\n"); + logW("write not handled!"); break; } } -SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { +SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { + if (version<0x150) { + lastError="VGM version is too low"; + return NULL; + } stop(); repeatPattern=false; setOrder(0); - isBusy.lock(); + BUSY_BEGIN_SOFT; double origRate=got.rate; got.rate=44100; // determine loop point @@ -431,7 +620,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { int loopRow=0; int loopEnd=0; walkSong(loopOrder,loopRow,loopEnd); - logI("loop point: %d %d\n",loopOrder,loopRow); + logI("loop point: %d %d",loopOrder,loopRow); warnings=""; curOrder=0; @@ -506,7 +695,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { // write header w->write("Vgm ",4); w->writeI(0); // will be written later - w->writeI(0x171); // VGM 1.71 + w->writeI(version); bool willExport[32]; bool isSecond[32]; @@ -526,6 +715,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { bool writePCESamples=false; bool writeADPCM=false; bool writeSegaPCM=false; + bool writeX1010=false; bool writeQSound=false; for (int i=0; iversion) continue; switch (song.system[i]) { case DIV_SYSTEM_SMS: if (!hasSN) { @@ -609,10 +800,24 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { howManyChips++; } break; + case DIV_SYSTEM_X1_010: + if (!hasX1) { + hasX1=disCont[i].dispatch->chipClock; + willExport[i]=true; + writeX1010=true; + } else if (!(hasX1&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasX1|=0x40000000; + howManyChips++; + } + break; case DIV_SYSTEM_YM2610: case DIV_SYSTEM_YM2610_FULL: + case DIV_SYSTEM_YM2610B: case DIV_SYSTEM_YM2610_EXT: case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B_EXT: if (!hasOPNB) { hasOPNB=disCont[i].dispatch->chipClock; willExport[i]=true; @@ -623,6 +828,9 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { hasOPNB|=0x40000000; howManyChips++; } + if (((song.system[i]==DIV_SYSTEM_YM2610B) || (song.system[i]==DIV_SYSTEM_YM2610B_EXT)) && (!(hasOPNB&0x80000000))) { // YM2610B flag + hasOPNB|=0x80000000; + } break; case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: @@ -673,6 +881,33 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { howManyChips++; } break; + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_VRC7: + if (!hasOPLL) { + hasOPLL=disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasOPLL&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasOPLL|=0x40000000; + howManyChips++; + } + break; + case DIV_SYSTEM_FDS: + if (!hasNES) { + hasNES=0x80000000|disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasNES&0x80000000)) { + hasNES|=0x80000000; + willExport[i]=true; + } else if (!(hasNES&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasNES|=0xc0000000; + howManyChips++; + } + break; case DIV_SYSTEM_LYNX: if (!hasLynx) { hasLynx=disCont[i].dispatch->chipClock; @@ -698,6 +933,57 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { addWarning("dual QSound is not supported by the VGM format"); } break; + case DIV_SYSTEM_SWAN: + if (!hasSwan) { + hasSwan=disCont[i].dispatch->chipClock; + willExport[i]=true; + // funny enough, VGM doesn't have support for WSC's sound DMA by design + // so DAC stream it goes + // since WS has the same PCM format as YM2612 DAC, I can just reuse this flag + writeDACSamples=true; + } else if (!(hasSwan&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasSwan|=0x40000000; + howManyChips++; + } + break; + case DIV_SYSTEM_OPL: + case DIV_SYSTEM_OPL_DRUMS: + if (!hasOPL) { + hasOPL=disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasOPL&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasOPL|=0x40000000; + howManyChips++; + } + break; + case DIV_SYSTEM_OPL2: + case DIV_SYSTEM_OPL2_DRUMS: + if (!hasOPL2) { + hasOPL2=disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasOPL2&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasOPL2|=0x40000000; + howManyChips++; + } + break; + case DIV_SYSTEM_OPL3: + case DIV_SYSTEM_OPL3_DRUMS: + if (!hasOPL3) { + hasOPL3=disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasOPL3&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasOPL3|=0x40000000; + howManyChips++; + } + break; default: break; } @@ -724,66 +1010,138 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { w->writeI(0); // tick rate w->writeS(snNoiseConfig); w->writeC(snNoiseSize); - w->writeC(snFlags); + if (version>=0x151) { + w->writeC(snFlags); + } else { + w->writeC(0); + } w->writeI(hasOPN2); w->writeI(hasOPM); w->writeI(0); // data pointer. will be written later - w->writeI(hasSegaPCM); - w->writeI(segaPCMOffset); - w->writeI(hasRFC); - w->writeI(hasOPN); - w->writeI(hasOPNA); - w->writeI(hasOPNB); - w->writeI(hasOPL2); - w->writeI(hasOPL); - w->writeI(hasY8950); - w->writeI(hasOPL3); - w->writeI(hasOPL4); - w->writeI(hasOPX); - w->writeI(hasZ280); - w->writeI(hasRFC1); - w->writeI(hasPWM); - w->writeI(hasAY); - w->writeC(ayConfig); - w->writeC(ayFlags); - w->writeC(ayFlags); // OPN - w->writeC(ayFlags); // OPNA + if (version>=0x151) { + w->writeI(hasSegaPCM); + w->writeI(segaPCMOffset); + w->writeI(hasRFC); + w->writeI(hasOPN); + w->writeI(hasOPNA); + w->writeI(hasOPNB); + w->writeI(hasOPL2); + w->writeI(hasOPL); + w->writeI(hasY8950); + w->writeI(hasOPL3); + w->writeI(hasOPL4); + w->writeI(hasOPX); + w->writeI(hasZ280); + w->writeI(hasRFC1); + w->writeI(hasPWM); + w->writeI(hasAY); + w->writeC(ayConfig); + w->writeC(ayFlags); + w->writeC(ayFlags); // OPN + w->writeC(ayFlags); // OPNA + } else { + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeC(0); + w->writeC(0); + w->writeC(0); // OPN + w->writeC(0); // OPNA + } + // currently not used but is part of 1.60 w->writeC(0); // volume w->writeC(0); // reserved w->writeC(0); // loop count + // 1.51 w->writeC(0); // loop modifier - w->writeI(hasGB); - w->writeI(hasNES); - w->writeI(hasMultiPCM); - w->writeI(hasuPD7759); - w->writeI(hasOKIM6258); - w->writeC(0); // flags - w->writeC(0); // K flags - w->writeC(0); // C140 chip type - w->writeC(0); // reserved - w->writeI(hasOKIM6295); - w->writeI(hasK051649); - w->writeI(hasK054539); - w->writeI(hasPCE); - w->writeI(hasNamco); - w->writeI(hasK053260); - w->writeI(hasPOKEY); - w->writeI(hasQSound); - w->writeI(hasSCSP); + + if (version>=0x161) { + w->writeI(hasGB); + w->writeI(hasNES); + w->writeI(hasMultiPCM); + w->writeI(hasuPD7759); + w->writeI(hasOKIM6258); + w->writeC(0); // flags + w->writeC(0); // K flags + w->writeC(0); // C140 chip type + w->writeC(0); // reserved + w->writeI(hasOKIM6295); + w->writeI(hasK051649); + w->writeI(hasK054539); + w->writeI(hasPCE); + w->writeI(hasNamco); + w->writeI(hasK053260); + w->writeI(hasPOKEY); + w->writeI(hasQSound); + } else { + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeC(0); // flags + w->writeC(0); // K flags + w->writeC(0); // C140 chip type + w->writeC(0); // reserved + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + } + if (version>=0x171) { + w->writeI(hasSCSP); + } else { + w->writeI(0); + } + // 1.70 w->writeI(0); // extra header - w->writeI(hasSwan); - w->writeI(hasVSU); - w->writeI(hasSAA); - w->writeI(hasES5503); - w->writeI(hasES5505); - w->writeC(0); // 5503 chans - w->writeC(0); // 5505 chans - w->writeC(0); // C352 clock divider - w->writeC(0); // reserved - w->writeI(hasX1); - w->writeI(hasC352); - w->writeI(hasGA20); - w->writeI(hasLynx); + // 1.71 + if (version>=0x171) { + w->writeI(hasSwan); + w->writeI(hasVSU); + w->writeI(hasSAA); + w->writeI(hasES5503); + w->writeI(hasES5505); + w->writeC(0); // 5503 chans + w->writeC(0); // 5505 chans + w->writeC(0); // C352 clock divider + w->writeC(0); // reserved + w->writeI(hasX1); + w->writeI(hasC352); + w->writeI(hasGA20); + w->writeI(hasLynx); + } else { + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeC(0); // 5503 chans + w->writeC(0); // 5505 chans + w->writeC(0); // C352 clock divider + w->writeC(0); // reserved + w->writeI(0); + w->writeI(0); + w->writeI(0); + w->writeI(0); + } for (int i=0; i<6; i++) { // reserved w->writeI(0); } @@ -804,7 +1162,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { unsigned int sampleSeek=0; for (int i=0; ioff8=sampleSeek; sampleSeek+=sample->length8; } @@ -895,6 +1253,16 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { w->write(adpcmAMem,adpcmAMemLen); } + if (writeADPCM && adpcmBMemLen>0) { + w->writeC(0x67); + w->writeC(0x66); + w->writeC(0x83); + w->writeI(adpcmBMemLen+8); + w->writeI(adpcmBMemLen); + w->writeI(0); + w->write(adpcmBMem,adpcmBMemLen); + } + if (writeQSound && qsoundMemLen>0) { // always write a whole bank unsigned int blockSize=(qsoundMemLen+0xffff)&(~0xffff); @@ -910,6 +1278,16 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { w->write(qsoundMem,blockSize); } + if (writeX1010 && x1_010MemLen>0) { + w->writeC(0x67); + w->writeC(0x66); + w->writeC(0x91); + w->writeI(x1_010MemLen+8); + w->writeI(x1_010MemLen); + w->writeI(0); + w->write(x1_010Mem,x1_010MemLen); + } + // initialize streams int streamID=0; for (int i=0; iwriteC(0x90); + w->writeC(streamID); + w->writeC(isSecond[i]?0xa1:0x21); + w->writeC(0); // port + w->writeC(0x09); // DAC + + w->writeC(0x91); + w->writeC(streamID); + w->writeC(0); + w->writeC(1); + w->writeC(0); + + w->writeC(0x92); + w->writeC(streamID); + w->writeI(24000); // default + streamID++; + break; default: break; } @@ -1051,7 +1447,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { if (waitTime>0) { w->writeC(0x61); w->writeS(waitTime); - printf("wait is: %f\n",waitTime); + logV("wait is: %f",waitTime); totalWait-=waitTime; tickCount+=waitTime; } @@ -1165,8 +1561,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { freelance=false; extValuePresent=false; - logI("%d register writes total.\n",writeCount); + logI("%d register writes total.",writeCount); - isBusy.unlock(); + BUSY_END; return w; } diff --git a/src/engine/waveSynth.cpp b/src/engine/waveSynth.cpp new file mode 100644 index 000000000..4d776ba4c --- /dev/null +++ b/src/engine/waveSynth.cpp @@ -0,0 +1,147 @@ +#include "waveSynth.h" +#include "engine.h" +#include "instrument.h" + +bool DivWaveSynth::activeChanged() { + if (activeChangedB) { + activeChangedB=false; + return true; + } + return false; +} + +bool DivWaveSynth::tick() { + bool updated=first; + first=false; + if (!state.enabled) return updated; + + if (--divCounter<=0) { + // run effect + switch (state.effect) { + case DIV_WS_INVERT: + for (int i=0; i<=state.speed; i++) { + output[pos]=height-output[pos]; + if (++pos>=width) pos=0; + } + updated=true; + break; + case DIV_WS_ADD: + for (int i=0; i<=state.speed; i++) { + output[pos]+=MIN(height,state.param1); + if (output[pos]>=height) output[pos]-=height; + if (++pos>=width) pos=0; + } + updated=true; + break; + case DIV_WS_SUBTRACT: + for (int i=0; i<=state.speed; i++) { + output[pos]+=MIN(height,state.param1); + if (output[pos]<0) output[pos]+=height; + if (++pos>=width) pos=0; + } + updated=true; + break; + case DIV_WS_AVERAGE: + for (int i=0; i<=state.speed; i++) { + int pos1=(pos+1>=width)?0:(pos+1); + output[pos]=(output[pos]*state.param1+output[pos1]*(256-state.param1))>>8; + if (output[pos]<0) output[pos]=0; + if (output[pos]>height) output[pos]=height; + if (++pos>=width) pos=0; + } + updated=true; + break; + case DIV_WS_PHASE: + for (int i=0; i<=state.speed; i++) { + output[pos]=wave1[(pos+stage)%width]; + if (++pos>=width) { + pos=0; + if (++stage>=width) stage=0; + } + } + updated=true; + break; + case DIV_WS_WIPE: + break; + case DIV_WS_FADE: + break; + case DIV_WS_PING_PONG: + break; + case DIV_WS_OVERLAY: + break; + case DIV_WS_NEGATIVE_OVERLAY: + break; + case DIV_WS_PHASE_DUAL: + break; + } + divCounter=state.rateDivider; + } + + return updated; +} + +void DivWaveSynth::changeWave1(int num) { + DivWavetable* w1=e->getWave(num); + for (int i=0; imax<1 || w1->len<1) { + wave1[i]=0; + output[i]=0; + } else { + int data=w1->data[i*w1->len/width]*height/w1->max; + if (data<0) data=0; + if (data>height) data=height; + wave1[i]=data; + output[i]=data; + } + } + first=true; +} + +void DivWaveSynth::changeWave2(int num) { + DivWavetable* w2=e->getWave(num); + for (int i=0; imax<1 || w2->len<1) { + wave2[i]=0; + } else { + int data=w2->data[i*w2->len/width]*height/w2->max; + if (data<0) data=0; + if (data>height) data=height; + wave2[i]=data; + } + } + first=true; +} + +void DivWaveSynth::setEngine(DivEngine* engine) { + e=engine; +} + +void DivWaveSynth::init(DivInstrument* which, int w, int h, bool insChanged) { + width=w; + height=h; + if (width<0) width=0; + if (width>256) width=256; + if (e==NULL) return; + if (which==NULL) { + if (state.enabled) activeChangedB=true; + state=DivInstrumentWaveSynth(); + return; + } + if (!which->ws.enabled) { + if (state.enabled) activeChangedB=true; + state=DivInstrumentWaveSynth(); + return; + } else { + if (!state.enabled) activeChangedB=true; + } + state=which->ws; + if (insChanged || !state.global) { + pos=0; + stage=0; + divCounter=1+state.rateDivider; + first=true; + + changeWave1(state.wave1); + changeWave2(state.wave2); + } +} diff --git a/src/engine/waveSynth.h b/src/engine/waveSynth.h new file mode 100644 index 000000000..ccd8b23de --- /dev/null +++ b/src/engine/waveSynth.h @@ -0,0 +1,84 @@ +/** + * 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 _WAVESYNTH_H +#define _WAVESYNTH_H + +#include "instrument.h" +#include "wavetable.h" + +class DivEngine; + +class DivWaveSynth { + DivEngine* e; + DivInstrumentWaveSynth state; + int pos, stage, divCounter, width, height; + bool first, activeChangedB; + unsigned char wave1[256]; + unsigned char wave2[256]; + public: + /** + * the output. + */ + int output[256]; + /** + * check whether the "active" status has changed. + * @return truth. + */ + bool activeChanged(); + /** + * tick this DivWaveSynth. + * @return whether the wave has changed. + */ + bool tick(); + /** + * change the first wave. + * @param num wavetable number. + */ + void changeWave1(int num); + /** + * change the second wave. + * @param num wavetable number. + */ + void changeWave2(int num); + /** + * initialize this DivWaveSynth. + * @param which the instrument. + * @param width the system's wave width. + * @param height the system's wave height. + * @param insChanged whether the instrument has changed. + */ + void init(DivInstrument* which, int width, int height, bool insChanged=false); + void setEngine(DivEngine* engine); + DivWaveSynth(): + e(NULL), + pos(0), + stage(0), + divCounter(0), + width(32), + height(31), + first(false), + activeChangedB(false) { + memset(wave1,0,256); + memset(wave2,0,256); + memset(output,0,sizeof(int)*256); + } +}; + +#endif diff --git a/src/engine/wavetable.cpp b/src/engine/wavetable.cpp index 5d8c8a1d3..db6c33b2d 100644 --- a/src/engine/wavetable.cpp +++ b/src/engine/wavetable.cpp @@ -73,12 +73,12 @@ bool DivWavetable::save(const char* path) { FILE* outFile=ps_fopen(path,"wb"); if (outFile==NULL) { - logE("could not save wavetable: %s!\n",strerror(errno)); + 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!\n"); + logW("did not write entire wavetable!"); } fclose(outFile); w->finish(); diff --git a/src/engine/wavetable.h b/src/engine/wavetable.h index ff98bf3f2..616b048cf 100644 --- a/src/engine/wavetable.h +++ b/src/engine/wavetable.h @@ -26,8 +26,25 @@ struct DivWavetable { int len, min, max; int data[256]; + /** + * save the wavetable to a SafeWriter. + * @param w the SafeWriter in question. + */ void putWaveData(SafeWriter* w); + + /** + * read wavetable data in .fuw format. + * @param reader the reader. + * @param version the format version. + * @return a DivDataErrors. + */ DivDataErrors readWaveData(SafeReader& reader, short version); + + /** + * save this wavetable to a file. + * @param path file path. + * @return whether it was successful. + */ bool save(const char* path); DivWavetable(): len(32), diff --git a/src/engine/winStuff.cpp b/src/engine/winStuff.cpp index 1830a301c..79065e88b 100644 --- a/src/engine/winStuff.cpp +++ b/src/engine/winStuff.cpp @@ -33,15 +33,15 @@ String getWinConfigPath() { configPath=path; configPath+=L"\\furnace"; if (!PathIsDirectoryW(configPath.c_str())) { - logI("creating config dir...\n"); + logI("creating config dir..."); int mkdirRet; if ((mkdirRet=SHCreateDirectory(NULL,configPath.c_str()))!=ERROR_SUCCESS) { - logW("could not make config dir! (%.8x)\n",mkdirRet); + logW("could not make config dir! (%.8x)",mkdirRet); configPath=L"."; } } } else { - logW("unable to determine config directory! (%.8x)\n",configHR); + logW("unable to determine config directory! (%.8x)",configHR); configPath=L"."; } return utf16To8(configPath.c_str()); diff --git a/src/gui/about.cpp b/src/gui/about.cpp new file mode 100644 index 000000000..386b01b0c --- /dev/null +++ b/src/gui/about.cpp @@ -0,0 +1,214 @@ +/** + * 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 "gui.h" +#include + +const char* aboutLine[]={ + "tildearrow", + "is proud to present", + "", + ("Furnace " DIV_VERSION), + "", + "the free software chiptune tracker,", + "compatible with DefleMask modules.", + "", + "zero disassembly.", + "just clean-room design,", + "time and dedication.", + "", + "> CREDITS <", + "", + "-- program --", + "tildearrow", + "akumanatt", + "cam900", + "djtuBIG-MaliceX", + "laoo", + "superctr", + "", + "-- graphics/UI design --", + "tildearrow", + "BlastBrothers", + "Raijin", + "", + "-- documentation --", + "tildearrow", + "freq-mod", + "nicco1690", + "DeMOSic", + "cam900", + "", + "-- demo songs --", + "0x5066", + "ActualNK358", + "breakthetargets", + "CaptainMalware", + "kleeder", + "Mahbod Karamoozian", + "nicco1690", + "NikonTeen", + "SuperJet Spade", + "TheDuccinator", + "TheRealHedgehogSonic", + "tildearrow", + "Ultraprogramer", + "", + "-- additional feedback/fixes --", + "fd", + "OPNA2608", + "plane", + "TheEssem", + "", + "powered by:", + "Dear ImGui by Omar Cornut", + "SDL2 by Sam Lantinga", + "zlib by Jean-loup Gailly", + "and Mark Adler", + "libsndfile by Erik de Castro Lopo", + "Nuked-OPM & Nuked-OPN2 by Nuke.YKT", + "ymfm by Aaron Giles", + "MAME SN76496 by Nicola Salmoria", + "MAME AY-3-8910 by Couriersud", + "with AY8930 fixes by Eulous", + "MAME SAA1099 by Juergen Buchmueller and Manuel Abadia", + "SAASound", + "SameBoy by Lior Halphon", + "Mednafen PCE", + "puNES by FHorse", + "reSID by Dag Lem", + "Stella by Stella Team", + "QSound emulator by Ian Karlsson and Valley Bell", + "", + "greetings to:", + "Delek", + "fd", + "ILLUMIDARO", + "all members of Deflers of Noice!", + "", + "copyright © 2021-2022 tildearrow", + "(and contributors).", + "licensed under GPLv2+! see", + "LICENSE for more information.", + "", + "help Furnace grow:", + "https://github.com/tildearrow/furnace", + "", + "contact tildearrow at:", + "https://tildearrow.org/?p=contact", + "", + "disclaimer:", + "despite the fact this program works", + "with the .dmf file format, it is NOT", + "affiliated with Delek or DefleMask in", + "any way, nor it is a replacement for", + "the original program.", + "", + "it also comes with ABSOLUTELY NO WARRANTY.", + "", + "thanks to all contributors/bug reporters!" +}; + +const size_t aboutCount=sizeof(aboutLine)/sizeof(aboutLine[0]); + +void FurnaceGUI::drawAbout() { + // do stuff + if (ImGui::Begin("About Furnace",NULL,ImGuiWindowFlags_Modal|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoDocking|ImGuiWindowFlags_NoTitleBar)) { + ImGui::SetWindowPos(ImVec2(0,0)); + ImGui::SetWindowSize(ImVec2(scrW*dpiScale,scrH*dpiScale)); + ImGui::PushFont(bigFont); + ImDrawList* dl=ImGui::GetWindowDrawList(); + float r=0; + float g=0; + float b=0; + float peakMix=settings.partyTime?((peak[0]+peak[1])*0.5):0.3; + ImGui::ColorConvertHSVtoRGB(aboutHue,1.0,0.25+MIN(0.75f,peakMix*0.75f),r,g,b); + dl->AddRectFilled(ImVec2(0,0),ImVec2(scrW*dpiScale,scrH*dpiScale),0xff000000); + bool skip=false; + bool skip2=false; + for (int i=(-80-sin(double(aboutSin)*2*M_PI/120.0)*80.0)*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.25,g*0.25,b*0.25,1.0))); + } + } + + skip=false; + skip2=false; + for (int i=(-80-cos(double(aboutSin)*2*M_PI/120.0)*80.0)*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.5,g*0.5,b*0.5,1.0))); + } + } + + skip=false; + skip2=false; + for (int i=(-160+fmod(aboutSin*2,160))*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.75,g*0.75,b*0.75,1.0))); + } + } + + for (size_t i=0; iscrH*dpiScale) continue; + dl->AddText(bigFont,bigFont->FontSize, + ImVec2(posX+dpiScale,posY+dpiScale), + 0xff000000,aboutLine[i]); + dl->AddText(bigFont,bigFont->FontSize, + ImVec2(posX+dpiScale,posY-dpiScale), + 0xff000000,aboutLine[i]); + dl->AddText(bigFont,bigFont->FontSize, + ImVec2(posX-dpiScale,posY+dpiScale), + 0xff000000,aboutLine[i]); + dl->AddText(bigFont,bigFont->FontSize, + ImVec2(posX-dpiScale,posY-dpiScale), + 0xff000000,aboutLine[i]); + dl->AddText(bigFont,bigFont->FontSize, + ImVec2(posX,posY), + 0xffffffff,aboutLine[i]); + } + ImGui::PopFont(); + + float timeScale=60.0f*ImGui::GetIO().DeltaTime; + + aboutHue+=(0.001+peakMix*0.004)*timeScale; + aboutScroll+=(2+(peakMix>0.78)*3)*timeScale; + aboutSin+=(1+(peakMix>0.75)*2)*timeScale; + + while (aboutHue>1) aboutHue--; + while (aboutSin>=2400) aboutSin-=2400; + if (aboutScroll>(42*aboutCount+scrH)) aboutScroll=-20; + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_ABOUT; + ImGui::End(); +} diff --git a/src/gui/actionUtil.h b/src/gui/actionUtil.h new file mode 100644 index 000000000..752a54423 --- /dev/null +++ b/src/gui/actionUtil.h @@ -0,0 +1,41 @@ +/** + * 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 DETERMINE_FIRST \ + int firstChannel=0; \ + for (int i=0; igetTotalChannelCount(); i++) { \ + if (e->song.chanShow[i]) { \ + firstChannel=i; \ + break; \ + } \ + } + +#define DETERMINE_LAST \ + int lastChannel=0; \ + for (int i=e->getTotalChannelCount()-1; i>=0; i--) { \ + if (e->song.chanShow[i]) { \ + lastChannel=i+1; \ + break; \ + } \ + } + +#define DETERMINE_FIRST_LAST \ + DETERMINE_FIRST \ + DETERMINE_LAST + diff --git a/src/gui/channels.cpp b/src/gui/channels.cpp new file mode 100644 index 000000000..5b2a7e0c6 --- /dev/null +++ b/src/gui/channels.cpp @@ -0,0 +1,53 @@ +/** + * 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" + +void FurnaceGUI::drawChannels() { + if (nextWindow==GUI_WINDOW_CHANNELS) { + channelsOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!channelsOpen) return; + if (ImGui::Begin("Channels",&channelsOpen)) { + if (ImGui::BeginTable("ChannelList",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,48.0f*dpiScale); + for (int i=0; igetTotalChannelCount(); i++) { + ImGui::PushID(i); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Checkbox("##Visible",&e->song.chanShow[i]); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextWithHint("##ChanName",e->getChannelName(i),&e->song.chanName[i]); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputTextWithHint("##ChanShortName",e->getChannelShortName(i),&e->song.chanShortName[i]); + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_CHANNELS; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/compatFlags.cpp b/src/gui/compatFlags.cpp new file mode 100644 index 000000000..15a900b2d --- /dev/null +++ b/src/gui/compatFlags.cpp @@ -0,0 +1,164 @@ +/** + * 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" + +void FurnaceGUI::drawCompatFlags() { + if (nextWindow==GUI_WINDOW_COMPAT_FLAGS) { + compatFlagsOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!compatFlagsOpen) return; + if (ImGui::Begin("Compatibility Flags",&compatFlagsOpen)) { + ImGui::TextWrapped("these flags are designed to provide better DefleMask/older Furnace compatibility."); + ImGui::Checkbox("Limit slide range",&e->song.limitSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, slides are limited to a compatible range.\nmay cause problems with slides in negative octaves."); + } + ImGui::Checkbox("Linear pitch control",&e->song.linearPitch); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("linear pitch:\n- slides work in frequency/period space\n- E5xx and 04xx effects work in tonality space\nnon-linear pitch:\n- slides work in frequency/period space\n- E5xx and 04xx effects work on frequency/period space"); + } + ImGui::Checkbox("Proper noise layout on NES and PC Engine",&e->song.properNoiseLayout); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("use a proper noise channel note mapping (0-15) instead of a rather unusual compatible one.\nunlocks all noise frequencies on PC Engine."); + } + ImGui::Checkbox("Game Boy instrument duty is wave volume",&e->song.waveDutyIsVol); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("if enabled, an instrument with duty macro in the wave channel will be mapped to wavetable volume."); + } + + ImGui::Checkbox("Restart macro on portamento",&e->song.resetMacroOnPorta); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, a portamento effect will reset the channel's macro if used in combination with a note."); + } + ImGui::Checkbox("Legacy volume slides",&e->song.legacyVolumeSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulate glitchy volume slide behavior by silently overflowing the volume when the slide goes below 0."); + } + ImGui::Checkbox("Compatible arpeggio",&e->song.compatibleArpeggio); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("delay arpeggio by one tick on every new note."); + } + ImGui::Checkbox("Reset slides after note off",&e->song.noteOffResetsSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, note off will reset the channel's slide effect."); + } + ImGui::Checkbox("Reset portamento after reaching target",&e->song.targetResetsSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, the slide effect is disabled after it reaches its target."); + } + ImGui::Checkbox("Ignore duplicate slide effects",&e->song.ignoreDuplicateSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("if this is on, only the first slide of a row in a channel will be considered."); + } + ImGui::Checkbox("Continuous vibrato",&e->song.continuousVibrato); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, vibrato will not be reset on a new note."); + } + ImGui::Checkbox("Broken DAC mode",&e->song.brokenDACMode); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, the DAC in YM2612 will be disabled if there isn't any sample playing."); + } + ImGui::Checkbox("Auto-insert one tick gap between notes",&e->song.oneTickCut); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, a one-tick note cut will be inserted between non-legato/non-portamento notes.\nthis simulates the behavior of some Amiga/SNES music engines."); + } + ImGui::Checkbox("Broken speed alternation",&e->song.brokenSpeedSel); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("determines next speed based on whether the row is odd/even instead of alternating between speeds."); + } + ImGui::Checkbox("Don't slide on the first tick of a row",&e->song.noSlidesOnFirstTick); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulates ProTracker's behavior of not applying volume/pitch slides on the first tick of a row."); + } + ImGui::Checkbox("Reset arpeggio position on row change",&e->song.rowResetsArpPos); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulates ProTracker's behavior of arpeggio being bound to the current tick of a row."); + } + ImGui::Checkbox("Ignore 0Dxx on the last order",&e->song.ignoreJumpAtEnd); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("if this is on, a jump to next row effect will not take place when it is on the last order of a song."); + } + ImGui::Checkbox("Buggy portamento after pitch slide",&e->song.buggyPortaAfterSlide); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulates a bug in where portamento does not work after sliding."); + } + ImGui::Checkbox("Apply Game Boy envelope on note-less instrument change",&e->song.gbInsAffectsEnvelope); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("if this is on, an instrument change will also affect the envelope."); + } + + ImGui::Text("Loop modality:"); + if (ImGui::RadioButton("Reset channels",e->song.loopModality==0)) { + e->song.loopModality=0; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("select to reset channels on loop. may trigger a voltage click on every loop!"); + } + if (ImGui::RadioButton("Soft reset channels",e->song.loopModality==1)) { + e->song.loopModality=1; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("select to turn channels off on loop."); + } + if (ImGui::RadioButton("Do nothing",e->song.loopModality==2)) { + e->song.loopModality=2; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("select to not reset channels on loop."); + } + + ImGui::Separator(); + + ImGui::TextWrapped("the following flags are for compatibility with older Furnace versions."); + + ImGui::Checkbox("Arpeggio inhibits non-porta slides",&e->song.arpNonPorta); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.5.5"); + } + ImGui::Checkbox("Wack FM algorithm macro",&e->song.algMacroBehavior); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.5.5"); + } + ImGui::Checkbox("Broken shortcut slides (E1xy/E2xy)",&e->song.brokenShortcutSlides); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.5.7"); + } + ImGui::Checkbox("Stop portamento on note off",&e->song.stopPortaOnNoteOff); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } + ImGui::Checkbox("Allow instrument change during slides",&e->song.newInsTriggersInPorta); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } + ImGui::Checkbox("Reset note to base on arpeggio stop",&e->song.arp0Reset); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } + ImGui::Checkbox("ExtCh channel status is shared among operators",&e->song.sharedExtStat); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6"); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_COMPAT_FLAGS; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/cursor.cpp b/src/gui/cursor.cpp new file mode 100644 index 000000000..421b6d486 --- /dev/null +++ b/src/gui/cursor.cpp @@ -0,0 +1,294 @@ +/** + * 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 "actionUtil.h" + +void FurnaceGUI::startSelection(int xCoarse, int xFine, int y) { + if (xCoarse!=selStart.xCoarse || xFine!=selStart.xFine || y!=selStart.y) { + curNibble=false; + } + cursor.xCoarse=xCoarse; + cursor.xFine=xFine; + cursor.y=y; + selStart.xCoarse=xCoarse; + selStart.xFine=xFine; + selStart.y=y; + selEnd.xCoarse=xCoarse; + selEnd.xFine=xFine; + selEnd.y=y; + selecting=true; + e->setMidiBaseChan(cursor.xCoarse); +} + +void FurnaceGUI::updateSelection(int xCoarse, int xFine, int y) { + if (!selecting) return; + selEnd.xCoarse=xCoarse; + selEnd.xFine=xFine; + selEnd.y=y; +} + +void FurnaceGUI::finishSelection() { + // swap points if needed + if (selEnd.ygetTotalChannelCount(); + + if (selStart.xCoarse<0) selStart.xCoarse=0; + if (selStart.xCoarse>=chanCount) selStart.xCoarse=chanCount-1; + if (selStart.y<0) selStart.y=0; + if (selStart.y>=e->song.patLen) selStart.y=e->song.patLen-1; + if (selEnd.xCoarse<0) selEnd.xCoarse=0; + if (selEnd.xCoarse>=chanCount) selEnd.xCoarse=chanCount-1; + if (selEnd.y<0) selEnd.y=0; + if (selEnd.y>=e->song.patLen) selEnd.y=e->song.patLen-1; + if (cursor.xCoarse<0) cursor.xCoarse=0; + if (cursor.xCoarse>=chanCount) cursor.xCoarse=chanCount-1; + if (cursor.y<0) cursor.y=0; + if (cursor.y>=e->song.patLen) cursor.y=e->song.patLen-1; + + if (e->song.chanCollapse[selEnd.xCoarse]) { + selStart.xFine=0; + } + if (e->song.chanCollapse[selEnd.xCoarse]) { + selEnd.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; + } + + e->setMidiBaseChan(cursor.xCoarse); +} + +void FurnaceGUI::moveCursor(int x, int y, bool select) { + if (!select) { + finishSelection(); + } + + DETERMINE_FIRST_LAST; + + curNibble=false; + if (x!=0) { + demandScrollX=true; + if (x>0) { + for (int i=0; i=(e->song.chanCollapse[cursor.xCoarse]?1:(3+e->song.pat[cursor.xCoarse].effectRows*2))) { + cursor.xFine=0; + if (++cursor.xCoarse>=lastChannel) { + if (settings.wrapHorizontal!=0 && !select) { + cursor.xCoarse=firstChannel; + if (settings.wrapHorizontal==2) y++; + } else { + cursor.xCoarse=lastChannel-1; + cursor.xFine=e->song.chanCollapse[cursor.xCoarse]?0:(2+e->song.pat[cursor.xCoarse].effectRows*2); + } + } else { + while (!e->song.chanShow[cursor.xCoarse]) { + cursor.xCoarse++; + if (cursor.xCoarse>=e->getTotalChannelCount()) break; + } + } + } + } + } else { + for (int i=0; i<-x; i++) { + if (--cursor.xFine<0) { + if (--cursor.xCoarsesong.pat[cursor.xCoarse].effectRows*2; + if (settings.wrapHorizontal==2) y--; + } else { + cursor.xCoarse=firstChannel; + cursor.xFine=0; + } + } else { + while (!e->song.chanShow[cursor.xCoarse]) { + cursor.xCoarse--; + if (cursor.xCoarse<0) break; + } + if (e->song.chanCollapse[cursor.xCoarse]) { + cursor.xFine=0; + } else { + cursor.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; + } + } + } + } + } + } + if (y!=0) { + if (y>0) { + for (int i=0; i=e->song.patLen) { + if (settings.wrapVertical!=0 && !select) { + cursor.y=0; + if (settings.wrapVertical==2) { + if (!e->isPlaying() && e->getOrder()<(e->song.ordersLen-1)) { + e->setOrder(e->getOrder()+1); + } else { + cursor.y=e->song.patLen-1; + } + } + } else { + cursor.y=e->song.patLen-1; + } + } + } + } else { + for (int i=0; i<-y; i++) { + cursor.y--; + if (cursor.y<0) { + if (settings.wrapVertical!=0 && !select) { + cursor.y=e->song.patLen-1; + if (settings.wrapVertical==2) { + if (!e->isPlaying() && e->getOrder()>0) { + e->setOrder(e->getOrder()-1); + } else { + cursor.y=0; + } + } + } else { + cursor.y=0; + } + } + } + } + } + if (!select) { + selStart=cursor; + } + selEnd=cursor; + updateScroll(cursor.y); + e->setMidiBaseChan(cursor.xCoarse); +} + +void FurnaceGUI::moveCursorPrevChannel(bool overflow) { + finishSelection(); + curNibble=false; + + DETERMINE_FIRST_LAST; + + do { + cursor.xCoarse--; + if (cursor.xCoarse<0) break; + } while (!e->song.chanShow[cursor.xCoarse]); + if (cursor.xCoarsesetMidiBaseChan(cursor.xCoarse); + + selStart=cursor; + selEnd=cursor; + demandScrollX=true; +} + +void FurnaceGUI::moveCursorNextChannel(bool overflow) { + finishSelection(); + curNibble=false; + + DETERMINE_FIRST_LAST; + + do { + cursor.xCoarse++; + if (cursor.xCoarse>=e->getTotalChannelCount()) break; + } while (!e->song.chanShow[cursor.xCoarse]); + if (cursor.xCoarse>=lastChannel) { + if (overflow) { + cursor.xCoarse=firstChannel; + } else { + cursor.xCoarse=lastChannel-1; + } + } + e->setMidiBaseChan(cursor.xCoarse); + + selStart=cursor; + selEnd=cursor; + demandScrollX=true; +} + +void FurnaceGUI::moveCursorTop(bool select) { + finishSelection(); + curNibble=false; + if (cursor.y==0) { + DETERMINE_FIRST; + cursor.xCoarse=firstChannel; + cursor.xFine=0; + demandScrollX=true; + } else { + cursor.y=0; + } + selStart=cursor; + if (!select) { + selEnd=cursor; + } + e->setMidiBaseChan(cursor.xCoarse); + updateScroll(cursor.y); +} + +void FurnaceGUI::moveCursorBottom(bool select) { + finishSelection(); + curNibble=false; + if (cursor.y==e->song.patLen-1) { + DETERMINE_LAST; + cursor.xCoarse=lastChannel-1; + if (cursor.xCoarse<0) cursor.xCoarse=0; + cursor.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; + demandScrollX=true; + } else { + cursor.y=e->song.patLen-1; + } + if (!select) { + selStart=cursor; + } + selEnd=cursor; + e->setMidiBaseChan(cursor.xCoarse); + updateScroll(cursor.y); +} + +void FurnaceGUI::editAdvance() { + finishSelection(); + cursor.y+=editStep; + if (cursor.y>=e->song.patLen) cursor.y=e->song.patLen-1; + selStart=cursor; + selEnd=cursor; + updateScroll(cursor.y); +} \ No newline at end of file diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp new file mode 100644 index 000000000..9557cc57d --- /dev/null +++ b/src/gui/dataList.cpp @@ -0,0 +1,371 @@ +/** + * 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 "IconsFontAwesome4.h" +#include "misc/cpp/imgui_stdlib.h" +#include "plot_nolerp.h" +#include "guiConst.h" +#include + +const char* sampleNote[12]={ + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" +}; + +void FurnaceGUI::drawInsList() { + if (nextWindow==GUI_WINDOW_INS_LIST) { + insListOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!insListOpen) return; + if (ImGui::Begin("Instruments",&insListOpen)) { + if (ImGui::Button(ICON_FA_PLUS "##InsAdd")) { + doAction(GUI_ACTION_INS_LIST_ADD); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FILES_O "##InsClone")) { + doAction(GUI_ACTION_INS_LIST_DUPLICATE); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##InsLoad")) { + doAction(GUI_ACTION_INS_LIST_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FLOPPY_O "##InsSave")) { + doAction(GUI_ACTION_INS_LIST_SAVE); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("InsUp",ImGuiDir_Up)) { + doAction(GUI_ACTION_INS_LIST_MOVE_UP); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("InsDown",ImGuiDir_Down)) { + doAction(GUI_ACTION_INS_LIST_MOVE_DOWN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TIMES "##InsDelete")) { + doAction(GUI_ACTION_INS_LIST_DELETE); + } + ImGui::Separator(); + if (ImGui::BeginTable("InsListScroll",1,ImGuiTableFlags_ScrollY)) { + if (settings.unifiedDataView) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_TASKS " Instruments"); + ImGui::Indent(); + } + + for (int i=0; i<(int)e->song.ins.size(); i++) { + DivInstrument* ins=e->song.ins[i]; + String name; + switch (ins->type) { + case DIV_INS_FM: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FM]); + name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_STD: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_STD]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_GB: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_GB]); + name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_C64: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_C64]); + name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_AMIGA: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AMIGA]); + name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_PCE: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PCE]); + name=fmt::sprintf(ICON_FA_ID_BADGE " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_AY: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_AY8930: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY8930]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_TIA: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_TIA]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_SAA1099: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SAA1099]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_VIC: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VIC]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_PET: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PET]); + name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_VRC6: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VRC6]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_VRC6_SAW: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VRC6_SAW]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_OPLL: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPLL]); + name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_OPL: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPL]); + name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_FDS: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FDS]); + name=fmt::sprintf(ICON_FA_FLOPPY_O " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_VBOY: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VBOY]); + name=fmt::sprintf(ICON_FA_BINOCULARS " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_N163: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_N163]); + name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_SCC: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SCC]); + name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_OPZ: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPZ]); + name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_POKEY: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_POKEY]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_BEEPER: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_BEEPER]); + name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_SWAN: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SWAN]); + name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_MIKEY: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MIKEY]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_VERA: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VERA]); + name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d",i,ins->name,i); + break; + case DIV_INS_X1_010: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_X1_010]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + break; + default: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_UNKNOWN]); + name=fmt::sprintf(ICON_FA_QUESTION " %.2X: %s##_INS%d",i,ins->name,i); + break; + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(name.c_str(),curIns==i)) { + curIns=i; + } + if (settings.insFocusesPattern && patternOpen && ImGui::IsItemActivated()) { + nextWindow=GUI_WINDOW_PATTERN; + curIns=i; + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s",(ins->type>DIV_INS_MAX)?"Unknown":insTypes[ins->type]); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + insEditOpen=true; + nextWindow=GUI_WINDOW_INS_EDIT; + } + } + } + + if (settings.unifiedDataView) { + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_AREA_CHART " Wavetables"); + ImGui::Indent(); + actualWaveList(); + ImGui::Unindent(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_FA_VOLUME_UP " Samples"); + ImGui::Indent(); + actualSampleList(); + ImGui::Unindent(); + } + + ImGui::EndTable(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_LIST; + ImGui::End(); +} + +void FurnaceGUI::drawWaveList() { + if (nextWindow==GUI_WINDOW_WAVE_LIST) { + waveListOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!waveListOpen) return; + if (ImGui::Begin("Wavetables",&waveListOpen)) { + if (ImGui::Button(ICON_FA_PLUS "##WaveAdd")) { + doAction(GUI_ACTION_WAVE_LIST_ADD); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FILES_O "##WaveClone")) { + doAction(GUI_ACTION_WAVE_LIST_DUPLICATE); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##WaveLoad")) { + doAction(GUI_ACTION_WAVE_LIST_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FLOPPY_O "##WaveSave")) { + doAction(GUI_ACTION_WAVE_LIST_SAVE); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("WaveUp",ImGuiDir_Up)) { + doAction(GUI_ACTION_WAVE_LIST_UP); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("WaveDown",ImGuiDir_Down)) { + doAction(GUI_ACTION_WAVE_LIST_DOWN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TIMES "##WaveDelete")) { + doAction(GUI_ACTION_WAVE_LIST_DELETE); + } + ImGui::Separator(); + if (ImGui::BeginTable("WaveListScroll",1,ImGuiTableFlags_ScrollY)) { + actualWaveList(); + ImGui::EndTable(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_WAVE_LIST; + ImGui::End(); +} + +void FurnaceGUI::drawSampleList() { + if (nextWindow==GUI_WINDOW_SAMPLE_LIST) { + sampleListOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!sampleListOpen) return; + if (ImGui::Begin("Samples",&sampleListOpen)) { + if (ImGui::Button(ICON_FA_FILE "##SampleAdd")) { + doAction(GUI_ACTION_SAMPLE_LIST_ADD); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##SampleLoad")) { + doAction(GUI_ACTION_SAMPLE_LIST_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FLOPPY_O "##SampleSave")) { + doAction(GUI_ACTION_SAMPLE_LIST_SAVE); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("SampleUp",ImGuiDir_Up)) { + doAction(GUI_ACTION_SAMPLE_LIST_MOVE_UP); + } + ImGui::SameLine(); + if (ImGui::ArrowButton("SampleDown",ImGuiDir_Down)) { + doAction(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TIMES "##SampleDelete")) { + doAction(GUI_ACTION_SAMPLE_LIST_DELETE); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_VOLUME_UP "##PreviewSampleL")) { + doAction(GUI_ACTION_SAMPLE_LIST_PREVIEW); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_VOLUME_OFF "##StopSampleL")) { + doAction(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW); + } + ImGui::Separator(); + if (ImGui::BeginTable("SampleListScroll",1,ImGuiTableFlags_ScrollY)) { + actualSampleList(); + ImGui::EndTable(); + } + ImGui::Unindent(); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_LIST; + ImGui::End(); +} + +void FurnaceGUI::actualWaveList() { + float wavePreview[256]; + for (int i=0; i<(int)e->song.wave.size(); i++) { + DivWavetable* wave=e->song.wave[i]; + for (int i=0; ilen; i++) { + wavePreview[i]=wave->data[i]; + } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { + curWave=i; + } + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + waveEditOpen=true; + } + } + ImGui::SameLine(); + PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); + } +} + +void FurnaceGUI::actualSampleList() { + for (int i=0; i<(int)e->song.sample.size(); i++) { + DivSample* sample=e->song.sample[i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(fmt::sprintf("%d: %s##_SAM%d",i,sample->name,i).c_str(),curSample==i)) { + curSample=i; + samplePos=0; + updateSampleTex=true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Bank %d: %s",i/12,sampleNote[i%12]); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + sampleEditOpen=true; + } + } + } +} diff --git a/src/gui/debug.cpp b/src/gui/debug.cpp index 8b06f6dec..f1c974e8b 100644 --- a/src/gui/debug.cpp +++ b/src/gui/debug.cpp @@ -29,11 +29,16 @@ #include "../engine/platform/arcade.h" #include "../engine/platform/ym2610.h" #include "../engine/platform/ym2610ext.h" +#include "../engine/platform/ym2610b.h" +#include "../engine/platform/ym2610bext.h" #include "../engine/platform/ay.h" #include "../engine/platform/ay8930.h" #include "../engine/platform/tia.h" #include "../engine/platform/saa.h" #include "../engine/platform/amiga.h" +#include "../engine/platform/x1_010.h" +#include "../engine/platform/n163.h" +#include "../engine/platform/vrc6.h" #include "../engine/platform/dummy.h" #define GENESIS_DEBUG \ @@ -230,6 +235,104 @@ void putDispatchChan(void* data, int chanNum, int type) { ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); break; } + case DIV_SYSTEM_X1_010: { + DivPlatformX1_010::Channel* ch=(DivPlatformX1_010::Channel*)data; + ImGui::Text("> X1-010"); + ImGui::Text("* freq: %.4x",ch->freq); + ImGui::Text(" - base: %d",ch->baseFreq); + ImGui::Text(" - pitch: %d",ch->pitch); + ImGui::Text("- note: %d",ch->note); + ImGui::Text("- wave: %d",ch->wave); + ImGui::Text("- sample: %d",ch->sample); + ImGui::Text("- ins: %d",ch->ins); + ImGui::Text("- pan: %d",ch->pan); + ImGui::Text("* envelope:"); + ImGui::Text(" - shape: %d",ch->env.shape); + ImGui::Text(" - period: %.2x",ch->env.period); + ImGui::Text(" - slide: %.2x",ch->env.slide); + ImGui::Text(" - slidefrac: %.2x",ch->env.slidefrac); + ImGui::Text(" - autoEnvNum: %.2x",ch->autoEnvNum); + ImGui::Text(" - autoEnvDen: %.2x",ch->autoEnvDen); + ImGui::Text("- WaveBank: %d",ch->waveBank); + ImGui::Text("- vol: %.2x",ch->vol); + ImGui::Text("- outVol: %.2x",ch->outVol); + ImGui::Text("- Lvol: %.2x",ch->lvol); + ImGui::Text("- Rvol: %.2x",ch->rvol); + ImGui::TextColored(ch->active?colorOn:colorOff,">> Active"); + ImGui::TextColored(ch->insChanged?colorOn:colorOff,">> InsChanged"); + ImGui::TextColored(ch->envChanged?colorOn:colorOff,">> EnvChanged"); + ImGui::TextColored(ch->freqChanged?colorOn:colorOff,">> FreqChanged"); + ImGui::TextColored(ch->keyOn?colorOn:colorOff,">> KeyOn"); + ImGui::TextColored(ch->keyOff?colorOn:colorOff,">> KeyOff"); + ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); + ImGui::TextColored(ch->furnacePCM?colorOn:colorOff,">> FurnacePCM"); + ImGui::TextColored(ch->pcm?colorOn:colorOff,">> PCM"); + ImGui::TextColored(ch->env.flag.envEnable?colorOn:colorOff,">> EnvEnable"); + ImGui::TextColored(ch->env.flag.envOneshot?colorOn:colorOff,">> EnvOneshot"); + ImGui::TextColored(ch->env.flag.envSplit?colorOn:colorOff,">> EnvSplit"); + ImGui::TextColored(ch->env.flag.envHinvR?colorOn:colorOff,">> EnvHinvR"); + ImGui::TextColored(ch->env.flag.envVinvR?colorOn:colorOff,">> EnvVinvR"); + ImGui::TextColored(ch->env.flag.envHinvL?colorOn:colorOff,">> EnvHinvL"); + ImGui::TextColored(ch->env.flag.envVinvL?colorOn:colorOff,">> EnvVinvL"); + break; + } + case DIV_SYSTEM_N163: { + DivPlatformN163::Channel* ch=(DivPlatformN163::Channel*)data; + ImGui::Text("> N163"); + ImGui::Text("* freq: %.4x",ch->freq); + ImGui::Text(" - base: %d",ch->baseFreq); + ImGui::Text(" - pitch: %d",ch->pitch); + ImGui::Text("- note: %d",ch->note); + ImGui::Text("- wave: %d",ch->wave); + ImGui::Text("- wavepos: %d",ch->wavePos); + ImGui::Text("- wavelen: %d",ch->waveLen); + ImGui::Text("- wavemode: %d",ch->waveMode); + ImGui::Text("- loadwave: %d",ch->loadWave); + ImGui::Text("- loadpos: %d",ch->loadPos); + ImGui::Text("- loadlen: %d",ch->loadLen); + ImGui::Text("- loadmode: %d",ch->loadMode); + ImGui::Text("- ins: %d",ch->ins); + ImGui::Text("- vol: %.2x",ch->vol); + ImGui::Text("- outVol: %.2x",ch->outVol); + ImGui::Text("- resVol: %.2x",ch->resVol); + ImGui::TextColored(ch->active?colorOn:colorOff,">> Active"); + ImGui::TextColored(ch->insChanged?colorOn:colorOff,">> InsChanged"); + ImGui::TextColored(ch->freqChanged?colorOn:colorOff,">> FreqChanged"); + ImGui::TextColored(ch->volumeChanged?colorOn:colorOff,">> VolumeChanged"); + ImGui::TextColored(ch->waveChanged?colorOn:colorOff,">> WaveChanged"); + ImGui::TextColored(ch->waveUpdated?colorOn:colorOff,">> WaveUpdated"); + ImGui::TextColored(ch->keyOn?colorOn:colorOff,">> KeyOn"); + ImGui::TextColored(ch->keyOff?colorOn:colorOff,">> KeyOff"); + ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); + break; + } + case DIV_SYSTEM_VRC6: { + DivPlatformVRC6::Channel* ch=(DivPlatformVRC6::Channel*)data; + ImGui::Text("> VRC6"); + ImGui::Text("* freq: %d",ch->freq); + ImGui::Text(" - base: %d",ch->baseFreq); + ImGui::Text(" - pitch: %d",ch->pitch); + ImGui::Text("- note: %d",ch->note); + ImGui::Text("* DAC:"); + ImGui::Text(" - period: %d",ch->dacPeriod); + ImGui::Text(" - rate: %d",ch->dacRate); + ImGui::Text(" - out: %d",ch->dacOut); + ImGui::Text(" - pos: %d",ch->dacPos); + ImGui::Text(" - sample: %d",ch->dacSample); + ImGui::Text("- ins: %d",ch->ins); + ImGui::Text("- duty: %d",ch->duty); + ImGui::Text("- vol: %.2x",ch->vol); + ImGui::Text("- outVol: %.2x",ch->outVol); + ImGui::TextColored(ch->active?colorOn:colorOff,">> Active"); + ImGui::TextColored(ch->insChanged?colorOn:colorOff,">> InsChanged"); + ImGui::TextColored(ch->freqChanged?colorOn:colorOff,">> FreqChanged"); + ImGui::TextColored(ch->keyOn?colorOn:colorOff,">> KeyOn"); + ImGui::TextColored(ch->keyOff?colorOn:colorOff,">> KeyOff"); + ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); + ImGui::TextColored(ch->pcm?colorOn:colorOff,">> DAC"); + ImGui::TextColored(ch->furnaceDac?colorOn:colorOff,">> FurnaceDAC"); + break; + } default: ImGui::Text("Unknown system! Help!"); break; diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp new file mode 100644 index 000000000..31830681f --- /dev/null +++ b/src/gui/debugWindow.cpp @@ -0,0 +1,250 @@ +/** + * 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 "debug.h" +#include "IconsFontAwesome4.h" +#include + +void FurnaceGUI::drawDebug() { + static int bpOrder; + static int bpRow; + static int bpTick; + static bool bpOn; + if (nextWindow==GUI_WINDOW_DEBUG) { + debugOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!debugOpen) return; + ImGui::SetNextWindowSizeConstraints(ImVec2(400.0f*dpiScale,200.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (ImGui::Begin("Debug",&debugOpen,ImGuiWindowFlags_NoDocking)) { + ImGui::Text("NOTE: use with caution."); + if (ImGui::TreeNode("Debug Controls")) { + if (e->isHalted()) { + if (ImGui::Button("Resume")) e->resume(); + } else { + if (ImGui::Button("Pause")) e->halt(); + } + ImGui::SameLine(); + if (ImGui::Button("Frame Advance")) e->haltWhen(DIV_HALT_TICK); + ImGui::SameLine(); + if (ImGui::Button("Row Advance")) e->haltWhen(DIV_HALT_ROW); + ImGui::SameLine(); + if (ImGui::Button("Pattern Advance")) e->haltWhen(DIV_HALT_PATTERN); + + if (ImGui::Button("Panic")) e->syncReset(); + ImGui::SameLine(); + if (ImGui::Button("Abort")) { + abort(); + } + ImGui::TreePop(); + } + if (ImGui::TreeNode("Breakpoint")) { + ImGui::InputInt("Order",&bpOrder); + ImGui::InputInt("Row",&bpRow); + ImGui::InputInt("Tick",&bpTick); + ImGui::Checkbox("Enable",&bpOn); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Dispatch Status")) { + ImGui::Text("for best results set latency to minimum or use the Frame Advance button."); + ImGui::Columns(e->getTotalChannelCount()); + for (int i=0; igetTotalChannelCount(); i++) { + void* ch=e->getDispatchChanState(i); + ImGui::TextColored(uiColors[GUI_COLOR_ACCENT_PRIMARY],"Ch. %d: %d, %d",i,e->dispatchOfChan[i],e->dispatchChanOfChan[i]); + if (ch==NULL) { + ImGui::Text("NULL"); + } else { + putDispatchChan(ch,e->dispatchChanOfChan[i],e->sysOfChan[i]); + } + ImGui::NextColumn(); + } + ImGui::Columns(); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Playback Status")) { + ImGui::Text("for best results set latency to minimum or use the Frame Advance button."); + ImGui::Columns(e->getTotalChannelCount()); + for (int i=0; igetTotalChannelCount(); i++) { + DivChannelState* ch=e->getChanState(i); + ImGui::TextColored(uiColors[GUI_COLOR_ACCENT_PRIMARY],"Channel %d:",i); + if (ch==NULL) { + ImGui::Text("NULL"); + } else { + ImGui::Text("* General:"); + ImGui::Text("- note = %d",ch->note); + ImGui::Text("- oldNote = %d",ch->oldNote); + ImGui::Text("- pitch = %d",ch->pitch); + ImGui::Text("- portaSpeed = %d",ch->portaSpeed); + ImGui::Text("- portaNote = %d",ch->portaNote); + ImGui::Text("- volume = %.4x",ch->volume); + ImGui::Text("- volSpeed = %d",ch->volSpeed); + ImGui::Text("- cut = %d",ch->cut); + ImGui::Text("- rowDelay = %d",ch->rowDelay); + ImGui::Text("- volMax = %.4x",ch->volMax); + ImGui::Text("- delayOrder = %d",ch->delayOrder); + ImGui::Text("- delayRow = %d",ch->delayRow); + ImGui::Text("- retrigSpeed = %d",ch->retrigSpeed); + ImGui::Text("- retrigTick = %d",ch->retrigTick); + ImGui::PushStyleColor(ImGuiCol_Text,(ch->vibratoDepth>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); + ImGui::Text("* Vibrato:"); + ImGui::Text("- depth = %d",ch->vibratoDepth); + ImGui::Text("- rate = %d",ch->vibratoRate); + ImGui::Text("- pos = %d",ch->vibratoPos); + ImGui::Text("- dir = %d",ch->vibratoDir); + ImGui::Text("- fine = %d",ch->vibratoFine); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text,(ch->tremoloDepth>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); + ImGui::Text("* Tremolo:"); + ImGui::Text("- depth = %d",ch->tremoloDepth); + ImGui::Text("- rate = %d",ch->tremoloRate); + ImGui::Text("- pos = %d",ch->tremoloPos); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text,(ch->arp>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); + ImGui::Text("* Arpeggio:"); + ImGui::Text("- arp = %.2X",ch->arp); + ImGui::Text("- stage = %d",ch->arpStage); + ImGui::Text("- ticks = %d",ch->arpTicks); + ImGui::PopStyleColor(); + ImGui::Text("* Miscellaneous:"); + ImGui::TextColored(ch->doNote?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Do Note"); + ImGui::TextColored(ch->legato?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Legato"); + ImGui::TextColored(ch->portaStop?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> PortaStop"); + ImGui::TextColored(ch->keyOn?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Key On"); + ImGui::TextColored(ch->keyOff?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Key Off"); + ImGui::TextColored(ch->nowYouCanStop?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> NowYouCanStop"); + ImGui::TextColored(ch->stopOnOff?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Stop on Off"); + ImGui::TextColored(ch->arpYield?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Arp Yield"); + ImGui::TextColored(ch->delayLocked?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> DelayLocked"); + ImGui::TextColored(ch->inPorta?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> InPorta"); + ImGui::TextColored(ch->scheduledSlideReset?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> SchedSlide"); + } + ImGui::NextColumn(); + } + ImGui::Columns(); + 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())) { + 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; + break; + } + } + ImGui::EndCombo(); + } + ImGui::Text("Program"); + if (pgProgram.empty()) { + ImGui::Text("-nothing here-"); + } else { + char id[32]; + for (size_t index=0; indexpoke(pgSys,pgProgram); + } + ImGui::SameLine(); + if (ImGui::Button("Clear")) { + pgProgram.clear(); + } + + ImGui::Text("Address"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(100.0f*dpiScale); + ImGui::InputInt("##PAddress",&pgAddr,0,0,ImGuiInputTextFlags_CharsHexadecimal); + ImGui::SameLine(); + ImGui::Text("Value"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(100.0f*dpiScale); + ImGui::InputInt("##PValue",&pgVal,0,0,ImGuiInputTextFlags_CharsHexadecimal); + ImGui::SameLine(); + if (ImGui::Button("Write")) { + e->poke(pgSys,pgAddr,pgVal); + } + ImGui::SameLine(); + if (ImGui::Button("Add")) { + pgProgram.push_back(DivRegWrite(pgAddr,pgVal)); + } + if (ImGui::TreeNode("Register Cheatsheet")) { + const char** sheet=e->getRegisterSheet(pgSys); + if (sheet==NULL) { + ImGui::Text("no cheatsheet available for this system."); + } else { + if (ImGui::BeginTable("RegisterSheet",2,ImGuiTableFlags_SizingFixedSame)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Name"); + ImGui::TableNextColumn(); + ImGui::Text("Address"); + for (int i=0; sheet[i]!=NULL; i+=2) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s",sheet[i]); + ImGui::TableNextColumn(); + ImGui::Text("$%s",sheet[i+1]); + } + ImGui::EndTable(); + } + } + ImGui::TreePop(); + } + ImGui::TreePop(); + } + if (ImGui::TreeNode("User Interface")) { + if (ImGui::Button("Inspect")) { + inspectorOpen=!inspectorOpen; + } + ImGui::TreePop(); + } + if (ImGui::TreeNode("Settings")) { + if (ImGui::Button("Sync")) syncSettings(); + ImGui::SameLine(); + if (ImGui::Button("Commit")) commitSettings(); + ImGui::SameLine(); + if (ImGui::Button("Force Load")) e->loadConf(); + ImGui::SameLine(); + if (ImGui::Button("Force Save")) e->saveConf(); + ImGui::TreePop(); + } + ImGui::Text("Song format version %d",e->song.version); + ImGui::Text("Furnace version " DIV_VERSION " (%d)",DIV_ENGINE_VERSION); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_DEBUG; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp new file mode 100644 index 000000000..a1b974e36 --- /dev/null +++ b/src/gui/doAction.cpp @@ -0,0 +1,1171 @@ +/** + * 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 +#include + +#include "actionUtil.h" +#include "sampleUtil.h" + +void FurnaceGUI::doAction(int what) { + switch (what) { + case GUI_ACTION_OPEN: + if (modified) { + showWarning("Unsaved changes! Are you sure?",GUI_WARN_OPEN); + } else { + openFileDialog(GUI_FILE_OPEN); + } + break; + case GUI_ACTION_OPEN_BACKUP: + if (modified) { + showWarning("Unsaved changes! Are you sure?",GUI_WARN_OPEN_BACKUP); + } else { + if (load(backupPath)>0) { + showError("No backup available! (or unable to open it)"); + } + } + break; + case GUI_ACTION_SAVE: + if (curFileName=="" || curFileName==backupPath || e->song.version>=0xff00) { + openFileDialog(GUI_FILE_SAVE); + } else { + if (save(curFileName,e->song.isDMF?e->song.version:0)>0) { + showError(fmt::sprintf("Error while saving file! (%s)",lastError)); + } + } + break; + case GUI_ACTION_SAVE_AS: + openFileDialog(GUI_FILE_SAVE); + break; + case GUI_ACTION_UNDO: + if (curWindow==GUI_WINDOW_SAMPLE_EDIT) { + doUndoSample(); + } else { + doUndo(); + } + break; + case GUI_ACTION_REDO: + if (curWindow==GUI_WINDOW_SAMPLE_EDIT) { + doRedoSample(); + } else { + doRedo(); + } + break; + case GUI_ACTION_PLAY_TOGGLE: + if (e->isPlaying() && !e->isStepping()) { + stop(); + } else { + play(); + } + break; + case GUI_ACTION_PLAY: + play(); + break; + case GUI_ACTION_STOP: + stop(); + break; + case GUI_ACTION_PLAY_REPEAT: + play(); + e->setRepeatPattern(true); + break; + case GUI_ACTION_PLAY_CURSOR: + if (e->isPlaying() && !e->isStepping()) { + stop(); + } else { + play(cursor.y); + } + break; + case GUI_ACTION_STEP_ONE: + e->stepOne(cursor.y); + break; + case GUI_ACTION_OCTAVE_UP: + if (++curOctave>7) { + curOctave=7; + } else { + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + } + break; + case GUI_ACTION_OCTAVE_DOWN: + if (--curOctave<-5) { + curOctave=-5; + } else { + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + } + break; + case GUI_ACTION_INS_UP: + if (--curIns<-1) curIns=-1; + break; + case GUI_ACTION_INS_DOWN: + if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; + break; + case GUI_ACTION_STEP_UP: + if (++editStep>64) editStep=64; + break; + case GUI_ACTION_STEP_DOWN: + if (--editStep<0) editStep=0; + break; + case GUI_ACTION_TOGGLE_EDIT: + edit=!edit; + break; + case GUI_ACTION_METRONOME: + e->setMetronome(!e->getMetronome()); + break; + case GUI_ACTION_REPEAT_PATTERN: + e->setRepeatPattern(!e->getRepeatPattern()); + break; + case GUI_ACTION_FOLLOW_ORDERS: + followOrders=!followOrders; + break; + case GUI_ACTION_FOLLOW_PATTERN: + followPattern=!followPattern; + break; + case GUI_ACTION_PANIC: + e->syncReset(); + break; + + case GUI_ACTION_WINDOW_EDIT_CONTROLS: + nextWindow=GUI_WINDOW_EDIT_CONTROLS; + break; + case GUI_ACTION_WINDOW_ORDERS: + nextWindow=GUI_WINDOW_ORDERS; + break; + case GUI_ACTION_WINDOW_INS_LIST: + nextWindow=GUI_WINDOW_INS_LIST; + break; + case GUI_ACTION_WINDOW_INS_EDIT: + nextWindow=GUI_WINDOW_INS_EDIT; + break; + case GUI_ACTION_WINDOW_SONG_INFO: + nextWindow=GUI_WINDOW_SONG_INFO; + break; + case GUI_ACTION_WINDOW_PATTERN: + nextWindow=GUI_WINDOW_PATTERN; + break; + case GUI_ACTION_WINDOW_WAVE_LIST: + nextWindow=GUI_WINDOW_WAVE_LIST; + break; + case GUI_ACTION_WINDOW_WAVE_EDIT: + nextWindow=GUI_WINDOW_WAVE_EDIT; + break; + case GUI_ACTION_WINDOW_SAMPLE_LIST: + nextWindow=GUI_WINDOW_SAMPLE_LIST; + break; + case GUI_ACTION_WINDOW_SAMPLE_EDIT: + nextWindow=GUI_WINDOW_SAMPLE_EDIT; + break; + case GUI_ACTION_WINDOW_ABOUT: + nextWindow=GUI_WINDOW_ABOUT; + break; + case GUI_ACTION_WINDOW_SETTINGS: + nextWindow=GUI_WINDOW_SETTINGS; + break; + case GUI_ACTION_WINDOW_MIXER: + nextWindow=GUI_WINDOW_MIXER; + break; + case GUI_ACTION_WINDOW_DEBUG: + nextWindow=GUI_WINDOW_DEBUG; + break; + case GUI_ACTION_WINDOW_OSCILLOSCOPE: + nextWindow=GUI_WINDOW_OSCILLOSCOPE; + break; + case GUI_ACTION_WINDOW_VOL_METER: + nextWindow=GUI_WINDOW_VOL_METER; + break; + case GUI_ACTION_WINDOW_STATS: + nextWindow=GUI_WINDOW_STATS; + break; + case GUI_ACTION_WINDOW_COMPAT_FLAGS: + nextWindow=GUI_WINDOW_COMPAT_FLAGS; + break; + case GUI_ACTION_WINDOW_PIANO: + nextWindow=GUI_WINDOW_PIANO; + break; + case GUI_ACTION_WINDOW_NOTES: + nextWindow=GUI_WINDOW_NOTES; + break; + case GUI_ACTION_WINDOW_CHANNELS: + nextWindow=GUI_WINDOW_CHANNELS; + break; + case GUI_ACTION_WINDOW_REGISTER_VIEW: + nextWindow=GUI_WINDOW_REGISTER_VIEW; + break; + case GUI_ACTION_WINDOW_LOG: + nextWindow=GUI_WINDOW_LOG; + break; + + case GUI_ACTION_COLLAPSE_WINDOW: + collapseWindow=true; + break; + case GUI_ACTION_CLOSE_WINDOW: + switch (curWindow) { + case GUI_WINDOW_EDIT_CONTROLS: + editControlsOpen=false; + break; + case GUI_WINDOW_SONG_INFO: + songInfoOpen=false; + break; + case GUI_WINDOW_ORDERS: + ordersOpen=false; + break; + case GUI_WINDOW_INS_LIST: + insListOpen=false; + break; + case GUI_WINDOW_PATTERN: + patternOpen=false; + break; + case GUI_WINDOW_INS_EDIT: + insEditOpen=false; + break; + case GUI_WINDOW_WAVE_LIST: + waveListOpen=false; + break; + case GUI_WINDOW_WAVE_EDIT: + waveEditOpen=false; + break; + case GUI_WINDOW_SAMPLE_LIST: + sampleListOpen=false; + break; + case GUI_WINDOW_SAMPLE_EDIT: + sampleEditOpen=false; + break; + case GUI_WINDOW_MIXER: + mixerOpen=false; + break; + case GUI_WINDOW_ABOUT: + aboutOpen=false; + break; + case GUI_WINDOW_SETTINGS: + settingsOpen=false; + break; + case GUI_WINDOW_DEBUG: + debugOpen=false; + break; + case GUI_WINDOW_OSCILLOSCOPE: + oscOpen=false; + break; + case GUI_WINDOW_VOL_METER: + volMeterOpen=false; + break; + case GUI_WINDOW_STATS: + statsOpen=false; + break; + case GUI_WINDOW_COMPAT_FLAGS: + compatFlagsOpen=false; + break; + case GUI_WINDOW_PIANO: + pianoOpen=false; + break; + case GUI_WINDOW_NOTES: + notesOpen=false; + break; + case GUI_WINDOW_CHANNELS: + channelsOpen=false; + break; + case GUI_WINDOW_REGISTER_VIEW: + regViewOpen=false; + break; + default: + break; + } + curWindow=GUI_WINDOW_NOTHING; + break; + + case GUI_ACTION_PAT_NOTE_UP: + doTranspose(1); + break; + case GUI_ACTION_PAT_NOTE_DOWN: + doTranspose(-1); + break; + case GUI_ACTION_PAT_OCTAVE_UP: + doTranspose(12); + break; + case GUI_ACTION_PAT_OCTAVE_DOWN: + doTranspose(-12); + break; + case GUI_ACTION_PAT_SELECT_ALL: + doSelectAll(); + break; + case GUI_ACTION_PAT_CUT: + doCopy(true); + break; + case GUI_ACTION_PAT_COPY: + doCopy(false); + break; + case GUI_ACTION_PAT_PASTE: + doPaste(); + break; + case GUI_ACTION_PAT_PASTE_MIX: + doPaste(GUI_PASTE_MODE_MIX_FG); + break; + case GUI_ACTION_PAT_PASTE_MIX_BG: + doPaste(GUI_PASTE_MODE_MIX_BG); + break; + case GUI_ACTION_PAT_PASTE_FLOOD: + doPaste(GUI_PASTE_MODE_FLOOD); + break; + case GUI_ACTION_PAT_PASTE_OVERFLOW: + doPaste(GUI_PASTE_MODE_OVERFLOW); + break; + case GUI_ACTION_PAT_CURSOR_UP: + moveCursor(0,-MAX(1,settings.scrollStep?editStep:1),false); + break; + case GUI_ACTION_PAT_CURSOR_DOWN: + moveCursor(0,MAX(1,settings.scrollStep?editStep:1),false); + break; + case GUI_ACTION_PAT_CURSOR_LEFT: + moveCursor(-1,0,false); + break; + case GUI_ACTION_PAT_CURSOR_RIGHT: + moveCursor(1,0,false); + break; + case GUI_ACTION_PAT_CURSOR_UP_ONE: + moveCursor(0,-1,false); + break; + case GUI_ACTION_PAT_CURSOR_DOWN_ONE: + moveCursor(0,1,false); + break; + case GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL: + moveCursorPrevChannel(false); + break; + case GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL: + moveCursorNextChannel(false); + break; + case GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL: + moveCursorNextChannel(true); + break; + case GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL: + moveCursorPrevChannel(true); + break; + case GUI_ACTION_PAT_CURSOR_BEGIN: + moveCursorTop(false); + break; + case GUI_ACTION_PAT_CURSOR_END: + moveCursorBottom(false); + break; + case GUI_ACTION_PAT_CURSOR_UP_COARSE: + moveCursor(0,-16,false); + break; + case GUI_ACTION_PAT_CURSOR_DOWN_COARSE: + moveCursor(0,16,false); + break; + case GUI_ACTION_PAT_SELECTION_UP: + moveCursor(0,-MAX(1,settings.scrollStep?editStep:1),true); + break; + case GUI_ACTION_PAT_SELECTION_DOWN: + moveCursor(0,MAX(1,settings.scrollStep?editStep:1),true); + break; + case GUI_ACTION_PAT_SELECTION_LEFT: + moveCursor(-1,0,true); + break; + case GUI_ACTION_PAT_SELECTION_RIGHT: + moveCursor(1,0,true); + break; + case GUI_ACTION_PAT_SELECTION_UP_ONE: + moveCursor(0,-1,true); + break; + case GUI_ACTION_PAT_SELECTION_DOWN_ONE: + moveCursor(0,1,true); + break; + case GUI_ACTION_PAT_SELECTION_BEGIN: + moveCursorTop(true); + break; + case GUI_ACTION_PAT_SELECTION_END: + moveCursorBottom(true); + break; + case GUI_ACTION_PAT_SELECTION_UP_COARSE: + moveCursor(0,-16,true); + break; + case GUI_ACTION_PAT_SELECTION_DOWN_COARSE: + moveCursor(0,16,true); + break; + case GUI_ACTION_PAT_DELETE: + doDelete(); + if (settings.stepOnDelete) { + moveCursor(0,editStep,false); + } + break; + case GUI_ACTION_PAT_PULL_DELETE: + doPullDelete(); + break; + case GUI_ACTION_PAT_INSERT: + doInsert(); + if (settings.stepOnInsert) { + moveCursor(0,editStep,false); + } + break; + case GUI_ACTION_PAT_MUTE_CURSOR: + if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; + e->toggleMute(cursor.xCoarse); + break; + case GUI_ACTION_PAT_SOLO_CURSOR: + if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; + e->toggleSolo(cursor.xCoarse); + break; + case GUI_ACTION_PAT_UNMUTE_ALL: + e->unmuteAll(); + break; + case GUI_ACTION_PAT_NEXT_ORDER: + if (e->getOrder()song.ordersLen-1) { + e->setOrder(e->getOrder()+1); + } + break; + case GUI_ACTION_PAT_PREV_ORDER: + if (e->getOrder()>0) { + e->setOrder(e->getOrder()-1); + } + break; + case GUI_ACTION_PAT_COLLAPSE: + if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; + e->song.chanCollapse[cursor.xCoarse]=!e->song.chanCollapse[cursor.xCoarse]; + break; + case GUI_ACTION_PAT_INCREASE_COLUMNS: + if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; + e->song.pat[cursor.xCoarse].effectRows++; + if (e->song.pat[cursor.xCoarse].effectRows>8) e->song.pat[cursor.xCoarse].effectRows=8; + break; + case GUI_ACTION_PAT_DECREASE_COLUMNS: + if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; + e->song.pat[cursor.xCoarse].effectRows--; + if (e->song.pat[cursor.xCoarse].effectRows<1) e->song.pat[cursor.xCoarse].effectRows=1; + break; + case GUI_ACTION_PAT_INTERPOLATE: + doInterpolate(); + break; + case GUI_ACTION_PAT_INVERT_VALUES: + doInvertValues(); + break; + case GUI_ACTION_PAT_FLIP_SELECTION: + doFlip(); + break; + case GUI_ACTION_PAT_COLLAPSE_ROWS: + doCollapse(2); + break; + case GUI_ACTION_PAT_EXPAND_ROWS: + doExpand(2); + break; + case GUI_ACTION_PAT_COLLAPSE_PAT: // TODO + break; + case GUI_ACTION_PAT_EXPAND_PAT: // TODO + break; + case GUI_ACTION_PAT_COLLAPSE_SONG: // TODO + break; + case GUI_ACTION_PAT_EXPAND_SONG: // TODO + break; + case GUI_ACTION_PAT_LATCH: // TODO + break; + + case GUI_ACTION_INS_LIST_ADD: + curIns=e->addInstrument(cursor.xCoarse); + MARK_MODIFIED; + break; + case GUI_ACTION_INS_LIST_DUPLICATE: + if (curIns>=0 && curIns<(int)e->song.ins.size()) { + int prevIns=curIns; + curIns=e->addInstrument(cursor.xCoarse); + (*e->song.ins[curIns])=(*e->song.ins[prevIns]); + MARK_MODIFIED; + } + break; + case GUI_ACTION_INS_LIST_OPEN: + openFileDialog(GUI_FILE_INS_OPEN); + break; + case GUI_ACTION_INS_LIST_SAVE: + if (curIns>=0 && curIns<(int)e->song.ins.size()) openFileDialog(GUI_FILE_INS_SAVE); + break; + case GUI_ACTION_INS_LIST_MOVE_UP: + if (e->moveInsUp(curIns)) curIns--; + break; + case GUI_ACTION_INS_LIST_MOVE_DOWN: + if (e->moveInsDown(curIns)) curIns++; + break; + case GUI_ACTION_INS_LIST_DELETE: + if (curIns>=0 && curIns<(int)e->song.ins.size()) { + e->delInstrument(curIns); + MARK_MODIFIED; + if (curIns>=(int)e->song.ins.size()) { + curIns--; + } + } + break; + case GUI_ACTION_INS_LIST_EDIT: + insEditOpen=true; + break; + case GUI_ACTION_INS_LIST_UP: + if (--curIns<0) curIns=0; + break; + case GUI_ACTION_INS_LIST_DOWN: + if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; + break; + + case GUI_ACTION_WAVE_LIST_ADD: + curWave=e->addWave(); + MARK_MODIFIED; + break; + case GUI_ACTION_WAVE_LIST_DUPLICATE: + if (curWave>=0 && curWave<(int)e->song.wave.size()) { + int prevWave=curWave; + curWave=e->addWave(); + (*e->song.wave[curWave])=(*e->song.wave[prevWave]); + MARK_MODIFIED; + } + break; + case GUI_ACTION_WAVE_LIST_OPEN: + openFileDialog(GUI_FILE_WAVE_OPEN); + break; + case GUI_ACTION_WAVE_LIST_SAVE: + if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE); + break; + case GUI_ACTION_WAVE_LIST_MOVE_UP: + if (e->moveWaveUp(curWave)) curWave--; + break; + case GUI_ACTION_WAVE_LIST_MOVE_DOWN: + if (e->moveWaveDown(curWave)) curWave++; + break; + case GUI_ACTION_WAVE_LIST_DELETE: + if (curWave>=0 && curWave<(int)e->song.wave.size()) { + e->delWave(curWave); + MARK_MODIFIED; + if (curWave>=(int)e->song.wave.size()) { + curWave--; + } + } + break; + case GUI_ACTION_WAVE_LIST_EDIT: + waveEditOpen=true; + break; + case GUI_ACTION_WAVE_LIST_UP: + if (--curWave<0) curWave=0; + break; + case GUI_ACTION_WAVE_LIST_DOWN: + if (++curWave>=(int)e->song.wave.size()) curWave=((int)e->song.wave.size())-1; + break; + + case GUI_ACTION_SAMPLE_LIST_ADD: + curSample=e->addSample(); + MARK_MODIFIED; + break; + case GUI_ACTION_SAMPLE_LIST_OPEN: + openFileDialog(GUI_FILE_SAMPLE_OPEN); + break; + case GUI_ACTION_SAMPLE_LIST_SAVE: + if (curSample>=0 && curSample<(int)e->song.sample.size()) openFileDialog(GUI_FILE_SAMPLE_SAVE); + break; + case GUI_ACTION_SAMPLE_LIST_MOVE_UP: + if (e->moveSampleUp(curSample)) curSample--; + break; + case GUI_ACTION_SAMPLE_LIST_MOVE_DOWN: + if (e->moveSampleDown(curSample)) curSample++; + break; + case GUI_ACTION_SAMPLE_LIST_DELETE: + e->delSample(curSample); + MARK_MODIFIED; + if (curSample>=(int)e->song.sample.size()) { + curSample--; + } + break; + case GUI_ACTION_SAMPLE_LIST_EDIT: + sampleEditOpen=true; + break; + case GUI_ACTION_SAMPLE_LIST_UP: + if (--curSample<0) curSample=0; + break; + case GUI_ACTION_SAMPLE_LIST_DOWN: + if (++curSample>=(int)e->song.sample.size()) curSample=((int)e->song.sample.size())-1; + break; + case GUI_ACTION_SAMPLE_LIST_PREVIEW: + e->previewSample(curSample); + break; + case GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW: + e->stopSamplePreview(); + break; + + case GUI_ACTION_SAMPLE_SELECT: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + sampleDragMode=false; + break; + case GUI_ACTION_SAMPLE_DRAW: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + sampleDragMode=true; + break; + case GUI_ACTION_SAMPLE_CUT: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + SAMPLE_OP_BEGIN; + + if (end-start<1) break; + + sample->prepareUndo(true); + + if (sampleClipboard!=NULL) { + delete[] sampleClipboard; + } + sampleClipboard=new short[end-start]; + sampleClipboardLen=end-start; + memcpy(sampleClipboard,&(sample->data16[start]),sizeof(short)*(end-start)); + + e->lockEngine([this,sample,start,end]() { + sample->strip(start,end); + updateSampleTex=true; + + e->renderSamples(); + }); + sampleSelStart=-1; + sampleSelEnd=-1; + MARK_MODIFIED; + + break; + } + case GUI_ACTION_SAMPLE_COPY: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + SAMPLE_OP_BEGIN; + + if (end-start<1) break; + + if (sampleClipboard!=NULL) { + delete[] sampleClipboard; + } + sampleClipboard=new short[end-start]; + sampleClipboardLen=end-start; + memcpy(sampleClipboard,&(sample->data16[start]),sizeof(short)*(end-start)); + break; + } + case GUI_ACTION_SAMPLE_PASTE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + if (sampleClipboard==NULL || sampleClipboardLen<1) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?sample->samples:sampleSelStart; + + e->lockEngine([this,sample,pos]() { + if (!sample->insert(pos,sampleClipboardLen)) { + showError("couldn't paste! make sure your sample is 8 or 16-bit."); + } else { + if (sample->depth==8) { + for (size_t i=0; idata8[pos+i]=sampleClipboard[i]>>8; + } + } else { + memcpy(&(sample->data16[pos]),sampleClipboard,sizeof(short)*sampleClipboardLen); + } + } + e->renderSamples(); + }); + sampleSelStart=pos; + sampleSelEnd=pos+sampleClipboardLen; + updateSampleTex=true; + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_PASTE_REPLACE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + if (sampleClipboard==NULL || sampleClipboardLen<1) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?0:sampleSelStart; + + e->lockEngine([this,sample,pos]() { + if (sample->depth==8) { + for (size_t i=0; i=sample->samples) break; + sample->data8[pos+i]=sampleClipboard[i]>>8; + } + } else { + for (size_t i=0; i=sample->samples) break; + sample->data16[pos+i]=sampleClipboard[i]; + } + } + e->renderSamples(); + }); + sampleSelStart=pos; + sampleSelEnd=pos+sampleClipboardLen; + if (sampleSelEnd>(int)sample->samples) sampleSelEnd=sample->samples; + updateSampleTex=true; + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_PASTE_MIX: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + if (sampleClipboard==NULL || sampleClipboardLen<1) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?0:sampleSelStart; + + e->lockEngine([this,sample,pos]() { + if (sample->depth==8) { + for (size_t i=0; i=sample->samples) break; + int val=sample->data8[pos+i]+(sampleClipboard[i]>>8); + if (val>127) val=127; + if (val<-128) val=-128; + sample->data8[pos+i]=val; + } + } else { + for (size_t i=0; i=sample->samples) break; + int val=sample->data16[pos+i]+sampleClipboard[i]; + if (val>32767) val=32767; + if (val<-32768) val=-32768; + sample->data16[pos+i]=val; + } + } + e->renderSamples(); + }); + sampleSelStart=pos; + sampleSelEnd=pos+sampleClipboardLen; + if (sampleSelEnd>(int)sample->samples) sampleSelEnd=sample->samples; + updateSampleTex=true; + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_SELECT_ALL: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sampleDragActive=false; + sampleSelStart=0; + sampleSelEnd=sample->samples; + break; + } + case GUI_ACTION_SAMPLE_RESIZE: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + openSampleResizeOpt=true; + break; + case GUI_ACTION_SAMPLE_RESAMPLE: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + openSampleResampleOpt=true; + break; + case GUI_ACTION_SAMPLE_AMPLIFY: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + openSampleAmplifyOpt=true; + break; + case GUI_ACTION_SAMPLE_NORMALIZE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + float maxVal=0.0f; + + if (sample->depth==16) { + for (unsigned int i=start; idata16[i]/32767.0f); + if (val>maxVal) maxVal=val; + } + if (maxVal>1.0f) maxVal=1.0f; + if (maxVal>0.0f) { + float vol=1.0f/maxVal; + 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) { + for (unsigned int i=start; idata8[i]/127.0f); + if (val>maxVal) maxVal=val; + } + if (maxVal>1.0f) maxVal=1.0f; + if (maxVal>0.0f) { + float vol=1.0f/maxVal; + for (unsigned int i=start; idata8[i]*vol; + if (val<-128) val=-128; + if (val>127) val=127; + sample->data8[i]=val; + } + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_FADE_IN: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + 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) { + for (unsigned int i=start; idata8[i]*float(i-start)/float(end-start); + if (val<-128) val=-128; + if (val>127) val=127; + sample->data8[i]=val; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_FADE_OUT: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + 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) { + for (unsigned int i=start; idata8[i]*float(end-i)/float(end-start); + if (val<-128) val=-128; + if (val>127) val=127; + sample->data8[i]=val; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_INSERT: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + openSampleSilenceOpt=true; + break; + case GUI_ACTION_SAMPLE_SILENCE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + for (unsigned int i=start; idata16[i]=0; + } + } else if (sample->depth==8) { + for (unsigned int i=start; idata8[i]=0; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_DELETE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + sample->strip(start,end); + updateSampleTex=true; + + e->renderSamples(); + }); + sampleSelStart=-1; + sampleSelEnd=-1; + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_TRIM: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + sample->trim(start,end); + updateSampleTex=true; + + e->renderSamples(); + }); + sampleSelStart=-1; + sampleSelEnd=-1; + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_REVERSE: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + for (unsigned int i=start; idata16[i]^=sample->data16[ri]; + sample->data16[ri]^=sample->data16[i]; + sample->data16[i]^=sample->data16[ri]; + } + } else if (sample->depth==8) { + for (unsigned int i=start; idata8[i]^=sample->data8[ri]; + sample->data8[ri]^=sample->data8[i]; + sample->data8[i]^=sample->data8[ri]; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_INVERT: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + for (unsigned int i=start; idata16[i]=-sample->data16[i]; + if (sample->data16[i]==-32768) sample->data16[i]=32767; + } + } else if (sample->depth==8) { + for (unsigned int i=start; idata8[i]=-sample->data8[i]; + if (sample->data16[i]==-128) sample->data16[i]=127; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_SIGN: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + DivSample* sample=e->song.sample[curSample]; + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + + if (sample->depth==16) { + for (unsigned int i=start; idata16[i]^=0x8000; + } + } else if (sample->depth==8) { + for (unsigned int i=start; idata8[i]^=0x80; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + break; + } + case GUI_ACTION_SAMPLE_FILTER: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + openSampleFilterOpt=true; + break; + case GUI_ACTION_SAMPLE_PREVIEW: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + e->previewSample(curSample); + break; + case GUI_ACTION_SAMPLE_STOP_PREVIEW: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + e->stopSamplePreview(); + break; + case GUI_ACTION_SAMPLE_ZOOM_IN: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + double zoomPercent=100.0/sampleZoom; + zoomPercent+=10.0; + if (zoomPercent>10000.0) zoomPercent=10000.0; + if (zoomPercent<1.0) zoomPercent=1.0; + sampleZoom=100.0/zoomPercent; + if (sampleZoom<0.01) sampleZoom=0.01; + sampleZoomAuto=false; + updateSampleTex=true; + break; + } + case GUI_ACTION_SAMPLE_ZOOM_OUT: { + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + double zoomPercent=100.0/sampleZoom; + zoomPercent-=10.0; + if (zoomPercent>10000.0) zoomPercent=10000.0; + if (zoomPercent<1.0) zoomPercent=1.0; + sampleZoom=100.0/zoomPercent; + if (sampleZoom<0.01) sampleZoom=0.01; + sampleZoomAuto=false; + updateSampleTex=true; + break; + } + case GUI_ACTION_SAMPLE_ZOOM_AUTO: + if (curSample<0 || curSample>=(int)e->song.sample.size()) break; + if (sampleZoomAuto) { + sampleZoom=1.0; + sampleZoomAuto=false; + updateSampleTex=true; + } else { + sampleZoomAuto=true; + updateSampleTex=true; + } + break; + + case GUI_ACTION_ORDERS_UP: + if (e->getOrder()>0) { + e->setOrder(e->getOrder()-1); + } + break; + case GUI_ACTION_ORDERS_DOWN: + if (e->getOrder()song.ordersLen-1) { + e->setOrder(e->getOrder()+1); + } + break; + case GUI_ACTION_ORDERS_LEFT: { + DETERMINE_FIRST; + + do { + orderCursor--; + if (orderCursorsong.chanShow[orderCursor]); + break; + } + case GUI_ACTION_ORDERS_RIGHT: { + DETERMINE_LAST; + + do { + orderCursor++; + if (orderCursor>=lastChannel) { + orderCursor=lastChannel-1; + break; + } + } while (!e->song.chanShow[orderCursor]); + break; + } + case GUI_ACTION_ORDERS_INCREASE: { + if (orderCursor<0 || orderCursor>=e->getTotalChannelCount()) break; + int curOrder=e->getOrder(); + if (e->song.orders.ord[orderCursor][curOrder]<0x7f) { + e->song.orders.ord[orderCursor][curOrder]++; + } + break; + } + case GUI_ACTION_ORDERS_DECREASE: { + if (orderCursor<0 || orderCursor>=e->getTotalChannelCount()) break; + int curOrder=e->getOrder(); + if (e->song.orders.ord[orderCursor][curOrder]>0) { + e->song.orders.ord[orderCursor][curOrder]--; + } + break; + } + case GUI_ACTION_ORDERS_EDIT_MODE: + orderEditMode++; + if (orderEditMode>3) orderEditMode=0; + break; + case GUI_ACTION_ORDERS_LINK: + changeAllOrders=!changeAllOrders; + break; + case GUI_ACTION_ORDERS_ADD: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->addOrder(false,false); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_DUPLICATE: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->addOrder(true,false); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_DEEP_CLONE: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->deepCloneOrder(false); + makeUndo(GUI_UNDO_CHANGE_ORDER); + if (!e->getWarnings().empty()) { + showWarning(e->getWarnings(),GUI_WARN_GENERIC); + } + break; + case GUI_ACTION_ORDERS_DUPLICATE_END: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->addOrder(true,true); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_DEEP_CLONE_END: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->deepCloneOrder(true); + makeUndo(GUI_UNDO_CHANGE_ORDER); + if (!e->getWarnings().empty()) { + showWarning(e->getWarnings(),GUI_WARN_GENERIC); + } + break; + case GUI_ACTION_ORDERS_REMOVE: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->deleteOrder(); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_MOVE_UP: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->moveOrderUp(); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_MOVE_DOWN: + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->moveOrderDown(); + makeUndo(GUI_UNDO_CHANGE_ORDER); + break; + case GUI_ACTION_ORDERS_REPLAY: + e->setOrder(e->getOrder()); + break; + } +} \ No newline at end of file diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp new file mode 100644 index 000000000..5e8d72574 --- /dev/null +++ b/src/gui/editControls.cpp @@ -0,0 +1,338 @@ +/** + * 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 "IconsFontAwesome4.h" + +void FurnaceGUI::drawEditControls() { + if (nextWindow==GUI_WINDOW_EDIT_CONTROLS) { + editControlsOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!editControlsOpen) return; + switch (settings.controlLayout) { + case 0: // classic + if (ImGui::Begin("Play/Edit Controls",&editControlsOpen)) { + ImGui::Text("Octave"); + ImGui::SameLine(); + if (ImGui::InputInt("##Octave",&curOctave,1,1)) { + if (curOctave>7) curOctave=7; + if (curOctave<-5) curOctave=-5; + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::Text("Edit Step"); + ImGui::SameLine(); + if (ImGui::InputInt("##EditStep",&editStep,1,1)) { + if (editStep>=e->song.patLen) editStep=e->song.patLen-1; + if (editStep<0) editStep=0; + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + play(); + } + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_STOP "##Stop")) { + stop(); + } + ImGui::SameLine(); + ImGui::Checkbox("Edit",&edit); + ImGui::SameLine(); + bool metro=e->getMetronome(); + if (ImGui::Checkbox("Metronome",&metro)) { + e->setMetronome(metro); + } + + ImGui::Text("Follow"); + ImGui::SameLine(); + unimportant(ImGui::Checkbox("Orders",&followOrders)); + ImGui::SameLine(); + unimportant(ImGui::Checkbox("Pattern",&followPattern)); + + bool repeatPattern=e->getRepeatPattern(); + if (ImGui::Checkbox("Repeat pattern",&repeatPattern)) { + e->setRepeatPattern(repeatPattern); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { + e->stepOne(cursor.y); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; + ImGui::End(); + break; + case 1: // compact + if (ImGui::Begin("Play/Edit Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { + if (ImGui::Button(ICON_FA_STOP "##Stop")) { + stop(); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + play(); + } + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { + e->stepOne(cursor.y); + } + + ImGui::SameLine(); + bool repeatPattern=e->getRepeatPattern(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { + e->setRepeatPattern(!repeatPattern); + } + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { + edit=!edit; + } + ImGui::PopStyleColor(); + + ImGui::SameLine(); + bool metro=e->getMetronome(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { + e->setMetronome(!metro); + } + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::Text("Octave"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(96.0f*dpiScale); + if (ImGui::InputInt("##Octave",&curOctave,1,1)) { + if (curOctave>7) curOctave=7; + if (curOctave<-5) curOctave=-5; + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::SameLine(); + ImGui::Text("Edit Step"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(96.0f*dpiScale); + if (ImGui::InputInt("##EditStep",&editStep,1,1)) { + if (editStep>=e->song.patLen) editStep=e->song.patLen-1; + if (editStep<0) editStep=0; + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::SameLine(); + ImGui::Text("Follow"); + ImGui::SameLine(); + unimportant(ImGui::Checkbox("Orders",&followOrders)); + ImGui::SameLine(); + unimportant(ImGui::Checkbox("Pattern",&followPattern)); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; + ImGui::End(); + break; + case 2: // compact vertical + if (ImGui::Begin("Play/Edit Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(e->isPlaying())); + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + play(); + } + ImGui::PopStyleColor(); + if (ImGui::Button(ICON_FA_STOP "##Stop")) { + stop(); + } + if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { + e->stepOne(cursor.y); + } + + bool repeatPattern=e->getRepeatPattern(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { + e->setRepeatPattern(!repeatPattern); + } + ImGui::PopStyleColor(); + + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { + edit=!edit; + } + ImGui::PopStyleColor(); + + bool metro=e->getMetronome(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { + e->setMetronome(!metro); + } + ImGui::PopStyleColor(); + + ImGui::Text("Oct."); + float avail=ImGui::GetContentRegionAvail().x; + ImGui::SetNextItemWidth(avail); + if (ImGui::InputInt("##Octave",&curOctave,0,0)) { + if (curOctave>7) curOctave=7; + if (curOctave<-5) curOctave=-5; + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::Text("Step"); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputInt("##EditStep",&editStep,0,0)) { + if (editStep>=e->song.patLen) editStep=e->song.patLen-1; + if (editStep<0) editStep=0; + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::Text("Foll."); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(followOrders)); + if (ImGui::SmallButton("Ord##FollowOrders")) { handleUnimportant + followOrders=!followOrders; + } + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(followPattern)); + if (ImGui::SmallButton("Pat##FollowPattern")) { handleUnimportant + followPattern=!followPattern; + } + ImGui::PopStyleColor(); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; + ImGui::End(); + break; + case 3: // split + if (ImGui::Begin("Play Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { + if (e->isPlaying()) { + ImGui::PushStyleColor(ImGuiCol_Button,uiColors[GUI_COLOR_TOGGLE_ON]); + if (ImGui::Button(ICON_FA_STOP "##Stop")) { + stop(); + } + ImGui::PopStyleColor(); + } else { + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + play(); + } + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLAY_CIRCLE "##PlayAgain")) { + play(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { + e->stepOne(cursor.y); + } + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(edit)); + if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { + edit=!edit; + } + ImGui::PopStyleColor(); + + bool metro=e->getMetronome(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(metro)); + if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { + e->setMetronome(!metro); + } + ImGui::PopStyleColor(); + + ImGui::SameLine(); + bool repeatPattern=e->getRepeatPattern(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(repeatPattern)); + if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { + e->setRepeatPattern(!repeatPattern); + } + ImGui::PopStyleColor(); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; + ImGui::End(); + + if (ImGui::Begin("Edit Controls",&editControlsOpen)) { + ImGui::Columns(2); + ImGui::Text("Octave"); + ImGui::SameLine(); + float cursor=ImGui::GetCursorPosX(); + float avail=ImGui::GetContentRegionAvail().x; + ImGui::SetNextItemWidth(avail); + if (ImGui::InputInt("##Octave",&curOctave,1,1)) { + if (curOctave>7) curOctave=7; + if (curOctave<-5) curOctave=-5; + for (size_t i=0; inoteOff(activeNotes[i].chan); + } + activeNotes.clear(); + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + + ImGui::Text("Step"); + ImGui::SameLine(); + ImGui::SetCursorPosX(cursor); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputInt("##EditStep",&editStep,1,1)) { + if (editStep>=e->song.patLen) editStep=e->song.patLen-1; + if (editStep<0) editStep=0; + + if (settings.insFocusesPattern && !ImGui::IsItemActive() && patternOpen) { + nextWindow=GUI_WINDOW_PATTERN; + } + } + ImGui::NextColumn(); + + unimportant(ImGui::Checkbox("Follow orders",&followOrders)); + unimportant(ImGui::Checkbox("Follow pattern",&followPattern)); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; + ImGui::End(); + break; + } +} \ No newline at end of file diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp new file mode 100644 index 000000000..96f827f32 --- /dev/null +++ b/src/gui/editing.cpp @@ -0,0 +1,1023 @@ +/** + * 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 "../ta-log.h" +#include "guiConst.h" +#include + +#include "actionUtil.h" + +const char* noteNameNormal(short note, short octave) { + if (note==100) { // note cut + return "OFF"; + } else if (note==101) { // note off and envelope release + return "==="; + } else if (note==102) { // envelope release only + return "REL"; + } else if (octave==0 && note==0) { + return "..."; + } + int seek=(note+(signed char)octave*12)+60; + if (seek<0 || seek>=180) { + return "???"; + } + return noteNames[seek]; +} + +void FurnaceGUI::prepareUndo(ActionType action) { + int order=e->getOrder(); + switch (action) { + case GUI_UNDO_CHANGE_ORDER: + oldOrders=e->song.orders; + oldOrdersLen=e->song.ordersLen; + break; + case GUI_UNDO_PATTERN_EDIT: + case GUI_UNDO_PATTERN_DELETE: + case GUI_UNDO_PATTERN_PULL: + case GUI_UNDO_PATTERN_PUSH: + case GUI_UNDO_PATTERN_CUT: + case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: + for (int i=0; igetTotalChannelCount(); i++) { + e->song.pat[i].getPattern(e->song.orders.ord[i][order],false)->copyOn(oldPat[i]); + } + break; + } +} + +void FurnaceGUI::makeUndo(ActionType action) { + bool doPush=false; + UndoStep s; + s.type=action; + s.cursor=cursor; + s.selStart=selStart; + s.selEnd=selEnd; + int order=e->getOrder(); + s.order=order; + s.nibble=curNibble; + switch (action) { + case GUI_UNDO_CHANGE_ORDER: + for (int i=0; isong.orders.ord[i][j]) { + s.ord.push_back(UndoOrderData(i,j,oldOrders.ord[i][j],e->song.orders.ord[i][j])); + } + } + } + s.oldOrdersLen=oldOrdersLen; + s.newOrdersLen=e->song.ordersLen; + if (oldOrdersLen!=e->song.ordersLen) { + doPush=true; + } + if (!s.ord.empty()) { + doPush=true; + } + break; + case GUI_UNDO_PATTERN_EDIT: + case GUI_UNDO_PATTERN_DELETE: + case GUI_UNDO_PATTERN_PULL: + case GUI_UNDO_PATTERN_PUSH: + case GUI_UNDO_PATTERN_CUT: + case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: + for (int i=0; igetTotalChannelCount(); i++) { + DivPattern* p=e->song.pat[i].getPattern(e->song.orders.ord[i][order],false); + for (int j=0; jsong.patLen; j++) { + for (int k=0; k<32; k++) { + if (p->data[j][k]!=oldPat[i]->data[j][k]) { + s.pat.push_back(UndoPatternData(i,e->song.orders.ord[i][order],j,k,oldPat[i]->data[j][k],p->data[j][k])); + } + } + } + } + if (!s.pat.empty()) { + doPush=true; + } + break; + } + if (doPush) { + MARK_MODIFIED; + undoHist.push_back(s); + redoHist.clear(); + if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); + } +} + +void FurnaceGUI::doSelectAll() { + finishSelection(); + curNibble=false; + if (selStart.xFine==0 && selEnd.xFine==2+e->song.pat[selEnd.xCoarse].effectRows*2) { + if (selStart.y==0 && selEnd.y==e->song.patLen-1) { // select entire pattern + selStart.xCoarse=0; + selStart.xFine=0; + selEnd.xCoarse=e->getTotalChannelCount()-1; + selEnd.xFine=2+e->song.pat[selEnd.xCoarse].effectRows*2; + } else { // select entire column + selStart.y=0; + selEnd.y=e->song.patLen-1; + } + } else { + int selStartX=0; + int selEndX=0; + // find row position + for (SelectionPoint i; i.xCoarse!=selStart.xCoarse || i.xFine!=selStart.xFine; selStartX++) { + i.xFine++; + if (i.xFine>=3+e->song.pat[i.xCoarse].effectRows*2) { + i.xFine=0; + i.xCoarse++; + } + } + for (SelectionPoint i; i.xCoarse!=selEnd.xCoarse || i.xFine!=selEnd.xFine; selEndX++) { + i.xFine++; + if (i.xFine>=3+e->song.pat[i.xCoarse].effectRows*2) { + i.xFine=0; + i.xCoarse++; + } + } + + float aspect=float(selEndX-selStartX+1)/float(selEnd.y-selStart.y+1); + if (aspect<=1.0f && !(selStart.y==0 && selEnd.y==e->song.patLen-1)) { // up-down + selStart.y=0; + selEnd.y=e->song.patLen-1; + } else { // left-right + selStart.xFine=0; + selEnd.xFine=2+e->song.pat[selEnd.xCoarse].effectRows*2; + } + } +} + +#define maskOut(x) \ + if (x==0) { \ + if (!opMaskNote) continue; \ + } else if (x==1) { \ + if (!opMaskIns) continue; \ + } else if (x==2) { \ + if (!opMaskVol) continue; \ + } else if (((x)&1)==0) { \ + if (!opMaskEffectVal) continue; \ + } else if (((x)&1)==1) { \ + if (!opMaskEffect) continue; \ + } + +void FurnaceGUI::doDelete() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_DELETE); + curNibble=false; + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][iFine]=0; + if (selStart.y==selEnd.y) pat->data[j][2]=-1; + } + pat->data[j][iFine+1]=(iFine<1)?0:-1; + + if (selStart.y==selEnd.y && iFine>2 && iFine&1 && settings.effectDeletionAltersValue) { + pat->data[j][iFine+2]=-1; + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_DELETE); +} + +void FurnaceGUI::doPullDelete() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_PULL); + curNibble=false; + + if (settings.pullDeleteBehavior) { + if (--selStart.y<0) selStart.y=0; + if (--selEnd.y<0) selEnd.y=0; + if (--cursor.y<0) cursor.y=0; + updateScroll(cursor.y); + } + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen; j++) { + if (jsong.patLen-1) { + if (iFine==0) { + pat->data[j][iFine]=pat->data[j+1][iFine]; + } + pat->data[j][iFine+1]=pat->data[j+1][iFine+1]; + } else { + if (iFine==0) { + pat->data[j][iFine]=0; + } + pat->data[j][iFine+1]=(iFine<1)?0:-1; + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_PULL); +} + +void FurnaceGUI::doInsert() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_PUSH); + curNibble=false; + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen-1; j>=selStart.y; j--) { + if (j==selStart.y) { + if (iFine==0) { + pat->data[j][iFine]=0; + } + pat->data[j][iFine+1]=(iFine<1)?0:-1; + } else { + if (iFine==0) { + pat->data[j][iFine]=pat->data[j-1][iFine]; + } + pat->data[j][iFine+1]=pat->data[j-1][iFine+1]; + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_PUSH); +} + +void FurnaceGUI::doTranspose(int amount) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_DELETE); + curNibble=false; + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + int origOctave=(signed char)pat->data[j][1]; + if (origNote!=0 && origNote!=100 && origNote!=101 && origNote!=102) { + origNote+=amount; + while (origNote>12) { + origNote-=12; + origOctave++; + } + while (origNote<1) { + origNote+=12; + origOctave--; + } + if (origOctave==9 && origNote>11) { + origNote=11; + origOctave=9; + } + if (origOctave>9) { + origNote=11; + origOctave=9; + } + if (origOctave<-5) { + origNote=1; + origOctave=-5; + } + pat->data[j][0]=origNote; + pat->data[j][1]=(unsigned char)origOctave; + } + } else { + int top=255; + if (iFine==1) { + if (e->song.ins.empty()) continue; + top=e->song.ins.size()-1; + } else if (iFine==2) { // volume + top=e->getMaxVolumeChan(iCoarse); + } + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=MIN(top,MAX(0,pat->data[j][iFine+1]+amount)); + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_DELETE); +} + +void FurnaceGUI::doCopy(bool cut) { + finishSelection(); + if (cut) { + curNibble=false; + prepareUndo(GUI_UNDO_PATTERN_CUT); + } + clipboard=fmt::sprintf("org.tildearrow.furnace - Pattern Data (%d)\n%d",DIV_ENGINE_VERSION,selStart.xFine); + + for (int j=selStart.y; j<=selEnd.y; j++) { + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + if (iFine>3 && !(iFine&1)) { + iFine--; + } + int ord=e->getOrder(); + clipboard+='\n'; + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0],pat->data[j][1]); + if (cut) { + pat->data[j][0]=0; + pat->data[j][1]=0; + } + } else { + if (pat->data[j][iFine+1]==-1) { + clipboard+=".."; + } else { + clipboard+=fmt::sprintf("%.2X",pat->data[j][iFine+1]); + } + if (cut) { + pat->data[j][iFine+1]=-1; + } + } + } + clipboard+='|'; + iFine=0; + } + } + SDL_SetClipboardText(clipboard.c_str()); + + if (cut) { + makeUndo(GUI_UNDO_PATTERN_CUT); + } +} + +void FurnaceGUI::doPaste(PasteMode mode) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_PASTE); + char* clipText=SDL_GetClipboardText(); + if (clipText!=NULL) { + if (clipText[0]) { + clipboard=clipText; + } + SDL_free(clipText); + } + std::vector data; + String tempS; + for (char i: clipboard) { + if (i=='\r') continue; + if (i=='\n') { + data.push_back(tempS); + tempS=""; + continue; + } + tempS+=i; + } + data.push_back(tempS); + + int startOff=-1; + bool invalidData=false; + if (data.size()<2) return; + if (data[0]!=fmt::sprintf("org.tildearrow.furnace - Pattern Data (%d)",DIV_ENGINE_VERSION)) return; + if (sscanf(data[1].c_str(),"%d",&startOff)!=1) return; + if (startOff<0) return; + + DETERMINE_LAST; + + int j=cursor.y; + char note[4]; + int ord=e->getOrder(); + for (size_t i=2; isong.patLen; i++) { + size_t charPos=0; + int iCoarse=cursor.xCoarse; + int iFine=(startOff>2 && cursor.xFine>2)?(((cursor.xFine-1)&(~1))|1):startOff; + + String& line=data[i]; + + while (charPossong.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + if (line[charPos]=='|') { + iCoarse++; + if (iCoarsesong.chanShow[iCoarse]) { + iCoarse++; + if (iCoarse>=lastChannel) break; + } + iFine=0; + charPos++; + continue; + } + if (iFine==0) { + if (charPos>=line.size()) { + invalidData=true; + break; + } + note[0]=line[charPos++]; + if (charPos>=line.size()) { + invalidData=true; + break; + } + note[1]=line[charPos++]; + if (charPos>=line.size()) { + invalidData=true; + break; + } + note[2]=line[charPos++]; + note[3]=0; + + if (iFine==0 && !opMaskNote) { + iFine++; + continue; + } + + if ((mode==GUI_PASTE_MODE_MIX_BG || mode==GUI_PASTE_MODE_MIX_FG) && strcmp(note,"...")==0) { + // do nothing. + } else { + if (mode!=GUI_PASTE_MODE_MIX_BG || (pat->data[j][0]==0 && pat->data[j][1]==0)) { + if (!decodeNote(note,pat->data[j][0],pat->data[j][1])) { + invalidData=true; + break; + } + } + } + } else { + if (charPos>=line.size()) { + invalidData=true; + break; + } + note[0]=line[charPos++]; + if (charPos>=line.size()) { + invalidData=true; + break; + } + note[1]=line[charPos++]; + note[2]=0; + + if (iFine==1) { + if (!opMaskIns) { + iFine++; + continue; + } + } else if (iFine==2) { + if (!opMaskVol) { + iFine++; + continue; + } + } else if ((iFine&1)==0) { + if (!opMaskEffectVal) { + iFine++; + continue; + } + } else if ((iFine&1)==1) { + if (!opMaskEffect) { + iFine++; + continue; + } + } + + if (strcmp(note,"..")==0) { + if (!(mode==GUI_PASTE_MODE_MIX_BG || mode==GUI_PASTE_MODE_MIX_FG)) { + pat->data[j][iFine+1]=-1; + } + } else { + unsigned int val=0; + if (sscanf(note,"%2X",&val)!=1) { + invalidData=true; + break; + } + if (mode!=GUI_PASTE_MODE_MIX_BG || pat->data[j][iFine+1]==-1) { + if (iFine<(3+e->song.pat[iCoarse].effectRows*2)) pat->data[j][iFine+1]=val; + } + } + } + iFine++; + } + + if (invalidData) { + logW("invalid clipboard data! failed at line %d char %d",i,charPos); + logW("%s",line.c_str()); + break; + } + j++; + if (mode==GUI_PASTE_MODE_OVERFLOW && j>=e->song.patLen && ordsong.ordersLen-1) { + j=0; + ord++; + } + + if (mode==GUI_PASTE_MODE_FLOOD && i==data.size()-1) { + i=1; + } + } + if (settings.cursorPastePos) { + cursor.y=j; + updateScroll(cursor.y); + } + + makeUndo(GUI_UNDO_PATTERN_PASTE); +} + +void FurnaceGUI::doChangeIns(int ins) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_CHANGE_INS); + + int iCoarse=selStart.xCoarse; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][2]!=-1 || !(pat->data[j][0]==0 && pat->data[j][1]==0)) { + pat->data[j][2]=ins; + } + } + } + + makeUndo(GUI_UNDO_PATTERN_CHANGE_INS); +} + +void FurnaceGUI::doInterpolate() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_INTERPOLATE); + + std::vector> points; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][iFine+1]!=-1) { + points.emplace(points.end(),j,pat->data[j][iFine+1]); + } + } + + if (points.size()>1) for (size_t j=0; j& curPoint=points[j]; + std::pair& nextPoint=points[j+1]; + double distance=nextPoint.first-curPoint.first; + for (int k=0; k<(nextPoint.first-curPoint.first); k++) { + pat->data[k+curPoint.first][iFine+1]=curPoint.second+((nextPoint.second-curPoint.second)*(double)k/distance); + } + } + } else { + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][0]!=0 && pat->data[j][1]!=0) { + if (pat->data[j][0]!=100 && pat->data[j][0]!=101 && pat->data[j][0]!=102) { + points.emplace(points.end(),j,pat->data[j][0]+pat->data[j][1]*12); + } + } + } + + if (points.size()>1) for (size_t j=0; j& curPoint=points[j]; + std::pair& nextPoint=points[j+1]; + double distance=nextPoint.first-curPoint.first; + for (int k=0; k<(nextPoint.first-curPoint.first); k++) { + int val=curPoint.second+((nextPoint.second-curPoint.second)*(double)k/distance); + pat->data[k+curPoint.first][0]=val%12; + pat->data[k+curPoint.first][1]=val/12; + if (pat->data[k+curPoint.first][0]==0) { + pat->data[k+curPoint.first][0]=12; + pat->data[k+curPoint.first][1]--; + } + pat->data[k+curPoint.first][1]&=255; + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_INTERPOLATE); +} + +void FurnaceGUI::doFade(int p0, int p1, bool mode) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_FADE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + if (selEnd.y-selStart.y<1) continue; + for (int j=selStart.y; j<=selEnd.y; j++) { + double fraction=double(j-selStart.y)/double(selEnd.y-selStart.y); + int value=p0+double(p1-p0)*fraction; + if (mode) { // nibble + value&=15; + pat->data[j][iFine+1]=MIN(absoluteTop,value|(value<<4)); + } else { // byte + pat->data[j][iFine+1]=MIN(absoluteTop,value); + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_FADE); +} + +void FurnaceGUI::doInvertValues() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_INVERT_VAL); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + top=e->song.ins.size()-1; + } else if (iFine==2) { // volume + top=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=top-pat->data[j][iFine+1]; + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_INVERT_VAL); +} + +void FurnaceGUI::doScale(float top) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_SCALE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (pat->data[j][iFine+1]==-1) continue; + pat->data[j][iFine+1]=MIN(absoluteTop,(double)pat->data[j][iFine+1]*(top/100.0f)); + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_SCALE); +} + +void FurnaceGUI::doRandomize(int bottom, int top, bool mode) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_RANDOMIZE); + + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.ins.empty()) continue; + absoluteTop=e->song.ins.size()-1; + } else if (iFine==2) { // volume + absoluteTop=e->getMaxVolumeChan(iCoarse); + } + for (int j=selStart.y; j<=selEnd.y; j++) { + int value=0; + int value2=0; + if (top-bottom<=0) { + value=MIN(absoluteTop,bottom); + value2=MIN(absoluteTop,bottom); + } else { + value=MIN(absoluteTop,bottom+(rand()%(top-bottom+1))); + value2=MIN(absoluteTop,bottom+(rand()%(top-bottom+1))); + } + if (mode) { + value&=15; + value2&=15; + pat->data[j][iFine+1]=value|(value2<<4); + } else { + pat->data[j][iFine+1]=value; + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_RANDOMIZE); +} + +void FurnaceGUI::doFlip() { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_FLIP); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=selStart.y; j<=selEnd.y; j++) { + if (iFine==0) { + pat->data[j][0]=patBuffer.data[selEnd.y-j+selStart.y][0]; + } + pat->data[j][iFine+1]=patBuffer.data[selEnd.y-j+selStart.y][iFine+1]; + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_FLIP); +} + +void FurnaceGUI::doCollapse(int divider) { + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_COLLAPSE); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=selEnd.y-selStart.y; j++) { + if (j*divider>=selEnd.y-selStart.y) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + } else { + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y][iFine+1]; + + if (iFine==0) { + for (int k=1; k=selEnd.y-selStart.y) break; + if (!(pat->data[j+selStart.y][0]==0 && pat->data[j+selStart.y][1]==0)) break; + pat->data[j+selStart.y][0]=patBuffer.data[j*divider+selStart.y+k][0]; + pat->data[j+selStart.y][1]=patBuffer.data[j*divider+selStart.y+k][1]; + } + } else { + for (int k=1; k=selEnd.y-selStart.y) break; + if (pat->data[j+selStart.y][iFine+1]!=-1) break; + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j*divider+selStart.y+k][iFine+1]; + } + } + } + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_COLLAPSE); +} + +void FurnaceGUI::doExpand(int multiplier) { + if (multiplier<1) return; + + finishSelection(); + prepareUndo(GUI_UNDO_PATTERN_EXPAND); + + DivPattern patBuffer; + int iCoarse=selStart.xCoarse; + int iFine=selStart.xFine; + int ord=e->getOrder(); + for (; iCoarse<=selEnd.xCoarse; iCoarse++) { + if (!e->song.chanShow[iCoarse]) continue; + DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); + for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; + } + patBuffer.data[j][iFine+1]=pat->data[j][iFine+1]; + } + for (int j=0; j<=(selEnd.y-selStart.y)*multiplier; j++) { + if ((j+selStart.y)>=e->song.patLen) break; + if ((j%multiplier)!=0) { + if (iFine==0) { + pat->data[j+selStart.y][0]=0; + pat->data[j+selStart.y][1]=0; + } else { + pat->data[j+selStart.y][iFine+1]=-1; + } + continue; + } + if (iFine==0) { + pat->data[j+selStart.y][0]=patBuffer.data[j/multiplier+selStart.y][0]; + } + pat->data[j+selStart.y][iFine+1]=patBuffer.data[j/multiplier+selStart.y][iFine+1]; + } + } + iFine=0; + } + + makeUndo(GUI_UNDO_PATTERN_EXPAND); +} + +void FurnaceGUI::doUndo() { + if (undoHist.empty()) return; + UndoStep& us=undoHist.back(); + redoHist.push_back(us); + MARK_MODIFIED; + + switch (us.type) { + case GUI_UNDO_CHANGE_ORDER: + e->song.ordersLen=us.oldOrdersLen; + for (UndoOrderData& i: us.ord) { + e->song.orders.ord[i.chan][i.ord]=i.oldVal; + } + break; + case GUI_UNDO_PATTERN_EDIT: + case GUI_UNDO_PATTERN_DELETE: + case GUI_UNDO_PATTERN_PULL: + case GUI_UNDO_PATTERN_PUSH: + case GUI_UNDO_PATTERN_CUT: + case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: + for (UndoPatternData& i: us.pat) { + DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); + p->data[i.row][i.col]=i.oldVal; + } + if (!e->isPlaying()) { + cursor=us.cursor; + selStart=us.selStart; + selEnd=us.selEnd; + curNibble=us.nibble; + updateScroll(cursor.y); + e->setOrder(us.order); + } + break; + } + + undoHist.pop_back(); +} + +void FurnaceGUI::doRedo() { + if (redoHist.empty()) return; + UndoStep& us=redoHist.back(); + undoHist.push_back(us); + MARK_MODIFIED; + + switch (us.type) { + case GUI_UNDO_CHANGE_ORDER: + e->song.ordersLen=us.newOrdersLen; + for (UndoOrderData& i: us.ord) { + e->song.orders.ord[i.chan][i.ord]=i.newVal; + } + break; + case GUI_UNDO_PATTERN_EDIT: + case GUI_UNDO_PATTERN_DELETE: + case GUI_UNDO_PATTERN_PULL: + case GUI_UNDO_PATTERN_PUSH: + case GUI_UNDO_PATTERN_CUT: + case GUI_UNDO_PATTERN_PASTE: + case GUI_UNDO_PATTERN_CHANGE_INS: + case GUI_UNDO_PATTERN_INTERPOLATE: + case GUI_UNDO_PATTERN_FADE: + case GUI_UNDO_PATTERN_SCALE: + case GUI_UNDO_PATTERN_RANDOMIZE: + case GUI_UNDO_PATTERN_INVERT_VAL: + case GUI_UNDO_PATTERN_FLIP: + case GUI_UNDO_PATTERN_COLLAPSE: + case GUI_UNDO_PATTERN_EXPAND: + for (UndoPatternData& i: us.pat) { + DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); + p->data[i.row][i.col]=i.newVal; + } + if (!e->isPlaying()) { + cursor=us.cursor; + selStart=us.selStart; + selEnd=us.selEnd; + curNibble=us.nibble; + updateScroll(cursor.y); + e->setOrder(us.order); + } + + break; + } + + redoHist.pop_back(); +} \ No newline at end of file diff --git a/src/gui/fileDialog.cpp b/src/gui/fileDialog.cpp new file mode 100644 index 000000000..7bfafb4a3 --- /dev/null +++ b/src/gui/fileDialog.cpp @@ -0,0 +1,118 @@ +#include "fileDialog.h" +#include "ImGuiFileDialog.h" +#include "../ta-log.h" + +#include "../../extern/pfd-fixed/portable-file-dialogs.h" + +bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=false; + curPath=path; + logD("opening load file dialog with curPath %s",curPath.c_str()); + if (sysDialog) { + dialogO=new pfd::open_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { + if (opened) return false; + saving=true; + curPath=path; + logD("opening save file dialog with curPath %s",curPath.c_str()); + if (sysDialog) { + dialogS=new pfd::save_file(header,path,filter); + } else { + ImGuiFileDialog::Instance()->DpiScale=dpiScale; + ImGuiFileDialog::Instance()->OpenModal("FileDialog",header,noSysFilter,path,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + } + opened=true; + return true; +} + +bool FurnaceGUIFileDialog::accepted() { + if (sysDialog) { + return (fileName!=""); + } else { + return ImGuiFileDialog::Instance()->IsOk(); + } +} + +void FurnaceGUIFileDialog::close() { + if (sysDialog) { + if (saving) { + if (dialogS!=NULL) { + delete dialogS; + dialogS=NULL; + } + } else { + if (dialogO!=NULL) { + delete dialogO; + dialogO=NULL; + } + } + } else { + ImGuiFileDialog::Instance()->Close(); + } + opened=false; +} + +bool FurnaceGUIFileDialog::render(const ImVec2& min, const ImVec2& max) { + if (sysDialog) { + 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()); + return true; + } + } + } else { + if (dialogO!=NULL) { + if (dialogO->ready(0)) { + if (dialogO->result().empty()) { + fileName=""; + 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()); + } + return true; + } + } + } + return false; + } else { + return ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,min,max); + } +} + +String FurnaceGUIFileDialog::getPath() { + if (sysDialog) { + if (curPath.size()>1) { + if (curPath[curPath.size()-1]==DIR_SEPARATOR) { + curPath=curPath.substr(0,curPath.size()-1); + } + } + logD("curPath: %s",curPath.c_str()); + return curPath; + } else { + return ImGuiFileDialog::Instance()->GetCurrentPath(); + } +} + +String FurnaceGUIFileDialog::getFileName() { + if (sysDialog) { + return fileName; + } else { + return ImGuiFileDialog::Instance()->GetFilePathName(); + } +} diff --git a/src/gui/fileDialog.h b/src/gui/fileDialog.h new file mode 100644 index 000000000..b7f21abf0 --- /dev/null +++ b/src/gui/fileDialog.h @@ -0,0 +1,32 @@ +#include "../ta-utils.h" +#include "imgui.h" +#include + +namespace pfd { + class open_file; + class save_file; +} + +class FurnaceGUIFileDialog { + bool sysDialog; + bool opened; + bool saving; + String curPath; + String fileName; + pfd::open_file* dialogO; + pfd::save_file* dialogS; + public: + bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + 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); + String getPath(); + String getFileName(); + FurnaceGUIFileDialog(bool system): + sysDialog(system), + opened(false), + saving(false), + dialogO(NULL), + dialogS(NULL) {} +}; diff --git a/src/gui/fonts.h b/src/gui/fonts.h index 8f808a061..a0a42d251 100644 --- a/src/gui/fonts.h +++ b/src/gui/fonts.h @@ -46,3 +46,8 @@ extern const unsigned int builtinFontLen[]; extern const unsigned int* builtinFontM[]; extern const unsigned int builtinFontMLen[]; #endif + +// "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)"" +// not just that. somebody rewrite it already so I can load these glyphs at run-time and support +// all languages at once, instead of having to load Unifont's 65k+ characters and blow the GPU up in the +// process! \ No newline at end of file diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index ee0c02370..948489775 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -20,18 +20,17 @@ #define _USE_MATH_DEFINES #include "gui.h" #include "util.h" -#include "debug.h" -#include "fonts.h" #include "icon.h" #include "../ta-log.h" #include "../fileutils.h" #include "imgui.h" +#include "imgui_internal.h" #include "imgui_impl_sdl.h" #include "imgui_impl_sdlrenderer.h" -#include "imgui_internal.h" #include "ImGuiFileDialog.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include "plot_nolerp.h" #include "guiConst.h" #include "intConst.h" #include @@ -51,20 +50,24 @@ extern "C" { #include #include "../utfutils.h" #define LAYOUT_INI "\\layout.ini" +#define BACKUP_FUR "\\backup.fur" #else #include #include #include #define LAYOUT_INI "/layout.ini" +#define BACKUP_FUR "/backup.fur" #endif -bool Particle::update() { - pos.x+=speed.x; - pos.y+=speed.y; - speed.x*=friction; - speed.y*=friction; - speed.y+=gravity; - life-=lifeSpeed; +#include "actionUtil.h" + +bool Particle::update(float frameTime) { + pos.x+=speed.x*frameTime; + pos.y+=speed.y*frameTime; + speed.x*=1.0-((1.0-friction)*frameTime); + speed.y*=1.0-((1.0-friction)*frameTime); + speed.y+=gravity*frameTime; + life-=lifeSpeed*frameTime; return (life>0); } @@ -72,23 +75,6 @@ void FurnaceGUI::bindEngine(DivEngine* eng) { e=eng; } -const char* noteNameNormal(short note, short octave) { - if (note==100) { // note cut - return "OFF"; - } else if (note==101) { // note off and envelope release - return "==="; - } else if (note==102) { // envelope release only - return "REL"; - } else if (octave==0 && note==0) { - return "..."; - } - int seek=(note+(signed char)octave*12)+60; - if (seek<0 || seek>=180) { - return "???"; - } - return noteNames[seek]; -} - const char* FurnaceGUI::noteName(short note, short octave) { if (note==100) { return "OFF"; @@ -217,22 +203,30 @@ void FurnaceGUI::encodeMMLStr(String& target, unsigned char* macro, unsigned cha } } -void FurnaceGUI::encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel) { +void FurnaceGUI::encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel, bool hex) { target=""; char buf[32]; for (int i=0; i*((T*)p_max)) { \ + if (wheelY<0) { \ + if ((*((T*)p_data)-wheelY)>*((T*)p_min)) { \ + *((T*)p_data)=*((T*)p_min); \ + } else { \ + *((T*)p_data)-=wheelY; \ + } \ + } else { \ + if ((*((T*)p_data)-wheelY)<*((T*)p_max)) { \ + *((T*)p_data)=*((T*)p_max); \ + } else { \ + *((T*)p_data)-=wheelY; \ + } \ + } \ + } else { \ + if (wheelY>0) { \ + if ((*((T*)p_data)+wheelY)>*((T*)p_max)) { \ + *((T*)p_data)=*((T*)p_max); \ + } else { \ + *((T*)p_data)+=wheelY; \ + } \ + } else { \ + if ((*((T*)p_data)+wheelY)<*((T*)p_min)) { \ + *((T*)p_data)=*((T*)p_min); \ + } else { \ + *((T*)p_data)+=wheelY; \ + } \ + } \ + } \ + } + +bool FurnaceGUI::CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { + if (ImGui::SliderScalar(label,data_type,p_data,p_min,p_max,format,flags)) { + return true; + } + if (ImGui::IsItemHovered() && ctrlWheeling) { + switch (data_type) { + case ImGuiDataType_U8: + CW_ADDITION(unsigned char); + break; + case ImGuiDataType_S8: + CW_ADDITION(signed char); + break; + case ImGuiDataType_U16: + CW_ADDITION(unsigned short); + break; + case ImGuiDataType_S16: + CW_ADDITION(short); + break; + case ImGuiDataType_U32: + CW_ADDITION(unsigned int); + break; + case ImGuiDataType_S32: + CW_ADDITION(int); + break; + case ImGuiDataType_Float: + CW_ADDITION(float); + break; + case ImGuiDataType_Double: + CW_ADDITION(double); + break; + } + return true; + } + return false; +} + +bool FurnaceGUI::CWVSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { + if (ImGui::VSliderScalar(label,size,data_type,p_data,p_min,p_max,format,flags)) { + return true; + } + if (ImGui::IsItemHovered() && ctrlWheeling) { + switch (data_type) { + case ImGuiDataType_U8: + CW_ADDITION(unsigned char); + break; + case ImGuiDataType_S8: + CW_ADDITION(signed char); + break; + case ImGuiDataType_U16: + CW_ADDITION(unsigned short); + break; + case ImGuiDataType_S16: + CW_ADDITION(short); + break; + case ImGuiDataType_U32: + CW_ADDITION(unsigned int); + break; + case ImGuiDataType_S32: + CW_ADDITION(int); + break; + case ImGuiDataType_Float: + CW_ADDITION(float); + break; + case ImGuiDataType_Double: + CW_ADDITION(double); + break; + } + return true; + } + return false; +} + +bool FurnaceGUI::CWSliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { + return CWSliderScalar(label,ImGuiDataType_S32,v,&v_min,&v_max,format,flags); +} + +bool FurnaceGUI::CWSliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { + return CWSliderScalar(label,ImGuiDataType_Float,v,&v_min,&v_max,format,flags); +} + +bool FurnaceGUI::CWVSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { + return CWVSliderScalar(label,size,ImGuiDataType_S32,v,&v_min,&v_max,format,flags); +} + const char* FurnaceGUI::getSystemName(DivSystem which) { if (settings.chipNames) { return e->getSystemChips(which); @@ -419,14 +544,50 @@ void FurnaceGUI::setFileName(String name) { curFileName=ret; } #endif + updateWindowTitle(); } void FurnaceGUI::updateWindowTitle() { - if (e->song.name.empty()) { - SDL_SetWindowTitle(sdlWin,fmt::sprintf("Furnace (%s)",e->getSongSystemName()).c_str()); - } else { - SDL_SetWindowTitle(sdlWin,fmt::sprintf("%s - Furnace (%s)",e->song.name,e->getSongSystemName()).c_str()); + String title; + switch (settings.titleBarInfo) { + case 0: + title="Furnace"; + break; + case 1: + if (e->song.name.empty()) { + title="Furnace"; + } else { + title=fmt::sprintf("%s - Furnace",e->song.name); + } + break; + case 2: + if (curFileName.empty()) { + title="Furnace"; + } else { + String shortName; + size_t pos=curFileName.rfind(DIR_SEPARATOR); + if (pos==String::npos) { + shortName=curFileName; + } else { + shortName=curFileName.substr(pos+1); + } + title=fmt::sprintf("%s - Furnace",shortName); + } + break; + case 3: + if (curFileName.empty()) { + title="Furnace"; + } else { + title=fmt::sprintf("%s - Furnace",curFileName); + } + break; } + + if (settings.titleBarSys) { + title+=fmt::sprintf(" (%s)",e->getSongSystemName()); + } + + if (sdlWin!=NULL) SDL_SetWindowTitle(sdlWin,title.c_str()); } const char* defaultLayout="[Window][DockSpaceViewport_11111111]\n\ @@ -435,75 +596,64 @@ Size=1280,731\n\ Collapsed=0\n\ \n\ [Window][Debug##Default]\n\ -Pos=60,60\n\ +Pos=54,0\n\ Size=400,400\n\ Collapsed=0\n\ \n\ [Window][Play/Edit Controls]\n\ -Pos=390,24\n\ -Size=243,177\n\ +Pos=181,208\n\ +Size=45,409\n\ Collapsed=0\n\ -DockId=0x00000009,0\n\ \n\ [Window][Song Information]\n\ -Pos=951,24\n\ -Size=329,240\n\ +Pos=978,24\n\ +Size=302,217\n\ Collapsed=0\n\ DockId=0x00000004,0\n\ \n\ [Window][Orders]\n\ Pos=0,24\n\ -Size=388,240\n\ +Size=345,217\n\ Collapsed=0\n\ -DockId=0x00000005,0\n\ +DockId=0x00000007,0\n\ \n\ [Window][Instruments]\n\ -Pos=635,24\n\ -Size=314,240\n\ +Pos=653,24\n\ +Size=323,217\n\ Collapsed=0\n\ -DockId=0x00000008,1\n\ +DockId=0x00000006,2\n\ \n\ [Window][Wavetables]\n\ -Pos=635,24\n\ -Size=314,240\n\ +Pos=653,24\n\ +Size=323,217\n\ Collapsed=0\n\ -DockId=0x00000008,2\n\ +DockId=0x00000006,1\n\ \n\ [Window][Samples]\n\ -Pos=635,24\n\ -Size=314,240\n\ +Pos=653,24\n\ +Size=323,217\n\ Collapsed=0\n\ -DockId=0x00000008,0\n\ +DockId=0x00000006,0\n\ \n\ [Window][Pattern]\n\ -Pos=0,266\n\ -Size=1246,489\n\ +Pos=0,243\n\ +Size=1246,512\n\ Collapsed=0\n\ DockId=0x0000000B,0\n\ \n\ -[Window][Open File##FileDialog]\n\ -Pos=213,99\n\ -Size=853,557\n\ -Collapsed=0\n\ -\n\ [Window][Instrument Editor]\n\ -Pos=324,130\n\ -Size=951,623\n\ +Pos=372,102\n\ +Size=682,604\n\ Collapsed=0\n\ \n\ [Window][Warning]\n\ -Pos=516,339\n\ +Pos=481,338\n\ Size=346,71\n\ Collapsed=0\n\ \n\ -[Window][Load Sample##FileDialog]\n\ -Pos=340,177\n\ -Size=600,400\n\ -Collapsed=0\n\ -\n\ [Window][Sample Editor]\n\ -Pos=238,298\n\ -Size=551,286\n\ +Pos=531,176\n\ +Size=613,416\n\ Collapsed=0\n\ \n\ [Window][About Furnace]\n\ @@ -515,29 +665,61 @@ Pos=340,177\n\ Size=600,400\n\ Collapsed=0\n\ \n\ -[Window][Load Wavetable##FileDialog]\n\ -Pos=340,177\n\ -Size=600,400\n\ -Collapsed=0\n\ -\n\ [Window][Wavetable Editor]\n\ -Pos=228,81\n\ -Size=580,368\n\ -Collapsed=0\n\ -\n\ -[Window][Save Wavetable##FileDialog]\n\ -Pos=340,177\n\ -Size=600,400\n\ +Pos=253,295\n\ +Size=748,378\n\ Collapsed=0\n\ \n\ [Window][Settings]\n\ -Pos=495,97\n\ -Size=552,559\n\ +Pos=655,224\n\ +Size=601,508\n\ Collapsed=0\n\ \n\ [Window][Error]\n\ -Pos=488,342\n\ -Size=304,71\n\ +Pos=491,342\n\ +Size=514,71\n\ +Collapsed=0\n\ +\n\ +[Window][Mixer]\n\ +Pos=63,55\n\ +Size=450,215\n\ +Collapsed=0\n\ +\n\ +[Window][Oscilloscope]\n\ +Pos=347,94\n\ +Size=304,105\n\ +Collapsed=0\n\ +DockId=0x0000000E,0\n\ +\n\ +[Window][Volume Meter]\n\ +Pos=1248,243\n\ +Size=32,512\n\ +Collapsed=0\n\ +DockId=0x0000000C,0\n\ +\n\ +[Window][Debug]\n\ +Pos=113,148\n\ +Size=945,473\n\ +Collapsed=0\n\ +\n\ +[Window][Load Sample##FileDialog]\n\ +Pos=40,0\n\ +Size=1200,755\n\ +Collapsed=0\n\ +\n\ +[Window][Open File##FileDialog]\n\ +Pos=250,143\n\ +Size=779,469\n\ +Collapsed=0\n\ +\n\ +[Window][Export Audio##FileDialog]\n\ +Pos=339,177\n\ +Size=601,400\n\ +Collapsed=0\n\ +\n\ +[Window][Rendering...]\n\ +Pos=585,342\n\ +Size=114,71\n\ Collapsed=0\n\ \n\ [Window][Export VGM##FileDialog]\n\ @@ -545,43 +727,84 @@ Pos=340,177\n\ Size=600,400\n\ Collapsed=0\n\ \n\ -[Window][Mixer]\n\ -Pos=60,60\n\ -Size=450,215\n\ +[Window][Warning##Save FileFileDialogOverWriteDialog]\n\ +Pos=390,351\n\ +Size=500,71\n\ Collapsed=0\n\ \n\ -[Window][Oscilloscope]\n\ -Pos=390,203\n\ -Size=243,61\n\ +[Window][Statistics]\n\ +Pos=596,307\n\ +Size=512,219\n\ +Collapsed=0\n\ +\n\ +[Window][Warning##Export VGMFileDialogOverWriteDialog]\n\ +Pos=390,351\n\ +Size=500,71\n\ +Collapsed=0\n\ +\n\ +[Window][Compatibility Flags]\n\ +Pos=682,287\n\ +Size=347,262\n\ +Collapsed=0\n\ +\n\ +[Window][Song Comments]\n\ +Pos=60,60\n\ +Size=395,171\n\ +Collapsed=0\n\ +\n\ +[Window][Warning##Export AudioFileDialogOverWriteDialog]\n\ +Pos=381,351\n\ +Size=500,71\n\ +Collapsed=0\n\ +\n\ +[Window][Select Font##FileDialog]\n\ +Pos=340,177\n\ +Size=600,400\n\ +Collapsed=0\n\ +\n\ +[Window][Channels]\n\ +Pos=60,60\n\ +Size=368,449\n\ +Collapsed=0\n\ +\n\ +[Window][Register View]\n\ +Pos=847,180\n\ +Size=417,393\n\ +Collapsed=0\n\ +\n\ +[Window][New Song]\n\ +Pos=267,110\n\ +Size=746,534\n\ +Collapsed=0\n\ +\n\ +[Window][Edit Controls]\n\ +Pos=347,24\n\ +Size=304,68\n\ +Collapsed=0\n\ +DockId=0x0000000D,0\n\ +\n\ +[Window][Play Controls]\n\ +Pos=347,201\n\ +Size=304,40\n\ Collapsed=0\n\ DockId=0x0000000A,0\n\ \n\ -[Window][Volume Meter]\n\ -Pos=1248,266\n\ -Size=32,489\n\ -Collapsed=0\n\ -DockId=0x0000000C,0\n\ -\n\ -[Window][Debug]\n\ -Pos=38,96\n\ -Size=1243,574\n\ -Collapsed=0\n\ -\n\ [Docking][Data]\n\ -DockSpace ID=0x8B93E3BD Window=0xA787BDB4 Pos=0,24 Size=1280,731 Split=Y Selected=0x6C01C512\n\ - DockNode ID=0x00000001 Parent=0x8B93E3BD SizeRef=1280,240 Split=X Selected=0xF3094A52\n\ - DockNode ID=0x00000003 Parent=0x00000001 SizeRef=949,231 Split=X Selected=0x65CC51DC\n\ - DockNode ID=0x00000005 Parent=0x00000003 SizeRef=388,231 Selected=0xE283F8D8\n\ - DockNode ID=0x00000006 Parent=0x00000003 SizeRef=559,231 Split=X Selected=0x756E3877\n\ - DockNode ID=0x00000007 Parent=0x00000006 SizeRef=243,231 Split=Y Selected=0xD2BA8AA2\n\ - DockNode ID=0x00000009 Parent=0x00000007 SizeRef=220,177 Selected=0xD2BA8AA2\n\ - DockNode ID=0x0000000A Parent=0x00000007 SizeRef=220,61 HiddenTabBar=1 Selected=0x608FDEB4\n\ - DockNode ID=0x00000008 Parent=0x00000006 SizeRef=314,231 Selected=0xD62F6EEB\n\ - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=329,231 Selected=0xF3094A52\n\ - DockNode ID=0x00000002 Parent=0x8B93E3BD SizeRef=1280,489 Split=X Selected=0x6C01C512\n\ - DockNode ID=0x0000000B Parent=0x00000002 SizeRef=1246,503 CentralNode=1 Selected=0x6C01C512\n\ - DockNode ID=0x0000000C Parent=0x00000002 SizeRef=32,503 HiddenTabBar=1 Selected=0xD67E3EB0\n\ -"; +DockSpace ID=0x8B93E3BD Window=0xA787BDB4 Pos=0,24 Size=1280,731 Split=Y Selected=0x6C01C512\n\ + DockNode ID=0x00000001 Parent=0x8B93E3BD SizeRef=1280,217 Split=X Selected=0xF3094A52\n\ + DockNode ID=0x00000003 Parent=0x00000001 SizeRef=976,231 Split=X Selected=0x65CC51DC\n\ + DockNode ID=0x00000007 Parent=0x00000003 SizeRef=345,231 HiddenTabBar=1 Selected=0x8F5BFC9A\n\ + DockNode ID=0x00000008 Parent=0x00000003 SizeRef=629,231 Split=X Selected=0xD2AD486B\n\ + DockNode ID=0x00000005 Parent=0x00000008 SizeRef=304,406 Split=Y Selected=0x6D682373\n\ + DockNode ID=0x00000009 Parent=0x00000005 SizeRef=292,175 Split=Y Selected=0x6D682373\n\ + DockNode ID=0x0000000D Parent=0x00000009 SizeRef=292,68 HiddenTabBar=1 Selected=0xE57B1A9D\n\ + DockNode ID=0x0000000E Parent=0x00000009 SizeRef=292,105 HiddenTabBar=1 Selected=0x6D682373\n\ + DockNode ID=0x0000000A Parent=0x00000005 SizeRef=292,40 HiddenTabBar=1 Selected=0x0DE44CFF\n\ + DockNode ID=0x00000006 Parent=0x00000008 SizeRef=323,406 Selected=0xD2AD486B\n\ + DockNode ID=0x00000004 Parent=0x00000001 SizeRef=302,231 Selected=0x60B9D088\n\ + DockNode ID=0x00000002 Parent=0x8B93E3BD SizeRef=1280,512 Split=X Selected=0x6C01C512\n\ + DockNode ID=0x0000000B Parent=0x00000002 SizeRef=1246,503 CentralNode=1 HiddenTabBar=1 Selected=0xB9ADD0D5\n\ + DockNode ID=0x0000000C Parent=0x00000002 SizeRef=32,503 HiddenTabBar=1 Selected=0x644DA2C1\n\n"; void FurnaceGUI::prepareLayout() { FILE* check; @@ -592,10 +815,10 @@ void FurnaceGUI::prepareLayout() { } // copy initial layout - logI("loading default layout.\n"); + logI("loading default layout."); check=ps_fopen(finalLayoutPath,"w"); if (check==NULL) { - logW("could not write default layout!\n"); + logW("could not write default layout!"); return; } @@ -603,2321 +826,14 @@ void FurnaceGUI::prepareLayout() { fclose(check); } -void FurnaceGUI::drawEditControls() { - if (nextWindow==GUI_WINDOW_EDIT_CONTROLS) { - editControlsOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!editControlsOpen) return; - switch (settings.controlLayout) { - case 0: // classic - if (ImGui::Begin("Play/Edit Controls",&editControlsOpen)) { - ImGui::Text("Octave"); - ImGui::SameLine(); - if (ImGui::InputInt("##Octave",&curOctave,1,1)) { - if (curOctave>7) curOctave=7; - if (curOctave<-5) curOctave=-5; - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - - ImGui::Text("Edit Step"); - ImGui::SameLine(); - if (ImGui::InputInt("##EditStep",&editStep,1,1)) { - if (editStep>=e->song.patLen) editStep=e->song.patLen-1; - if (editStep<0) editStep=0; - } - - if (ImGui::Button(ICON_FA_PLAY "##Play")) { - play(); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_STOP "##Stop")) { - stop(); - } - ImGui::SameLine(); - ImGui::Checkbox("Edit",&edit); - ImGui::SameLine(); - bool metro=e->getMetronome(); - if (ImGui::Checkbox("Metronome",&metro)) { - e->setMetronome(metro); - } - - ImGui::Text("Follow"); - ImGui::SameLine(); - ImGui::Checkbox("Orders",&followOrders); - ImGui::SameLine(); - ImGui::Checkbox("Pattern",&followPattern); - - bool repeatPattern=e->getRepeatPattern(); - if (ImGui::Checkbox("Repeat pattern",&repeatPattern)) { - e->setRepeatPattern(repeatPattern); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { - e->stepOne(cursor.y); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; - ImGui::End(); - break; - case 1: // compact - if (ImGui::Begin("Play/Edit Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { - if (ImGui::Button(ICON_FA_STOP "##Stop")) { - stop(); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_PLAY "##Play")) { - play(); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { - e->stepOne(cursor.y); - } - - ImGui::SameLine(); - bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(repeatPattern)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { - e->setRepeatPattern(!repeatPattern); - } - ImGui::PopStyleColor(); - - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(edit)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { - edit=!edit; - } - ImGui::PopStyleColor(); - - ImGui::SameLine(); - bool metro=e->getMetronome(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(metro)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { - e->setMetronome(!metro); - } - ImGui::PopStyleColor(); - - ImGui::SameLine(); - ImGui::Text("Octave"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(96.0f*dpiScale); - if (ImGui::InputInt("##Octave",&curOctave,1,1)) { - if (curOctave>7) curOctave=7; - if (curOctave<-5) curOctave=-5; - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - - ImGui::SameLine(); - ImGui::Text("Edit Step"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(96.0f*dpiScale); - if (ImGui::InputInt("##EditStep",&editStep,1,1)) { - if (editStep>=e->song.patLen) editStep=e->song.patLen-1; - if (editStep<0) editStep=0; - } - - ImGui::SameLine(); - ImGui::Text("Follow"); - ImGui::SameLine(); - ImGui::Checkbox("Orders",&followOrders); - ImGui::SameLine(); - ImGui::Checkbox("Pattern",&followPattern); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; - ImGui::End(); - break; - case 2: // compact vertical - if (ImGui::Begin("Play/Edit Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { - if (ImGui::Button(ICON_FA_PLAY "##Play")) { - play(); - } - if (ImGui::Button(ICON_FA_STOP "##Stop")) { - stop(); - } - if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { - e->stepOne(cursor.y); - } - - bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(repeatPattern)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { - e->setRepeatPattern(!repeatPattern); - } - ImGui::PopStyleColor(); - - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(edit)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { - edit=!edit; - } - ImGui::PopStyleColor(); - - bool metro=e->getMetronome(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(metro)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { - e->setMetronome(!metro); - } - ImGui::PopStyleColor(); - - ImGui::Text("Oct."); - float avail=ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(avail); - if (ImGui::InputInt("##Octave",&curOctave,0,0)) { - if (curOctave>7) curOctave=7; - if (curOctave<-5) curOctave=-5; - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - - ImGui::Text("Step"); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputInt("##EditStep",&editStep,0,0)) { - if (editStep>=e->song.patLen) editStep=e->song.patLen-1; - if (editStep<0) editStep=0; - } - - ImGui::Text("Foll."); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(followOrders)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::SmallButton("Ord##FollowOrders")) { - followOrders=!followOrders; - } - ImGui::PopStyleColor(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(followPattern)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::SmallButton("Pat##FollowPattern")) { - followPattern=!followPattern; - } - ImGui::PopStyleColor(); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; - ImGui::End(); - break; - case 3: // split - if (ImGui::Begin("Play Controls",&editControlsOpen,ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { - if (e->isPlaying()) { - if (ImGui::Button(ICON_FA_STOP "##Stop")) { - stop(); - } - } else { - if (ImGui::Button(ICON_FA_PLAY "##Play")) { - play(); - } - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_PLAY_CIRCLE "##PlayAgain")) { - play(); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_ARROW_DOWN "##StepOne")) { - e->stepOne(cursor.y); - } - - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(edit)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_CIRCLE "##Edit")) { - edit=!edit; - } - ImGui::PopStyleColor(); - - bool metro=e->getMetronome(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(metro)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_BELL_O "##Metronome")) { - e->setMetronome(!metro); - } - ImGui::PopStyleColor(); - - ImGui::SameLine(); - bool repeatPattern=e->getRepeatPattern(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(repeatPattern)?0.6f:0.2f,0.2f,1.0f)); - if (ImGui::Button(ICON_FA_REPEAT "##RepeatPattern")) { - e->setRepeatPattern(!repeatPattern); - } - ImGui::PopStyleColor(); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; - ImGui::End(); - - if (ImGui::Begin("Edit Controls",&editControlsOpen)) { - ImGui::Columns(2); - ImGui::Text("Octave"); - ImGui::SameLine(); - float cursor=ImGui::GetCursorPosX(); - float avail=ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(avail); - if (ImGui::InputInt("##Octave",&curOctave,1,1)) { - if (curOctave>7) curOctave=7; - if (curOctave<-5) curOctave=-5; - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - - ImGui::Text("Step"); - ImGui::SameLine(); - ImGui::SetCursorPosX(cursor); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputInt("##EditStep",&editStep,1,1)) { - if (editStep>=e->song.patLen) editStep=e->song.patLen-1; - if (editStep<0) editStep=0; - } - ImGui::NextColumn(); - - ImGui::Checkbox("Follow orders",&followOrders); - ImGui::Checkbox("Follow pattern",&followPattern); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_EDIT_CONTROLS; - ImGui::End(); - break; - } -} - -void FurnaceGUI::drawSongInfo() { - if (nextWindow==GUI_WINDOW_SONG_INFO) { - songInfoOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!songInfoOpen) return; - if (ImGui::Begin("Song Information",&songInfoOpen)) { - if (ImGui::BeginTable("NameAuthor",2,ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Name"); - ImGui::TableNextColumn(); - float avail=ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(avail); - if (ImGui::InputText("##Name",&e->song.name)) updateWindowTitle(); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Author"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - ImGui::InputText("##Author",&e->song.author); - ImGui::EndTable(); - } - - if (ImGui::BeginTable("OtherProps",3,ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.0); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("TimeBase"); - ImGui::TableNextColumn(); - float avail=ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(avail); - unsigned char realTB=e->song.timeBase+1; - if (ImGui::InputScalar("##TimeBase",ImGuiDataType_U8,&realTB,&_ONE,&_THREE)) { - if (realTB<1) realTB=1; - if (realTB>16) realTB=16; - e->song.timeBase=realTB-1; - } - ImGui::TableNextColumn(); - float hl=e->song.hilightA; - if (hl<=0.0f) hl=4.0f; - float timeBase=e->song.timeBase+1; - float speedSum=e->song.speed1+e->song.speed2; - if (timeBase<1.0f) timeBase=1.0f; - if (speedSum<1.0f) speedSum=1.0f; - ImGui::Text("%.2f BPM",120.0f*(float)e->song.hz/(timeBase*hl*speedSum)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Speed"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->song.speed1,&_ONE,&_THREE)) { - if (e->song.speed1<1) e->song.speed1=1; - if (e->isPlaying()) play(); - } - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->song.speed2,&_ONE,&_THREE)) { - if (e->song.speed2<1) e->song.speed2=1; - if (e->isPlaying()) play(); - } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Highlight"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - ImGui::InputScalar("##Highlight1",ImGuiDataType_U8,&e->song.hilightA,&_ONE,&_THREE); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - ImGui::InputScalar("##Highlight2",ImGuiDataType_U8,&e->song.hilightB,&_ONE,&_THREE); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Pattern Length"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - int patLen=e->song.patLen; - if (ImGui::InputInt("##PatLength",&patLen,1,3)) { - if (patLen<1) patLen=1; - if (patLen>256) patLen=256; - e->song.patLen=patLen; - } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Song Length"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - int ordLen=e->song.ordersLen; - if (ImGui::InputInt("##OrdLength",&ordLen,1,3)) { - if (ordLen<1) ordLen=1; - if (ordLen>127) ordLen=127; - e->song.ordersLen=ordLen; - } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Tick Rate"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - int setHz=e->song.hz; - if (ImGui::InputInt("##Rate",&setHz)) { - if (setHz<10) setHz=10; - if (setHz>999) setHz=999; - e->setSongRate(setHz,setHz<52); - } - if (e->song.hz==50) { - ImGui::TableNextColumn(); - ImGui::Text("PAL"); - } - if (e->song.hz==60) { - ImGui::TableNextColumn(); - ImGui::Text("NTSC"); - } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Tuning (A-4)"); - ImGui::TableNextColumn(); - float tune=e->song.tuning; - ImGui::SetNextItemWidth(avail); - if (ImGui::InputFloat("##Tuning",&tune,1.0f,3.0f,"%g")) { - if (tune<220.0f) tune=220.0f; - if (tune>880.0f) tune=880.0f; - e->song.tuning=tune; - } - ImGui::EndTable(); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SONG_INFO; - ImGui::End(); -} - -void FurnaceGUI::drawInsList() { - if (nextWindow==GUI_WINDOW_INS_LIST) { - insListOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!insListOpen) return; - if (ImGui::Begin("Instruments",&insListOpen)) { - if (ImGui::Button(ICON_FA_PLUS "##InsAdd")) { - doAction(GUI_ACTION_INS_LIST_ADD); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FILES_O "##InsClone")) { - doAction(GUI_ACTION_INS_LIST_DUPLICATE); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FOLDER_OPEN "##InsLoad")) { - doAction(GUI_ACTION_INS_LIST_OPEN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FLOPPY_O "##InsSave")) { - doAction(GUI_ACTION_INS_LIST_SAVE); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("InsUp",ImGuiDir_Up)) { - doAction(GUI_ACTION_INS_LIST_MOVE_UP); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("InsDown",ImGuiDir_Down)) { - doAction(GUI_ACTION_INS_LIST_MOVE_DOWN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_TIMES "##InsDelete")) { - doAction(GUI_ACTION_INS_LIST_DELETE); - } - ImGui::Separator(); - if (ImGui::BeginTable("InsListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.ins.size(); i++) { - DivInstrument* ins=e->song.ins[i]; - String name; - switch (ins->type) { - case DIV_INS_FM: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FM]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_STD: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_STD]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_GB: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_GB]); - name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_C64: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_C64]); - name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_AMIGA: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AMIGA]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_PCE: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PCE]); - name=fmt::sprintf(ICON_FA_ID_BADGE " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_AY: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_AY8930: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY8930]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_TIA: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_TIA]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_SAA1099: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SAA1099]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_VIC: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VIC]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_PET: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PET]); - name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_VRC6: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VRC6]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_OPLL: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPLL]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_OPL: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPL]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_FDS: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FDS]); - name=fmt::sprintf(ICON_FA_FLOPPY_O " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_VBOY: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VBOY]); - name=fmt::sprintf(ICON_FA_BINOCULARS " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_N163: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_N163]); - name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_SCC: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SCC]); - name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_OPZ: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPZ]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_POKEY: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_POKEY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_BEEPER: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_BEEPER]); - name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_SWAN: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SWAN]); - name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - case DIV_INS_MIKEY: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MIKEY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - default: - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_UNKNOWN]); - name=fmt::sprintf(ICON_FA_QUESTION " %.2X: %s##_INS%d\n",i,ins->name,i); - break; - } - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::Selectable(name.c_str(),curIns==i)) { - curIns=i; - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - insEditOpen=true; - } - } - } - ImGui::EndTable(); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_LIST; - ImGui::End(); -} - -const char* sampleNote[12]={ - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" -}; - -void FurnaceGUI::drawSampleList() { - if (nextWindow==GUI_WINDOW_SAMPLE_LIST) { - sampleListOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!sampleListOpen) return; - if (ImGui::Begin("Samples",&sampleListOpen)) { - if (ImGui::Button(ICON_FA_PLUS "##SampleAdd")) { - doAction(GUI_ACTION_SAMPLE_LIST_ADD); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FOLDER_OPEN "##SampleLoad")) { - doAction(GUI_ACTION_SAMPLE_LIST_OPEN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FLOPPY_O "##SampleSave")) { - doAction(GUI_ACTION_SAMPLE_LIST_SAVE); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("SampleUp",ImGuiDir_Up)) { - doAction(GUI_ACTION_SAMPLE_LIST_MOVE_UP); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("SampleDown",ImGuiDir_Down)) { - doAction(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_TIMES "##SampleDelete")) { - doAction(GUI_ACTION_SAMPLE_LIST_DELETE); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_UP "##PreviewSampleL")) { - doAction(GUI_ACTION_SAMPLE_LIST_PREVIEW); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_OFF "##StopSampleL")) { - doAction(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW); - } - ImGui::Separator(); - if (ImGui::BeginTable("SampleListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.sample.size(); i++) { - DivSample* sample=e->song.sample[i]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if ((i%12)==0) { - if (i>0) ImGui::Unindent(); - ImGui::Text("Bank %d",i/12); - ImGui::Indent(); - } - if (ImGui::Selectable(fmt::sprintf("%s: %s##_SAM%d",sampleNote[i%12],sample->name,i).c_str(),curSample==i)) { - curSample=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - sampleEditOpen=true; - } - } - } - ImGui::EndTable(); - } - ImGui::Unindent(); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_LIST; - ImGui::End(); -} - -void FurnaceGUI::drawSampleEdit() { - if (nextWindow==GUI_WINDOW_SAMPLE_EDIT) { - sampleEditOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!sampleEditOpen) return; - if (ImGui::Begin("Sample Editor",&sampleEditOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { - if (curSample<0 || curSample>=(int)e->song.sample.size()) { - ImGui::Text("no sample selected"); - } else { - DivSample* sample=e->song.sample[curSample]; - ImGui::InputText("Name",&sample->name); - ImGui::Text("Length: %d",sample->samples); - ImGui::Text("Type: %d-bit",sample->depth); - if (ImGui::InputInt("Rate (Hz)",&sample->rate,10,200)) { - if (sample->rate<100) sample->rate=100; - if (sample->rate>96000) sample->rate=96000; - } - if (ImGui::InputInt("Pitch of C-4 (Hz)",&sample->centerRate,10,200)) { - if (sample->centerRate<100) sample->centerRate=100; - if (sample->centerRate>65535) sample->centerRate=65535; - } - ImGui::Text("effective rate: %dHz",e->getEffectiveSampleRate(sample->rate)); - bool doLoop=(sample->loopStart>=0); - if (ImGui::Checkbox("Loop",&doLoop)) { - if (doLoop) { - sample->loopStart=0; - } else { - sample->loopStart=-1; - } - } - if (doLoop) { - ImGui::SameLine(); - if (ImGui::InputInt("##LoopPosition",&sample->loopStart,1,10)) { - if (sample->loopStart<0 || sample->loopStart>=(int)sample->samples) { - sample->loopStart=0; - } - } - } - if (ImGui::Button("Apply")) { - e->renderSamplesP(); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_UP "##PreviewSample")) { - e->previewSample(curSample); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_VOLUME_OFF "##StopSample")) { - e->stopSamplePreview(); - } - ImGui::Separator(); - bool considerations=false; - ImGui::Text("notes:"); - if (sample->loopStart>=0) { - considerations=true; - ImGui::Text("- sample won't loop on Neo Geo ADPCM"); - if (sample->loopStart&1) { - ImGui::Text("- sample loop start will be aligned to the nearest even sample on Amiga"); - } - } - if (sample->samples&1) { - considerations=true; - ImGui::Text("- sample length will be aligned to the nearest even sample on Amiga"); - } - if (sample->samples>65535) { - considerations=true; - ImGui::Text("- maximum sample length on Sega PCM is 65536 samples"); - } - if (sample->samples>2097151) { - considerations=true; - ImGui::Text("- maximum sample length on Neo Geo ADPCM is 2097152 samples"); - } - if (!considerations) { - ImGui::Text("- none"); - } - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_EDIT; - ImGui::End(); -} - -void FurnaceGUI::drawMixer() { - if (nextWindow==GUI_WINDOW_MIXER) { - mixerOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!mixerOpen) return; - ImGui::SetNextWindowSizeConstraints(ImVec2(400.0f*dpiScale,200.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - if (ImGui::Begin("Mixer",&mixerOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { - char id[32]; - if (ImGui::SliderFloat("Master Volume",&e->song.masterVol,0,3,"%.2fx")) { - if (e->song.masterVol<0) e->song.masterVol=0; - if (e->song.masterVol>3) e->song.masterVol=3; - } - for (int i=0; isong.systemLen; i++) { - snprintf(id,31,"MixS%d",i); - bool doInvert=e->song.systemVol[i]&128; - signed char vol=e->song.systemVol[i]&127; - ImGui::PushID(id); - ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i])); - ImGui::SameLine(ImGui::GetWindowWidth()-(82.0f*dpiScale)); - if (ImGui::Checkbox("Invert",&doInvert)) { - e->song.systemVol[i]^=128; - } - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(50.0f*dpiScale)); - if (ImGui::SliderScalar("Volume",ImGuiDataType_S8,&vol,&_ZERO,&_ONE_HUNDRED_TWENTY_SEVEN)) { - e->song.systemVol[i]=(e->song.systemVol[i]&128)|vol; - } - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(50.0f*dpiScale)); - ImGui::SliderScalar("Panning",ImGuiDataType_S8,&e->song.systemPan[i],&_MINUS_ONE_HUNDRED_TWENTY_SEVEN,&_ONE_HUNDRED_TWENTY_SEVEN); - - ImGui::PopID(); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_MIXER; - ImGui::End(); -} - -void FurnaceGUI::drawOsc() { - if (nextWindow==GUI_WINDOW_OSCILLOSCOPE) { - oscOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!oscOpen) return; - ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0,0)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(0,0)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing,ImVec2(0,0)); - if (ImGui::Begin("Oscilloscope",&oscOpen)) { - float values[512]; - for (int i=0; i<512; i++) { - int pos=i*e->oscSize/512; - values[i]=(e->oscBuf[0][pos]+e->oscBuf[1][pos])*0.5f; - } - //ImGui::SetCursorPos(ImVec2(0,0)); - ImGui::BeginDisabled(); - ImGui::PlotLines("##SingleOsc",values,512,0,NULL,-1.0f,1.0f,ImGui::GetContentRegionAvail()); - ImGui::EndDisabled(); - } - ImGui::PopStyleVar(3); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_OSCILLOSCOPE; - ImGui::End(); -} - -void FurnaceGUI::drawVolMeter() { - if (nextWindow==GUI_WINDOW_VOL_METER) { - volMeterOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!volMeterOpen) return; - if (--isClipping<0) isClipping=0; - ImGui::SetNextWindowSizeConstraints(ImVec2(6.0f*dpiScale,6.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0,0)); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0,0)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(0,0)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing,ImVec2(0,0)); - if (ImGui::Begin("Volume Meter",&volMeterOpen)) { - ImDrawList* dl=ImGui::GetWindowDrawList(); - bool aspectRatio=(ImGui::GetWindowSize().x/ImGui::GetWindowSize().y)>1.0; - - ImVec2 minArea=ImVec2( - ImGui::GetWindowPos().x+ImGui::GetCursorPos().x, - ImGui::GetWindowPos().y+ImGui::GetCursorPos().y - ); - ImVec2 maxArea=ImVec2( - ImGui::GetWindowPos().x+ImGui::GetCursorPos().x+ImGui::GetContentRegionAvail().x, - ImGui::GetWindowPos().y+ImGui::GetCursorPos().y+ImGui::GetContentRegionAvail().y - ); - ImRect rect=ImRect(minArea,maxArea); - ImGuiStyle& style=ImGui::GetStyle(); - ImGui::ItemSize(ImVec2(4.0f,4.0f),style.FramePadding.y); - ImU32 lowColor=ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_LOW]); - if (ImGui::ItemAdd(rect,ImGui::GetID("volMeter"))) { - ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); - for (int i=0; i<2; i++) { - peak[i]*=0.95; - if (peak[i]<0.0001) peak[i]=0.0; - for (int j=0; joscSize; j++) { - if (fabs(e->oscBuf[i][j])>peak[i]) { - peak[i]=fabs(e->oscBuf[i][j]); - } - } - float logPeak=(20*log10(peak[i])/36.0); - if (logPeak==NAN) logPeak=0.0; - if (logPeak<-1.0) logPeak=-1.0; - if (logPeak>0.0) { - isClipping=8; - logPeak=0.0; - } - logPeak+=1.0; - ImU32 highColor=ImGui::GetColorU32( - ImLerp(uiColors[GUI_COLOR_VOLMETER_LOW],uiColors[GUI_COLOR_VOLMETER_HIGH],logPeak) - ); - ImRect s; - if (aspectRatio) { - s=ImRect( - ImLerp(rect.Min,rect.Max,ImVec2(0,float(i)*0.5)), - ImLerp(rect.Min,rect.Max,ImVec2(logPeak,float(i+1)*0.5)) - ); - if (i==0) s.Max.y-=dpiScale; - if (isClipping) { - dl->AddRectFilled(s.Min,s.Max,ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_PEAK])); - } else { - dl->AddRectFilledMultiColor(s.Min,s.Max,lowColor,highColor,highColor,lowColor); - } - } else { - s=ImRect( - ImLerp(rect.Min,rect.Max,ImVec2(float(i)*0.5,1.0-logPeak)), - ImLerp(rect.Min,rect.Max,ImVec2(float(i+1)*0.5,1.0)) - ); - if (i==0) s.Max.x-=dpiScale; - if (isClipping) { - dl->AddRectFilled(s.Min,s.Max,ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_PEAK])); - } else { - dl->AddRectFilledMultiColor(s.Min,s.Max,highColor,highColor,lowColor,lowColor); - } - } - } - if (ImGui::IsItemHovered()) { - if (aspectRatio) { - ImGui::SetTooltip("%.1fdB",36*((ImGui::GetMousePos().x-ImGui::GetItemRectMin().x)/(rect.Max.x-rect.Min.x)-1.0)); - } else { - ImGui::SetTooltip("%.1fdB",-(36+36*((ImGui::GetMousePos().y-ImGui::GetItemRectMin().y)/(rect.Max.y-rect.Min.y)-1.0))); - } - } - } - } - ImGui::PopStyleVar(4); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_VOL_METER; - ImGui::End(); -} - -const char* aboutLine[]={ - "tildearrow", - "is proud to present", - "", - ("Furnace " DIV_VERSION), - "", - "the free software chiptune tracker,", - "compatible with DefleMask modules.", - "", - "zero disassembly.", - "just clean-room design,", - "time and dedication.", - "", - "> CREDITS <", - "", - "-- program --", - "tildearrow", - "laoo", - "superctr", - "", - "-- graphics/UI design --", - "tildearrow", - "BlastBrothers", - "", - "-- documentation --", - "tildearrow", - "freq-mod", - "nicco1690", - "DeMOSic", - "cam900", - "", - "-- demo songs --", - "0x5066", - "breakthetargets", - "kleeder", - "nicco1690", - "NikonTeen", - "SuperJet Spade", - "TheDuccinator", - "tildearrow", - "Ultraprogramer", - "", - "-- additional feedback/fixes --", - "fd", - "OPNA2608", - "plane", - "TheEssem", - "", - "powered by:", - "Dear ImGui by Omar Cornut", - "SDL2 by Sam Lantinga", - "zlib by Jean-loup Gailly", - "and Mark Adler", - "libsndfile by Erik de Castro Lopo", - "Nuked-OPM & Nuked-OPN2 by Nuke.YKT", - "ymfm by Aaron Giles", - "MAME SN76496 by Nicola Salmoria", - "MAME AY-3-8910 by Couriersud", - "with AY8930 fixes by Eulous", - "MAME SAA1099 by Juergen Buchmueller and Manuel Abadia", - "SAASound", - "SameBoy by Lior Halphon", - "Mednafen PCE", - "puNES by FHorse", - "reSID by Dag Lem", - "Stella by Stella Team", - "QSound emulator by Ian Karlsson and Valley Bell", - "", - "greetings to:", - "Delek", - "fd", - "ILLUMIDARO", - "all members of Deflers of Noice!", - "", - "copyright © 2021-2022 tildearrow", - "(and contributors).", - "licensed under GPLv2+! see", - "LICENSE for more information.", - "", - "help Furnace grow:", - "https://github.com/tildearrow/furnace", - "", - "contact tildearrow at:", - "https://tildearrow.org/?p=contact", - "", - "disclaimer:", - "despite the fact this program works", - "with the .dmf file format, it is NOT", - "affiliated with Delek or DefleMask in", - "any way, nor it is a replacement for", - "the original program.", - "", - "it also comes with ABSOLUTELY NO WARRANTY.", - "", - "look out for Furnace 0.6 coming somewhere", - "before the equinox with more systems", - "and plenty of other things...", - "", - "thanks to all contributors/bug reporters!" -}; - -const size_t aboutCount = sizeof(aboutLine)/sizeof(aboutLine[0]); - -void FurnaceGUI::drawAbout() { - // do stuff - if (ImGui::Begin("About Furnace",NULL,ImGuiWindowFlags_Modal|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoDocking|ImGuiWindowFlags_NoTitleBar)) { - ImGui::SetWindowPos(ImVec2(0,0)); - ImGui::SetWindowSize(ImVec2(scrW*dpiScale,scrH*dpiScale)); - ImGui::PushFont(bigFont); - ImDrawList* dl=ImGui::GetWindowDrawList(); - float r=0; - float g=0; - float b=0; - float peakMix=settings.partyTime?((peak[0]+peak[1])*0.5):0.3; - ImGui::ColorConvertHSVtoRGB(aboutHue,1.0,0.25+MIN(0.75f,peakMix*0.75f),r,g,b); - dl->AddRectFilled(ImVec2(0,0),ImVec2(scrW*dpiScale,scrH*dpiScale),0xff000000); - bool skip=false; - bool skip2=false; - for (int i=(-80-sin(double(aboutSin)*2*M_PI/120.0)*80.0)*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.25,g*0.25,b*0.25,1.0))); - } - } - - skip=false; - skip2=false; - for (int i=(-80-cos(double(aboutSin)*2*M_PI/120.0)*80.0)*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.5,g*0.5,b*0.5,1.0))); - } - } - - skip=false; - skip2=false; - for (int i=(-160+fmod(aboutSin*2,160))*2; iAddRectFilled(ImVec2(i*dpiScale,j*dpiScale),ImVec2((i+160)*dpiScale,(j+160)*dpiScale),ImGui::GetColorU32(ImVec4(r*0.75,g*0.75,b*0.75,1.0))); - } - } - - for (size_t i=0; iscrH*dpiScale) continue; - dl->AddText(bigFont,bigFont->FontSize, - ImVec2(posX+dpiScale,posY+dpiScale), - 0xff000000,aboutLine[i]); - dl->AddText(bigFont,bigFont->FontSize, - ImVec2(posX+dpiScale,posY-dpiScale), - 0xff000000,aboutLine[i]); - dl->AddText(bigFont,bigFont->FontSize, - ImVec2(posX-dpiScale,posY+dpiScale), - 0xff000000,aboutLine[i]); - dl->AddText(bigFont,bigFont->FontSize, - ImVec2(posX-dpiScale,posY-dpiScale), - 0xff000000,aboutLine[i]); - dl->AddText(bigFont,bigFont->FontSize, - ImVec2(posX,posY), - 0xffffffff,aboutLine[i]); - } - ImGui::PopFont(); - - float timeScale=60.0f*ImGui::GetIO().DeltaTime; - - aboutHue+=(0.001+peakMix*0.004)*timeScale; - aboutScroll+=(2+(peakMix>0.78)*3)*timeScale; - aboutSin+=(1+(peakMix>0.75)*2)*timeScale; - - while (aboutHue>1) aboutHue--; - while (aboutSin>=2400) aboutSin-=2400; - if (aboutScroll>(42*aboutCount+scrH)) aboutScroll=-20; - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_ABOUT; - ImGui::End(); -} - -void FurnaceGUI::drawDebug() { - static int bpOrder; - static int bpRow; - static int bpTick; - static bool bpOn; - if (nextWindow==GUI_WINDOW_DEBUG) { - debugOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!debugOpen) return; - ImGui::SetNextWindowSizeConstraints(ImVec2(400.0f*dpiScale,200.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - if (ImGui::Begin("Debug",&debugOpen,ImGuiWindowFlags_NoDocking)) { - ImGui::Text("NOTE: use with caution."); - if (ImGui::TreeNode("Debug Controls")) { - if (e->isHalted()) { - if (ImGui::Button("Resume")) e->resume(); - } else { - if (ImGui::Button("Pause")) e->halt(); - } - ImGui::SameLine(); - if (ImGui::Button("Frame Advance")) e->haltWhen(DIV_HALT_TICK); - ImGui::SameLine(); - if (ImGui::Button("Row Advance")) e->haltWhen(DIV_HALT_ROW); - ImGui::SameLine(); - if (ImGui::Button("Pattern Advance")) e->haltWhen(DIV_HALT_PATTERN); - - if (ImGui::Button("Panic")) e->syncReset(); - ImGui::SameLine(); - if (ImGui::Button("Abort")) { - abort(); - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Breakpoint")) { - ImGui::InputInt("Order",&bpOrder); - ImGui::InputInt("Row",&bpRow); - ImGui::InputInt("Tick",&bpTick); - ImGui::Checkbox("Enable",&bpOn); - ImGui::TreePop(); - } - if (ImGui::TreeNode("Dispatch Status")) { - ImGui::Text("for best results set latency to minimum or use the Frame Advance button."); - ImGui::Columns(e->getTotalChannelCount()); - for (int i=0; igetTotalChannelCount(); i++) { - void* ch=e->getDispatchChanState(i); - ImGui::TextColored(uiColors[GUI_COLOR_ACCENT_PRIMARY],"Ch. %d: %d, %d",i,e->dispatchOfChan[i],e->dispatchChanOfChan[i]); - if (ch==NULL) { - ImGui::Text("NULL"); - } else { - putDispatchChan(ch,e->dispatchChanOfChan[i],e->sysOfChan[i]); - } - ImGui::NextColumn(); - } - ImGui::Columns(); - ImGui::TreePop(); - } - if (ImGui::TreeNode("Playback Status")) { - ImGui::Text("for best results set latency to minimum or use the Frame Advance button."); - ImGui::Columns(e->getTotalChannelCount()); - for (int i=0; igetTotalChannelCount(); i++) { - DivChannelState* ch=e->getChanState(i); - ImGui::TextColored(uiColors[GUI_COLOR_ACCENT_PRIMARY],"Channel %d:",i); - if (ch==NULL) { - ImGui::Text("NULL"); - } else { - ImGui::Text("* General:"); - ImGui::Text("- note = %d",ch->note); - ImGui::Text("- oldNote = %d",ch->oldNote); - ImGui::Text("- pitch = %d",ch->pitch); - ImGui::Text("- portaSpeed = %d",ch->portaSpeed); - ImGui::Text("- portaNote = %d",ch->portaNote); - ImGui::Text("- volume = %.4x",ch->volume); - ImGui::Text("- volSpeed = %d",ch->volSpeed); - ImGui::Text("- cut = %d",ch->cut); - ImGui::Text("- rowDelay = %d",ch->rowDelay); - ImGui::Text("- volMax = %.4x",ch->volMax); - ImGui::Text("- delayOrder = %d",ch->delayOrder); - ImGui::Text("- delayRow = %d",ch->delayRow); - ImGui::Text("- retrigSpeed = %d",ch->retrigSpeed); - ImGui::Text("- retrigTick = %d",ch->retrigTick); - ImGui::PushStyleColor(ImGuiCol_Text,(ch->vibratoDepth>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); - ImGui::Text("* Vibrato:"); - ImGui::Text("- depth = %d",ch->vibratoDepth); - ImGui::Text("- rate = %d",ch->vibratoRate); - ImGui::Text("- pos = %d",ch->vibratoPos); - ImGui::Text("- dir = %d",ch->vibratoDir); - ImGui::Text("- fine = %d",ch->vibratoFine); - ImGui::PopStyleColor(); - ImGui::PushStyleColor(ImGuiCol_Text,(ch->tremoloDepth>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); - ImGui::Text("* Tremolo:"); - ImGui::Text("- depth = %d",ch->tremoloDepth); - ImGui::Text("- rate = %d",ch->tremoloRate); - ImGui::Text("- pos = %d",ch->tremoloPos); - ImGui::PopStyleColor(); - ImGui::PushStyleColor(ImGuiCol_Text,(ch->arp>0)?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_TEXT]); - ImGui::Text("* Arpeggio:"); - ImGui::Text("- arp = %.2X",ch->arp); - ImGui::Text("- stage = %d",ch->arpStage); - ImGui::Text("- ticks = %d",ch->arpTicks); - ImGui::PopStyleColor(); - ImGui::Text("* Miscellaneous:"); - ImGui::TextColored(ch->doNote?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Do Note"); - ImGui::TextColored(ch->legato?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Legato"); - ImGui::TextColored(ch->portaStop?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> PortaStop"); - ImGui::TextColored(ch->keyOn?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Key On"); - ImGui::TextColored(ch->keyOff?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Key Off"); - ImGui::TextColored(ch->nowYouCanStop?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> NowYouCanStop"); - ImGui::TextColored(ch->stopOnOff?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Stop on Off"); - ImGui::TextColored(ch->arpYield?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> Arp Yield"); - ImGui::TextColored(ch->delayLocked?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> DelayLocked"); - ImGui::TextColored(ch->inPorta?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> InPorta"); - ImGui::TextColored(ch->scheduledSlideReset?uiColors[GUI_COLOR_MACRO_VOLUME]:uiColors[GUI_COLOR_HEADER],">> SchedSlide"); - } - ImGui::NextColumn(); - } - ImGui::Columns(); - 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())) { - 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; - break; - } - } - ImGui::EndCombo(); - } - ImGui::Text("Program"); - if (pgProgram.empty()) { - ImGui::Text("-nothing here-"); - } else { - char id[32]; - for (size_t index=0; indexpoke(pgSys,pgProgram); - } - ImGui::SameLine(); - if (ImGui::Button("Clear")) { - pgProgram.clear(); - } - - ImGui::Text("Address"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(100.0f*dpiScale); - ImGui::InputInt("##PAddress",&pgAddr,0,0,ImGuiInputTextFlags_CharsHexadecimal); - ImGui::SameLine(); - ImGui::Text("Value"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(100.0f*dpiScale); - ImGui::InputInt("##PValue",&pgVal,0,0,ImGuiInputTextFlags_CharsHexadecimal); - ImGui::SameLine(); - if (ImGui::Button("Write")) { - e->poke(pgSys,pgAddr,pgVal); - } - ImGui::SameLine(); - if (ImGui::Button("Add")) { - pgProgram.push_back(DivRegWrite(pgAddr,pgVal)); - } - if (ImGui::TreeNode("Register Cheatsheet")) { - const char** sheet=e->getRegisterSheet(pgSys); - if (sheet==NULL) { - ImGui::Text("no cheatsheet available for this system."); - } else { - if (ImGui::BeginTable("RegisterSheet",2,ImGuiTableFlags_SizingFixedSame)) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("Name"); - ImGui::TableNextColumn(); - ImGui::Text("Address"); - for (int i=0; sheet[i]!=NULL; i+=2) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("%s",sheet[i]); - ImGui::TableNextColumn(); - ImGui::Text("$%s",sheet[i+1]); - } - ImGui::EndTable(); - } - } - ImGui::TreePop(); - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Settings")) { - if (ImGui::Button("Sync")) syncSettings(); - ImGui::SameLine(); - if (ImGui::Button("Commit")) commitSettings(); - ImGui::SameLine(); - if (ImGui::Button("Force Load")) e->loadConf(); - ImGui::SameLine(); - if (ImGui::Button("Force Save")) e->saveConf(); - ImGui::TreePop(); - } - ImGui::Text("Song format version %d",e->song.version); - ImGui::Text("Furnace version " DIV_VERSION " (%d)",DIV_ENGINE_VERSION); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_DEBUG; - ImGui::End(); -} - -void FurnaceGUI::drawStats() { - if (nextWindow==GUI_WINDOW_STATS) { - statsOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!statsOpen) return; - if (ImGui::Begin("Statistics",&statsOpen)) { - String adpcmAUsage=fmt::sprintf("%d/16384KB",e->adpcmAMemLen/1024); - String adpcmBUsage=fmt::sprintf("%d/16384KB",e->adpcmBMemLen/1024); - String qsoundUsage=fmt::sprintf("%d/16384KB",e->qsoundMemLen/1024); - ImGui::Text("ADPCM-A"); - ImGui::SameLine(); - ImGui::ProgressBar(((float)e->adpcmAMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmAUsage.c_str()); - ImGui::Text("ADPCM-B"); - ImGui::SameLine(); - ImGui::ProgressBar(((float)e->adpcmBMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmBUsage.c_str()); - ImGui::Text("QSound"); - ImGui::SameLine(); - ImGui::ProgressBar(((float)e->qsoundMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),qsoundUsage.c_str()); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_STATS; - ImGui::End(); -} - -void FurnaceGUI::drawCompatFlags() { - if (nextWindow==GUI_WINDOW_COMPAT_FLAGS) { - compatFlagsOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!compatFlagsOpen) return; - if (ImGui::Begin("Compatibility Flags",&compatFlagsOpen)) { - ImGui::TextWrapped("these flags are stored in the song when saving in .fur format, and are automatically enabled when saving in .dmf format."); - ImGui::Checkbox("Limit slide range",&e->song.limitSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("when enabled, slides are limited to a compatible range.\nmay cause problems with slides in negative octaves."); - } - ImGui::Checkbox("Linear pitch control",&e->song.linearPitch); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("linear pitch:\n- slides work in frequency/period space\n- E5xx and 04xx effects work in tonality space\nnon-linear pitch:\n- slides work in frequency/period space\n- E5xx and 04xx effects work on frequency/period space"); - } - ImGui::Checkbox("Proper noise layout on NES and PC Engine",&e->song.properNoiseLayout); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a proper noise channel note mapping (0-15) instead of a rather unusual compatible one.\nunlocks all noise frequencies on PC Engine."); - } - ImGui::Checkbox("Game Boy instrument duty is wave volume",&e->song.waveDutyIsVol); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("if enabled, an instrument with duty macro in the wave channel will be mapped to wavetable volume."); - } - - ImGui::Checkbox("Restart macro on portamento",&e->song.resetMacroOnPorta); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("when enabled, a portamento effect will reset the channel's macro if used in combination with a note."); - } - ImGui::Checkbox("Legacy volume slides",&e->song.legacyVolumeSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("simulate glitchy volume slide behavior by silently overflowing the volume when the slide goes below 0."); - } - ImGui::Checkbox("Compatible arpeggio",&e->song.compatibleArpeggio); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("delay arpeggio by one tick on every new note."); - } - ImGui::Checkbox("Reset slides after note off",&e->song.noteOffResetsSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("when enabled, note off will reset the channel's slide effect."); - } - ImGui::Checkbox("Reset portamento after reaching target",&e->song.targetResetsSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("when enabled, the slide effect is disabled after it reaches its target."); - } - ImGui::Checkbox("Ignore duplicate slide effects",&e->song.ignoreDuplicateSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("if this is on, only the first slide of a row in a channel will be considered."); - } - - ImGui::Text("Loop modality:"); - if (ImGui::RadioButton("Reset channels",e->song.loopModality==0)) { - e->song.loopModality=0; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("select to reset channels on loop. may trigger a voltage click on every loop!"); - } - if (ImGui::RadioButton("Soft reset channels",e->song.loopModality==1)) { - e->song.loopModality=1; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("select to turn channels off on loop."); - } - if (ImGui::RadioButton("Do nothing",e->song.loopModality==2)) { - e->song.loopModality=2; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("select to not reset channels on loop."); - } - - ImGui::Separator(); - - ImGui::TextWrapped("the following flags are for compatibility with older Furnace versions."); - - ImGui::Checkbox("Arpeggio inhibits non-porta slides",&e->song.arpNonPorta); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("behavior changed in 0.5.5"); - } - ImGui::Checkbox("Wack FM algorithm macro",&e->song.algMacroBehavior); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("behavior changed in 0.5.5"); - } - ImGui::Checkbox("Broken shortcut slides (E1xy/E2xy)",&e->song.brokenShortcutSlides); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("behavior changed in 0.5.7"); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_COMPAT_FLAGS; - ImGui::End(); -} - -void FurnaceGUI::drawPiano() { - if (nextWindow==GUI_WINDOW_PIANO) { - pianoOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!pianoOpen) return; - if (ImGui::Begin("Piano",&pianoOpen)) { - for (int i=0; igetTotalChannelCount(); i++) { - DivChannelState* cs=e->getChanState(i); - if (cs->keyOn) { - const char* noteName=NULL; - if (cs->note<-60 || cs->note>120) { - noteName="???"; - } else { - noteName=noteNames[cs->note+60]; - } - ImGui::Text("%d: %s",i,noteName); - } - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_PIANO; - ImGui::End(); -} - -// NOTE: please don't ask me to enable text wrap. -// Dear ImGui doesn't have that feature. D: -void FurnaceGUI::drawNotes() { - if (nextWindow==GUI_WINDOW_NOTES) { - notesOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!notesOpen) return; - if (ImGui::Begin("Song Comments",¬esOpen)) { - ImGui::InputTextMultiline("##SongNotes",&e->song.notes,ImGui::GetContentRegionAvail()); - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_NOTES; - ImGui::End(); -} - -void FurnaceGUI::drawChannels() { - if (nextWindow==GUI_WINDOW_CHANNELS) { - channelsOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!channelsOpen) return; - if (ImGui::Begin("Channels",&channelsOpen)) { - if (ImGui::BeginTable("ChannelList",3)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,48.0f*dpiScale); - for (int i=0; igetTotalChannelCount(); i++) { - ImGui::PushID(i); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Checkbox("##Visible",&e->song.chanShow[i]); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputTextWithHint("##ChanName",e->getChannelName(i),&e->song.chanName[i]); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputTextWithHint("##ChanShortName",e->getChannelShortName(i),&e->song.chanShortName[i]); - ImGui::PopID(); - } - ImGui::EndTable(); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_CHANNELS; - ImGui::End(); -} - -void FurnaceGUI::drawRegView() { - if (nextWindow==GUI_WINDOW_REGISTER_VIEW) { - channelsOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!regViewOpen) return; - if (ImGui::Begin("Register View",®ViewOpen)) { - for (int i=0; isong.systemLen; i++) { - ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i])); - int size=0; - int depth=8; - unsigned char* regPool=e->getRegisterPool(i,size,depth); - unsigned short* regPoolW=(unsigned short*)regPool; - if (regPool==NULL) { - ImGui::Text("- no register pool available"); - } else { - ImGui::PushFont(patFont); - if (ImGui::BeginTable("Memory",17)) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - for (int i=0; i<16; i++) { - ImGui::TableNextColumn(); - ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX]," %X",i); - } - for (int i=0; i<=((size-1)>>4); i++) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX],"%.2X",i*16); - for (int j=0; j<16; j++) { - ImGui::TableNextColumn(); - if (i*16+j>=size) continue; - if (depth == 8) { - ImGui::Text("%.2x",regPool[i*16+j]); - } else if (depth == 16) { - ImGui::Text("%.4x",regPoolW[i*16+j]); - } else { - ImGui::Text("??"); - } - } - } - ImGui::EndTable(); - } - ImGui::PopFont(); - } - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REGISTER_VIEW; - ImGui::End(); -} - -void FurnaceGUI::startSelection(int xCoarse, int xFine, int y) { - if (xCoarse!=selStart.xCoarse || xFine!=selStart.xFine || y!=selStart.y) { - curNibble=false; - } - cursor.xCoarse=xCoarse; - cursor.xFine=xFine; - cursor.y=y; - selStart.xCoarse=xCoarse; - selStart.xFine=xFine; - selStart.y=y; - selEnd.xCoarse=xCoarse; - selEnd.xFine=xFine; - selEnd.y=y; - selecting=true; -} - -void FurnaceGUI::updateSelection(int xCoarse, int xFine, int y) { - if (!selecting) return; - selEnd.xCoarse=xCoarse; - selEnd.xFine=xFine; - selEnd.y=y; -} - -void FurnaceGUI::finishSelection() { - // swap points if needed - if (selEnd.ygetTotalChannelCount(); - - if (selStart.xCoarse<0) selStart.xCoarse=0; - if (selStart.xCoarse>=chanCount) selStart.xCoarse=chanCount-1; - if (selStart.y<0) selStart.y=0; - if (selStart.y>=e->song.patLen) selStart.y=e->song.patLen-1; - if (selEnd.xCoarse<0) selEnd.xCoarse=0; - if (selEnd.xCoarse>=chanCount) selEnd.xCoarse=chanCount-1; - if (selEnd.y<0) selEnd.y=0; - if (selEnd.y>=e->song.patLen) selEnd.y=e->song.patLen-1; - if (cursor.xCoarse<0) cursor.xCoarse=0; - if (cursor.xCoarse>=chanCount) cursor.xCoarse=chanCount-1; - if (cursor.y<0) cursor.y=0; - if (cursor.y>=e->song.patLen) cursor.y=e->song.patLen-1; - - if (e->song.chanCollapse[selEnd.xCoarse]) { - selStart.xFine=0; - } - if (e->song.chanCollapse[selEnd.xCoarse]) { - selEnd.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; - } -} - -#define DETERMINE_FIRST \ - int firstChannel=0; \ - for (int i=0; igetTotalChannelCount(); i++) { \ - if (e->song.chanShow[i]) { \ - firstChannel=i; \ - break; \ - } \ - } \ - -#define DETERMINE_LAST \ - int lastChannel=0; \ - for (int i=e->getTotalChannelCount()-1; i>=0; i--) { \ - if (e->song.chanShow[i]) { \ - lastChannel=i+1; \ - break; \ - } \ - } - -#define DETERMINE_FIRST_LAST \ - DETERMINE_FIRST \ - DETERMINE_LAST - -void FurnaceGUI::moveCursor(int x, int y, bool select) { - if (!select) { - finishSelection(); - } - - DETERMINE_FIRST_LAST; - - curNibble=false; - if (x!=0) { - demandScrollX=true; - if (x>0) { - for (int i=0; i=(e->song.chanCollapse[cursor.xCoarse]?1:(3+e->song.pat[cursor.xCoarse].effectRows*2))) { - cursor.xFine=0; - if (++cursor.xCoarse>=lastChannel) { - if (settings.wrapHorizontal!=0 && !select) { - cursor.xCoarse=firstChannel; - if (settings.wrapHorizontal==2) y++; - } else { - cursor.xCoarse=lastChannel-1; - cursor.xFine=e->song.chanCollapse[cursor.xCoarse]?0:(2+e->song.pat[cursor.xCoarse].effectRows*2); - } - } else { - while (!e->song.chanShow[cursor.xCoarse]) { - cursor.xCoarse++; - if (cursor.xCoarse>=e->getTotalChannelCount()) break; - } - } - } - } - } else { - for (int i=0; i<-x; i++) { - if (--cursor.xFine<0) { - if (--cursor.xCoarsesong.pat[cursor.xCoarse].effectRows*2; - if (settings.wrapHorizontal==2) y--; - } else { - cursor.xCoarse=firstChannel; - cursor.xFine=0; - } - } else { - while (!e->song.chanShow[cursor.xCoarse]) { - cursor.xCoarse--; - if (cursor.xCoarse<0) break; - } - if (e->song.chanCollapse[cursor.xCoarse]) { - cursor.xFine=0; - } else { - cursor.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; - } - } - } - } - } - } - if (y!=0) { - if (y>0) { - for (int i=0; i=e->song.patLen) { - if (settings.wrapVertical!=0 && !select) { - cursor.y=0; - if (settings.wrapVertical==2) { - if (!e->isPlaying() && e->getOrder()<(e->song.ordersLen-1)) { - e->setOrder(e->getOrder()+1); - } else { - cursor.y=e->song.patLen-1; - } - } - } else { - cursor.y=e->song.patLen-1; - } - } - } - } else { - for (int i=0; i<-y; i++) { - cursor.y--; - if (cursor.y<0) { - if (settings.wrapVertical!=0 && !select) { - cursor.y=e->song.patLen-1; - if (settings.wrapVertical==2) { - if (!e->isPlaying() && e->getOrder()>0) { - e->setOrder(e->getOrder()-1); - } else { - cursor.y=0; - } - } - } else { - cursor.y=0; - } - } - } - } - } - if (!select) { - selStart=cursor; - } - selEnd=cursor; - updateScroll(cursor.y); -} - -void FurnaceGUI::moveCursorPrevChannel(bool overflow) { - finishSelection(); - curNibble=false; - - DETERMINE_FIRST_LAST; - - do { - cursor.xCoarse--; - if (cursor.xCoarse<0) break; - } while (!e->song.chanShow[cursor.xCoarse]); - if (cursor.xCoarse=e->getTotalChannelCount()) break; - } while (!e->song.chanShow[cursor.xCoarse]); - if (cursor.xCoarse>=lastChannel) { - if (overflow) { - cursor.xCoarse=firstChannel; - } else { - cursor.xCoarse=lastChannel-1; - } - } - - selStart=cursor; - selEnd=cursor; -} - -void FurnaceGUI::moveCursorTop(bool select) { - finishSelection(); - curNibble=false; - if (cursor.y==0) { - DETERMINE_FIRST; - cursor.xCoarse=firstChannel; - cursor.xFine=0; - } else { - cursor.y=0; - } - selStart=cursor; - if (!select) { - selEnd=cursor; - } - updateScroll(cursor.y); -} - -void FurnaceGUI::moveCursorBottom(bool select) { - finishSelection(); - curNibble=false; - if (cursor.y==e->song.patLen-1) { - DETERMINE_LAST; - cursor.xCoarse=lastChannel-1; - cursor.xFine=2+e->song.pat[cursor.xCoarse].effectRows*2; - } else { - cursor.y=e->song.patLen-1; - } - if (!select) { - selStart=cursor; - } - selEnd=cursor; - updateScroll(cursor.y); -} - -void FurnaceGUI::editAdvance() { - finishSelection(); - cursor.y+=editStep; - if (cursor.y>=e->song.patLen) cursor.y=e->song.patLen-1; - selStart=cursor; - selEnd=cursor; - updateScroll(cursor.y); -} - -void FurnaceGUI::prepareUndo(ActionType action) { - int order=e->getOrder(); - switch (action) { - case GUI_UNDO_CHANGE_ORDER: - oldOrders=e->song.orders; - oldOrdersLen=e->song.ordersLen; - break; - case GUI_UNDO_PATTERN_EDIT: - case GUI_UNDO_PATTERN_DELETE: - case GUI_UNDO_PATTERN_PULL: - case GUI_UNDO_PATTERN_PUSH: - case GUI_UNDO_PATTERN_CUT: - case GUI_UNDO_PATTERN_PASTE: - for (int i=0; igetTotalChannelCount(); i++) { - e->song.pat[i].getPattern(e->song.orders.ord[i][order],false)->copyOn(oldPat[i]); - } - break; - } -} - -void FurnaceGUI::makeUndo(ActionType action) { - bool doPush=false; - UndoStep s; - s.type=action; - s.cursor=cursor; - s.selStart=selStart; - s.selEnd=selEnd; - int order=e->getOrder(); - s.order=order; - s.nibble=curNibble; - switch (action) { - case GUI_UNDO_CHANGE_ORDER: - for (int i=0; isong.orders.ord[i][j]) { - s.ord.push_back(UndoOrderData(i,j,oldOrders.ord[i][j],e->song.orders.ord[i][j])); - } - } - } - s.oldOrdersLen=oldOrdersLen; - s.newOrdersLen=e->song.ordersLen; - if (oldOrdersLen!=e->song.ordersLen) { - doPush=true; - } - if (!s.ord.empty()) { - doPush=true; - } - break; - case GUI_UNDO_PATTERN_EDIT: - case GUI_UNDO_PATTERN_DELETE: - case GUI_UNDO_PATTERN_PULL: - case GUI_UNDO_PATTERN_PUSH: - case GUI_UNDO_PATTERN_CUT: - case GUI_UNDO_PATTERN_PASTE: - for (int i=0; igetTotalChannelCount(); i++) { - DivPattern* p=e->song.pat[i].getPattern(e->song.orders.ord[i][order],false); - for (int j=0; jsong.patLen; j++) { - for (int k=0; k<32; k++) { - if (p->data[j][k]!=oldPat[i]->data[j][k]) { - s.pat.push_back(UndoPatternData(i,e->song.orders.ord[i][order],j,k,oldPat[i]->data[j][k],p->data[j][k])); - } - } - } - } - if (!s.pat.empty()) { - doPush=true; - } - break; - } - if (doPush) { - modified=true; - undoHist.push_back(s); - redoHist.clear(); - if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); - } -} - -void FurnaceGUI::doSelectAll() { - finishSelection(); - curNibble=false; - if (selStart.xFine==0 && selEnd.xFine==2+e->song.pat[selEnd.xCoarse].effectRows*2) { - if (selStart.y==0 && selEnd.y==e->song.patLen-1) { // select entire pattern - selStart.xCoarse=0; - selStart.xFine=0; - selEnd.xCoarse=e->getTotalChannelCount()-1; - selEnd.xFine=2+e->song.pat[selEnd.xCoarse].effectRows*2; - } else { // select entire column - selStart.y=0; - selEnd.y=e->song.patLen-1; - } - } else { - int selStartX=0; - int selEndX=0; - // find row position - for (SelectionPoint i; i.xCoarse!=selStart.xCoarse || i.xFine!=selStart.xFine; selStartX++) { - i.xFine++; - if (i.xFine>=3+e->song.pat[i.xCoarse].effectRows*2) { - i.xFine=0; - i.xCoarse++; - } - } - for (SelectionPoint i; i.xCoarse!=selEnd.xCoarse || i.xFine!=selEnd.xFine; selEndX++) { - i.xFine++; - if (i.xFine>=3+e->song.pat[i.xCoarse].effectRows*2) { - i.xFine=0; - i.xCoarse++; - } - } - - float aspect=float(selEndX-selStartX+1)/float(selEnd.y-selStart.y+1); - if (aspect<1.0f && !(selStart.y==0 && selEnd.y==e->song.patLen-1)) { // up-down - selStart.y=0; - selEnd.y=e->song.patLen-1; - } else { // left-right - selStart.xFine=0; - selEnd.xFine=2+e->song.pat[selEnd.xCoarse].effectRows*2; - } - } -} - -void FurnaceGUI::doDelete() { - finishSelection(); - prepareUndo(GUI_UNDO_PATTERN_DELETE); - curNibble=false; - - int iCoarse=selStart.xCoarse; - int iFine=selStart.xFine; - int ord=e->getOrder(); - for (; iCoarse<=selEnd.xCoarse; iCoarse++) { - if (!e->song.chanShow[iCoarse]) continue; - DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][iFine]=0; - if (selStart.y==selEnd.y) pat->data[j][2]=-1; - } - pat->data[j][iFine+1]=(iFine<1)?0:-1; - } - } - iFine=0; - } - - makeUndo(GUI_UNDO_PATTERN_DELETE); -} - -void FurnaceGUI::doPullDelete() { - finishSelection(); - prepareUndo(GUI_UNDO_PATTERN_PULL); - curNibble=false; - - if (settings.pullDeleteBehavior) { - if (--selStart.y<0) selStart.y=0; - if (--selEnd.y<0) selEnd.y=0; - if (--cursor.y<0) cursor.y=0; - updateScroll(cursor.y); - } - - int iCoarse=selStart.xCoarse; - int iFine=selStart.xFine; - int ord=e->getOrder(); - for (; iCoarse<=selEnd.xCoarse; iCoarse++) { - if (!e->song.chanShow[iCoarse]) continue; - DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen; j++) { - if (jsong.patLen-1) { - if (iFine==0) { - pat->data[j][iFine]=pat->data[j+1][iFine]; - } - pat->data[j][iFine+1]=pat->data[j+1][iFine+1]; - } else { - if (iFine==0) { - pat->data[j][iFine]=0; - } - pat->data[j][iFine+1]=(iFine<1)?0:-1; - } - } - } - iFine=0; - } - - makeUndo(GUI_UNDO_PATTERN_PULL); -} - -void FurnaceGUI::doInsert() { - finishSelection(); - prepareUndo(GUI_UNDO_PATTERN_PUSH); - curNibble=false; - - int iCoarse=selStart.xCoarse; - int iFine=selStart.xFine; - int ord=e->getOrder(); - for (; iCoarse<=selEnd.xCoarse; iCoarse++) { - if (!e->song.chanShow[iCoarse]) continue; - DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsesong.patLen-1; j>=selStart.y; j--) { - if (j==selStart.y) { - if (iFine==0) { - pat->data[j][iFine]=0; - } - pat->data[j][iFine+1]=(iFine<1)?0:-1; - } else { - if (iFine==0) { - pat->data[j][iFine]=pat->data[j-1][iFine]; - } - pat->data[j][iFine+1]=pat->data[j-1][iFine+1]; - } - } - } - iFine=0; - } - - makeUndo(GUI_UNDO_PATTERN_PUSH); -} - -void FurnaceGUI::doTranspose(int amount) { - finishSelection(); - prepareUndo(GUI_UNDO_PATTERN_DELETE); - curNibble=false; - - int iCoarse=selStart.xCoarse; - int iFine=selStart.xFine; - int ord=e->getOrder(); - for (; iCoarse<=selEnd.xCoarse; iCoarse++) { - if (!e->song.chanShow[iCoarse]) continue; - DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0]; - int origOctave=(signed char)pat->data[j][1]; - if (origNote!=0 && origNote!=100 && origNote!=101 && origNote!=102) { - origNote+=amount; - while (origNote>12) { - origNote-=12; - origOctave++; - } - while (origNote<1) { - origNote+=12; - origOctave--; - } - if (origOctave>7) { - origNote=12; - origOctave=7; - } - if (origOctave<-5) { - origNote=1; - origOctave=-5; - } - pat->data[j][0]=origNote; - pat->data[j][1]=(unsigned char)origOctave; - } - } - } - } - iFine=0; - } - - makeUndo(GUI_UNDO_PATTERN_DELETE); -} - -void FurnaceGUI::doCopy(bool cut) { - finishSelection(); - if (cut) { - curNibble=false; - prepareUndo(GUI_UNDO_PATTERN_CUT); - } - clipboard=fmt::sprintf("org.tildearrow.furnace - Pattern Data (%d)\n%d",DIV_ENGINE_VERSION,selStart.xFine); - - for (int j=selStart.y; j<=selEnd.y; j++) { - int iCoarse=selStart.xCoarse; - int iFine=selStart.xFine; - if (iFine>3 && !(iFine&1)) { - iFine--; - } - int ord=e->getOrder(); - clipboard+='\n'; - for (; iCoarse<=selEnd.xCoarse; iCoarse++) { - if (!e->song.chanShow[iCoarse]) continue; - DivPattern* pat=e->song.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - for (; iFine<3+e->song.pat[iCoarse].effectRows*2 && (iCoarsedata[j][0],pat->data[j][1]); - if (cut) { - pat->data[j][0]=0; - pat->data[j][1]=0; - } - } else { - if (pat->data[j][iFine+1]==-1) { - clipboard+=".."; - } else { - clipboard+=fmt::sprintf("%.2X",pat->data[j][iFine+1]); - } - if (cut) { - pat->data[j][iFine+1]=-1; - } - } - } - clipboard+='|'; - iFine=0; - } - } - SDL_SetClipboardText(clipboard.c_str()); - - if (cut) { - makeUndo(GUI_UNDO_PATTERN_CUT); - } -} - -void FurnaceGUI::doPaste() { - finishSelection(); - prepareUndo(GUI_UNDO_PATTERN_PASTE); - char* clipText=SDL_GetClipboardText(); - if (clipText!=NULL) { - if (clipText[0]) { - clipboard=clipText; - } - SDL_free(clipText); - } - std::vector data; - String tempS; - for (char i: clipboard) { - if (i=='\r') continue; - if (i=='\n') { - data.push_back(tempS); - tempS=""; - continue; - } - tempS+=i; - } - data.push_back(tempS); - - int startOff=-1; - bool invalidData=false; - if (data.size()<2) return; - if (data[0]!=fmt::sprintf("org.tildearrow.furnace - Pattern Data (%d)",DIV_ENGINE_VERSION)) return; - if (sscanf(data[1].c_str(),"%d",&startOff)!=1) return; - if (startOff<0) return; - - DETERMINE_LAST; - - int j=cursor.y; - char note[4]; - int ord=e->getOrder(); - for (size_t i=2; isong.patLen; i++) { - size_t charPos=0; - int iCoarse=cursor.xCoarse; - int iFine=(startOff>2 && cursor.xFine>2)?(((cursor.xFine-1)&(~1))|1):startOff; - - String& line=data[i]; - - while (charPossong.pat[iCoarse].getPattern(e->song.orders.ord[iCoarse][ord],true); - if (line[charPos]=='|') { - iCoarse++; - if (iCoarsesong.chanShow[iCoarse]) { - iCoarse++; - if (iCoarse>=lastChannel) break; - } - iFine=0; - charPos++; - continue; - } - if (iFine==0) { - if (charPos>=line.size()) { - invalidData=true; - break; - } - note[0]=line[charPos++]; - if (charPos>=line.size()) { - invalidData=true; - break; - } - note[1]=line[charPos++]; - if (charPos>=line.size()) { - invalidData=true; - break; - } - note[2]=line[charPos++]; - note[3]=0; - - if (!decodeNote(note,pat->data[j][0],pat->data[j][1])) { - invalidData=true; - break; - } - } else { - if (charPos>=line.size()) { - invalidData=true; - break; - } - note[0]=line[charPos++]; - if (charPos>=line.size()) { - invalidData=true; - break; - } - note[1]=line[charPos++]; - note[2]=0; - - if (strcmp(note,"..")==0) { - pat->data[j][iFine+1]=-1; - } else { - unsigned int val=0; - if (sscanf(note,"%2X",&val)!=1) { - invalidData=true; - break; - } - if (iFine<(3+e->song.pat[iCoarse].effectRows*2)) pat->data[j][iFine+1]=val; - } - } - iFine++; - } - - if (invalidData) { - logW("invalid clipboard data! failed at line %d char %d\n",i,charPos); - logW("%s\n",line.c_str()); - break; - } - j++; - } - - makeUndo(GUI_UNDO_PATTERN_PASTE); -} - -void FurnaceGUI::doUndo() { - if (undoHist.empty()) return; - UndoStep& us=undoHist.back(); - redoHist.push_back(us); - modified=true; - - switch (us.type) { - case GUI_UNDO_CHANGE_ORDER: - e->song.ordersLen=us.oldOrdersLen; - for (UndoOrderData& i: us.ord) { - e->song.orders.ord[i.chan][i.ord]=i.oldVal; - } - break; - case GUI_UNDO_PATTERN_EDIT: - case GUI_UNDO_PATTERN_DELETE: - case GUI_UNDO_PATTERN_PULL: - case GUI_UNDO_PATTERN_PUSH: - case GUI_UNDO_PATTERN_CUT: - case GUI_UNDO_PATTERN_PASTE: - for (UndoPatternData& i: us.pat) { - DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); - p->data[i.row][i.col]=i.oldVal; - } - if (!e->isPlaying()) { - cursor=us.cursor; - selStart=us.selStart; - selEnd=us.selEnd; - curNibble=us.nibble; - updateScroll(cursor.y); - e->setOrder(us.order); - } - break; - } - - undoHist.pop_back(); -} - -void FurnaceGUI::doRedo() { - if (redoHist.empty()) return; - UndoStep& us=redoHist.back(); - undoHist.push_back(us); - modified=true; - - switch (us.type) { - case GUI_UNDO_CHANGE_ORDER: - e->song.ordersLen=us.newOrdersLen; - for (UndoOrderData& i: us.ord) { - e->song.orders.ord[i.chan][i.ord]=i.newVal; - } - break; - case GUI_UNDO_PATTERN_EDIT: - case GUI_UNDO_PATTERN_DELETE: - case GUI_UNDO_PATTERN_PULL: - case GUI_UNDO_PATTERN_PUSH: - case GUI_UNDO_PATTERN_CUT: - case GUI_UNDO_PATTERN_PASTE: - for (UndoPatternData& i: us.pat) { - DivPattern* p=e->song.pat[i.chan].getPattern(i.pat,true); - p->data[i.row][i.col]=i.newVal; - } - if (!e->isPlaying()) { - cursor=us.cursor; - selStart=us.selStart; - selEnd=us.selEnd; - curNibble=us.nibble; - updateScroll(cursor.y); - e->setOrder(us.order); - } - - break; - } - - redoHist.pop_back(); +float FurnaceGUI::calcBPM(int s1, int s2, float hz) { + float hl=e->song.hilightA; + if (hl<=0.0f) hl=4.0f; + float timeBase=e->song.timeBase+1; + float speedSum=s1+s2; + if (timeBase<1.0f) timeBase=1.0f; + if (speedSum<1.0f) speedSum=1.0f; + return 120.0f*hz/(timeBase*hl*speedSum); } void FurnaceGUI::play(int row) { @@ -2941,7 +857,15 @@ void FurnaceGUI::stop() { activeNotes.clear(); } -void FurnaceGUI::previewNote(int refChan, int note) { +void FurnaceGUI::previewNote(int refChan, int note, bool autoNote) { + if (autoNote) { + e->setMidiBaseChan(refChan); + e->synchronized([this,note]() { + e->autoNoteOn(-1,curIns,note); + }); + return; + } + bool chanBusy[DIV_MAX_CHANS]; memset(chanBusy,0,DIV_MAX_CHANS*sizeof(bool)); for (ActiveNote& i: activeNotes) { @@ -2963,16 +887,25 @@ void FurnaceGUI::previewNote(int refChan, int note) { //printf("FAILED TO FIND CHANNEL!\n"); } -void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode) { - if (activeNotes.empty()) return; +void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) { + if (activeNotes.empty() && !autoNote) return; try { int key=noteKeys.at(scancode); int num=12*curOctave+key; + if (num<-60) num=-60; // C-(-5) + if (num>119) num=119; // B-9 if (key==100) return; if (key==101) return; if (key==102) return; + if (autoNote) { + e->synchronized([this,num]() { + e->autoNoteOff(-1,num); + }); + return; + } + for (size_t i=0; inoteOff(activeNotes[i].chan); @@ -2985,630 +918,119 @@ void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode) { } } -void FurnaceGUI::doAction(int what) { - switch (what) { - case GUI_ACTION_OPEN: - if (modified) { - showWarning("Unsaved changes! Are you sure?",GUI_WARN_OPEN); - } else { - openFileDialog(GUI_FILE_OPEN); - } - break; - case GUI_ACTION_SAVE: - if (curFileName=="") { - openFileDialog(GUI_FILE_SAVE); - } else { - if (save(curFileName,e->song.isDMF?e->song.version:0)>0) { - showError(fmt::sprintf("Error while saving file! (%s)",lastError)); - } - } - break; - case GUI_ACTION_SAVE_AS: - openFileDialog(GUI_FILE_SAVE); - break; - case GUI_ACTION_UNDO: - doUndo(); - break; - case GUI_ACTION_REDO: - doRedo(); - break; - case GUI_ACTION_PLAY_TOGGLE: - if (e->isPlaying() && !e->isStepping()) { - stop(); - } else { - play(); - } - break; - case GUI_ACTION_PLAY: - play(); - break; - case GUI_ACTION_STOP: - stop(); - break; - case GUI_ACTION_PLAY_REPEAT: - play(); - e->setRepeatPattern(true); - break; - case GUI_ACTION_PLAY_CURSOR: - if (e->isPlaying() && !e->isStepping()) { - stop(); - } else { - play(cursor.y); - } - break; - case GUI_ACTION_STEP_ONE: - e->stepOne(cursor.y); - break; - case GUI_ACTION_OCTAVE_UP: - if (++curOctave>7) { - curOctave=7; - } else { - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - break; - case GUI_ACTION_OCTAVE_DOWN: - if (--curOctave<-5) { - curOctave=-5; - } else { - for (size_t i=0; inoteOff(activeNotes[i].chan); - } - activeNotes.clear(); - } - break; - case GUI_ACTION_INS_UP: - if (--curIns<-1) curIns=-1; - break; - case GUI_ACTION_INS_DOWN: - if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; - break; - case GUI_ACTION_STEP_UP: - if (++editStep>64) editStep=64; - break; - case GUI_ACTION_STEP_DOWN: - if (--editStep<0) editStep=0; - break; - case GUI_ACTION_TOGGLE_EDIT: - edit=!edit; - break; - case GUI_ACTION_METRONOME: - e->setMetronome(!e->getMetronome()); - break; - case GUI_ACTION_REPEAT_PATTERN: - e->setRepeatPattern(!e->getRepeatPattern()); - break; - case GUI_ACTION_FOLLOW_ORDERS: - followOrders=!followOrders; - break; - case GUI_ACTION_FOLLOW_PATTERN: - followPattern=!followPattern; - break; - case GUI_ACTION_PANIC: - e->syncReset(); - break; +void FurnaceGUI::noteInput(int num, int key, int vol) { + DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); + + prepareUndo(GUI_UNDO_PATTERN_EDIT); - case GUI_ACTION_WINDOW_EDIT_CONTROLS: - nextWindow=GUI_WINDOW_EDIT_CONTROLS; - break; - case GUI_ACTION_WINDOW_ORDERS: - nextWindow=GUI_WINDOW_ORDERS; - break; - case GUI_ACTION_WINDOW_INS_LIST: - nextWindow=GUI_WINDOW_INS_LIST; - break; - case GUI_ACTION_WINDOW_INS_EDIT: - nextWindow=GUI_WINDOW_INS_EDIT; - break; - case GUI_ACTION_WINDOW_SONG_INFO: - nextWindow=GUI_WINDOW_SONG_INFO; - break; - case GUI_ACTION_WINDOW_PATTERN: - nextWindow=GUI_WINDOW_PATTERN; - break; - case GUI_ACTION_WINDOW_WAVE_LIST: - nextWindow=GUI_WINDOW_WAVE_LIST; - break; - case GUI_ACTION_WINDOW_WAVE_EDIT: - nextWindow=GUI_WINDOW_WAVE_EDIT; - break; - case GUI_ACTION_WINDOW_SAMPLE_LIST: - nextWindow=GUI_WINDOW_SAMPLE_LIST; - break; - case GUI_ACTION_WINDOW_SAMPLE_EDIT: - nextWindow=GUI_WINDOW_SAMPLE_EDIT; - break; - case GUI_ACTION_WINDOW_ABOUT: - nextWindow=GUI_WINDOW_ABOUT; - break; - case GUI_ACTION_WINDOW_SETTINGS: - nextWindow=GUI_WINDOW_SETTINGS; - break; - case GUI_ACTION_WINDOW_MIXER: - nextWindow=GUI_WINDOW_MIXER; - break; - case GUI_ACTION_WINDOW_DEBUG: - nextWindow=GUI_WINDOW_DEBUG; - break; - case GUI_ACTION_WINDOW_OSCILLOSCOPE: - nextWindow=GUI_WINDOW_OSCILLOSCOPE; - break; - case GUI_ACTION_WINDOW_VOL_METER: - nextWindow=GUI_WINDOW_VOL_METER; - break; - case GUI_ACTION_WINDOW_STATS: - nextWindow=GUI_WINDOW_STATS; - break; - case GUI_ACTION_WINDOW_COMPAT_FLAGS: - nextWindow=GUI_WINDOW_COMPAT_FLAGS; - break; - case GUI_ACTION_WINDOW_PIANO: - nextWindow=GUI_WINDOW_PIANO; - break; - case GUI_ACTION_WINDOW_NOTES: - nextWindow=GUI_WINDOW_NOTES; - break; - case GUI_ACTION_WINDOW_CHANNELS: - nextWindow=GUI_WINDOW_CHANNELS; - break; - case GUI_ACTION_WINDOW_REGISTER_VIEW: - nextWindow=GUI_WINDOW_REGISTER_VIEW; - break; - - case GUI_ACTION_COLLAPSE_WINDOW: - collapseWindow=true; - break; - case GUI_ACTION_CLOSE_WINDOW: - switch (curWindow) { - case GUI_WINDOW_EDIT_CONTROLS: - editControlsOpen=false; - break; - case GUI_WINDOW_SONG_INFO: - songInfoOpen=false; - break; - case GUI_WINDOW_ORDERS: - ordersOpen=false; - break; - case GUI_WINDOW_INS_LIST: - insListOpen=false; - break; - case GUI_WINDOW_PATTERN: - patternOpen=false; - break; - case GUI_WINDOW_INS_EDIT: - insEditOpen=false; - break; - case GUI_WINDOW_WAVE_LIST: - waveListOpen=false; - break; - case GUI_WINDOW_WAVE_EDIT: - waveEditOpen=false; - break; - case GUI_WINDOW_SAMPLE_LIST: - sampleListOpen=false; - break; - case GUI_WINDOW_SAMPLE_EDIT: - sampleEditOpen=false; - break; - case GUI_WINDOW_MIXER: - mixerOpen=false; - break; - case GUI_WINDOW_ABOUT: - aboutOpen=false; - break; - case GUI_WINDOW_SETTINGS: - settingsOpen=false; - break; - case GUI_WINDOW_DEBUG: - debugOpen=false; - break; - case GUI_WINDOW_OSCILLOSCOPE: - oscOpen=false; - break; - case GUI_WINDOW_VOL_METER: - volMeterOpen=false; - break; - case GUI_WINDOW_STATS: - statsOpen=false; - break; - case GUI_WINDOW_COMPAT_FLAGS: - compatFlagsOpen=false; - break; - case GUI_WINDOW_PIANO: - pianoOpen=false; - break; - case GUI_WINDOW_NOTES: - notesOpen=false; - break; - case GUI_WINDOW_CHANNELS: - channelsOpen=false; - break; - case GUI_WINDOW_REGISTER_VIEW: - regViewOpen=false; - break; - default: - break; - } - curWindow=GUI_WINDOW_NOTHING; - break; - - case GUI_ACTION_PAT_NOTE_UP: - doTranspose(1); - break; - case GUI_ACTION_PAT_NOTE_DOWN: - doTranspose(-1); - break; - case GUI_ACTION_PAT_OCTAVE_UP: - doTranspose(12); - break; - case GUI_ACTION_PAT_OCTAVE_DOWN: - doTranspose(-12); - break; - case GUI_ACTION_PAT_SELECT_ALL: - doSelectAll(); - break; - case GUI_ACTION_PAT_CUT: - doCopy(true); - break; - case GUI_ACTION_PAT_COPY: - doCopy(false); - break; - case GUI_ACTION_PAT_PASTE: - doPaste(); - break; - case GUI_ACTION_PAT_CURSOR_UP: - moveCursor(0,-MAX(1,settings.scrollStep?editStep:1),false); - break; - case GUI_ACTION_PAT_CURSOR_DOWN: - moveCursor(0,MAX(1,settings.scrollStep?editStep:1),false); - break; - case GUI_ACTION_PAT_CURSOR_LEFT: - moveCursor(-1,0,false); - break; - case GUI_ACTION_PAT_CURSOR_RIGHT: - moveCursor(1,0,false); - break; - case GUI_ACTION_PAT_CURSOR_UP_ONE: - moveCursor(0,-1,false); - break; - case GUI_ACTION_PAT_CURSOR_DOWN_ONE: - moveCursor(0,1,false); - break; - case GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL: - moveCursorPrevChannel(false); - break; - case GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL: - moveCursorNextChannel(false); - break; - case GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL: - moveCursorNextChannel(true); - break; - case GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL: - moveCursorPrevChannel(true); - break; - case GUI_ACTION_PAT_CURSOR_BEGIN: - moveCursorTop(false); - break; - case GUI_ACTION_PAT_CURSOR_END: - moveCursorBottom(false); - break; - case GUI_ACTION_PAT_CURSOR_UP_COARSE: - moveCursor(0,-16,false); - break; - case GUI_ACTION_PAT_CURSOR_DOWN_COARSE: - moveCursor(0,16,false); - break; - case GUI_ACTION_PAT_SELECTION_UP: - moveCursor(0,-MAX(1,settings.scrollStep?editStep:1),true); - break; - case GUI_ACTION_PAT_SELECTION_DOWN: - moveCursor(0,MAX(1,settings.scrollStep?editStep:1),true); - break; - case GUI_ACTION_PAT_SELECTION_LEFT: - moveCursor(-1,0,true); - break; - case GUI_ACTION_PAT_SELECTION_RIGHT: - moveCursor(1,0,true); - break; - case GUI_ACTION_PAT_SELECTION_UP_ONE: - moveCursor(0,-1,true); - break; - case GUI_ACTION_PAT_SELECTION_DOWN_ONE: - moveCursor(0,1,true); - break; - case GUI_ACTION_PAT_SELECTION_BEGIN: - moveCursorTop(true); - break; - case GUI_ACTION_PAT_SELECTION_END: - moveCursorBottom(true); - break; - case GUI_ACTION_PAT_SELECTION_UP_COARSE: - moveCursor(0,-16,true); - break; - case GUI_ACTION_PAT_SELECTION_DOWN_COARSE: - moveCursor(0,16,true); - break; - case GUI_ACTION_PAT_DELETE: - doDelete(); - if (settings.stepOnDelete) { - moveCursor(0,editStep,false); - } - break; - case GUI_ACTION_PAT_PULL_DELETE: - doPullDelete(); - break; - case GUI_ACTION_PAT_INSERT: - doInsert(); - break; - case GUI_ACTION_PAT_MUTE_CURSOR: - if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; - e->toggleMute(cursor.xCoarse); - break; - case GUI_ACTION_PAT_SOLO_CURSOR: - if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; - e->toggleSolo(cursor.xCoarse); - break; - case GUI_ACTION_PAT_UNMUTE_ALL: - e->unmuteAll(); - break; - case GUI_ACTION_PAT_NEXT_ORDER: - if (e->getOrder()song.ordersLen-1) { - e->setOrder(e->getOrder()+1); - } - break; - case GUI_ACTION_PAT_PREV_ORDER: - if (e->getOrder()>0) { - e->setOrder(e->getOrder()-1); - } - break; - case GUI_ACTION_PAT_COLLAPSE: - if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; - e->song.chanCollapse[cursor.xCoarse]=!e->song.chanCollapse[cursor.xCoarse]; - break; - case GUI_ACTION_PAT_INCREASE_COLUMNS: - if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; - e->song.pat[cursor.xCoarse].effectRows++; - if (e->song.pat[cursor.xCoarse].effectRows>8) e->song.pat[cursor.xCoarse].effectRows=8; - break; - case GUI_ACTION_PAT_DECREASE_COLUMNS: - if (cursor.xCoarse<0 || cursor.xCoarse>=e->getTotalChannelCount()) break; - e->song.pat[cursor.xCoarse].effectRows--; - if (e->song.pat[cursor.xCoarse].effectRows<1) e->song.pat[cursor.xCoarse].effectRows=1; - break; - - case GUI_ACTION_INS_LIST_ADD: - curIns=e->addInstrument(cursor.xCoarse); - modified=true; - break; - case GUI_ACTION_INS_LIST_DUPLICATE: - if (curIns>=0 && curIns<(int)e->song.ins.size()) { - int prevIns=curIns; - curIns=e->addInstrument(cursor.xCoarse); - (*e->song.ins[curIns])=(*e->song.ins[prevIns]); - modified=true; - } - break; - case GUI_ACTION_INS_LIST_OPEN: - openFileDialog(GUI_FILE_INS_OPEN); - break; - case GUI_ACTION_INS_LIST_SAVE: - if (curIns>=0 && curIns<(int)e->song.ins.size()) openFileDialog(GUI_FILE_INS_SAVE); - break; - case GUI_ACTION_INS_LIST_MOVE_UP: - if (e->moveInsUp(curIns)) curIns--; - break; - case GUI_ACTION_INS_LIST_MOVE_DOWN: - if (e->moveInsDown(curIns)) curIns++; - break; - case GUI_ACTION_INS_LIST_DELETE: - if (curIns>=0 && curIns<(int)e->song.ins.size()) { - e->delInstrument(curIns); - modified=true; - if (curIns>=(int)e->song.ins.size()) { - curIns--; - } - } - break; - case GUI_ACTION_INS_LIST_EDIT: - insEditOpen=true; - break; - case GUI_ACTION_INS_LIST_UP: - if (--curIns<0) curIns=0; - break; - case GUI_ACTION_INS_LIST_DOWN: - if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; - break; - - case GUI_ACTION_WAVE_LIST_ADD: - curWave=e->addWave(); - modified=true; - break; - case GUI_ACTION_WAVE_LIST_DUPLICATE: - if (curWave>=0 && curWave<(int)e->song.wave.size()) { - int prevWave=curWave; - curWave=e->addWave(); - (*e->song.wave[curWave])=(*e->song.wave[prevWave]); - modified=true; - } - break; - case GUI_ACTION_WAVE_LIST_OPEN: - openFileDialog(GUI_FILE_WAVE_OPEN); - break; - case GUI_ACTION_WAVE_LIST_SAVE: - if (curWave>=0 && curWave<(int)e->song.wave.size()) openFileDialog(GUI_FILE_WAVE_SAVE); - break; - case GUI_ACTION_WAVE_LIST_MOVE_UP: - if (e->moveWaveUp(curWave)) curWave--; - break; - case GUI_ACTION_WAVE_LIST_MOVE_DOWN: - if (e->moveWaveDown(curWave)) curWave++; - break; - case GUI_ACTION_WAVE_LIST_DELETE: - if (curWave>=0 && curWave<(int)e->song.wave.size()) { - e->delWave(curWave); - modified=true; - if (curWave>=(int)e->song.wave.size()) { - curWave--; - } - } - break; - case GUI_ACTION_WAVE_LIST_EDIT: - waveEditOpen=true; - break; - case GUI_ACTION_WAVE_LIST_UP: - if (--curWave<0) curWave=0; - break; - case GUI_ACTION_WAVE_LIST_DOWN: - if (++curWave>=(int)e->song.wave.size()) curWave=((int)e->song.wave.size())-1; - break; - - case GUI_ACTION_SAMPLE_LIST_ADD: - curSample=e->addSample(); - modified=true; - break; - case GUI_ACTION_SAMPLE_LIST_OPEN: - openFileDialog(GUI_FILE_SAMPLE_OPEN); - break; - case GUI_ACTION_SAMPLE_LIST_SAVE: - if (curSample>=0 && curSample<(int)e->song.sample.size()) openFileDialog(GUI_FILE_SAMPLE_SAVE); - break; - case GUI_ACTION_SAMPLE_LIST_MOVE_UP: - if (e->moveSampleUp(curSample)) curSample--; - break; - case GUI_ACTION_SAMPLE_LIST_MOVE_DOWN: - if (e->moveSampleDown(curSample)) curSample++; - break; - case GUI_ACTION_SAMPLE_LIST_DELETE: - e->delSample(curSample); - modified=true; - if (curSample>=(int)e->song.sample.size()) { - curSample--; - } - break; - case GUI_ACTION_SAMPLE_LIST_EDIT: - sampleEditOpen=true; - break; - case GUI_ACTION_SAMPLE_LIST_UP: - if (--curSample<0) curSample=0; - break; - case GUI_ACTION_SAMPLE_LIST_DOWN: - if (++curSample>=(int)e->song.sample.size()) curSample=((int)e->song.sample.size())-1; - break; - case GUI_ACTION_SAMPLE_LIST_PREVIEW: - e->previewSample(curSample); - break; - case GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW: - e->stopSamplePreview(); - break; - - case GUI_ACTION_ORDERS_UP: - if (e->getOrder()>0) { - e->setOrder(e->getOrder()-1); - } - break; - case GUI_ACTION_ORDERS_DOWN: - if (e->getOrder()song.ordersLen-1) { - e->setOrder(e->getOrder()+1); - } - break; - case GUI_ACTION_ORDERS_LEFT: { - DETERMINE_FIRST; - - do { - orderCursor--; - if (orderCursorsong.chanShow[orderCursor]); - break; + if (key==100) { // note off + pat->data[cursor.y][0]=100; + pat->data[cursor.y][1]=0; + } else if (key==101) { // note off + env release + pat->data[cursor.y][0]=101; + pat->data[cursor.y][1]=0; + } else if (key==102) { // env release only + pat->data[cursor.y][0]=102; + pat->data[cursor.y][1]=0; + } else { + pat->data[cursor.y][0]=num%12; + pat->data[cursor.y][1]=num/12; + if (pat->data[cursor.y][0]==0) { + pat->data[cursor.y][0]=12; + pat->data[cursor.y][1]--; } - case GUI_ACTION_ORDERS_RIGHT: { - DETERMINE_LAST; + pat->data[cursor.y][1]=(unsigned char)pat->data[cursor.y][1]; + if (latchIns==-2) { + pat->data[cursor.y][2]=curIns; + } else if (latchIns!=-1 && !e->song.ins.empty()) { + pat->data[cursor.y][2]=MIN(((int)e->song.ins.size())-1,latchIns); + } + int maxVol=e->getMaxVolumeChan(cursor.xCoarse); + if (latchVol!=-1) { + pat->data[cursor.y][3]=MIN(maxVol,latchVol); + } else if (vol!=-1) { + pat->data[cursor.y][3]=(vol*maxVol)/127; + } + if (latchEffect!=-1) pat->data[cursor.y][4]=latchEffect; + if (latchEffectVal!=-1) pat->data[cursor.y][5]=latchEffectVal; + } + makeUndo(GUI_UNDO_PATTERN_EDIT); + editAdvance(); + curNibble=false; +} - do { - orderCursor++; - if (orderCursor>=lastChannel) { - orderCursor=lastChannel-1; - break; +void FurnaceGUI::valueInput(int num, bool direct, int target) { + DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); + prepareUndo(GUI_UNDO_PATTERN_EDIT); + if (target==-1) target=cursor.xFine+1; + if (direct) { + pat->data[cursor.y][target]=num&0xff; + } else { + if (pat->data[cursor.y][target]==-1) pat->data[cursor.y][target]=0; + pat->data[cursor.y][target]=((pat->data[cursor.y][target]<<4)|num)&0xff; + } + if (cursor.xFine==1) { // instrument + if (pat->data[cursor.y][target]>=(int)e->song.ins.size()) { + pat->data[cursor.y][target]&=0x0f; + if (pat->data[cursor.y][target]>=(int)e->song.ins.size()) { + pat->data[cursor.y][target]=(int)e->song.ins.size()-1; + } + } + makeUndo(GUI_UNDO_PATTERN_EDIT); + if (direct) { + curNibble=false; + } else { + if (e->song.ins.size()<16) { + curNibble=false; + editAdvance(); + } else { + curNibble=!curNibble; + if (!curNibble) editAdvance(); + } + } + } else if (cursor.xFine==2) { + if (curNibble) { + if (pat->data[cursor.y][target]>e->getMaxVolumeChan(cursor.xCoarse)) pat->data[cursor.y][target]=e->getMaxVolumeChan(cursor.xCoarse); + } else { + pat->data[cursor.y][target]&=15; + } + makeUndo(GUI_UNDO_PATTERN_EDIT); + if (direct) { + curNibble=false; + } else { + if (e->getMaxVolumeChan(cursor.xCoarse)<16) { + curNibble=false; + editAdvance(); + } else { + curNibble=!curNibble; + if (!curNibble) editAdvance(); + } + } + } else { + makeUndo(GUI_UNDO_PATTERN_EDIT); + if (direct) { + curNibble=false; + } else { + curNibble=!curNibble; + if (!curNibble) { + if (!settings.effectCursorDir) { + editAdvance(); + } else { + if (settings.effectCursorDir==2) { + if (++cursor.xFine>=(3+(e->song.pat[cursor.xCoarse].effectRows*2))) { + cursor.xFine=3; + } + } else { + if (cursor.xFine&1) { + cursor.xFine++; + } else { + editAdvance(); + cursor.xFine--; + } + } } - } while (!e->song.chanShow[orderCursor]); - break; + } } - case GUI_ACTION_ORDERS_INCREASE: { - if (orderCursor<0 || orderCursor>=e->getTotalChannelCount()) break; - int curOrder=e->getOrder(); - if (e->song.orders.ord[orderCursor][curOrder]<0x7f) { - e->song.orders.ord[orderCursor][curOrder]++; - } - break; - } - case GUI_ACTION_ORDERS_DECREASE: { - if (orderCursor<0 || orderCursor>=e->getTotalChannelCount()) break; - int curOrder=e->getOrder(); - if (e->song.orders.ord[orderCursor][curOrder]>0) { - e->song.orders.ord[orderCursor][curOrder]--; - } - break; - } - case GUI_ACTION_ORDERS_EDIT_MODE: - orderEditMode++; - if (orderEditMode>3) orderEditMode=0; - break; - case GUI_ACTION_ORDERS_LINK: - changeAllOrders=!changeAllOrders; - break; - case GUI_ACTION_ORDERS_ADD: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->addOrder(false,false); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_DUPLICATE: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->addOrder(true,false); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_DEEP_CLONE: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->deepCloneOrder(false); - makeUndo(GUI_UNDO_CHANGE_ORDER); - if (!e->getWarnings().empty()) { - showWarning(e->getWarnings(),GUI_WARN_GENERIC); - } - break; - case GUI_ACTION_ORDERS_DUPLICATE_END: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->addOrder(true,true); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_DEEP_CLONE_END: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->deepCloneOrder(true); - makeUndo(GUI_UNDO_CHANGE_ORDER); - if (!e->getWarnings().empty()) { - showWarning(e->getWarnings(),GUI_WARN_GENERIC); - } - break; - case GUI_ACTION_ORDERS_REMOVE: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->deleteOrder(); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_MOVE_UP: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->moveOrderUp(); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_MOVE_DOWN: - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->moveOrderDown(); - makeUndo(GUI_UNDO_CHANGE_ORDER); - break; - case GUI_ACTION_ORDERS_REPLAY: - e->setOrder(e->getOrder()); - break; } } @@ -3672,82 +1094,21 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { int key=noteKeys.at(ev.key.keysym.scancode); int num=12*curOctave+key; - if (edit) { - DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); - - prepareUndo(GUI_UNDO_PATTERN_EDIT); + if (num<-60) num=-60; // C-(-5) + if (num>119) num=119; // B-9 - if (key==100) { // note off - pat->data[cursor.y][0]=100; - pat->data[cursor.y][1]=0; - } else if (key==101) { // note off + env release - pat->data[cursor.y][0]=101; - pat->data[cursor.y][1]=0; - } else if (key==102) { // env release only - pat->data[cursor.y][0]=102; - pat->data[cursor.y][1]=0; - } else { - pat->data[cursor.y][0]=num%12; - pat->data[cursor.y][1]=num/12; - if (pat->data[cursor.y][0]==0) { - pat->data[cursor.y][0]=12; - pat->data[cursor.y][1]--; - } - pat->data[cursor.y][1]=(unsigned char)pat->data[cursor.y][1]; - pat->data[cursor.y][2]=curIns; - previewNote(cursor.xCoarse,num); - } - makeUndo(GUI_UNDO_PATTERN_EDIT); - editAdvance(); - curNibble=false; - } else { - if (key!=100 && key!=101 && key!=102) { - previewNote(cursor.xCoarse,num); - } + if (edit) { + noteInput(num,key); + } + if (key!=100 && key!=101 && key!=102) { + previewNote(cursor.xCoarse,num); } } catch (std::out_of_range& e) { } } else if (edit) { // value try { int num=valueKeys.at(ev.key.keysym.sym); - DivPattern* pat=e->song.pat[cursor.xCoarse].getPattern(e->song.orders.ord[cursor.xCoarse][e->getOrder()],true); - prepareUndo(GUI_UNDO_PATTERN_EDIT); - if (pat->data[cursor.y][cursor.xFine+1]==-1) pat->data[cursor.y][cursor.xFine+1]=0; - pat->data[cursor.y][cursor.xFine+1]=((pat->data[cursor.y][cursor.xFine+1]<<4)|num)&0xff; - if (cursor.xFine==1) { // instrument - if (pat->data[cursor.y][cursor.xFine+1]>=(int)e->song.ins.size()) { - pat->data[cursor.y][cursor.xFine+1]&=0x0f; - if (pat->data[cursor.y][cursor.xFine+1]>=(int)e->song.ins.size()) { - pat->data[cursor.y][cursor.xFine+1]=(int)e->song.ins.size()-1; - } - } - makeUndo(GUI_UNDO_PATTERN_EDIT); - if (e->song.ins.size()<16) { - curNibble=false; - editAdvance(); - } else { - curNibble=!curNibble; - if (!curNibble) editAdvance(); - } - } else if (cursor.xFine==2) { - if (curNibble) { - if (pat->data[cursor.y][cursor.xFine+1]>e->getMaxVolumeChan(cursor.xCoarse)) pat->data[cursor.y][cursor.xFine+1]=e->getMaxVolumeChan(cursor.xCoarse); - } else { - pat->data[cursor.y][cursor.xFine+1]&=15; - } - makeUndo(GUI_UNDO_PATTERN_EDIT); - if (e->getMaxVolumeChan(cursor.xCoarse)<16) { - curNibble=false; - editAdvance(); - } else { - curNibble=!curNibble; - if (!curNibble) editAdvance(); - } - } else { - makeUndo(GUI_UNDO_PATTERN_EDIT); - curNibble=!curNibble; - if (!curNibble) editAdvance(); - } + valueInput(num); } catch (std::out_of_range& e) { } } @@ -3769,7 +1130,9 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { int num=valueKeys.at(ev.key.keysym.sym); if (orderCursor>=0 && orderCursorgetTotalChannelCount()) { int curOrder=e->getOrder(); - e->song.orders.ord[orderCursor][curOrder]=((e->song.orders.ord[orderCursor][curOrder]<<4)|num)&0x7f; + e->lockSave([this,curOrder,num]() { + e->song.orders.ord[orderCursor][curOrder]=((e->song.orders.ord[orderCursor][curOrder]<<4)|num); + }); if (orderEditMode==2 || orderEditMode==3) { curNibble=!curNibble; if (!curNibble) { @@ -3789,6 +1152,16 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { } } break; + case GUI_WINDOW_SAMPLE_EDIT: + try { + int action=actionMapSample.at(mapped); + if (action>0) { + doAction(action); + return; + } + } catch (std::out_of_range& e) { + } + break; case GUI_WINDOW_INS_LIST: try { int action=actionMapInsList.at(mapped); @@ -3844,7 +1217,7 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { int key=noteKeys.at(ev.key.keysym.scancode); int num=12*curOctave+key; if (key!=100 && key!=101 && key!=102) { - previewNote(cursor.xCoarse,num); + previewNote(cursor.xCoarse,num,true); } } catch (std::out_of_range& e) { } @@ -3888,7 +1261,7 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { } void FurnaceGUI::keyUp(SDL_Event& ev) { - stopPreviewNote(ev.key.keysym.scancode); + stopPreviewNote(ev.key.keysym.scancode,curWindow!=GUI_WINDOW_PATTERN); if (wavePreviewOn) { if (ev.key.keysym.scancode==wavePreviewKey) { wavePreviewOn=false; @@ -3915,59 +1288,228 @@ bool dirExists(String what) { } void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { - if (!dirExists(workingDir)) workingDir=getHomeDir(); - ImGuiFileDialog::Instance()->DpiScale=dpiScale; + bool hasOpened=false; switch (type) { case GUI_FILE_OPEN: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Open File","compatible files{.fur,.dmf},.*",workingDir); + if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Open File", + {"compatible files", "*.fur *.dmf *.mod", + "all files", ".*"}, + "compatible files{.fur,.dmf,.mod},.*", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","Furnace song{.fur},DefleMask 1.1 module{.dmf}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save File", + {"Furnace song", "*.fur", + "DefleMask 1.1.3 module", "*.dmf"}, + "Furnace song{.fur},DefleMask 1.1.3 module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_SAVE_DMF_LEGACY: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save File","DefleMask 1.0/legacy module{.dmf}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save File", + {"DefleMask 1.0/legacy module", "*.dmf"}, + "DefleMask 1.0/legacy module{.dmf}", + workingDirSong, + dpiScale + ); break; case GUI_FILE_INS_OPEN: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Instrument","compatible files{.fui,.dmp,.tfi,.vgi},.*",workingDir); + if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Load Instrument", + {"compatible files", "*.fui *.dmp *.tfi *.vgi *.s3i *.sbi", + "all files", ".*"}, + "compatible files{.fui,.dmp,.tfi,.vgi,.s3i,.sbi},.*", + workingDirIns, + dpiScale + ); break; case GUI_FILE_INS_SAVE: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Instrument","Furnace instrument{.fui}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Instrument", + {"Furnace instrument", "*.fui"}, + "Furnace instrument{.fui}", + workingDirIns, + dpiScale + ); break; case GUI_FILE_WAVE_OPEN: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Wavetable","compatible files{.fuw,.dmw},.*",workingDir); + if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Load Wavetable", + {"compatible files", "*.fuw *.dmw", + "all files", ".*"}, + "compatible files{.fuw,.dmw},.*", + workingDirWave, + dpiScale + ); break; case GUI_FILE_WAVE_SAVE: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Wavetable","Furnace wavetable{.fuw}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirWave)) workingDirWave=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Wavetable", + {"Furnace wavetable", ".fuw"}, + "Furnace wavetable{.fuw}", + workingDirWave, + dpiScale + ); break; case GUI_FILE_SAMPLE_OPEN: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Load Sample","Wave file{.wav},.*",workingDir); + if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Load Sample", + {"Wave file", "*.wav", + "all files", ".*"}, + "Wave file{.wav},.*", + workingDirSample, + dpiScale + ); break; case GUI_FILE_SAMPLE_SAVE: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Save Sample","Wave file{.wav}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); + hasOpened=fileDialog->openSave( + "Save Sample", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirSample, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_ONE: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_SYS: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Audio", + {"Wave file", "*.wav"}, + "Wave file{.wav}", + workingDirAudioExport, + dpiScale + ); break; case GUI_FILE_EXPORT_VGM: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export VGM",".vgm",workingDir,1,nullptr,ImGuiFileDialogFlags_ConfirmOverwrite); + if (!dirExists(workingDirVGMExport)) workingDirVGMExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export VGM", + {"VGM file", "*.vgm"}, + "VGM file{.vgm}", + workingDirVGMExport, + dpiScale + ); break; case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; case GUI_FILE_LOAD_MAIN_FONT: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDir); + if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); break; case GUI_FILE_LOAD_PAT_FONT: - ImGuiFileDialog::Instance()->OpenModal("FileDialog","Select Font","compatible files{.ttf,.otf,.ttc}",workingDir); + if (!dirExists(workingDirFont)) workingDirFont=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Select Font", + {"compatible files", "*.ttf *.otf *.ttc"}, + "compatible files{.ttf,.otf,.ttc}", + workingDirFont, + dpiScale + ); + break; + case GUI_FILE_IMPORT_COLORS: + if (!dirExists(workingDirColors)) workingDirColors=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Select Color File", + {"configuration files", "*.cfgc"}, + "configuration files{.cfgc}", + workingDirColors, + dpiScale + ); + break; + case GUI_FILE_IMPORT_KEYBINDS: + if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Select Keybind File", + {"configuration files", "*.cfgk"}, + "configuration files{.cfgk}", + workingDirKeybinds, + dpiScale + ); + break; + case GUI_FILE_IMPORT_LAYOUT: + if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Select Layout File", + {".ini files", "*.ini"}, + ".ini files{.ini}", + workingDirKeybinds, + dpiScale + ); + break; + case GUI_FILE_EXPORT_COLORS: + if (!dirExists(workingDirColors)) workingDirColors=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Colors", + {"configuration files", "*.cfgc"}, + "configuration files{.cfgc}", + workingDirColors, + dpiScale + ); + break; + case GUI_FILE_EXPORT_KEYBINDS: + if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Keybinds", + {"configuration files", "*.cfgk"}, + "configuration files{.cfgk}", + workingDirKeybinds, + dpiScale + ); + break; + case GUI_FILE_EXPORT_LAYOUT: + if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Layout", + {".ini files", "*.ini"}, + ".ini files{.ini}", + workingDirKeybinds, + dpiScale + ); break; } - curFileDialog=type; + if (hasOpened) curFileDialog=type; //ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NavEnableKeyboard; } @@ -3997,7 +1539,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { memset(&zl,0,sizeof(z_stream)); ret=deflateInit(&zl,Z_DEFAULT_COMPRESSION); if (ret!=Z_OK) { - logE("zlib error!\n"); + logE("zlib error!"); lastError="compression error"; fclose(outFile); w->finish(); @@ -4009,7 +1551,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { zl.avail_out=131072; zl.next_out=zbuf; if ((ret=deflate(&zl,Z_NO_FLUSH))==Z_STREAM_ERROR) { - logE("zlib stream error!\n"); + logE("zlib stream error!"); lastError="zlib stream error"; deflateEnd(&zl); fclose(outFile); @@ -4019,7 +1561,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { size_t amount=131072-zl.avail_out; if (amount>0) { if (fwrite(zbuf,1,amount,outFile)!=amount) { - logE("did not write entirely: %s!\n",strerror(errno)); + logE("did not write entirely: %s!",strerror(errno)); lastError=strerror(errno); deflateEnd(&zl); fclose(outFile); @@ -4031,7 +1573,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { zl.avail_out=131072; zl.next_out=zbuf; if ((ret=deflate(&zl,Z_FINISH))==Z_STREAM_ERROR) { - logE("zlib finish stream error!\n"); + logE("zlib finish stream error!"); lastError="zlib finish stream error"; deflateEnd(&zl); fclose(outFile); @@ -4040,7 +1582,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { } if (131072-zl.avail_out>0) { if (fwrite(zbuf,1,131072-zl.avail_out,outFile)!=(131072-zl.avail_out)) { - logE("did not write entirely: %s!\n",strerror(errno)); + logE("did not write entirely: %s!",strerror(errno)); lastError=strerror(errno); deflateEnd(&zl); fclose(outFile); @@ -4051,7 +1593,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { deflateEnd(&zl); #else if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) { - logE("did not write entirely: %s!\n",strerror(errno)); + logE("did not write entirely: %s!",strerror(errno)); lastError=strerror(errno); fclose(outFile); w->finish(); @@ -4062,6 +1604,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { w->finish(); curFileName=path; modified=false; + updateWindowTitle(); if (!e->getWarnings().empty()) { showWarning(e->getWarnings(),GUI_WARN_GENERIC); } @@ -4070,7 +1613,7 @@ int FurnaceGUI::save(String path, int dmfVersion) { int FurnaceGUI::load(String path) { if (!path.empty()) { - logI("loading module...\n"); + logI("loading module..."); FILE* f=ps_fopen(path.c_str(),"rb"); if (f==NULL) { perror("error"); @@ -4092,7 +1635,7 @@ int FurnaceGUI::load(String path) { } if (len<1) { if (len==0) { - printf("that file is empty!\n"); + logE("that file is empty!"); lastError="file is empty"; } else { perror("tell error"); @@ -4119,7 +1662,7 @@ int FurnaceGUI::load(String path) { fclose(f); if (!e->load(file,(size_t)len)) { lastError=e->getLastError(); - logE("could not open file!\n"); + logE("could not open file!"); return 1; } } @@ -4128,6 +1671,8 @@ int FurnaceGUI::load(String path) { curNibble=false; orderNibble=false; orderCursor=-1; + samplePos=0; + updateSampleTex=true; selStart=SelectionPoint(); selEnd=SelectionPoint(); cursor=SelectionPoint(); @@ -4220,7 +1765,39 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { if (ynotifyWaveChange(curWave); - modified=true; + MARK_MODIFIED; + } + } + if (sampleDragActive) { + int x=samplePos+round(double(dragX-sampleDragStart.x)*sampleZoom); + int x1=samplePos+round(double(dragX-sampleDragStart.x+1)*sampleZoom); + if (x<0) x=0; + if (sampleDragMode) { + if (x>=(int)sampleDragLen) x=sampleDragLen-1; + } else { + if (x>(int)sampleDragLen) x=sampleDragLen; + } + if (x1<0) x1=0; + if (x1>=(int)sampleDragLen) x1=sampleDragLen-1; + double y=0.5-double(dragY-sampleDragStart.y)/sampleDragAreaSize.y; + if (sampleDragMode) { // draw + if (sampleDrag16) { + int val=y*65536; + if (val<-32768) val=-32768; + if (val>32767) val=32767; + for (int i=x; i<=x1; i++) ((short*)sampleDragTarget)[i]=val; + } else { + int val=y*256; + if (val<-128) val=-128; + if (val>127) val=127; + for (int i=x; i<=x1; i++) ((signed char*)sampleDragTarget)[i]=val; + } + updateSampleTex=true; + } else { // select + if (sampleSelStart<0) { + sampleSelStart=x; + } + sampleSelEnd=x; } } } @@ -4248,7 +1825,201 @@ void FurnaceGUI::processDrags(int dragX, int dragY) { fileName+=x; \ } -#define BIND_FOR(x) getKeyName(actionKeys[x],true).c_str() +#define checkExtensionDual(x,y,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)) { \ + fileName+=fallback; \ + } + +void FurnaceGUI::editOptions(bool topMenu) { + char id[4096]; + if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_PAT_CUT))) doCopy(true); + if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_PAT_COPY))) doCopy(false); + if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_PAT_PASTE))) doPaste(); + if (ImGui::BeginMenu("paste special...")) { + if (ImGui::MenuItem("paste mix",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX))) doPaste(GUI_PASTE_MODE_MIX_FG); + if (ImGui::MenuItem("paste mix (background)",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX_BG))) doPaste(GUI_PASTE_MODE_MIX_BG); + if (ImGui::MenuItem("paste flood",BIND_FOR(GUI_ACTION_PAT_PASTE_FLOOD))) doPaste(GUI_PASTE_MODE_FLOOD); + if (ImGui::MenuItem("paste overflow",BIND_FOR(GUI_ACTION_PAT_PASTE_OVERFLOW))) doPaste(GUI_PASTE_MODE_OVERFLOW); + ImGui::EndMenu(); + } + if (ImGui::MenuItem("delete",BIND_FOR(GUI_ACTION_PAT_DELETE))) doDelete(); + if (topMenu) { + if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_PAT_SELECT_ALL))) doSelectAll(); + } + ImGui::Separator(); + + ImGui::Text("operation mask"); + ImGui::SameLine(); + + ImGui::PushFont(patFont); + if (ImGui::BeginTable("opMaskTable",5,ImGuiTableFlags_Borders|ImGuiTableFlags_SizingFixedFit|ImGuiTableFlags_NoHostExtendX)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ACTIVE]); + if (ImGui::Selectable(opMaskNote?"C-4##opMaskNote":"---##opMaskNote",opMaskNote,ImGuiSelectableFlags_DontClosePopups)) { + opMaskNote=!opMaskNote; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); + if (ImGui::Selectable(opMaskIns?"01##opMaskIns":"--##opMaskIns",opMaskIns,ImGuiSelectableFlags_DontClosePopups)) { + opMaskIns=!opMaskIns; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]); + if (ImGui::Selectable(opMaskVol?"7F##opMaskVol":"--##opMaskVol",opMaskVol,ImGuiSelectableFlags_DontClosePopups)) { + opMaskVol=!opMaskVol; + } + ImGui::PopStyleColor(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_PITCH]); + if (ImGui::Selectable(opMaskEffect?"04##opMaskEffect":"--##opMaskEffect",opMaskEffect,ImGuiSelectableFlags_DontClosePopups)) { + opMaskEffect=!opMaskEffect; + } + ImGui::TableNextColumn(); + if (ImGui::Selectable(opMaskEffectVal?"72##opMaskEffectVal":"--##opMaskEffectVal",opMaskEffectVal,ImGuiSelectableFlags_DontClosePopups)) { + opMaskEffectVal=!opMaskEffectVal; + } + ImGui::PopStyleColor(); + ImGui::EndTable(); + } + ImGui::PopFont(); + + ImGui::Text("input latch"); + if (ImGui::MenuItem("set latch",BIND_FOR(GUI_ACTION_PAT_LATCH))) { + // TODO + } + ImGui::Separator(); + + if (ImGui::MenuItem("note up",BIND_FOR(GUI_ACTION_PAT_NOTE_UP))) doTranspose(1); + if (ImGui::MenuItem("note down",BIND_FOR(GUI_ACTION_PAT_NOTE_DOWN))) doTranspose(-1); + if (ImGui::MenuItem("octave up",BIND_FOR(GUI_ACTION_PAT_OCTAVE_UP))) doTranspose(12); + if (ImGui::MenuItem("octave down",BIND_FOR(GUI_ACTION_PAT_OCTAVE_DOWN))) doTranspose(-12); + if (ImGui::InputInt("##TransposeAmount",&transposeAmount,1,1)) { + if (transposeAmount<-96) transposeAmount=-96; + if (transposeAmount>96) transposeAmount=96; + } + ImGui::SameLine(); + if (ImGui::Button("Transpose")) { + doTranspose(transposeAmount); + ImGui::CloseCurrentPopup(); + } + + ImGui::Separator(); + if (ImGui::MenuItem("interpolate",BIND_FOR(GUI_ACTION_PAT_INTERPOLATE))) doInterpolate(); + if (ImGui::BeginMenu("change instrument...")) { + if (e->song.ins.empty()) { + ImGui::Text("no instruments available"); + } + for (size_t i=0; isong.ins.size(); i++) { + snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str()); + if (ImGui::MenuItem(id)) { + doChangeIns(i); + } + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("gradient/fade...")) { + if (ImGui::InputInt("Start",&fadeMin,1,1)) { + if (fadeMin<0) fadeMin=0; + if (fadeMode) { + if (fadeMin>15) fadeMin=15; + } else { + if (fadeMin>255) fadeMin=255; + } + } + if (ImGui::InputInt("End",&fadeMax,1,1)) { + if (fadeMax<0) fadeMax=0; + if (fadeMode) { + if (fadeMax>15) fadeMax=15; + } else { + if (fadeMax>255) fadeMax=255; + } + } + if (ImGui::Checkbox("Nibble mode",&fadeMode)) { + if (fadeMode) { + if (fadeMin>15) fadeMin=15; + if (fadeMax>15) fadeMax=15; + } else { + if (fadeMin>255) fadeMin=255; + if (fadeMax>255) fadeMax=255; + } + } + if (ImGui::Button("Go ahead")) { + doFade(fadeMin,fadeMax,fadeMode); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("scale...")) { + if (ImGui::InputFloat("##ScaleMax",&scaleMax,1,1,"%.1f%%")) { + if (scaleMax<0.0f) scaleMax=0.0f; + if (scaleMax>25600.0f) scaleMax=25600.0f; + } + if (ImGui::Button("Scale")) { + doScale(scaleMax); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("randomize...")) { + if (ImGui::InputInt("Minimum",&randomizeMin,1,1)) { + if (randomizeMin<0) randomizeMin=0; + if (randomMode) { + if (randomizeMin>15) randomizeMin=15; + } else { + if (randomizeMin>255) randomizeMin=255; + } + if (randomizeMin>randomizeMax) randomizeMin=randomizeMax; + } + if (ImGui::InputInt("Maximum",&randomizeMax,1,1)) { + if (randomizeMax<0) randomizeMax=0; + if (randomizeMax15) randomizeMax=15; + } else { + if (randomizeMax>255) randomizeMax=255; + } + } + if (ImGui::Checkbox("Nibble mode",&randomMode)) { + if (randomMode) { + if (randomizeMin>15) randomizeMin=15; + if (randomizeMax>15) randomizeMax=15; + } else { + if (randomizeMin>255) randomizeMin=255; + if (randomizeMax>255) randomizeMax=255; + } + } + // TODO: add an option to set effect to specific value? + if (ImGui::Button("Randomize")) { + doRandomize(randomizeMin,randomizeMax,randomMode); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("invert values",BIND_FOR(GUI_ACTION_PAT_INVERT_VALUES))) doInvertValues(); + + ImGui::Separator(); + + if (ImGui::MenuItem("flip selection",BIND_FOR(GUI_ACTION_PAT_FLIP_SELECTION))) doFlip(); + if (ImGui::MenuItem("collapse",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_ROWS))) doCollapse(2); + if (ImGui::MenuItem("expand",BIND_FOR(GUI_ACTION_PAT_EXPAND_ROWS))) doExpand(2); + + if (topMenu) { + ImGui::Separator(); + ImGui::MenuItem("collapse pattern",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_PAT)); + ImGui::MenuItem("expand pattern",BIND_FOR(GUI_ACTION_PAT_EXPAND_PAT)); + + ImGui::Separator(); + ImGui::MenuItem("collapse song",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_SONG)); + ImGui::MenuItem("expand song",BIND_FOR(GUI_ACTION_PAT_EXPAND_SONG)); + } +} bool FurnaceGUI::loop() { while (!quit) { @@ -4269,15 +2040,15 @@ bool FurnaceGUI::loop() { #endif if (selecting) { // detect whether we have to scroll - if (motionYpatWindowPos.y+patWindowSize.y) { + if (motionY>patWindowPos.y+patWindowSize.y-2.0f*dpiScale) { addScroll(1); } } - if (macroDragActive || macroLoopDragActive || waveDragActive) { - int distance=fabs(motionXrel); + if (macroDragActive || macroLoopDragActive || waveDragActive || sampleDragActive) { + int distance=fabs((double)motionXrel); if (distance<1) distance=1; float start=motionX-motionXrel; float end=motionX; @@ -4293,7 +2064,9 @@ bool FurnaceGUI::loop() { break; } case SDL_MOUSEBUTTONUP: - if (macroDragActive || macroLoopDragActive || waveDragActive) modified=true; + if (macroDragActive || macroLoopDragActive || waveDragActive || (sampleDragActive && sampleDragMode)) { + MARK_MODIFIED; + } macroDragActive=false; macroDragBitMode=false; macroDragInitialValue=false; @@ -4302,6 +2075,19 @@ bool FurnaceGUI::loop() { macroDragLastY=-1; macroLoopDragActive=false; waveDragActive=false; + if (sampleDragActive) { + logD("stopping sample drag"); + if (sampleDragMode) { + e->renderSamplesP(); + } else { + if (sampleSelStart>sampleSelEnd) { + sampleSelStart^=sampleSelEnd; + sampleSelEnd^=sampleSelStart; + sampleSelStart^=sampleSelEnd; + } + } + } + sampleDragActive=false; if (selecting) { cursor=selEnd; finishSelection(); @@ -4322,6 +2108,10 @@ bool FurnaceGUI::loop() { bindSetPrevValue=0; } break; + case SDL_MOUSEWHEEL: + wheelX+=ev.wheel.x; + wheelY+=ev.wheel.y; + break; case SDL_WINDOWEVENT: switch (ev.window.event) { case SDL_WINDOWEVENT_RESIZED: @@ -4383,6 +2173,144 @@ bool FurnaceGUI::loop() { } } + while (true) { + midiLock.lock(); + if (midiQueue.empty()) { + midiLock.unlock(); + break; + } + TAMidiMessage msg=midiQueue.front(); + midiLock.unlock(); + + // parse message here + if (learning!=-1) { + if (learning>=0 && learning<(int)midiMap.binds.size()) { + midiMap.binds[learning].type=msg.type>>4; + midiMap.binds[learning].channel=msg.type&15; + midiMap.binds[learning].data1=msg.data[0]; + switch (msg.type&0xf0) { + case TA_MIDI_NOTE_OFF: + case TA_MIDI_NOTE_ON: + case TA_MIDI_AFTERTOUCH: + case TA_MIDI_PITCH_BEND: + case TA_MIDI_CONTROL: + midiMap.binds[learning].data2=msg.data[1]; + break; + default: + midiMap.binds[learning].data2=128; + break; + } + } + learning=-1; + } else { + int action=midiMap.at(msg); + if (action!=0) { + doAction(action); + } else switch (msg.type&0xf0) { + case TA_MIDI_NOTE_ON: + if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) { + if (midiMap.noteInput && edit && msg.data[1]!=0) { + noteInput( + msg.data[0]-12, + 0, + midiMap.volInput?((int)(pow((double)msg.data[1]/127.0,midiMap.volExp)*127.0)):-1 + ); + } + } else { + if (edit && msg.data[1]!=0) { + switch (midiMap.valueInputStyle) { + case 1: { + int val=msg.data[0]%24; + if (val<16) { + valueInput(val); + } + break; + } + case 2: + valueInput(msg.data[0]&15); + break; + case 3: + int val=altValues[msg.data[0]%24]; + if (val>=0) { + valueInput(val); + } + break; + } + } + } + break; + case TA_MIDI_PROGRAM: + if (midiMap.programChange) { + curIns=msg.data[0]; + if (curIns>=(int)e->song.ins.size()) curIns=e->song.ins.size()-1; + } + break; + case TA_MIDI_CONTROL: + bool gchanged=false; + if (msg.data[0]==midiMap.valueInputControlMSB) { + midiMap.valueInputCurMSB=msg.data[1]; + gchanged=true; + } + if (msg.data[0]==midiMap.valueInputControlLSB) { + midiMap.valueInputCurLSB=msg.data[1]; + gchanged=true; + } + if (msg.data[0]==midiMap.valueInputControlSingle) { + midiMap.valueInputCurSingle=msg.data[1]; + gchanged=true; + } + if (gchanged && cursor.xFine>0) { + switch (midiMap.valueInputStyle) { + case 4: // dual CC + valueInput(((midiMap.valueInputCurMSB>>3)<<4)|(midiMap.valueInputCurLSB>>3),true); + break; + case 5: // 14-bit + valueInput((midiMap.valueInputCurMSB<<1)|(midiMap.valueInputCurLSB>>6),true); + break; + case 6: // single CC + valueInput((midiMap.valueInputCurSingle*255)/127,true); + break; + } + } + + for (int i=0; i<18; i++) { + bool changed=false; + if (midiMap.valueInputSpecificStyle[i]!=0) { + if (msg.data[0]==midiMap.valueInputSpecificMSB[i]) { + changed=true; + midiMap.valueInputCurMSBS[i]=msg.data[1]; + } + if (msg.data[0]==midiMap.valueInputSpecificLSB[i]) { + changed=true; + midiMap.valueInputCurLSBS[i]=msg.data[1]; + } + if (msg.data[0]==midiMap.valueInputSpecificSingle[i]) { + changed=true; + midiMap.valueInputCurSingleS[i]=msg.data[1]; + } + + if (changed) switch (midiMap.valueInputStyle) { + case 1: // dual CC + valueInput(((midiMap.valueInputCurMSBS[i]>>3)<<4)|(midiMap.valueInputCurLSBS[i]>>3),true,i+2); + break; + case 2: // 14-bit + valueInput((midiMap.valueInputCurMSBS[i]<<1)|(midiMap.valueInputCurLSBS[i]>>6),true,i+2); + break; + case 3: // single CC + valueInput((midiMap.valueInputCurSingleS[i]*255)/127,true,i+2); + break; + } + } + } + break; + } + } + + midiLock.lock(); + midiQueue.pop(); + midiLock.unlock(); + } + ImGui_ImplSDLRenderer_NewFrame(); ImGui_ImplSDL2_NewFrame(sdlWin); ImGui::NewFrame(); @@ -4391,22 +2319,11 @@ bool FurnaceGUI::loop() { ImGui::BeginMainMenuBar(); if (ImGui::BeginMenu("file")) { - if (ImGui::MenuItem("new")) { + if (ImGui::MenuItem("new...")) { if (modified) { showWarning("Unsaved changes! Are you sure?",GUI_WARN_NEW); } else { - e->createNew(); - undoHist.clear(); - redoHist.clear(); - curFileName=""; - modified=false; - curNibble=false; - orderNibble=false; - orderCursor=-1; - selStart=SelectionPoint(); - selEnd=SelectionPoint(); - cursor=SelectionPoint(); - updateWindowTitle(); + displayNew=true; } } if (ImGui::MenuItem("open...",BIND_FOR(GUI_ACTION_OPEN))) { @@ -4418,7 +2335,7 @@ bool FurnaceGUI::loop() { } ImGui::Separator(); if (ImGui::MenuItem("save",BIND_FOR(GUI_ACTION_SAVE))) { - if (curFileName=="") { + if (curFileName=="" || curFileName==backupPath || e->song.version>=0xff00) { openFileDialog(GUI_FILE_SAVE); } else { if (save(curFileName,e->song.isDMF?e->song.version:0)>0) { @@ -4450,14 +2367,27 @@ bool FurnaceGUI::loop() { } if (ImGui::BeginMenu("export VGM...")) { ImGui::Text("settings:"); + if (ImGui::BeginCombo("format version",fmt::sprintf("%d.%.2x",vgmExportVersion>>8,vgmExportVersion&0xff).c_str())) { + for (int i=0; i<6; i++) { + if (ImGui::Selectable(fmt::sprintf("%d.%.2x",vgmVersions[i]>>8,vgmVersions[i]&0xff).c_str(),vgmExportVersion==vgmVersions[i])) { + vgmExportVersion=vgmVersions[i]; + } + } + ImGui::EndCombo(); + } ImGui::Checkbox("loop",&vgmExportLoop); - ImGui::Text("systems to export:");; + ImGui::Text("systems to export:"); bool hasOneAtLeast=false; for (int i=0; isong.systemLen; i++) { - ImGui::BeginDisabled(!e->isVGMExportable(e->song.system[i])); + int minVersion=e->minVGMVersion(e->song.system[i]); + ImGui::BeginDisabled(minVersion>vgmExportVersion || minVersion==0); ImGui::Checkbox(fmt::sprintf("%d. %s##_SYSV%d",i+1,getSystemName(e->song.system[i]),i).c_str(),&willExport[i]); ImGui::EndDisabled(); - if (!e->isVGMExportable(e->song.system[i])) { + 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); + } + } else if (minVersion==0) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("this system is not supported by the VGM format!"); } @@ -4466,7 +2396,7 @@ bool FurnaceGUI::loop() { } } ImGui::Text("select the systems you wish to export,"); - ImGui::Text("but only up to 2 of each type."); + ImGui::Text("but only up to %d of each type.",(vgmExportVersion>=0x151)?2:1); if (hasOneAtLeast) { if (ImGui::MenuItem("click to export")) { openFileDialog(GUI_FILE_EXPORT_VGM); @@ -4478,268 +2408,15 @@ bool FurnaceGUI::loop() { } ImGui::Separator(); if (ImGui::BeginMenu("add system...")) { - sysAddOption(DIV_SYSTEM_YM2612); - sysAddOption(DIV_SYSTEM_YM2612_EXT); - sysAddOption(DIV_SYSTEM_SMS); - sysAddOption(DIV_SYSTEM_GB); - sysAddOption(DIV_SYSTEM_PCE); - sysAddOption(DIV_SYSTEM_NES); - sysAddOption(DIV_SYSTEM_C64_8580); - sysAddOption(DIV_SYSTEM_C64_6581); - sysAddOption(DIV_SYSTEM_YM2151); - sysAddOption(DIV_SYSTEM_SEGAPCM); - sysAddOption(DIV_SYSTEM_SEGAPCM_COMPAT); - sysAddOption(DIV_SYSTEM_YM2610); - sysAddOption(DIV_SYSTEM_YM2610_EXT); - sysAddOption(DIV_SYSTEM_YM2610_FULL); - sysAddOption(DIV_SYSTEM_YM2610_FULL_EXT); - sysAddOption(DIV_SYSTEM_AY8910); - sysAddOption(DIV_SYSTEM_AMIGA); - sysAddOption(DIV_SYSTEM_OPLL); - sysAddOption(DIV_SYSTEM_VRC7); - sysAddOption(DIV_SYSTEM_TIA); - sysAddOption(DIV_SYSTEM_SAA1099); - sysAddOption(DIV_SYSTEM_AY8930); - sysAddOption(DIV_SYSTEM_LYNX); - sysAddOption(DIV_SYSTEM_QSOUND); + for (int j=0; availableSystems[j]; j++) { + sysAddOption((DivSystem)availableSystems[j]); + } ImGui::EndMenu(); } if (ImGui::BeginMenu("configure system...")) { 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())) { - unsigned int flags=e->song.systemFlags[i]; - bool restart=settings.restartOnFlagChange; - bool sysPal=flags&1; - switch (e->song.system[i]) { - case DIV_SYSTEM_YM2612: - case DIV_SYSTEM_YM2612_EXT: { - if (ImGui::RadioButton("NTSC (7.67MHz)",(flags&3)==0)) { - e->setSysFlags(i,(flags&0x80000000)|0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("PAL (7.61MHz)",(flags&3)==1)) { - e->setSysFlags(i,(flags&0x80000000)|1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("FM Towns (8MHz)",(flags&3)==2)) { - e->setSysFlags(i,(flags&0x80000000)|2,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("AtGames Genesis (6.13MHz)",(flags&3)==3)) { - e->setSysFlags(i,(flags&0x80000000)|3,restart); - updateWindowTitle(); - } - bool ladder=flags&0x80000000; - if (ImGui::Checkbox("Enable DAC distortion",&ladder)) { - e->setSysFlags(i,(flags&(~0x80000000))|(ladder?0x80000000:0),restart); - updateWindowTitle(); - } - break; - } - case DIV_SYSTEM_SMS: { - ImGui::Text("Clock rate:"); - if (ImGui::RadioButton("NTSC (3.58MHz)",(flags&3)==0)) { - e->setSysFlags(i,(flags&(~3))|0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("PAL (3.55MHz)",(flags&3)==1)) { - e->setSysFlags(i,(flags&(~3))|1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("BBC Micro (4MHz)",(flags&3)==2)) { - e->setSysFlags(i,(flags&(~3))|2,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("Half NTSC (1.79MHz)",(flags&3)==3)) { - e->setSysFlags(i,(flags&(~3))|3,restart); - updateWindowTitle(); - } - ImGui::Text("Chip type:"); - if (ImGui::RadioButton("Sega VDP/Master System",((flags>>2)&3)==0)) { - e->setSysFlags(i,(flags&(~12))|0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("TI SN76489",((flags>>2)&3)==1)) { - e->setSysFlags(i,(flags&(~12))|4,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("TI SN76489 with Atari-like short noise",((flags>>2)&3)==2)) { - e->setSysFlags(i,(flags&(~12))|8,restart); - updateWindowTitle(); - } - /*if (ImGui::RadioButton("Game Gear",(flags>>2)==3)) { - e->setSysFlags(i,(flags&3)|12); - }*/ - - bool noPhaseReset=flags&16; - if (ImGui::Checkbox("Disable noise period change phase reset",&noPhaseReset)) { - e->setSysFlags(i,(flags&(~16))|(noPhaseReset<<4),restart); - updateWindowTitle(); - } - break; - } - case DIV_SYSTEM_YM2151: - if (ImGui::RadioButton("NTSC (3.58MHz)",flags==0)) { - e->setSysFlags(i,0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("PAL (3.55MHz)",flags==1)) { - e->setSysFlags(i,1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("X68000 (4MHz)",flags==2)) { - e->setSysFlags(i,2,restart); - updateWindowTitle(); - } - break; - case DIV_SYSTEM_NES: - if (ImGui::RadioButton("NTSC (1.79MHz)",flags==0)) { - e->setSysFlags(i,0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("PAL (1.67MHz)",flags==1)) { - e->setSysFlags(i,1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("Dendy (1.77MHz)",flags==2)) { - e->setSysFlags(i,2,restart); - updateWindowTitle(); - } - break; - case DIV_SYSTEM_AY8910: - case DIV_SYSTEM_AY8930: { - ImGui::Text("Clock rate:"); - if (ImGui::RadioButton("1.79MHz (ZX Spectrum NTSC/MSX)",(flags&15)==0)) { - e->setSysFlags(i,(flags&(~15))|0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("1.77MHz (ZX Spectrum)",(flags&15)==1)) { - e->setSysFlags(i,(flags&(~15))|1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("1.75MHz (ZX Spectrum)",(flags&15)==2)) { - e->setSysFlags(i,(flags&(~15))|2,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("2MHz (Atari ST)",(flags&15)==3)) { - e->setSysFlags(i,(flags&(~15))|3,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("1.5MHz (Vectrex)",(flags&15)==4)) { - e->setSysFlags(i,(flags&(~15))|4,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("1MHz (Amstrad CPC)",(flags&15)==5)) { - e->setSysFlags(i,(flags&(~15))|5,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("0.89MHz (Sunsoft 5B)",(flags&15)==6)) { - e->setSysFlags(i,(flags&(~15))|6,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("1.67MHz (?)",(flags&15)==7)) { - e->setSysFlags(i,(flags&(~15))|7,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("0.83MHz (Sunsoft 5B on PAL)",(flags&15)==8)) { - e->setSysFlags(i,(flags&(~15))|8,restart); - updateWindowTitle(); - } - if (e->song.system[i]==DIV_SYSTEM_AY8910) { - ImGui::Text("Chip type:"); - if (ImGui::RadioButton("AY-3-8910",(flags&0x30)==0)) { - e->setSysFlags(i,(flags&(~0x30))|0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("YM2149(F)",(flags&0x30)==16)) { - e->setSysFlags(i,(flags&(~0x30))|16,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("Sunsoft 5B",(flags&0x30)==32)) { - e->setSysFlags(i,(flags&(~0x30))|32,restart); - updateWindowTitle(); - } - } - bool stereo=flags&0x40; - ImGui::BeginDisabled((flags&0x30)==32); - if (ImGui::Checkbox("Stereo##_AY_STEREO",&stereo)) { - e->setSysFlags(i,(flags&(~0x40))|(stereo?0x40:0),restart); - updateWindowTitle(); - } - ImGui::EndDisabled(); - break; - } - case DIV_SYSTEM_SAA1099: - if (ImGui::RadioButton("SAM Coupé (8MHz)",flags==0)) { - e->setSysFlags(i,0,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("NTSC (7.15MHz)",flags==1)) { - e->setSysFlags(i,1,restart); - updateWindowTitle(); - } - if (ImGui::RadioButton("PAL (7.09MHz)",flags==2)) { - e->setSysFlags(i,2,restart); - updateWindowTitle(); - } - break; - case DIV_SYSTEM_AMIGA: { - ImGui::Text("Stereo separation:"); - int stereoSep=(flags>>8)&127; - if (ImGui::SliderInt("##StereoSep",&stereoSep,0,127)) { - if (stereoSep<0) stereoSep=0; - if (stereoSep>127) stereoSep=127; - e->setSysFlags(i,(flags&1)|((stereoSep&127)<<8),restart); - updateWindowTitle(); - } - /* TODO LATER: I want 0.5 out already - if (ImGui::RadioButton("Amiga 500 (OCS)",(flags&2)==0)) { - e->setSysFlags(i,flags&1); - } - if (ImGui::RadioButton("Amiga 1200 (AGA)",(flags&2)==2)) { - e->setSysFlags(i,(flags&1)|2); - }*/ - sysPal=flags&1; - if (ImGui::Checkbox("PAL",&sysPal)) { - e->setSysFlags(i,(flags&2)|sysPal,restart); - updateWindowTitle(); - } - break; - } - case DIV_SYSTEM_QSOUND: { - ImGui::Text("Echo delay:"); - int echoBufSize=2725 - (flags & 4095); - if (ImGui::SliderInt("##EchoBufSize",&echoBufSize,0,2725)) { - if (echoBufSize<0) echoBufSize=0; - if (echoBufSize>2725) echoBufSize=2725; - e->setSysFlags(i,(flags & ~4095) | ((2725 - echoBufSize) & 4095),restart); - updateWindowTitle(); - } - ImGui::Text("Echo feedback:"); - int echoFeedback=(flags>>12)&255; - if (ImGui::SliderInt("##EchoFeedback",&echoFeedback,0,255)) { - if (echoFeedback<0) echoFeedback=0; - if (echoFeedback>255) echoFeedback=255; - e->setSysFlags(i,(flags & ~(255 << 12)) | ((echoFeedback & 255) << 12),restart); - updateWindowTitle(); - } - break; - } - case DIV_SYSTEM_GB: - case DIV_SYSTEM_YM2610: - case DIV_SYSTEM_YM2610_EXT: - case DIV_SYSTEM_YM2610_FULL: - case DIV_SYSTEM_YM2610_FULL_EXT: - case DIV_SYSTEM_YMU759: - ImGui::Text("nothing to configure"); - break; - default: - if (ImGui::Checkbox("PAL",&sysPal)) { - e->setSysFlags(i,sysPal,restart); - updateWindowTitle(); - } - break; - } + drawSysConf(i); ImGui::TreePop(); } } @@ -4748,30 +2425,9 @@ bool FurnaceGUI::loop() { if (ImGui::BeginMenu("change system...")) { 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())) { - sysChangeOption(i,DIV_SYSTEM_YM2612); - sysChangeOption(i,DIV_SYSTEM_YM2612_EXT); - sysChangeOption(i,DIV_SYSTEM_SMS); - sysChangeOption(i,DIV_SYSTEM_GB); - sysChangeOption(i,DIV_SYSTEM_PCE); - sysChangeOption(i,DIV_SYSTEM_NES); - sysChangeOption(i,DIV_SYSTEM_C64_8580); - sysChangeOption(i,DIV_SYSTEM_C64_6581); - sysChangeOption(i,DIV_SYSTEM_YM2151); - sysChangeOption(i,DIV_SYSTEM_SEGAPCM); - sysChangeOption(i,DIV_SYSTEM_SEGAPCM_COMPAT); - sysChangeOption(i,DIV_SYSTEM_YM2610); - sysChangeOption(i,DIV_SYSTEM_YM2610_EXT); - sysChangeOption(i,DIV_SYSTEM_YM2610_FULL); - sysChangeOption(i,DIV_SYSTEM_YM2610_FULL_EXT); - sysChangeOption(i,DIV_SYSTEM_AY8910); - sysChangeOption(i,DIV_SYSTEM_AMIGA); - sysChangeOption(i,DIV_SYSTEM_OPLL); - sysChangeOption(i,DIV_SYSTEM_VRC7); - sysChangeOption(i,DIV_SYSTEM_TIA); - sysChangeOption(i,DIV_SYSTEM_SAA1099); - sysChangeOption(i,DIV_SYSTEM_AY8930); - sysChangeOption(i,DIV_SYSTEM_LYNX); - sysChangeOption(i,DIV_SYSTEM_QSOUND); + for (int j=0; availableSystems[j]; j++) { + sysChangeOption(i,(DivSystem)availableSystems[j]); + } ImGui::EndMenu(); } } @@ -4788,6 +2444,10 @@ bool FurnaceGUI::loop() { ImGui::EndMenu(); } ImGui::Separator(); + if (ImGui::MenuItem("restore backup",BIND_FOR(GUI_ACTION_OPEN_BACKUP))) { + doAction(GUI_ACTION_OPEN_BACKUP); + } + ImGui::Separator(); if (ImGui::MenuItem("exit")) { if (modified) { showWarning("Unsaved changes! Are you sure you want to quit?",GUI_WARN_QUIT); @@ -4801,24 +2461,23 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("undo",BIND_FOR(GUI_ACTION_UNDO))) doUndo(); if (ImGui::MenuItem("redo",BIND_FOR(GUI_ACTION_REDO))) doRedo(); ImGui::Separator(); - if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_PAT_CUT))) doCopy(true); - if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_PAT_COPY))) doCopy(false); - if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_PAT_PASTE))) doPaste(); - if (ImGui::MenuItem("delete",BIND_FOR(GUI_ACTION_PAT_DELETE))) doDelete(); - if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_PAT_SELECT_ALL))) doSelectAll(); - ImGui::Separator(); - if (ImGui::MenuItem("note up",BIND_FOR(GUI_ACTION_PAT_NOTE_UP))) doTranspose(1); - if (ImGui::MenuItem("note down",BIND_FOR(GUI_ACTION_PAT_NOTE_DOWN))) doTranspose(-1); - if (ImGui::MenuItem("octave up",BIND_FOR(GUI_ACTION_PAT_OCTAVE_UP))) doTranspose(12); - if (ImGui::MenuItem("octave down",BIND_FOR(GUI_ACTION_PAT_OCTAVE_DOWN))) doTranspose(-12); + editOptions(true); /*ImGui::Separator(); ImGui::MenuItem("clear...");*/ ImGui::EndMenu(); } if (ImGui::BeginMenu("settings")) { + if (ImGui::MenuItem("lock layout",NULL,lockLayout)) { + lockLayout=!lockLayout; + } + if (ImGui::MenuItem("visualizer",NULL,fancyPattern)) { + fancyPattern=!fancyPattern; + e->enableCommandStream(fancyPattern); + e->getCommandStream(cmdStream); + cmdStream.clear(); + } if (ImGui::MenuItem("reset layout")) { - ImGui::LoadIniSettingsFromMemory(defaultLayout); - ImGui::SaveIniSettingsToDisk(finalLayoutPath); + showWarning("Are you sure you want to reset the workspace layout?",GUI_WARN_RESET_LAYOUT); } if (ImGui::MenuItem("settings...",BIND_FOR(GUI_ACTION_WINDOW_SETTINGS))) { syncSettings(); @@ -4847,6 +2506,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem("oscilloscope",BIND_FOR(GUI_ACTION_WINDOW_OSCILLOSCOPE),oscOpen)) oscOpen=!oscOpen; if (ImGui::MenuItem("volume meter",BIND_FOR(GUI_ACTION_WINDOW_VOL_METER),volMeterOpen)) volMeterOpen=!volMeterOpen; if (ImGui::MenuItem("register view",BIND_FOR(GUI_ACTION_WINDOW_REGISTER_VIEW),regViewOpen)) regViewOpen=!regViewOpen; + 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; ImGui::EndMenu(); @@ -4864,7 +2524,7 @@ bool FurnaceGUI::loop() { if (e->isPlaying()) { int totalTicks=e->getTotalTicks(); int totalSeconds=e->getTotalSeconds(); - ImGui::Text("| Speed %d:%d @ %dHz | Order %d/%d | Row %d/%d | %d:%.2d:%.2d.%.2d",e->getSpeed1(),e->getSpeed2(),e->getCurHz(),e->getOrder(),e->song.ordersLen,e->getRow(),e->song.patLen,totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); + ImGui::Text("| Speed %d:%d @ %gHz (%g BPM) | Order %d/%d | Row %d/%d | %d:%.2d:%.2d.%.2d",e->getSpeed1(),e->getSpeed2(),e->getCurHz(),calcBPM(e->getSpeed1(),e->getSpeed2(),e->getCurHz()),e->getOrder(),e->song.ordersLen,e->getRow(),e->song.patLen,totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); } else { bool hasInfo=false; String info; @@ -4928,21 +2588,24 @@ bool FurnaceGUI::loop() { } ImGui::EndMainMenuBar(); - ImGui::DockSpaceOverViewport(); + ImGui::DockSpaceOverViewport(NULL,lockLayout?(ImGuiDockNodeFlags_NoResize|ImGuiDockNodeFlags_NoCloseButton|ImGuiDockNodeFlags_NoDocking|ImGuiDockNodeFlags_NoDockingSplitMe|ImGuiDockNodeFlags_NoDockingSplitOther):0); + drawPattern(); drawEditControls(); drawSongInfo(); drawOrders(); - drawInsList(); - drawInsEdit(); - drawWaveList(); - drawWaveEdit(); drawSampleList(); drawSampleEdit(); + drawWaveList(); + drawWaveEdit(); + drawInsList(); + drawInsEdit(); drawMixer(); + + readOsc(); + drawOsc(); drawVolMeter(); - drawPattern(); drawSettings(); drawDebug(); drawStats(); @@ -4951,18 +2614,71 @@ bool FurnaceGUI::loop() { drawNotes(); drawChannels(); drawRegView(); + drawLog(); - if (ImGuiFileDialog::Instance()->Display("FileDialog",ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove,ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { + if (inspectorOpen) ImGui::ShowMetricsWindow(&inspectorOpen); + + if (firstFrame) { + firstFrame=false; + if (patternOpen) nextWindow=GUI_WINDOW_PATTERN; +#ifdef __APPLE__ + SDL_RaiseWindow(sdlWin); +#endif + } + + if (fileDialog->render(ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale))) { //ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_NavEnableKeyboard; - if (ImGuiFileDialog::Instance()->IsOk()) { - fileName=ImGuiFileDialog::Instance()->GetFilePathName(); + switch (curFileDialog) { + case GUI_FILE_OPEN: + case GUI_FILE_SAVE: + case GUI_FILE_SAVE_DMF_LEGACY: + workingDirSong=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_INS_OPEN: + case GUI_FILE_INS_SAVE: + workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_WAVE_OPEN: + case GUI_FILE_WAVE_SAVE: + workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_SAMPLE_OPEN: + case GUI_FILE_SAMPLE_SAVE: + workingDirSample=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_EXPORT_AUDIO_ONE: + case GUI_FILE_EXPORT_AUDIO_PER_SYS: + case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: + 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_LOAD_MAIN_FONT: + case GUI_FILE_LOAD_PAT_FONT: + workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_IMPORT_COLORS: + case GUI_FILE_EXPORT_COLORS: + workingDirColors=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_IMPORT_KEYBINDS: + case GUI_FILE_EXPORT_KEYBINDS: + workingDirKeybinds=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + case GUI_FILE_IMPORT_LAYOUT: + case GUI_FILE_EXPORT_LAYOUT: + workingDirLayout=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; + } + if (fileDialog->accepted()) { + fileName=fileDialog->getFileName(); if (fileName!="") { if (curFileDialog==GUI_FILE_SAVE) { - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { - checkExtension(".fur"); - } else { - checkExtension(".dmf"); - } + // we can't tell whether the user chose .dmf or .fur in the system file picker + const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song")?".fur":".dmf"; + checkExtensionDual(".fur",".dmf",fallbackExt); } if (curFileDialog==GUI_FILE_SAVE_DMF_LEGACY) { checkExtension(".dmf"); @@ -4982,6 +2698,15 @@ bool FurnaceGUI::loop() { if (curFileDialog==GUI_FILE_EXPORT_VGM) { checkExtension(".vgm"); } + if (curFileDialog==GUI_FILE_EXPORT_COLORS) { + checkExtension(".cfgc"); + } + if (curFileDialog==GUI_FILE_EXPORT_KEYBINDS) { + checkExtension(".cfgk"); + } + if (curFileDialog==GUI_FILE_EXPORT_LAYOUT) { + checkExtension(".ini"); + } String copyOfName=fileName; switch (curFileDialog) { case GUI_FILE_OPEN: @@ -4989,20 +2714,25 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf("Error while loading file! (%s)",lastError)); } break; - case GUI_FILE_SAVE: - printf("saving: %s\n",copyOfName.c_str()); - if (ImGuiFileDialog::Instance()->GetCurrentFilter()=="Furnace song") { + case GUI_FILE_SAVE: { + logD("saving: %s",copyOfName.c_str()); + String lowerCase=fileName; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + if ((lowerCase.size()<4 || lowerCase.rfind(".dmf")!=lowerCase.size()-4)) { if (save(copyOfName,0)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } } else { - if (save(copyOfName,25)>0) { + if (save(copyOfName,26)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } } break; + } case GUI_FILE_SAVE_DMF_LEGACY: - printf("saving: %s\n",copyOfName.c_str()); + logD("saving: %s",copyOfName.c_str()); if (save(copyOfName,24)>0) { showError(fmt::sprintf("Error while saving file! (%s)",lastError)); } @@ -5018,8 +2748,11 @@ bool FurnaceGUI::loop() { } break; case GUI_FILE_SAMPLE_OPEN: - e->addSampleFromFile(copyOfName.c_str()); - modified=true; + if (e->addSampleFromFile(copyOfName.c_str())==-1) { + showError(e->getLastError()); + } else { + MARK_MODIFIED; + } break; case GUI_FILE_SAMPLE_SAVE: if (curSample>=0 && curSample<(int)e->song.sample.size()) { @@ -5035,23 +2768,28 @@ bool FurnaceGUI::loop() { case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: exportAudio(copyOfName,DIV_EXPORT_MODE_MANY_CHAN); break; - case GUI_FILE_INS_OPEN: - if (e->addInstrumentFromFile(copyOfName.c_str())) { + case GUI_FILE_INS_OPEN: { + std::vector instruments=e->instrumentFromFile(copyOfName.c_str()); + if (!instruments.empty()) { if (!e->getWarnings().empty()) { showWarning(e->getWarnings(),GUI_WARN_GENERIC); } + for (DivInstrument* i: instruments) { + e->addInstrumentPtr(i); + } } else { showError("cannot load instrument! ("+e->getLastError()+")"); } break; + } case GUI_FILE_WAVE_OPEN: e->addWaveFromFile(copyOfName.c_str()); - modified=true; + MARK_MODIFIED; break; case GUI_FILE_EXPORT_VGM: { - SafeWriter* w=e->saveVGM(willExport,vgmExportLoop); + SafeWriter* w=e->saveVGM(willExport,vgmExportLoop,vgmExportVersion); if (w!=NULL) { - FILE* f=fopen(copyOfName.c_str(),"wb"); + FILE* f=ps_fopen(copyOfName.c_str(),"wb"); if (f!=NULL) { fwrite(w->getFinalBuf(),1,w->size(),f); fclose(f); @@ -5064,7 +2802,7 @@ bool FurnaceGUI::loop() { showWarning(e->getWarnings(),GUI_WARN_GENERIC); } } else { - showError("could not write VGM. dang it."); + showError(fmt::sprintf("could not write VGM! (%s)",e->getLastError())); } break; } @@ -5077,17 +2815,29 @@ bool FurnaceGUI::loop() { case GUI_FILE_LOAD_PAT_FONT: settings.patFontPath=copyOfName; break; + case GUI_FILE_IMPORT_COLORS: + importColors(copyOfName); + break; + case GUI_FILE_IMPORT_KEYBINDS: + importKeybinds(copyOfName); + break; + case GUI_FILE_IMPORT_LAYOUT: + importLayout(copyOfName); + break; + case GUI_FILE_EXPORT_COLORS: + exportColors(copyOfName); + break; + case GUI_FILE_EXPORT_KEYBINDS: + exportKeybinds(copyOfName); + break; + case GUI_FILE_EXPORT_LAYOUT: + exportLayout(copyOfName); + break; } curFileDialog=GUI_FILE_OPEN; } } - workingDir=ImGuiFileDialog::Instance()->GetCurrentPath(); -#ifdef _WIN32 - workingDir+='\\'; -#else - workingDir+='/'; -#endif - ImGuiFileDialog::Instance()->Close(); + fileDialog->close(); } if (warnQuit) { @@ -5105,6 +2855,11 @@ bool FurnaceGUI::loop() { ImGui::OpenPopup("Rendering..."); } + if (displayNew) { + displayNew=false; + ImGui::OpenPopup("New Song"); + } + if (nextWindow==GUI_WINDOW_ABOUT) { aboutOpen=true; nextWindow=GUI_WINDOW_NOTHING; @@ -5112,7 +2867,7 @@ bool FurnaceGUI::loop() { if (aboutOpen) drawAbout(); if (ImGui::BeginPopupModal("Rendering...",NULL,ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Please wait...\n"); + ImGui::Text("Please wait..."); if (ImGui::Button("Abort")) { if (e->haltAudioFile()) { ImGui::CloseCurrentPopup(); @@ -5124,6 +2879,13 @@ bool FurnaceGUI::loop() { ImGui::EndPopup(); } + ImGui::SetNextWindowSizeConstraints(ImVec2(400.0f*dpiScale,200.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (ImGui::BeginPopupModal("New Song",NULL,ImGuiWindowFlags_NoMove)) { + ImGui::SetWindowPos(ImVec2(((scrW*dpiScale)-ImGui::GetWindowSize().x)*0.5,((scrH*dpiScale)-ImGui::GetWindowSize().y)*0.5)); + drawNewSong(); + ImGui::EndPopup(); + } + if (ImGui::BeginPopupModal("Error",NULL,ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("%s",errorString.c_str()); if (ImGui::Button("OK")) { @@ -5141,28 +2903,33 @@ bool FurnaceGUI::loop() { quit=true; break; case GUI_WARN_NEW: - e->createNew(); - undoHist.clear(); - redoHist.clear(); - curFileName=""; - modified=false; - curNibble=false; - orderNibble=false; - orderCursor=-1; - selStart=SelectionPoint(); - selEnd=SelectionPoint(); - cursor=SelectionPoint(); - updateWindowTitle(); + displayNew=true; break; case GUI_WARN_OPEN: openFileDialog(GUI_FILE_OPEN); break; + case GUI_WARN_OPEN_BACKUP: + if (load(backupPath)>0) { + showError("No backup available! (or unable to open it)"); + } + break; case GUI_WARN_OPEN_DROP: if (load(nextFile)>0) { showError(fmt::sprintf("Error while loading file! (%s)",lastError)); } nextFile=""; break; + case GUI_WARN_RESET_LAYOUT: + ImGui::LoadIniSettingsFromMemory(defaultLayout); + ImGui::SaveIniSettingsToDisk(finalLayoutPath); + break; + case GUI_WARN_RESET_KEYBINDS: + resetKeybinds(); + break; + case GUI_WARN_RESET_COLORS: + resetColors(); + applyUISettings(false); + break; case GUI_WARN_GENERIC: break; } @@ -5176,6 +2943,39 @@ bool FurnaceGUI::loop() { ImGui::EndPopup(); } + // backup trigger + if (modified) { + if (backupTimer>0) { + backupTimer-=ImGui::GetIO().DeltaTime; + if (backupTimer<=0) { + backupTask=std::async(std::launch::async,[this]() -> bool { + if (backupPath==curFileName) { + logD("backup file open. not saving backup."); + return true; + } + logD("saving backup..."); + SafeWriter* w=e->saveFur(true); + + if (w!=NULL) { + FILE* outFile=ps_fopen(backupPath.c_str(),"wb"); + if (outFile!=NULL) { + if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) { + logW("did not write backup entirely: %s!",strerror(errno)); + w->finish(); + } + fclose(outFile); + } else { + logW("could not save backup: %s!",strerror(errno)); + w->finish(); + } + } + backupTimer=30.0; + return true; + }); + } + } + } + SDL_SetRenderDrawColor(sdlRend,uiColors[GUI_COLOR_BACKGROUND].x*255, uiColors[GUI_COLOR_BACKGROUND].y*255, uiColors[GUI_COLOR_BACKGROUND].z*255, @@ -5187,364 +2987,38 @@ bool FurnaceGUI::loop() { if (--soloTimeout<0) soloTimeout=0; + wheelX=0; + wheelY=0; + if (willCommit) { commitSettings(); willCommit=false; } + + if (SDL_GetWindowFlags(sdlWin)&SDL_WINDOW_MINIMIZED) { + SDL_Delay(100); + } } return false; } -void FurnaceGUI::parseKeybinds() { - actionMapGlobal.clear(); - actionMapPat.clear(); - actionMapInsList.clear(); - actionMapWaveList.clear(); - actionMapSampleList.clear(); - actionMapOrders.clear(); - - for (int i=GUI_ACTION_GLOBAL_MIN+1; igetConfInt(#target,ImGui::GetColorU32(def))); - -#ifdef _WIN32 -#define SYSTEM_FONT_PATH_1 "C:\\Windows\\Fonts\\segoeui.ttf" -#define SYSTEM_FONT_PATH_2 "C:\\Windows\\Fonts\\tahoma.ttf" -// TODO! -#define SYSTEM_FONT_PATH_3 "C:\\Windows\\Fonts\\tahoma.ttf" -// TODO! -#define SYSTEM_PAT_FONT_PATH_1 "C:\\Windows\\Fonts\\consola.ttf" -#define SYSTEM_PAT_FONT_PATH_2 "C:\\Windows\\Fonts\\cour.ttf" -// GOOD LUCK WITH THIS ONE - UNTESTED -#define SYSTEM_PAT_FONT_PATH_3 "C:\\Windows\\Fonts\\vgasys.fon" -#elif defined(__APPLE__) -#define SYSTEM_FONT_PATH_1 "/System/Library/Fonts/SFAANS.ttf" -#define SYSTEM_FONT_PATH_2 "/System/Library/Fonts/Helvetica.ttc" -#define SYSTEM_FONT_PATH_3 "/System/Library/Fonts/Helvetica.dfont" -#define SYSTEM_PAT_FONT_PATH_1 "/System/Library/Fonts/SFNSMono.ttf" -#define SYSTEM_PAT_FONT_PATH_2 "/System/Library/Fonts/Courier New.ttf" -#define SYSTEM_PAT_FONT_PATH_3 "/System/Library/Fonts/Courier New.ttf" -#else -#define SYSTEM_FONT_PATH_1 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" -#define SYSTEM_FONT_PATH_2 "/usr/share/fonts/TTF/DejaVuSans.ttf" -#define SYSTEM_FONT_PATH_3 "/usr/share/fonts/ubuntu/Ubuntu-R.ttf" -#define SYSTEM_PAT_FONT_PATH_1 "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" -#define SYSTEM_PAT_FONT_PATH_2 "/usr/share/fonts/TTF/DejaVuSansMono.ttf" -#define SYSTEM_PAT_FONT_PATH_3 "/usr/share/fonts/ubuntu/UbuntuMono-R.ttf" -#endif - -void FurnaceGUI::applyUISettings() { - ImGuiStyle sty; - ImGui::StyleColorsDark(&sty); - - if (settings.dpiScale>=0.5f) dpiScale=settings.dpiScale; - - GET_UI_COLOR(GUI_COLOR_BACKGROUND,ImVec4(0.1f,0.1f,0.1f,1.0f)); - GET_UI_COLOR(GUI_COLOR_FRAME_BACKGROUND,ImVec4(0.0f,0.0f,0.0f,0.85f)); - GET_UI_COLOR(GUI_COLOR_MODAL_BACKDROP,ImVec4(0.0f,0.0f,0.0f,0.55f)); - GET_UI_COLOR(GUI_COLOR_HEADER,ImVec4(0.2f,0.2f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_TEXT,ImVec4(1.0f,1.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_ACCENT_PRIMARY,ImVec4(0.06f,0.53f,0.98f,1.0f)); - GET_UI_COLOR(GUI_COLOR_ACCENT_SECONDARY,ImVec4(0.26f,0.59f,0.98f,1.0f)); - GET_UI_COLOR(GUI_COLOR_EDITING,ImVec4(0.2f,0.1f,0.1f,1.0f)); - GET_UI_COLOR(GUI_COLOR_SONG_LOOP,ImVec4(0.3f,0.5f,0.8f,0.4f)); - GET_UI_COLOR(GUI_COLOR_VOLMETER_LOW,ImVec4(0.2f,0.6f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_VOLMETER_HIGH,ImVec4(1.0f,0.9f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_VOLMETER_PEAK,ImVec4(1.0f,0.1f,0.1f,1.0f)); - GET_UI_COLOR(GUI_COLOR_MACRO_VOLUME,ImVec4(0.2f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_MACRO_PITCH,ImVec4(1.0f,0.8f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_MACRO_OTHER,ImVec4(0.0f,0.9f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_MACRO_WAVE,ImVec4(1.0f,0.4f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_FM,ImVec4(0.6f,0.9f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_STD,ImVec4(0.6f,1.0f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_GB,ImVec4(1.0f,1.0f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_C64,ImVec4(0.85f,0.8f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_AMIGA,ImVec4(1.0f,0.5f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_PCE,ImVec4(1.0f,0.8f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_AY,ImVec4(1.0f,0.5f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_AY8930,ImVec4(0.7f,0.5f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_TIA,ImVec4(1.0f,0.6f,0.4f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_SAA1099,ImVec4(0.3f,0.3f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_VIC,ImVec4(0.2f,1.0f,0.6f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_PET,ImVec4(1.0f,1.0f,0.8f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_VRC6,ImVec4(1.0f,0.9f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_OPLL,ImVec4(0.6f,0.7f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_OPL,ImVec4(0.3f,1.0f,0.9f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_FDS,ImVec4(0.8f,0.5f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_VBOY,ImVec4(1.0f,0.1f,0.1f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_N163,ImVec4(1.0f,0.4f,0.1f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_SCC,ImVec4(0.7f,1.0f,0.3f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_OPZ,ImVec4(0.2f,0.8f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_POKEY,ImVec4(0.5f,1.0f,0.3f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_BEEPER,ImVec4(0.0f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_SWAN,ImVec4(0.3f,0.5f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_MIKEY,ImVec4(0.5f,1.0f,0.3f,1.0f)); - GET_UI_COLOR(GUI_COLOR_INSTR_UNKNOWN,ImVec4(0.3f,0.3f,0.3f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_FM,ImVec4(0.2f,0.8f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_PULSE,ImVec4(0.4f,1.0f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_NOISE,ImVec4(0.8f,0.8f,0.8f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_PCM,ImVec4(1.0f,0.9f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_WAVE,ImVec4(1.0f,0.5f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_OP,ImVec4(0.2f,0.4f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_CHANNEL_MUTED,ImVec4(0.5f,0.5f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_CURSOR,ImVec4(0.1f,0.3f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_CURSOR_HOVER,ImVec4(0.2f,0.4f,0.6f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_CURSOR_ACTIVE,ImVec4(0.2f,0.5f,0.7f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_SELECTION,ImVec4(0.15f,0.15f,0.2f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_SELECTION_HOVER,ImVec4(0.2f,0.2f,0.3f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_SELECTION_ACTIVE,ImVec4(0.4f,0.4f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_HI_1,ImVec4(0.6f,0.6f,0.6f,0.2f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_HI_2,ImVec4(0.5f,0.8f,1.0f,0.2f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_ROW_INDEX,ImVec4(0.5f,0.8f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_ACTIVE,ImVec4(1.0f,1.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_INACTIVE,ImVec4(0.5f,0.5f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_INS,ImVec4(0.4f,0.7f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_MIN,ImVec4(0.0f,0.5f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_HALF,ImVec4(0.0f,0.75f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_MAX,ImVec4(0.0f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_INVALID,ImVec4(1.0f,0.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_PITCH,ImVec4(1.0f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_VOLUME,ImVec4(0.0f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_PANNING,ImVec4(0.0f,1.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SONG,ImVec4(1.0f,0.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_TIME,ImVec4(0.5f,0.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SPEED,ImVec4(1.0f,0.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY,ImVec4(0.5f,1.0f,0.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY,ImVec4(0.0f,1.0f,0.5f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_MISC,ImVec4(0.3f,0.3f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_EE_VALUE,ImVec4(0.0f,1.0f,1.0f,1.0f)); - GET_UI_COLOR(GUI_COLOR_PLAYBACK_STAT,ImVec4(0.6f,0.6f,0.6f,1.0f)); - - for (int i=0; i<64; i++) { - ImVec4 col1=uiColors[GUI_COLOR_PATTERN_VOLUME_MIN]; - ImVec4 col2=uiColors[GUI_COLOR_PATTERN_VOLUME_HALF]; - ImVec4 col3=uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]; - volColors[i]=ImVec4(col1.x+((col2.x-col1.x)*float(i)/64.0f), - col1.y+((col2.y-col1.y)*float(i)/64.0f), - col1.z+((col2.z-col1.z)*float(i)/64.0f), - 1.0f); - volColors[i+64]=ImVec4(col2.x+((col3.x-col2.x)*float(i)/64.0f), - col2.y+((col3.y-col2.y)*float(i)/64.0f), - col2.z+((col3.z-col2.z)*float(i)/64.0f), - 1.0f); - } - - float hue, sat, val; - - ImVec4 primaryActive=uiColors[GUI_COLOR_ACCENT_PRIMARY]; - ImVec4 primaryHover, primary; - primaryHover.w=primaryActive.w; - primary.w=primaryActive.w; - ImGui::ColorConvertRGBtoHSV(primaryActive.x,primaryActive.y,primaryActive.z,hue,sat,val); - ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.5,primaryHover.x,primaryHover.y,primaryHover.z); - ImGui::ColorConvertHSVtoRGB(hue,sat*0.8,val*0.35,primary.x,primary.y,primary.z); - - ImVec4 secondaryActive=uiColors[GUI_COLOR_ACCENT_SECONDARY]; - ImVec4 secondaryHover, secondary, secondarySemiActive; - secondarySemiActive.w=secondaryActive.w; - secondaryHover.w=secondaryActive.w; - secondary.w=secondaryActive.w; - ImGui::ColorConvertRGBtoHSV(secondaryActive.x,secondaryActive.y,secondaryActive.z,hue,sat,val); - ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.75,secondarySemiActive.x,secondarySemiActive.y,secondarySemiActive.z); - ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.5,secondaryHover.x,secondaryHover.y,secondaryHover.z); - ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.25,secondary.x,secondary.y,secondary.z); - - - sty.Colors[ImGuiCol_WindowBg]=uiColors[GUI_COLOR_FRAME_BACKGROUND]; - sty.Colors[ImGuiCol_ModalWindowDimBg]=uiColors[GUI_COLOR_MODAL_BACKDROP]; - sty.Colors[ImGuiCol_Text]=uiColors[GUI_COLOR_TEXT]; - - sty.Colors[ImGuiCol_Button]=primary; - sty.Colors[ImGuiCol_ButtonHovered]=primaryHover; - sty.Colors[ImGuiCol_ButtonActive]=primaryActive; - sty.Colors[ImGuiCol_Tab]=primary; - sty.Colors[ImGuiCol_TabHovered]=secondaryHover; - sty.Colors[ImGuiCol_TabActive]=secondarySemiActive; - sty.Colors[ImGuiCol_TabUnfocused]=primary; - sty.Colors[ImGuiCol_TabUnfocusedActive]=primaryHover; - sty.Colors[ImGuiCol_Header]=secondary; - sty.Colors[ImGuiCol_HeaderHovered]=secondaryHover; - sty.Colors[ImGuiCol_HeaderActive]=secondaryActive; - sty.Colors[ImGuiCol_ResizeGrip]=secondary; - sty.Colors[ImGuiCol_ResizeGripHovered]=secondaryHover; - sty.Colors[ImGuiCol_ResizeGripActive]=secondaryActive; - sty.Colors[ImGuiCol_FrameBg]=secondary; - sty.Colors[ImGuiCol_FrameBgHovered]=secondaryHover; - sty.Colors[ImGuiCol_FrameBgActive]=secondaryActive; - sty.Colors[ImGuiCol_SliderGrab]=primaryActive; - sty.Colors[ImGuiCol_SliderGrabActive]=primaryActive; - sty.Colors[ImGuiCol_TitleBgActive]=primary; - sty.Colors[ImGuiCol_CheckMark]=primaryActive; - sty.Colors[ImGuiCol_TextSelectedBg]=secondaryHover; - sty.Colors[ImGuiCol_PlotHistogram]=uiColors[GUI_COLOR_MACRO_OTHER]; - sty.Colors[ImGuiCol_PlotHistogramHovered]=uiColors[GUI_COLOR_MACRO_OTHER]; - - sty.ScaleAllSizes(dpiScale); - - ImGui::GetStyle()=sty; - - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_PITCH]; - pitchGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_ACTIVE]; - noteGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_PANNING]; - panGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_INS]; - insGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=volColors[i/2]; - volGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]; - sysCmd1Grad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - for (int i=0; i<256; i++) { - ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY]; - sysCmd2Grad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); - } - - // set to 800 for now due to problems with unifont - static const ImWchar loadEverything[]={0x20,0x800,0}; - - if (settings.mainFont<0 || settings.mainFont>6) settings.mainFont=0; - if (settings.patFont<0 || settings.patFont>6) settings.patFont=0; - - if (settings.mainFont==6 && settings.mainFontPath.empty()) { - logW("UI font path is empty! reverting to default font\n"); - settings.mainFont=0; - } - if (settings.patFont==6 && settings.patFontPath.empty()) { - logW("pattern font path is empty! reverting to default font\n"); - settings.patFont=0; - } - - if (settings.mainFont==6) { // custom font - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(settings.mainFontPath.c_str(),e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logW("could not load UI font! reverting to default font\n"); - settings.mainFont=0; - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load UI font! falling back to Proggy Clean.\n"); - mainFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - } else if (settings.mainFont==5) { // system font - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_1,e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_2,e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_3,e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logW("could not load UI font! reverting to default font\n"); - settings.mainFont=0; - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load UI font! falling back to Proggy Clean.\n"); - mainFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - } - } - } else { - if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load UI font! falling back to Proggy Clean.\n"); - mainFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - - ImFontConfig fc; - fc.MergeMode=true; - fc.GlyphMinAdvanceX=e->getConfInt("iconSize",16)*dpiScale; - static const ImWchar fontRange[]={ICON_MIN_FA,ICON_MAX_FA,0}; - if ((iconFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(iconFont_compressed_data,iconFont_compressed_size,e->getConfInt("iconSize",16)*dpiScale,&fc,fontRange))==NULL) { - logE("could not load icon font!\n"); - } - if (settings.mainFontSize==settings.patFontSize && settings.patFont<5 && builtinFontM[settings.patFont]==builtinFont[settings.mainFont]) { - logD("using main font for pat font.\n"); - patFont=mainFont; - } else { - if (settings.patFont==6) { // custom font - if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(settings.patFontPath.c_str(),e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logW("could not load pattern font! reverting to default font\n"); - settings.patFont=0; - if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load pattern font! falling back to Proggy Clean.\n"); - patFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - } else if (settings.patFont==5) { // system font - if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_1,e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_2,e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_3,e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logW("could not load pattern font! reverting to default font\n"); - settings.patFont=0; - if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load pattern font! falling back to Proggy Clean.\n"); - patFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - } - } - } else { - if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,loadEverything))==NULL) { - logE("could not load pattern font!\n"); - patFont=ImGui::GetIO().Fonts->AddFontDefault(); - } - } - } - if ((bigFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_plexSans_compressed_data,font_plexSans_compressed_size,40*dpiScale))==NULL) { - logE("could not load big UI font!\n"); - } -} - bool FurnaceGUI::init() { #ifndef __APPLE__ float dpiScaleF; #endif - workingDir=e->getConfString("lastDir",getHomeDir()); + String homeDir=getHomeDir(); + workingDir=e->getConfString("lastDir",homeDir); + workingDirSong=e->getConfString("lastDirSong",workingDir); + workingDirIns=e->getConfString("lastDirIns",workingDir); + workingDirWave=e->getConfString("lastDirWave",workingDir); + workingDirSample=e->getConfString("lastDirSample",workingDir); + workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir); + workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir); + workingDirFont=e->getConfString("lastDirFont",workingDir); + workingDirColors=e->getConfString("lastDirColors",workingDir); + workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir); + workingDirLayout=e->getConfString("lastDirLayout",workingDir); editControlsOpen=e->getConfBool("editControlsOpen",true); ordersOpen=e->getConfBool("ordersOpen",true); @@ -5566,6 +3040,11 @@ bool FurnaceGUI::init() { notesOpen=e->getConfBool("notesOpen",false); channelsOpen=e->getConfBool("channelsOpen",false); regViewOpen=e->getConfBool("regViewOpen",false); + logOpen=e->getConfBool("logOpen",false); + + tempoView=e->getConfBool("tempoView",true); + waveHex=e->getConfBool("waveHex",false); + lockLayout=e->getConfBool("lockLayout",false); syncSettings(); @@ -5591,7 +3070,7 @@ bool FurnaceGUI::init() { sdlWin=SDL_CreateWindow("Furnace",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,scrW*dpiScale,scrH*dpiScale,SDL_WINDOW_RESIZABLE|SDL_WINDOW_ALLOW_HIGHDPI); if (sdlWin==NULL) { - logE("could not open window!\n"); + logE("could not open window! %s",SDL_GetError()); return false; } @@ -5616,14 +3095,14 @@ bool FurnaceGUI::init() { SDL_FreeSurface(icon); free(furIcon); } else { - logW("could not create icon!\n"); + logW("could not create icon!"); } #endif sdlRend=SDL_CreateRenderer(sdlWin,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC|SDL_RENDERER_TARGETTEXTURE); if (sdlRend==NULL) { - logE("could not init renderer! %s\n",SDL_GetError()); + logE("could not init renderer! %s",SDL_GetError()); return false; } @@ -5640,52 +3119,46 @@ bool FurnaceGUI::init() { applyUISettings(); if (!ImGui::GetIO().Fonts->Build()) { - logE("error while building font atlas!\n"); + logE("error while building font atlas!"); showError("error while loading fonts! please check your settings."); ImGui::GetIO().Fonts->Clear(); mainFont=ImGui::GetIO().Fonts->AddFontDefault(); patFont=mainFont; ImGui_ImplSDLRenderer_DestroyFontsTexture(); if (!ImGui::GetIO().Fonts->Build()) { - logE("error again while building font atlas!\n"); + logE("error again while building font atlas!"); } } strncpy(finalLayoutPath,(e->getConfigPath()+String(LAYOUT_INI)).c_str(),4095); + backupPath=e->getConfigPath()+String(BACKUP_FUR); prepareLayout(); ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_DockingEnable; ImGui::GetIO().IniFilename=finalLayoutPath; ImGui::LoadIniSettingsFromDisk(finalLayoutPath); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeDir,"",ImVec4(0.0f,1.0f,1.0f,1.0f),ICON_FA_FOLDER_O); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeFile,"",ImVec4(0.7f,0.7f,0.7f,1.0f),ICON_FA_FILE_O); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fur",ImVec4(0.5f,1.0f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fui",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fuw",ImVec4(1.0f,0.75f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmf",ImVec4(0.5f,1.0f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmp",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmw",ImVec4(1.0f,0.75f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".wav",ImVec4(1.0f,1.0f,0.5f,1.0f),ICON_FA_FILE_AUDIO_O); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".vgm",ImVec4(1.0f,1.0f,0.5f,1.0f),ICON_FA_FILE_AUDIO_O); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttf",ImVec4(0.3f,1.0f,0.6f,1.0f),ICON_FA_FONT); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".otf",ImVec4(0.3f,1.0f,0.6f,1.0f),ICON_FA_FONT); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttc",ImVec4(0.3f,1.0f,0.6f,1.0f),ICON_FA_FONT); - - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".tfi",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".vgi",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fti",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".bti",ImVec4(1.0f,0.5f,0.5f,1.0f),ICON_FA_FILE); - updateWindowTitle(); for (int i=0; isetMidiCallback([this](const TAMidiMessage& msg) -> int { + midiLock.lock(); + midiQueue.push(msg); + midiLock.unlock(); + e->setMidiBaseChan(cursor.xCoarse); + if (midiMap.valueInputStyle!=0 && cursor.xFine!=0 && edit) return -2; + if (!midiMap.noteInput) return -2; + if (learning!=-1) return -2; + if (midiMap.at(msg)) return -2; + return curIns; + }); + return true; } @@ -5698,6 +3171,16 @@ bool FurnaceGUI::finish() { SDL_DestroyWindow(sdlWin); e->setConf("lastDir",workingDir); + e->setConf("lastDirSong",workingDirSong); + e->setConf("lastDirIns",workingDirIns); + e->setConf("lastDirWave",workingDirWave); + e->setConf("lastDirSample",workingDirSample); + e->setConf("lastDirAudioExport",workingDirAudioExport); + e->setConf("lastDirVGMExport",workingDirVGMExport); + e->setConf("lastDirFont",workingDirFont); + e->setConf("lastDirColors",workingDirColors); + e->setConf("lastDirKeybinds",workingDirKeybinds); + e->setConf("lastDirLayout",workingDirLayout); // commit last open windows e->setConf("editControlsOpen",editControlsOpen); @@ -5720,19 +3203,35 @@ bool FurnaceGUI::finish() { e->setConf("notesOpen",notesOpen); e->setConf("channelsOpen",channelsOpen); e->setConf("regViewOpen",regViewOpen); + e->setConf("logOpen",logOpen); // commit last window size e->setConf("lastWindowWidth",scrW); e->setConf("lastWindowHeight",scrH); + e->setConf("tempoView",tempoView); + e->setConf("waveHex",waveHex); + e->setConf("lockLayout",lockLayout); + for (int i=0; i #include +#include #include +#include +#include #include +#include "fileDialog.h" + +#define rightClickable if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::SetKeyboardFocusHere(-1); +#define ctrlWheeling ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && wheelY!=0) + +#define handleUnimportant if (settings.insFocusesPattern && patternOpen) {nextWindow=GUI_WINDOW_PATTERN;} +#define unimportant(x) if (x) {handleUnimportant} + +#define MARK_MODIFIED modified=true; + +#define TOGGLE_COLOR(x) ((x)?uiColors[GUI_COLOR_TOGGLE_ON]:uiColors[GUI_COLOR_TOGGLE_OFF]) + +#define BIND_FOR(x) getKeyName(actionKeys[x],true).c_str() + enum FurnaceGUIColors { GUI_COLOR_BACKGROUND=0, GUI_COLOR_FRAME_BACKGROUND, @@ -34,15 +54,47 @@ enum FurnaceGUIColors { GUI_COLOR_TEXT, GUI_COLOR_ACCENT_PRIMARY, GUI_COLOR_ACCENT_SECONDARY, + GUI_COLOR_BORDER, + GUI_COLOR_BORDER_SHADOW, + GUI_COLOR_TOGGLE_OFF, + GUI_COLOR_TOGGLE_ON, GUI_COLOR_EDITING, GUI_COLOR_SONG_LOOP, + + GUI_COLOR_FILE_DIR, + GUI_COLOR_FILE_SONG_NATIVE, + GUI_COLOR_FILE_SONG_IMPORT, + GUI_COLOR_FILE_INSTR, + GUI_COLOR_FILE_AUDIO, + GUI_COLOR_FILE_WAVE, + GUI_COLOR_FILE_VGM, + GUI_COLOR_FILE_FONT, + GUI_COLOR_FILE_OTHER, + + GUI_COLOR_OSC_BG1, + GUI_COLOR_OSC_BG2, + GUI_COLOR_OSC_BG3, + GUI_COLOR_OSC_BG4, + GUI_COLOR_OSC_BORDER, + GUI_COLOR_OSC_WAVE, + GUI_COLOR_OSC_WAVE_PEAK, + GUI_COLOR_OSC_REF, + GUI_COLOR_OSC_GUIDE, + GUI_COLOR_VOLMETER_LOW, GUI_COLOR_VOLMETER_HIGH, GUI_COLOR_VOLMETER_PEAK, + + GUI_COLOR_ORDER_ROW_INDEX, + GUI_COLOR_ORDER_ACTIVE, + GUI_COLOR_ORDER_SIMILAR, + GUI_COLOR_ORDER_INACTIVE, + GUI_COLOR_MACRO_VOLUME, GUI_COLOR_MACRO_PITCH, GUI_COLOR_MACRO_OTHER, GUI_COLOR_MACRO_WAVE, + GUI_COLOR_INSTR_FM, GUI_COLOR_INSTR_STD, GUI_COLOR_INSTR_GB, @@ -56,6 +108,7 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_VIC, GUI_COLOR_INSTR_PET, GUI_COLOR_INSTR_VRC6, + GUI_COLOR_INSTR_VRC6_SAW, GUI_COLOR_INSTR_OPLL, GUI_COLOR_INSTR_OPL, GUI_COLOR_INSTR_FDS, @@ -67,7 +120,10 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_BEEPER, GUI_COLOR_INSTR_SWAN, GUI_COLOR_INSTR_MIKEY, + GUI_COLOR_INSTR_VERA, + GUI_COLOR_INSTR_X1_010, GUI_COLOR_INSTR_UNKNOWN, + GUI_COLOR_CHANNEL_FM, GUI_COLOR_CHANNEL_PULSE, GUI_COLOR_CHANNEL_NOISE, @@ -75,6 +131,8 @@ enum FurnaceGUIColors { GUI_COLOR_CHANNEL_PCM, GUI_COLOR_CHANNEL_OP, GUI_COLOR_CHANNEL_MUTED, + + GUI_COLOR_PATTERN_PLAY_HEAD, GUI_COLOR_PATTERN_CURSOR, GUI_COLOR_PATTERN_CURSOR_HOVER, GUI_COLOR_PATTERN_CURSOR_ACTIVE, @@ -84,9 +142,17 @@ enum FurnaceGUIColors { GUI_COLOR_PATTERN_HI_1, GUI_COLOR_PATTERN_HI_2, GUI_COLOR_PATTERN_ROW_INDEX, + GUI_COLOR_PATTERN_ROW_INDEX_HI1, + GUI_COLOR_PATTERN_ROW_INDEX_HI2, GUI_COLOR_PATTERN_ACTIVE, GUI_COLOR_PATTERN_INACTIVE, + GUI_COLOR_PATTERN_ACTIVE_HI1, + GUI_COLOR_PATTERN_INACTIVE_HI1, + GUI_COLOR_PATTERN_ACTIVE_HI2, + GUI_COLOR_PATTERN_INACTIVE_HI2, GUI_COLOR_PATTERN_INS, + GUI_COLOR_PATTERN_INS_WARN, + GUI_COLOR_PATTERN_INS_ERROR, GUI_COLOR_PATTERN_VOLUME_MAX, GUI_COLOR_PATTERN_VOLUME_HALF, GUI_COLOR_PATTERN_VOLUME_MIN, @@ -100,6 +166,13 @@ enum FurnaceGUIColors { GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY, GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY, GUI_COLOR_PATTERN_EFFECT_MISC, + + GUI_COLOR_LOGLEVEL_ERROR, + GUI_COLOR_LOGLEVEL_WARNING, + GUI_COLOR_LOGLEVEL_INFO, + GUI_COLOR_LOGLEVEL_DEBUG, + GUI_COLOR_LOGLEVEL_TRACE, + GUI_COLOR_EE_VALUE, GUI_COLOR_PLAYBACK_STAT, GUI_COLOR_MAX @@ -128,7 +201,8 @@ enum FurnaceGUIWindows { GUI_WINDOW_PIANO, GUI_WINDOW_NOTES, GUI_WINDOW_CHANNELS, - GUI_WINDOW_REGISTER_VIEW + GUI_WINDOW_REGISTER_VIEW, + GUI_WINDOW_LOG }; enum FurnaceGUIFileDialogs { @@ -147,14 +221,24 @@ enum FurnaceGUIFileDialogs { GUI_FILE_EXPORT_VGM, GUI_FILE_EXPORT_ROM, GUI_FILE_LOAD_MAIN_FONT, - GUI_FILE_LOAD_PAT_FONT + GUI_FILE_LOAD_PAT_FONT, + GUI_FILE_IMPORT_COLORS, + GUI_FILE_IMPORT_KEYBINDS, + GUI_FILE_IMPORT_LAYOUT, + GUI_FILE_EXPORT_COLORS, + GUI_FILE_EXPORT_KEYBINDS, + GUI_FILE_EXPORT_LAYOUT }; enum FurnaceGUIWarnings { GUI_WARN_QUIT, GUI_WARN_NEW, GUI_WARN_OPEN, + GUI_WARN_OPEN_BACKUP, GUI_WARN_OPEN_DROP, + GUI_WARN_RESET_LAYOUT, + GUI_WARN_RESET_COLORS, + GUI_WARN_RESET_KEYBINDS, GUI_WARN_GENERIC }; @@ -167,6 +251,7 @@ enum FurnaceGUIFMAlgs { enum FurnaceGUIActions { GUI_ACTION_GLOBAL_MIN=0, GUI_ACTION_OPEN, + GUI_ACTION_OPEN_BACKUP, GUI_ACTION_SAVE, GUI_ACTION_SAVE_AS, GUI_ACTION_UNDO, @@ -212,6 +297,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_NOTES, GUI_ACTION_WINDOW_CHANNELS, GUI_ACTION_WINDOW_REGISTER_VIEW, + GUI_ACTION_WINDOW_LOG, GUI_ACTION_COLLAPSE_WINDOW, GUI_ACTION_CLOSE_WINDOW, @@ -226,6 +312,10 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_CUT, GUI_ACTION_PAT_COPY, GUI_ACTION_PAT_PASTE, + GUI_ACTION_PAT_PASTE_MIX, + GUI_ACTION_PAT_PASTE_MIX_BG, + GUI_ACTION_PAT_PASTE_FLOOD, + GUI_ACTION_PAT_PASTE_OVERFLOW, GUI_ACTION_PAT_CURSOR_UP, GUI_ACTION_PAT_CURSOR_DOWN, GUI_ACTION_PAT_CURSOR_LEFT, @@ -261,6 +351,17 @@ enum FurnaceGUIActions { GUI_ACTION_PAT_COLLAPSE, GUI_ACTION_PAT_INCREASE_COLUMNS, GUI_ACTION_PAT_DECREASE_COLUMNS, + GUI_ACTION_PAT_INTERPOLATE, + GUI_ACTION_PAT_FADE, + GUI_ACTION_PAT_INVERT_VALUES, + GUI_ACTION_PAT_FLIP_SELECTION, + GUI_ACTION_PAT_COLLAPSE_ROWS, + GUI_ACTION_PAT_EXPAND_ROWS, + GUI_ACTION_PAT_COLLAPSE_PAT, + GUI_ACTION_PAT_EXPAND_PAT, + GUI_ACTION_PAT_COLLAPSE_SONG, + GUI_ACTION_PAT_EXPAND_SONG, + GUI_ACTION_PAT_LATCH, GUI_ACTION_PAT_MAX, GUI_ACTION_INS_LIST_MIN, @@ -304,6 +405,36 @@ enum FurnaceGUIActions { GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW, GUI_ACTION_SAMPLE_LIST_MAX, + GUI_ACTION_SAMPLE_MIN, + GUI_ACTION_SAMPLE_SELECT, + GUI_ACTION_SAMPLE_DRAW, + GUI_ACTION_SAMPLE_CUT, + GUI_ACTION_SAMPLE_COPY, + GUI_ACTION_SAMPLE_PASTE, + GUI_ACTION_SAMPLE_PASTE_REPLACE, + GUI_ACTION_SAMPLE_PASTE_MIX, + GUI_ACTION_SAMPLE_SELECT_ALL, + GUI_ACTION_SAMPLE_RESIZE, + GUI_ACTION_SAMPLE_RESAMPLE, + GUI_ACTION_SAMPLE_AMPLIFY, + GUI_ACTION_SAMPLE_NORMALIZE, + GUI_ACTION_SAMPLE_FADE_IN, + GUI_ACTION_SAMPLE_FADE_OUT, + GUI_ACTION_SAMPLE_SILENCE, + GUI_ACTION_SAMPLE_INSERT, + GUI_ACTION_SAMPLE_DELETE, + GUI_ACTION_SAMPLE_TRIM, + GUI_ACTION_SAMPLE_REVERSE, + GUI_ACTION_SAMPLE_INVERT, + GUI_ACTION_SAMPLE_SIGN, + GUI_ACTION_SAMPLE_FILTER, + GUI_ACTION_SAMPLE_PREVIEW, + GUI_ACTION_SAMPLE_STOP_PREVIEW, + GUI_ACTION_SAMPLE_ZOOM_IN, + GUI_ACTION_SAMPLE_ZOOM_OUT, + GUI_ACTION_SAMPLE_ZOOM_AUTO, + GUI_ACTION_SAMPLE_MAX, + GUI_ACTION_ORDERS_MIN, GUI_ACTION_ORDERS_UP, GUI_ACTION_ORDERS_DOWN, @@ -327,12 +458,26 @@ enum FurnaceGUIActions { GUI_ACTION_MAX }; +enum PasteMode { + GUI_PASTE_MODE_NORMAL=0, + GUI_PASTE_MODE_MIX_FG, + GUI_PASTE_MODE_MIX_BG, + GUI_PASTE_MODE_FLOOD, + GUI_PASTE_MODE_OVERFLOW +}; + #define FURKMOD_CTRL (1<<31) #define FURKMOD_SHIFT (1<<29) #define FURKMOD_META (1<<28) #define FURKMOD_ALT (1<<27) #define FURK_MASK 0x40ffffff +#ifdef __APPLE__ +#define FURKMOD_CMD FURKMOD_META +#else +#define FURKMOD_CMD FURKMOD_CTRL +#endif + struct SelectionPoint { int xCoarse, xFine; int y; @@ -347,7 +492,16 @@ enum ActionType { GUI_UNDO_PATTERN_PULL, GUI_UNDO_PATTERN_PUSH, GUI_UNDO_PATTERN_CUT, - GUI_UNDO_PATTERN_PASTE + GUI_UNDO_PATTERN_PASTE, + GUI_UNDO_PATTERN_CHANGE_INS, + GUI_UNDO_PATTERN_INTERPOLATE, + GUI_UNDO_PATTERN_FADE, + GUI_UNDO_PATTERN_SCALE, + GUI_UNDO_PATTERN_RANDOMIZE, + GUI_UNDO_PATTERN_INVERT_VAL, + GUI_UNDO_PATTERN_FLIP, + GUI_UNDO_PATTERN_COLLAPSE, + GUI_UNDO_PATTERN_EXPAND }; struct UndoPatternData { @@ -383,12 +537,110 @@ struct UndoStep { std::vector pat; }; +// -1 = any +struct MIDIBind { + int type, channel, data1, data2; + int action; + MIDIBind(): + type(0), + channel(16), + data1(128), + data2(128), + action(0) {} +}; + +struct MIDIMap { + // access method: map[type][channel][data1][data2]; + // channel 16 = any + // data1 128 = any + // data2 128 = any + int**** map; + std::vector binds; + + bool noteInput, volInput, rawVolume, polyInput, directChannel, programChange, midiClock, midiTimeCode; + // 0: disabled + // + // 1: C- C# D- D# E- F- F# G- G# A- A# B- + // o1 1 3 6 8 A + // 0 2 4 5 7 9 B + // C- C# D- D# E- F- F# G- G# A- A# B- + // o2 D F + // C E + // + // 2: C- C# D- D# E- F- F# G- G# A- A# B- + // o1 1 3 6 8 A + // 0 2 4 5 7 9 B + // C- C# D- D# E- F- F# G- G# A- A# B- + // o2 D F 2 4 6 + // C E 0 1 3 5 7 + // + // 3: C- C# D- D# E- F- F# G- G# A- A# B- + // o1 A B C D E + // 0 1 2 3 4 5 6 + // C- C# D- D# E- F- F# G- G# A- A# B- + // o2 F + // 7 8 9 + // + // 4: use dual CC for value input (nibble) + // 5: use 14-bit CC for value input (MSB/LSB) + // 6: use single CC for value input (may be imprecise) + int valueInputStyle; + int valueInputControlMSB; // on 4 + int valueInputControlLSB; // on 4 + int valueInputControlSingle; + + // 0: disabled + // 1: use dual CC (nibble) + // 2: use 14-bit CC (MSB/LSB) + // 3: use single CC (may be imprecise) + int valueInputSpecificStyle[18]; + int valueInputSpecificMSB[18]; + int valueInputSpecificLSB[18]; + int valueInputSpecificSingle[18]; + float volExp; + + int valueInputCurMSB, valueInputCurLSB, valueInputCurSingle; + int valueInputCurMSBS[18]; + int valueInputCurLSBS[18]; + int valueInputCurSingleS[18]; + + void compile(); + void deinit(); + int at(const TAMidiMessage& where); + bool read(String path); + bool write(String path); + MIDIMap(): + map(NULL), + noteInput(true), + volInput(false), + rawVolume(false), + polyInput(false), + directChannel(false), + programChange(true), + midiClock(false), + midiTimeCode(false), + valueInputStyle(1), + volExp(1.0f), + valueInputCurMSB(0), + valueInputCurLSB(0), + valueInputCurSingle(0) { + memset(valueInputSpecificStyle,0,18*sizeof(int)); + memset(valueInputSpecificMSB,0,18*sizeof(int)); + memset(valueInputSpecificLSB,0,18*sizeof(int)); + memset(valueInputSpecificSingle,0,18*sizeof(int)); + + memset(valueInputCurMSBS,0,18*sizeof(int)); + memset(valueInputCurLSBS,0,18*sizeof(int)); + memset(valueInputCurSingleS,0,18*sizeof(int)); + } +}; + struct Particle { ImU32* colors; const char* type; ImVec2 pos, speed; float gravity, friction, life, lifeSpeed; - bool update(); + bool update(float frameTime); Particle(ImU32* color, const char* ty, float x, float y, float sX, float sY, float g, float fr, float l, float lS): colors(color), type(ty), @@ -400,22 +652,48 @@ struct Particle { lifeSpeed(lS) {} }; +struct FurnaceGUISysDef { + const char* name; + std::vector definition; + FurnaceGUISysDef(const char* n, std::initializer_list def): + name(n), definition(def) { + } +}; + +struct FurnaceGUISysCategory { + const char* name; + std::vector systems; + FurnaceGUISysCategory(const char* n): + name(n) {} + FurnaceGUISysCategory(): + name(NULL) {} +}; + class FurnaceGUI { DivEngine* e; SDL_Window* sdlWin; SDL_Renderer* sdlRend; + SDL_Texture* sampleTex; + int sampleTexW, sampleTexH; + bool updateSampleTex; + String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile; - String mmlString[12]; + String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport, workingDirVGMExport, workingDirFont, workingDirColors, workingDirKeybinds, workingDirLayout; + String mmlString[13]; String mmlStringW; bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop; + bool displayNew; bool willExport[32]; + int vgmExportVersion; FurnaceGUIFileDialogs curFileDialog; FurnaceGUIWarnings warnAction; + FurnaceGUIFileDialog* fileDialog; + int scrW, scrH; double dpiScale; @@ -423,10 +701,21 @@ class FurnaceGUI { double aboutScroll, aboutSin; float aboutHue; + double backupTimer; + std::future backupTask; + std::mutex backupLock; + String backupPath; + + std::mutex midiLock; + std::queue midiQueue; + MIDIMap midiMap; + int learning; + ImFont* mainFont; ImFont* iconFont; ImFont* patFont; ImFont* bigFont; + ImWchar* fontRange; ImVec4 uiColors[GUI_COLOR_MAX]; ImVec4 volColors[128]; ImU32 pitchGrad[256]; @@ -470,10 +759,35 @@ class FurnaceGUI { int statusDisplay; float dpiScale; int viewPrevPattern; + int guiColorsBase; + int avoidRaisingPattern; + int insFocusesPattern; + int stepOnInsert; + // TODO flags + int unifiedDataView; + int sysFileDialog; + // end + int roundedWindows; + int roundedButtons; + int roundedMenus; + int loadJapanese; + int fmLayout; + int susPosition; + int effectCursorDir; + int cursorPastePos; + int titleBarInfo; + int titleBarSys; + int frameBorders; + int effectDeletionAltersValue; + int oscRoundedCorners; + int oscTakesEntireWindow; + int oscBorder; unsigned int maxUndoSteps; String mainFontPath; String patFontPath; String audioDevice; + String midiInDevice; + String midiOutDevice; Settings(): mainFontSize(18), @@ -483,7 +797,7 @@ class FurnaceGUI { audioQuality(0), arcadeCore(0), ym2612Core(0), - saaCore(0), + saaCore(1), mainFont(0), patFont(0), audioRate(44100), @@ -505,32 +819,69 @@ class FurnaceGUI { scrollStep(0), sysSeparators(1), forceMono(0), - controlLayout(0), + controlLayout(3), restartOnFlagChange(1), statusDisplay(0), dpiScale(0.0f), viewPrevPattern(1), + guiColorsBase(0), + avoidRaisingPattern(0), + insFocusesPattern(1), + stepOnInsert(0), + unifiedDataView(0), + sysFileDialog(1), + roundedWindows(1), + roundedButtons(1), + roundedMenus(0), + loadJapanese(0), + fmLayout(0), + susPosition(0), + effectCursorDir(1), + cursorPastePos(1), + titleBarInfo(1), + titleBarSys(1), + frameBorders(0), + effectDeletionAltersValue(1), + oscRoundedCorners(1), + oscTakesEntireWindow(0), + oscBorder(1), maxUndoSteps(100), mainFontPath(""), patFontPath(""), - audioDevice("") {} + audioDevice(""), + midiInDevice(""), + midiOutDevice("") {} } settings; char finalLayoutPath[4096]; int curIns, curWave, curSample, curOctave, oldRow, oldOrder, oldOrder1, editStep, exportLoops, soloChan, soloTimeout, orderEditMode, orderCursor; - int loopOrder, loopRow, loopEnd, isClipping, extraChannelButtons, patNameTarget; + int loopOrder, loopRow, loopEnd, isClipping, extraChannelButtons, patNameTarget, newSongCategory; + int wheelX, wheelY; + bool editControlsOpen, ordersOpen, insListOpen, songInfoOpen, patternOpen, insEditOpen; bool waveListOpen, waveEditOpen, sampleListOpen, sampleEditOpen, aboutOpen, settingsOpen; - bool mixerOpen, debugOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; - bool pianoOpen, notesOpen, channelsOpen, regViewOpen; + bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; + bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen; + + /* there ought to be a better way... + bool editControlsDocked, ordersDocked, insListDocked, songInfoDocked, patternDocked, insEditDocked; + bool waveListDocked, waveEditDocked, sampleListDocked, sampleEditDocked, aboutDocked, settingsDocked; + bool mixerDocked, debugDocked, inspectorDocked, oscDocked, volMeterDocked, statsDocked, compatFlagsDocked; + bool pianoDocked, notesDocked, channelsDocked, regViewDocked; + */ + SelectionPoint selStart, selEnd, cursor; bool selecting, curNibble, orderNibble, followOrders, followPattern, changeAllOrders; - bool collapseWindow, demandScrollX, fancyPattern, wantPatName; + bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, lockLayout; FurnaceGUIWindows curWindow, nextWindow; float peak[2]; float patChanX[DIV_MAX_CHANS+1]; float patChanSlideY[DIV_MAX_CHANS+1]; + const int* nextDesc; + + bool opMaskNote, opMaskIns, opMaskVol, opMaskEffect, opMaskEffectVal; + short latchNote, latchIns, latchVol, latchEffect, latchEffectVal; // bit 31: ctrl // bit 30: reserved for SDL scancode mask @@ -543,6 +894,7 @@ class FurnaceGUI { std::map actionMapGlobal; std::map actionMapPat; std::map actionMapOrders; + std::map actionMapSample; std::map actionMapInsList; std::map actionMapWaveList; std::map actionMapSampleList; @@ -561,6 +913,8 @@ class FurnaceGUI { std::vector cmdStream; std::vector particles; + std::vector sysCategories; + bool wavePreviewOn; SDL_Scancode wavePreviewKey; int wavePreviewNote; @@ -589,6 +943,7 @@ class FurnaceGUI { bool macroDragInitialValueSet; bool macroDragInitialValue; bool macroDragChar; + bool macroDragLineMode; // TODO bool macroDragActive; ImVec2 macroLoopDragStart; @@ -615,23 +970,73 @@ class FurnaceGUI { ImVec2 threeChars, twoChars; SelectionPoint sel1, sel2; int dummyRows, demandX; + int transposeAmount, randomizeMin, randomizeMax, fadeMin, fadeMax; + float scaleMax; + bool fadeMode, randomMode; int oldOrdersLen; DivOrders oldOrders; - DivPattern* oldPat[128]; + DivPattern* oldPat[DIV_MAX_CHANS]; std::deque undoHist; std::deque redoHist; + // sample editor specific + double sampleZoom; + double prevSampleZoom; + int samplePos; + int resizeSize, silenceSize; + double resampleTarget; + int resampleStrat; + float amplifyVol; + int sampleSelStart, sampleSelEnd; + bool sampleDragActive, sampleDragMode, sampleDrag16, sampleZoomAuto; + void* sampleDragTarget; + ImVec2 sampleDragStart; + ImVec2 sampleDragAreaSize; + unsigned int sampleDragLen; + float sampleFilterL, sampleFilterB, sampleFilterH, sampleFilterRes, sampleFilterCutStart, sampleFilterCutEnd; + unsigned char sampleFilterPower; + short* sampleClipboard; + size_t sampleClipboardLen; + bool openSampleResizeOpt, openSampleResampleOpt, openSampleAmplifyOpt, openSampleSilenceOpt, openSampleFilterOpt; + + // oscilloscope + int oscTotal; + float oscValues[512]; + float oscZoom; + bool oscZoomSlider; + + // visualizer float keyHit[DIV_MAX_CHANS]; int lastIns[DIV_MAX_CHANS]; + + // log window + bool followLog; + 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); - void drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, unsigned char d2r, unsigned char rr, unsigned char sl, const ImVec2& size); + void drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, unsigned char d2r, unsigned char rr, unsigned char sl, float maxTl, float maxArDr, const ImVec2& size); + void drawSysConf(int i); + + // these ones offer ctrl-wheel fine value changes. + bool CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format=NULL, ImGuiSliderFlags flags=0); + bool CWVSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format=NULL, ImGuiSliderFlags flags=0); + bool CWSliderInt(const char* label, int* v, int v_min, int v_max, const char* format="%d", ImGuiSliderFlags flags=0); + bool CWSliderFloat(const char* label, float* v, float v_min, float v_max, const char* format="%.3f", ImGuiSliderFlags flags=0); + bool CWVSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format="%d", ImGuiSliderFlags flags=0); void updateWindowTitle(); void prepareLayout(); - void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord); + void readOsc(); + + float calcBPM(int s1, int s2, float hz); + + void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord, const DivPattern** patCache); + + void actualWaveList(); + void actualSampleList(); void drawEditControls(); void drawSongInfo(); @@ -655,11 +1060,23 @@ class FurnaceGUI { void drawAbout(); void drawSettings(); void drawDebug(); + void drawNewSong(); + void drawLog(); void parseKeybinds(); void promptKey(int which); void doAction(int what); + bool importColors(String path); + bool exportColors(String path); + bool importKeybinds(String path); + bool exportKeybinds(String path); + bool importLayout(String path); + bool exportLayout(String path); + + void resetColors(); + void resetKeybinds(); + void syncSettings(); void commitSettings(); void processDrags(int dragX, int dragY); @@ -682,15 +1099,30 @@ class FurnaceGUI { void doInsert(); void doTranspose(int amount); void doCopy(bool cut); - void doPaste(); + void doPaste(PasteMode mode=GUI_PASTE_MODE_NORMAL); + void doChangeIns(int ins); + void doInterpolate(); + void doFade(int p0, int p1, bool mode); + void doInvertValues(); + void doScale(float top); + void doRandomize(int bottom, int top, bool mode); + void doFlip(); + void doCollapse(int divider); + void doExpand(int multiplier); void doUndo(); void doRedo(); + void editOptions(bool topMenu); + void noteInput(int num, int key, int vol=-1); + void valueInput(int num, bool direct=false, int target=-1); + + void doUndoSample(); + void doRedoSample(); void play(int row=0); void stop(); - void previewNote(int refChan, int note); - void stopPreviewNote(SDL_Scancode scancode); + void previewNote(int refChan, int note, bool autoNote=false); + void stopPreviewNote(SDL_Scancode scancode, bool autoNote=false); void keyDown(SDL_Event& ev); void keyUp(SDL_Event& ev); @@ -700,13 +1132,14 @@ class FurnaceGUI { int load(String path); void exportAudio(String path, DivAudioExportModes mode); - void applyUISettings(); + void applyUISettings(bool updateFonts=true); + void initSystemPresets(); - void encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel); + void encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel, bool hex=false); void encodeMMLStr(String& target, unsigned char* macro, unsigned char macroLen, signed char macroLoop, signed char macroRel); void decodeMMLStr(String& source, unsigned char* macro, unsigned char& macroLen, signed char& macroLoop, int macroMin, int macroMax, signed char& macroRel); void decodeMMLStr(String& source, int* macro, unsigned char& macroLen, signed char& macroLoop, int macroMin, int macroMax, signed char& macroRel); - void decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMax); + void decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMax, bool hex=false); String encodeKeyMap(std::map& map); void decodeKeyMap(std::map& map, String source); @@ -722,8 +1155,11 @@ class FurnaceGUI { void updateScroll(int amount); void addScroll(int amount); void setFileName(String name); + void runBackupThread(); bool loop(); bool finish(); bool init(); FurnaceGUI(); }; + +#endif diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index c872b05bd..1b292638c 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -19,6 +19,8 @@ // guiConst: constants used in the GUI like arrays, strings and other stuff #include "guiConst.h" +#include "gui.h" +#include "../engine/song.h" const int opOrder[4]={ 0, 2, 1, 3 @@ -64,3 +66,472 @@ const char* pitchLabel[11]={ "1/6", "1/5", "1/4", "1/3", "1/2", "1x", "2x", "3x", "4x", "5x", "6x" }; +const int altValues[24]={ + 0, 10, 1, 11, 2, 3, 12, 4, 13, 5, 14, 6, 7, 15, 8, -1, 9, -1, -1, -1, -1, -1, -1, -1 +}; + +const int vgmVersions[6]={ + 0x150, + 0x151, + 0x160, + 0x161, + 0x170, + 0x171 +}; + +const char* insTypes[DIV_INS_MAX]={ + "Standard", + "FM (4-operator)", + "Game Boy", + "C64", + "Amiga/Sample", + "PC Engine", + "AY-3-8910/SSG", + "AY8930", + "TIA", + "SAA1099", + "VIC", + "PET", + "VRC6", + "FM (OPLL)", + "FM (OPL)", + "FDS", + "Virtual Boy", + "Namco 163", + "Konami SCC/Bubble System WSG", + "FM (OPZ)", + "POKEY", + "PC Beeper", + "WonderSwan", + "Atari Lynx", + "VERA", + "X1-010", + "VRC6 (saw)" +}; + +const char* sampleDepths[17]={ + "1-bit PCM", + "1-bit DPCM", + NULL, + NULL, + "QSound ADPCM", + "ADPCM-A", + "ADPCM-B", + "X68000 ADPCM", + "8-bit PCM", + NULL, // "BRR", + "VOX", + NULL, + NULL, + NULL, + NULL, + NULL, + "16-bit PCM" +}; + +const char* resampleStrats[]={ + "none", + "linear", + "cubic spline", + "blep synthesis", + "sinc", + "best possible" +}; + +#define D FurnaceGUIActionDef +#define NOT_AN_ACTION -1 + +// format: ("ACTION_ENUM", "Action name", defaultBind) +const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ + D("GLOBAL_MIN", "---Global", NOT_AN_ACTION), + D("OPEN", "Open file", FURKMOD_CMD|SDLK_o), + D("OPEN_BACKUP", "Restore backup", 0), + D("SAVE", "Save file", FURKMOD_CMD|SDLK_s), + D("SAVE_AS", "Save as", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_s), + D("UNDO", "Undo", FURKMOD_CMD|SDLK_z), + D("REDO", "Redo", FURKMOD_CMD|SDLK_y), + D("PLAY_TOGGLE", "Play/Stop (toggle)", SDLK_RETURN), + D("PLAY", "Play", 0), + D("STOP", "Stop", 0), + D("PLAY_REPEAT", "Play (repeat pattern)", 0), + D("PLAY_CURSOR", "Play from cursor", FURKMOD_SHIFT|SDLK_RETURN), + D("STEP_ONE", "Step row", FURKMOD_CMD|SDLK_RETURN), + D("OCTAVE_UP", "Octave up", SDLK_KP_MULTIPLY), + D("OCTAVE_DOWN", "Octave down", SDLK_KP_DIVIDE), + D("INS_UP", "Previous instrument", FURKMOD_SHIFT|SDLK_KP_DIVIDE), + D("INS_DOWN", "Next instrument", FURKMOD_SHIFT|SDLK_KP_MULTIPLY), + D("STEP_UP", "Increase edit step", FURKMOD_CMD|SDLK_KP_MULTIPLY), + D("STEP_DOWN", "Decrease edit step", FURKMOD_CMD|SDLK_KP_DIVIDE), + D("TOGGLE_EDIT", "Toggle edit mode", SDLK_SPACE), + D("METRONOME", "Metronome", FURKMOD_CMD|SDLK_m), + D("REPEAT_PATTERN", "Toggle repeat pattern", 0), + D("FOLLOW_ORDERS", "Follow orders", 0), + D("FOLLOW_PATTERN", "Follow pattern", 0), + D("PANIC", "Panic", SDLK_F12), + + D("WINDOW_EDIT_CONTROLS", "Edit Controls", 0), + D("WINDOW_ORDERS", "Orders", 0), + D("WINDOW_INS_LIST", "Instrument List", 0), + D("WINDOW_INS_EDIT", "Instrument Editor", 0), + D("WINDOW_SONG_INFO", "Song Information", 0), + D("WINDOW_PATTERN", "Pattern", 0), + D("WINDOW_WAVE_LIST", "Wavetable List", 0), + D("WINDOW_WAVE_EDIT", "Wavetable Editor", 0), + D("WINDOW_SAMPLE_LIST", "Sample List", 0), + D("WINDOW_SAMPLE_EDIT", "Sample Editor", 0), + D("WINDOW_ABOUT", "About", 0), + D("WINDOW_SETTINGS", "Settings", 0), + D("WINDOW_MIXER", "Mixer", 0), + D("WINDOW_DEBUG", "Debug Menu", 0), + D("WINDOW_OSCILLOSCOPE", "Oscilloscope", 0), + D("WINDOW_VOL_METER", "Volume Meter", 0), + D("WINDOW_STATS", "Statistics", 0), + D("WINDOW_COMPAT_FLAGS", "Compatibility Flags", 0), + D("WINDOW_PIANO", "Piano", 0), + D("WINDOW_NOTES", "Song Comments", 0), + D("WINDOW_CHANNELS", "Channels", 0), + D("WINDOW_REGISTER_VIEW", "Register View", 0), + D("WINDOW_LOG", "Log Viewer", 0), + + D("COLLAPSE_WINDOW", "Collapse/expand current window", 0), + D("CLOSE_WINDOW", "Close current window", FURKMOD_SHIFT|SDLK_ESCAPE), + D("GLOBAL_MAX", "", NOT_AN_ACTION), + + D("PAT_MIN", "---Pattern", NOT_AN_ACTION), + D("PAT_NOTE_UP", "Transpose (+1)", FURKMOD_CMD|SDLK_F2), + D("PAT_NOTE_DOWN", "Transpose (-1)", FURKMOD_CMD|SDLK_F1), + D("PAT_OCTAVE_UP", "Transpose (+1 octave)", FURKMOD_CMD|SDLK_F4), + D("PAT_OCTAVE_DOWN", "Transpose (-1 octave)", FURKMOD_CMD|SDLK_F3), + D("PAT_SELECT_ALL", "Select all", FURKMOD_CMD|SDLK_a), + D("PAT_CUT", "Cut", FURKMOD_CMD|SDLK_x), + D("PAT_COPY", "Copy", FURKMOD_CMD|SDLK_c), + D("PAT_PASTE", "Paste", FURKMOD_CMD|SDLK_v), + D("PAT_PASTE_MIX", "Paste Mix (foreground)", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_v), + D("PAT_PASTE_MIX_BG", "Paste Mix (background)", 0), + D("PAT_PASTE_FLOOD", "Paste Flood", 0), + D("PAT_PASTE_OVERFLOW", "Paste Overflow", 0), + D("PAT_CURSOR_UP", "Move cursor up", SDLK_UP), + D("PAT_CURSOR_DOWN", "Move cursor down", SDLK_DOWN), + D("PAT_CURSOR_LEFT", "Move cursor left", SDLK_LEFT), + D("PAT_CURSOR_RIGHT", "Move cursor right", SDLK_RIGHT), + D("PAT_CURSOR_UP_ONE", "Move cursor up by one (override Edit Step)", FURKMOD_SHIFT|SDLK_HOME), + D("PAT_CURSOR_DOWN_ONE", "Move cursor down by one (override Edit Step)", FURKMOD_SHIFT|SDLK_END), + D("PAT_CURSOR_LEFT_CHANNEL", "Move cursor to previous channel", 0), + D("PAT_CURSOR_RIGHT_CHANNEL", "Move cursor to next channel", 0), + D("PAT_CURSOR_NEXT_CHANNEL", "Move cursor to previous channel (overflow)", 0), + D("PAT_CURSOR_PREVIOUS_CHANNEL", "Move cursor to next channel (overflow)", 0), + D("PAT_CURSOR_BEGIN", "Move cursor to beginning of pattern", SDLK_HOME), + D("PAT_CURSOR_END", "Move cursor to end of pattern", SDLK_END), + D("PAT_CURSOR_UP_COARSE", "Move cursor up (coarse)", SDLK_PAGEUP), + D("PAT_CURSOR_DOWN_COARSE", "Move cursor down (coarse)", SDLK_PAGEDOWN), + D("PAT_SELECTION_UP", "Expand selection upwards", FURKMOD_SHIFT|SDLK_UP), + D("PAT_SELECTION_DOWN", "Expand selection downwards", FURKMOD_SHIFT|SDLK_DOWN), + D("PAT_SELECTION_LEFT", "Expand selection to the left", FURKMOD_SHIFT|SDLK_LEFT), + D("PAT_SELECTION_RIGHT", "Expand selection to the right", FURKMOD_SHIFT|SDLK_RIGHT), + D("PAT_SELECTION_UP_ONE", "Expand selection upwards by one (override Edit Step)", 0), + D("PAT_SELECTION_DOWN_ONE", "Expand selection downwards by one (override Edit Step)", 0), + D("PAT_SELECTION_BEGIN", "Expand selection to beginning of pattern", 0), + D("PAT_SELECTION_END", "Expand selection to end of pattern", 0), + D("PAT_SELECTION_UP_COARSE", "Expand selection upwards (coarse)", FURKMOD_SHIFT|SDLK_PAGEUP), + D("PAT_SELECTION_DOWN_COARSE", "Expand selection downwards (coarse)", FURKMOD_SHIFT|SDLK_PAGEDOWN), + D("PAT_DELETE", "Delete", SDLK_DELETE), + D("PAT_PULL_DELETE", "Pull delete", SDLK_BACKSPACE), + D("PAT_INSERT", "Insert", SDLK_INSERT), + D("PAT_MUTE_CURSOR", "Mute channel at cursor", FURKMOD_ALT|SDLK_F9), + D("PAT_SOLO_CURSOR", "Solo channel at cursor", FURKMOD_ALT|SDLK_F10), + D("PAT_UNMUTE_ALL", "Unmute all channels", FURKMOD_ALT|FURKMOD_SHIFT|SDLK_F9), + D("PAT_NEXT_ORDER", "Go to next order", 0), + D("PAT_PREV_ORDER", "Go to previous order", 0), + D("PAT_COLLAPSE", "Collapse channel at cursor", 0), + D("PAT_INCREASE_COLUMNS", "Increase effect columns", 0), + D("PAT_DECREASE_COLUMNS", "Decrease effect columns", 0), + D("PAT_INTERPOLATE", "Interpolate", 0), + D("PAT_FADE", "Fade", 0), + D("PAT_INVERT_VALUES", "Invert values", 0), + D("PAT_FLIP_SELECTION", "Flip selection", 0), + D("PAT_COLLAPSE_ROWS", "Collapse rows", 0), + D("PAT_EXPAND_ROWS", "Expand rows", 0), + D("PAT_COLLAPSE_PAT", "Collapse pattern", 0), + D("PAT_EXPAND_PAT", "Expand pattern", 0), + D("PAT_COLLAPSE_SONG", "Collapse song", 0), + D("PAT_EXPAND_SONG", "Expand song", 0), + D("PAT_LATCH", "Set note input latch", 0), + D("PAT_MAX", "", NOT_AN_ACTION), + + D("INS_LIST_MIN", "---Instrument list", NOT_AN_ACTION), + D("INS_LIST_ADD", "Add", SDLK_INSERT), + D("INS_LIST_DUPLICATE", "Duplicate", FURKMOD_CMD|SDLK_d), + D("INS_LIST_OPEN", "Open", 0), + D("INS_LIST_SAVE", "Save", 0), + D("INS_LIST_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), + D("INS_LIST_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), + D("INS_LIST_DELETE", "Delete", 0), + D("INS_LIST_EDIT", "Edit", FURKMOD_SHIFT|SDLK_RETURN), + D("INS_LIST_UP", "Cursor up", SDLK_UP), + D("INS_LIST_DOWN", "Cursor down", SDLK_DOWN), + D("INS_LIST_MAX", "", NOT_AN_ACTION), + + D("WAVE_LIST_MIN", "---Wavetable list", NOT_AN_ACTION), + 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_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), + D("WAVE_LIST_DELETE", "Delete", 0), + D("WAVE_LIST_EDIT", "Edit", FURKMOD_SHIFT|SDLK_RETURN), + D("WAVE_LIST_UP", "Cursor up", SDLK_UP), + D("WAVE_LIST_DOWN", "Cursor down", SDLK_DOWN), + D("WAVE_LIST_MAX", "", NOT_AN_ACTION), + + D("SAMPLE_LIST_MIN", "---Sample list", NOT_AN_ACTION), + 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_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), + D("SAMPLE_LIST_DELETE", "Delete", 0), + D("SAMPLE_LIST_EDIT", "Edit", FURKMOD_SHIFT|SDLK_RETURN), + D("SAMPLE_LIST_UP", "Cursor up", SDLK_UP), + D("SAMPLE_LIST_DOWN", "Cursor down", SDLK_DOWN), + D("SAMPLE_LIST_PREVIEW", "Preview", 0), + D("SAMPLE_LIST_STOP_PREVIEW", "Stop preview", 0), + D("SAMPLE_LIST_MAX", "", NOT_AN_ACTION), + + D("SAMPLE_MIN", "---Sample editor", NOT_AN_ACTION), + D("SAMPLE_SELECT", "Edit mode: Select", FURKMOD_SHIFT|SDLK_i), + D("SAMPLE_DRAW", "Edit mode: Draw", FURKMOD_SHIFT|SDLK_d), + D("SAMPLE_CUT", "Cut", FURKMOD_CMD|SDLK_x), + D("SAMPLE_COPY", "Copy", FURKMOD_CMD|SDLK_c), + D("SAMPLE_PASTE", "Paste", FURKMOD_CMD|SDLK_v), + D("SAMPLE_PASTE_REPLACE", "Paste replace", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_v), + D("SAMPLE_PASTE_MIX", "Paste mix", FURKMOD_CMD|FURKMOD_ALT|SDLK_v), + D("SAMPLE_SELECT_ALL", "Select all", FURKMOD_CMD|SDLK_a), + D("SAMPLE_RESIZE", "Resize", FURKMOD_CMD|SDLK_r), + D("SAMPLE_RESAMPLE", "Resample", FURKMOD_CMD|SDLK_e), + D("SAMPLE_AMPLIFY", "Amplify", FURKMOD_CMD|SDLK_b), + D("SAMPLE_NORMALIZE", "Normalize", FURKMOD_CMD|SDLK_n), + D("SAMPLE_FADE_IN", "Fade in", FURKMOD_CMD|SDLK_i), + D("SAMPLE_FADE_OUT", "Fade out", FURKMOD_CMD|SDLK_o), + D("SAMPLE_SILENCE", "Apply silence", FURKMOD_SHIFT|SDLK_DELETE), + D("SAMPLE_INSERT", "Insert silence", SDLK_INSERT), + D("SAMPLE_DELETE", "Delete", SDLK_DELETE), + D("SAMPLE_TRIM", "Trim", FURKMOD_CMD|SDLK_DELETE), + D("SAMPLE_REVERSE", "Reverse", FURKMOD_CMD|SDLK_t), + D("SAMPLE_INVERT", "Invert", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_t), + D("SAMPLE_SIGN", "Signed/unsigned exchange", FURKMOD_CMD|SDLK_u), + D("SAMPLE_FILTER", "Apply filter", FURKMOD_CMD|SDLK_f), + D("SAMPLE_PREVIEW", "Preview sample", 0), + D("SAMPLE_STOP_PREVIEW", "Stop sample preview", 0), + D("SAMPLE_ZOOM_IN", "Zoom in", FURKMOD_CMD|SDLK_EQUALS), + D("SAMPLE_ZOOM_OUT", "Zoom out", FURKMOD_CMD|SDLK_MINUS), + D("SAMPLE_ZOOM_AUTO", "Toggle auto-zoom", FURKMOD_CMD|SDLK_0), + D("SAMPLE_MAX", "", NOT_AN_ACTION), + + D("ORDERS_MIN", "---Orders", NOT_AN_ACTION), + D("ORDERS_UP", "Previous order", SDLK_UP), + D("ORDERS_DOWN", "Next order", SDLK_DOWN), + D("ORDERS_LEFT", "Cursor left", SDLK_LEFT), + D("ORDERS_RIGHT", "Cursor right", SDLK_RIGHT), + D("ORDERS_INCREASE", "Increase value", 0), + D("ORDERS_DECREASE", "Decrease value", 0), + D("ORDERS_EDIT_MODE", "Switch edit mode", 0), + D("ORDERS_LINK", "Toggle alter entire row", FURKMOD_CMD|SDLK_l), + D("ORDERS_ADD", "Add", SDLK_INSERT), + D("ORDERS_DUPLICATE", "Duplicate", FURKMOD_CMD|SDLK_d), + D("ORDERS_DEEP_CLONE", "Deep clone", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_d), + D("ORDERS_DUPLICATE_END", "Duplicate to end of song", FURKMOD_CMD|SDLK_e), + D("ORDERS_DEEP_CLONE_END", "Deep clone to end of song", FURKMOD_CMD|FURKMOD_SHIFT|SDLK_e), + D("ORDERS_REMOVE", "Remove", SDLK_DELETE), + D("ORDERS_MOVE_UP", "Move up", FURKMOD_SHIFT|SDLK_UP), + D("ORDERS_MOVE_DOWN", "Move down", FURKMOD_SHIFT|SDLK_DOWN), + D("ORDERS_REPLAY", "Replay", 0), + D("ORDERS_MAX", "", NOT_AN_ACTION), +}; +#undef D + +#define D(x,y,z) FurnaceGUIColorDef(#x,y,ImGui::ColorConvertFloat4ToU32(z)) + +const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={ + D(GUI_COLOR_BACKGROUND,"Background",ImVec4(0.1f,0.1f,0.1f,1.0f)), + D(GUI_COLOR_FRAME_BACKGROUND,"",ImVec4(0.0f,0.0f,0.0f,0.85f)), + D(GUI_COLOR_MODAL_BACKDROP,"",ImVec4(0.0f,0.0f,0.0f,0.55f)), + D(GUI_COLOR_HEADER,"",ImVec4(0.2f,0.2f,0.2f,1.0f)), + D(GUI_COLOR_TEXT,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_ACCENT_PRIMARY,"",ImVec4(0.06f,0.53f,0.98f,1.0f)), + D(GUI_COLOR_ACCENT_SECONDARY,"",ImVec4(0.26f,0.59f,0.98f,1.0f)), + D(GUI_COLOR_BORDER,"",ImVec4(0.43f,0.43f,0.5f,0.5f)), + D(GUI_COLOR_BORDER_SHADOW,"",ImVec4(0.0f,0.0f,0.0f,0.0f)), + D(GUI_COLOR_TOGGLE_OFF,"",ImVec4(0.2f,0.2f,0.2f,1.0f)), + D(GUI_COLOR_TOGGLE_ON,"",ImVec4(0.2f,0.6f,0.2f,1.0f)), + D(GUI_COLOR_EDITING,"",ImVec4(0.2f,0.1f,0.1f,1.0f)), + D(GUI_COLOR_SONG_LOOP,"",ImVec4(0.3f,0.5f,0.8f,0.4f)), + + D(GUI_COLOR_FILE_DIR,"",ImVec4(0.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_FILE_SONG_NATIVE,"",ImVec4(0.5f,1.0f,0.5f,1.0f)), + D(GUI_COLOR_FILE_SONG_IMPORT,"",ImVec4(0.5f,1.0f,0.8f,1.0f)), + D(GUI_COLOR_FILE_INSTR,"",ImVec4(1.0f,0.5f,0.5f,1.0f)), + D(GUI_COLOR_FILE_AUDIO,"",ImVec4(1.0f,1.0f,0.5f,1.0f)), + D(GUI_COLOR_FILE_WAVE,"",ImVec4(1.0f,0.75f,0.5f,1.0f)), + D(GUI_COLOR_FILE_VGM,"",ImVec4(1.0f,1.0f,0.5f,1.0f)), + D(GUI_COLOR_FILE_FONT,"",ImVec4(0.3f,1.0f,0.6f,1.0f)), + D(GUI_COLOR_FILE_OTHER,"",ImVec4(0.7f,0.7f,0.7f,1.0f)), + + D(GUI_COLOR_OSC_BG1,"",ImVec4(0.1f,0.18f,0.3f,1.0f)), + D(GUI_COLOR_OSC_BG2,"",ImVec4(0.1f,0.18f,0.3f,1.0f)), + D(GUI_COLOR_OSC_BG3,"",ImVec4(0.05f,0.15f,0.25f,1.0f)), + D(GUI_COLOR_OSC_BG4,"",ImVec4(0.05f,0.15f,0.25f,1.0f)), + D(GUI_COLOR_OSC_BORDER,"",ImVec4(0.4f,0.6f,0.95f,1.0f)), + D(GUI_COLOR_OSC_WAVE,"",ImVec4(0.95f,0.95f,1.0f,1.0f)), + D(GUI_COLOR_OSC_WAVE_PEAK,"",ImVec4(0.95f,0.95f,1.0f,1.0f)), + D(GUI_COLOR_OSC_REF,"",ImVec4(0.3f,0.3f,0.3f,1.0f)), + D(GUI_COLOR_OSC_GUIDE,"",ImVec4(0.3,0.3f,0.3f,1.0f)), + + D(GUI_COLOR_VOLMETER_LOW,"",ImVec4(0.2f,0.6f,0.2f,1.0f)), + D(GUI_COLOR_VOLMETER_HIGH,"",ImVec4(1.0f,0.9f,0.2f,1.0f)), + D(GUI_COLOR_VOLMETER_PEAK,"",ImVec4(1.0f,0.1f,0.1f,1.0f)), + + D(GUI_COLOR_ORDER_ROW_INDEX,"",ImVec4(0.5f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_ORDER_ACTIVE,"",ImVec4(0.4f,0.7f,1.0f,0.25f)), + D(GUI_COLOR_ORDER_SIMILAR,"",ImVec4(0.5f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_ORDER_INACTIVE,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), + + D(GUI_COLOR_MACRO_VOLUME,"",ImVec4(0.2f,1.0f,0.0f,1.0f)), + D(GUI_COLOR_MACRO_PITCH,"",ImVec4(1.0f,0.8f,0.0f,1.0f)), + D(GUI_COLOR_MACRO_OTHER,"",ImVec4(0.0f,0.9f,1.0f,1.0f)), + D(GUI_COLOR_MACRO_WAVE,"",ImVec4(1.0f,0.4f,0.0f,1.0f)), + + D(GUI_COLOR_INSTR_FM,"",ImVec4(0.6f,0.9f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_STD,"",ImVec4(0.6f,1.0f,0.5f,1.0f)), + D(GUI_COLOR_INSTR_GB,"",ImVec4(1.0f,1.0f,0.5f,1.0f)), + D(GUI_COLOR_INSTR_C64,"",ImVec4(0.85f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_AMIGA,"",ImVec4(1.0f,0.5f,0.5f,1.0f)), + D(GUI_COLOR_INSTR_PCE,"",ImVec4(1.0f,0.8f,0.5f,1.0f)), + D(GUI_COLOR_INSTR_AY,"",ImVec4(1.0f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_AY8930,"",ImVec4(0.7f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_TIA,"",ImVec4(1.0f,0.6f,0.4f,1.0f)), + D(GUI_COLOR_INSTR_SAA1099,"",ImVec4(0.3f,0.3f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_VIC,"",ImVec4(0.2f,1.0f,0.6f,1.0f)), + D(GUI_COLOR_INSTR_PET,"",ImVec4(1.0f,1.0f,0.8f,1.0f)), + D(GUI_COLOR_INSTR_VRC6,"",ImVec4(1.0f,0.9f,0.5f,1.0f)), + D(GUI_COLOR_INSTR_VRC6_SAW,"",ImVec4(0.8f,0.3f,0.0f,1.0f)), + D(GUI_COLOR_INSTR_OPLL,"",ImVec4(0.6f,0.7f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_OPL,"",ImVec4(0.3f,1.0f,0.9f,1.0f)), + D(GUI_COLOR_INSTR_FDS,"",ImVec4(0.8f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_VBOY,"",ImVec4(1.0f,0.1f,0.1f,1.0f)), + D(GUI_COLOR_INSTR_N163,"",ImVec4(1.0f,0.4f,0.1f,1.0f)), + D(GUI_COLOR_INSTR_SCC,"",ImVec4(0.7f,1.0f,0.3f,1.0f)), + D(GUI_COLOR_INSTR_OPZ,"",ImVec4(0.2f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_POKEY,"",ImVec4(0.5f,1.0f,0.3f,1.0f)), + D(GUI_COLOR_INSTR_BEEPER,"",ImVec4(0.0f,1.0f,0.0f,1.0f)), + D(GUI_COLOR_INSTR_SWAN,"",ImVec4(0.3f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_MIKEY,"",ImVec4(0.5f,1.0f,0.3f,1.0f)), + D(GUI_COLOR_INSTR_VERA,"",ImVec4(0.4f,0.6f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_X1_010,"",ImVec4(0.3f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_UNKNOWN,"",ImVec4(0.3f,0.3f,0.3f,1.0f)), + + D(GUI_COLOR_CHANNEL_FM,"",ImVec4(0.2f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_CHANNEL_PULSE,"",ImVec4(0.4f,1.0f,0.2f,1.0f)), + D(GUI_COLOR_CHANNEL_NOISE,"",ImVec4(0.8f,0.8f,0.8f,1.0f)), + D(GUI_COLOR_CHANNEL_WAVE,"",ImVec4(1.0f,0.9f,0.2f,1.0f)), + D(GUI_COLOR_CHANNEL_PCM,"",ImVec4(1.0f,0.5f,0.2f,1.0f)), + D(GUI_COLOR_CHANNEL_OP,"",ImVec4(0.2f,0.4f,1.0f,1.0f)), + D(GUI_COLOR_CHANNEL_MUTED,"",ImVec4(0.5f,0.5f,0.5f,1.0f)), + + D(GUI_COLOR_PATTERN_PLAY_HEAD,"",ImVec4(1.0f,1.0f,1.0f,0.25f)), + D(GUI_COLOR_PATTERN_CURSOR,"",ImVec4(0.1f,0.3f,0.5f,1.0f)), + D(GUI_COLOR_PATTERN_CURSOR_HOVER,"",ImVec4(0.2f,0.4f,0.6f,1.0f)), + D(GUI_COLOR_PATTERN_CURSOR_ACTIVE,"",ImVec4(0.2f,0.5f,0.7f,1.0f)), + D(GUI_COLOR_PATTERN_SELECTION,"",ImVec4(0.15f,0.15f,0.2f,1.0f)), + D(GUI_COLOR_PATTERN_SELECTION_HOVER,"",ImVec4(0.2f,0.2f,0.3f,1.0f)), + D(GUI_COLOR_PATTERN_SELECTION_ACTIVE,"",ImVec4(0.4f,0.4f,0.5f,1.0f)), + D(GUI_COLOR_PATTERN_HI_1,"",ImVec4(0.6f,0.6f,0.6f,0.2f)), + D(GUI_COLOR_PATTERN_HI_2,"",ImVec4(0.5f,0.8f,1.0f,0.2f)), + D(GUI_COLOR_PATTERN_ROW_INDEX,"",ImVec4(0.5f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_ROW_INDEX_HI1,"",ImVec4(0.5f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_ROW_INDEX_HI2,"",ImVec4(0.5f,0.8f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_ACTIVE,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_INACTIVE,"",ImVec4(0.5f,0.5f,0.5f,1.0f)), + D(GUI_COLOR_PATTERN_ACTIVE_HI1,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_INACTIVE_HI1,"",ImVec4(0.5f,0.5f,0.5f,1.0f)), + D(GUI_COLOR_PATTERN_ACTIVE_HI2,"",ImVec4(1.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_INACTIVE_HI2,"",ImVec4(0.5f,0.5f,0.5f,1.0f)), + D(GUI_COLOR_PATTERN_INS,"",ImVec4(0.4f,0.7f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_INS_WARN,"",ImVec4(1.0f,1.0f,0.1f,1.0f)), + D(GUI_COLOR_PATTERN_INS_ERROR,"",ImVec4(1.0f,0.1f,0.1f,1.0f)), + D(GUI_COLOR_PATTERN_VOLUME_MAX,"",ImVec4(0.0f,1.0f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_VOLUME_HALF,"",ImVec4(0.0f,0.75f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_VOLUME_MIN,"",ImVec4(0.0f,0.5f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_INVALID,"",ImVec4(1.0f,0.0f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_PITCH,"",ImVec4(1.0f,1.0f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_VOLUME,"",ImVec4(0.0f,1.0f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_PANNING,"",ImVec4(0.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_SONG,"",ImVec4(1.0f,0.0f,0.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_TIME,"",ImVec4(0.5f,0.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_SPEED,"",ImVec4(1.0f,0.0f,1.0f,1.0f)), + D(GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY,"",ImVec4(0.5f,1.0f,0.0f,1.0f)), + 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_LOGLEVEL_ERROR,"",ImVec4(1.0f,0.2f,0.2f,1.0f)), + D(GUI_COLOR_LOGLEVEL_WARNING,"",ImVec4(1.0f,1.0f,0.2f,1.0f)), + D(GUI_COLOR_LOGLEVEL_INFO,"",ImVec4(0.4f,1.0f,0.4f,1.0f)), + D(GUI_COLOR_LOGLEVEL_DEBUG,"",ImVec4(0.3f,0.5f,1.0f,1.0f)), + D(GUI_COLOR_LOGLEVEL_TRACE,"",ImVec4(0.8f,0.8f,0.8f,1.0f)), + + D(GUI_COLOR_EE_VALUE,"",ImVec4(0.0f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_PLAYBACK_STAT,"",ImVec4(0.6f,0.6f,0.6f,1.0f)), +}; +#undef D + +// define systems. +const int availableSystems[]={ + DIV_SYSTEM_YM2612, + DIV_SYSTEM_YM2612_EXT, + DIV_SYSTEM_SMS, + DIV_SYSTEM_GB, + DIV_SYSTEM_PCE, + DIV_SYSTEM_NES, + DIV_SYSTEM_C64_8580, + DIV_SYSTEM_C64_6581, + DIV_SYSTEM_YM2151, + DIV_SYSTEM_SEGAPCM, + DIV_SYSTEM_SEGAPCM_COMPAT, + DIV_SYSTEM_YM2610, + DIV_SYSTEM_YM2610_EXT, + DIV_SYSTEM_YM2610_FULL, + DIV_SYSTEM_YM2610_FULL_EXT, + DIV_SYSTEM_YM2610B, + DIV_SYSTEM_YM2610B_EXT, + DIV_SYSTEM_AY8910, + DIV_SYSTEM_AMIGA, + DIV_SYSTEM_PCSPKR, + DIV_SYSTEM_OPLL, + DIV_SYSTEM_OPLL_DRUMS, + DIV_SYSTEM_VRC7, + DIV_SYSTEM_OPL, + DIV_SYSTEM_OPL_DRUMS, + DIV_SYSTEM_OPL2, + DIV_SYSTEM_OPL2_DRUMS, + DIV_SYSTEM_OPL3, + DIV_SYSTEM_OPL3_DRUMS, + DIV_SYSTEM_OPZ, + DIV_SYSTEM_TIA, + DIV_SYSTEM_SAA1099, + DIV_SYSTEM_AY8930, + DIV_SYSTEM_LYNX, + DIV_SYSTEM_QSOUND, + DIV_SYSTEM_X1_010, + DIV_SYSTEM_SWAN, + DIV_SYSTEM_VERA, + DIV_SYSTEM_BUBSYS_WSG, + DIV_SYSTEM_N163, + DIV_SYSTEM_PET, + DIV_SYSTEM_VIC20, + DIV_SYSTEM_VRC6, + DIV_SYSTEM_FDS, + DIV_SYSTEM_MMC5, + 0 // don't remove this last one! +}; + diff --git a/src/gui/guiConst.h b/src/gui/guiConst.h index 3d252b536..93dd143a0 100644 --- a/src/gui/guiConst.h +++ b/src/gui/guiConst.h @@ -19,7 +19,31 @@ // guiConst: constants used in the GUI like arrays, strings and other stuff +struct FurnaceGUIActionDef { + const char* name; + const char* friendlyName; + int defaultBind; + FurnaceGUIActionDef(const char* n, const char* fn, int db): + name(n), friendlyName(fn), defaultBind(db) {} +}; + +struct FurnaceGUIColorDef { + const char* name; + const char* friendlyName; + unsigned int defaultColor; + FurnaceGUIColorDef(const char* n, const char* fn, unsigned int dc): + name(n), friendlyName(fn), defaultColor(dc) {} +}; + extern const int opOrder[4]; 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* resampleStrats[]; +extern const int availableSystems[]; +extern const FurnaceGUIActionDef guiActions[]; +extern const FurnaceGUIColorDef guiColors[]; +extern const int altValues[24]; +extern const int vgmVersions[6]; \ No newline at end of file diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 4790d61c1..3e1416128 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -27,41 +27,20 @@ #include #include "plot_nolerp.h" -const char* insTypes[24]={ - "Standard", - "FM (4-operator)", - "Game Boy", - "C64", - "Amiga/Sample", - "PC Engine", - "AY-3-8910/SSG", - "AY8930", - "TIA", - "SAA1099", - "VIC", - "PET", - "VRC6", - "FM (OPLL)", - "FM (OPL)", - "FDS", - "Virtual Boy", - "Namco 163", - "Konami SCC", - "FM (OPZ)", - "POKEY", - "PC Beeper", - "WonderSwan", - "Atari Lynx" -}; - const char* ssgEnvTypes[8]={ "Down Down Down", "Down.", "Down Up Down Up", "Down UP", "Up Up Up", "Up.", "Up Down Up Down", "Up DOWN" }; -const char* fmParamNames[3][27]={ - {"Algorithm", "Feedback", "LFO > Freq", "LFO > Amp", "Attack", "Decay", "Decay 2", "Release", "Sustain", "Level", "EnvScale", "Multiplier", "Detune", "Detune 2", "SSG-EG", "AM", "AM Depth", "Vibrato Depth", "EnvAlternate", "EnvAlternate", "LevelScale/key", "Sustain", "Vibrato", "Waveform", "EnvScale/key", "OP2 HalfSine", "OP1 HalfSine"}, - {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "SR", "RR", "SL", "TL", "KS", "MULT", "DT", "DT2", "SSG-EG", "AM", "AMD", "FMD", "EGT", "EGT", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM"}, - {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "D2R", "RR", "SL", "TL", "RS", "MULT", "DT", "DT2", "SSG-EG", "AM", "DAM", "DVB", "EGT", "EGS", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM"} +const char* fmParamNames[3][32]={ + {"Algorithm", "Feedback", "LFO > Freq", "LFO > Amp", "Attack", "Decay", "Decay 2", "Release", "Sustain", "Level", "EnvScale", "Multiplier", "Detune", "Detune 2", "SSG-EG", "AM", "AM Depth", "Vibrato Depth", "Sustained", "Sustained", "Level Scaling", "Sustain", "Vibrato", "Waveform", "Key Scale Rate", "OP2 Half Sine", "OP1 Half Sine", "EnvShift", "Reverb", "Fine", "LFO2 > Freq", "LFO2 > Amp"}, + {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "SR", "RR", "SL", "TL", "KS", "MULT", "DT", "DT2", "SSG-EG", "AM", "AMD", "FMD", "EGT", "EGT", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM", "EGS", "REV", "Fine", "FMS/PMS2", "AMS2"}, + {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "D2R", "RR", "SL", "TL", "RS", "MULT", "DT", "DT2", "SSG-EG", "AM", "DAM", "DVB", "EGT", "EGS", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM", "EGS", "REV", "Fine", "FMS/PMS2", "AMS2"} +}; + +const char* fmParamShortNames[3][32]={ + {"ALG", "FB", "FMS", "AMS", "A", "D", "D2", "R", "S", "TL", "RS", "ML", "DT", "DT2", "SSG", "AM", "DAM", "DVB", "SUS", "SUS", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM", "EGS", "REV", "Fine", "FMS2", "AMS2"}, + {"ALG", "FB", "FMS", "AMS", "A", "D", "SR", "R", "S", "TL", "KS", "ML", "DT", "DT2", "SSG", "AM", "AMD", "FMD", "EGT", "EGT", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM", "EGS", "REV", "Fine", "FMS2", "AMS2"}, + {"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]={ @@ -81,7 +60,15 @@ const char* opllInsNames[17]={ "Synth Bass", "Acoustic Bass", "Electric Guitar", - "Drums (compatibility only!)" + "Drums" +}; + +const char* oplWaveforms[8]={ + "Sine", "Half Sine", "Absolute Sine", "Quarter Sine", "Squished Sine", "Squished AbsSine", "Square", "Derived Square" +}; + +const char* opzWaveforms[8]={ + "Sine", "Triangle", "Cut Sine", "Cut Triangle", "Squished Sine", "Squished Triangle", "Squished AbsSine", "Squished AbsTriangle" }; enum FMParams { @@ -111,10 +98,20 @@ enum FMParams { FM_WS=23, FM_KSR=24, FM_DC=25, - FM_DM=26 + FM_DM=26, + FM_EGSHIFT=27, + FM_REV=28, + FM_FINE=29, + FM_FMS2=30, + FM_AMS2=31 }; #define FM_NAME(x) fmParamNames[settings.fmNames][x] +#define FM_SHORT_NAME(x) fmParamShortNames[settings.fmNames][x] + +const char* fmOperatorBits[5]={ + "op1", "op2", "op3", "op4", NULL +}; const char* c64ShapeBits[5]={ "triangle", "saw", "pulse", "noise", NULL @@ -148,10 +145,51 @@ const char* mikeyFeedbackBits[11] = { "0", "1", "2", "3", "4", "5", "7", "10", "11", "int", NULL }; +const char* x1_010EnvBits[8]={ + "enable", "oneshot", "split L/R", "HinvR", "VinvR", "HinvL", "VinvL", NULL +}; + +const char* n163UpdateBits[8]={ + "now", "every waveform changed", NULL +}; + +const char* oneBit[2]={ + "on", NULL +}; + const int orderedOps[4]={ 0, 2, 1, 3 }; +const char* singleWSEffects[6]={ + "None", + "Invert", + "Add", + "Subtract", + "Average", + "Phase", +}; + +const char* dualWSEffects[7]={ + "None (dual)", + "Wipe", + "Fade", + "Wipe (ping-pong)", + "Overlay", + "Negative Overlay", + "Phase (dual)", +}; + +const char* macroAbsoluteMode[2]={ + "Relative", + "Absolute" +}; + +const char* macroDummyMode[2]={ + "Bug", + "Bug" +}; + String macroHoverNote(int id, float val) { if (val<-60 || val>=120) return "???"; return fmt::sprintf("%d: %s",id,noteNames[(int)val+60]); @@ -183,6 +221,266 @@ String macroLFOWaves(int id, float val) { return "???"; } +void addAALine(ImDrawList* dl, const ImVec2& p1, const ImVec2& p2, const ImU32 color, float thickness=1.0f) { + ImVec2 pt[2]; + pt[0]=p1; + pt[1]=p2; + dl->AddPolyline(pt,2,color,ImDrawFlags_None,thickness); +} + +void FurnaceGUI::drawSSGEnv(unsigned char type, const ImVec2& size) { + ImDrawList* dl=ImGui::GetWindowDrawList(); + ImGuiWindow* window=ImGui::GetCurrentWindow(); + + ImVec2 minArea=window->DC.CursorPos; + ImVec2 maxArea=ImVec2( + minArea.x+size.x, + minArea.y+size.y + ); + ImRect rect=ImRect(minArea,maxArea); + ImGuiStyle& style=ImGui::GetStyle(); + ImU32 color=ImGui::GetColorU32(uiColors[GUI_COLOR_TEXT]); + ImGui::ItemSize(size,style.FramePadding.y); + if (ImGui::ItemAdd(rect,ImGui::GetID("ssgEnvDisplay"))) { + ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); + switch (type) { + case 0: + for (int i=0; i<4; i++) { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2((float)i/4.0f,0.2)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/4.0f,0.8)); + addAALine(dl,pos1,pos2,color); + pos1.x=pos2.x; + if (i<3) addAALine(dl,pos1,pos2,color); + } + break; + case 1: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.2)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.8)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.8)); + addAALine(dl,pos1,pos2,color); + break; + } + case 2: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.2)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.8)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.2)); + addAALine(dl,pos1,pos2,color); + + pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.8)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.2)); + addAALine(dl,pos1,pos2,color); + break; + } + case 3: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.2)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.8)); + addAALine(dl,pos1,pos2,color); + + pos1.x=pos2.x; + addAALine(dl,pos1,pos2,color); + + pos2=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.2)); + addAALine(dl,pos1,pos2,color); + break; + } + case 4: + for (int i=0; i<4; i++) { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2((float)i/4.0f,0.8)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/4.0f,0.2)); + addAALine(dl,pos1,pos2,color); + pos1.x=pos2.x; + if (i<3) addAALine(dl,pos1,pos2,color); + } + break; + case 5: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.8)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.2)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.2)); + addAALine(dl,pos1,pos2,color); + break; + } + case 6: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.8)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.2)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.8)); + addAALine(dl,pos1,pos2,color); + + pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.2)); + addAALine(dl,pos1,pos2,color); + + pos1=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.8)); + addAALine(dl,pos1,pos2,color); + break; + } + case 7: { + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,0.8)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.2)); + addAALine(dl,pos1,pos2,color); + + pos1.x=pos2.x; + addAALine(dl,pos1,pos2,color); + + pos2=ImLerp(rect.Min,rect.Max,ImVec2(1.0,0.8)); + addAALine(dl,pos1,pos2,color); + break; + } + } + } +} + +void FurnaceGUI::drawWaveform(unsigned char type, bool opz, const ImVec2& size) { + ImDrawList* dl=ImGui::GetWindowDrawList(); + ImGuiWindow* window=ImGui::GetCurrentWindow(); + + ImVec2 waveform[65]; + const size_t waveformLen=64; + + ImVec2 minArea=window->DC.CursorPos; + ImVec2 maxArea=ImVec2( + minArea.x+size.x, + minArea.y+size.y + ); + ImRect rect=ImRect(minArea,maxArea); + ImGuiStyle& style=ImGui::GetStyle(); + ImU32 color=ImGui::GetColorU32(uiColors[GUI_COLOR_TEXT]); + ImGui::ItemSize(size,style.FramePadding.y); + if (ImGui::ItemAdd(rect,ImGui::GetID("wsDisplay"))) { + ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); + if (opz) { + switch (type) { + case 0: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=sin(x*2.0*M_PI); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 1: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=pow(sin(x*2.0*M_PI),2.0); + if (x>=0.5) y=-y; + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 2: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=MAX(0.0,sin(x*2.0*M_PI)); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 3: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=pow(MAX(0.0,sin(x*2.0*M_PI)),2.0); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 4: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:sin(x*4.0*M_PI); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 5: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:pow(sin(x*4.0*M_PI),2.0); + if (x>=0.25) y=-y; + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 6: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:fabs(sin(x*4.0*M_PI)); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 7: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:pow(sin(x*4.0*M_PI),2.0); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + } + } else { + switch (type) { + case 0: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=sin(x*2.0*M_PI); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 1: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=MAX(0.0,sin(x*2.0*M_PI)); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 2: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=fabs(sin(x*2.0*M_PI)); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 3: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=fabs((tan(x*2.0*M_PI)>=0.0)?sin(x*2.0*M_PI):0.0); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 4: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:sin(x*4.0*M_PI); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 5: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?0.0:fabs(sin(x*4.0*M_PI)); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 6: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=(x>=0.5)?-1.0:1.0; + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + case 7: + for (size_t i=0; i<=waveformLen; i++) { + float x=(float)i/(float)waveformLen; + float y=pow(2.0*(x-0.5),3.0); + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5-y*0.4)); + } + break; + } + } + dl->AddPolyline(waveform,waveformLen+1,color,ImDrawFlags_None,dpiScale); + } +} + void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, const ImVec2& size) { ImDrawList* dl=ImGui::GetWindowDrawList(); ImGuiWindow* window=ImGui::GetCurrentWindow(); @@ -199,7 +497,6 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImGui::ItemSize(size,style.FramePadding.y); if (ImGui::ItemAdd(rect,ImGui::GetID("alg"))) { ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); - //ImReallyTiredOfThisGarbage(); const float circleRadius=6.0f*dpiScale+1.0f; switch (algType) { case FM_ALGS_4OP: @@ -211,11 +508,11 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.8,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); + addAALine(dl,pos1,pos2,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos3,colorL); + addAALine(dl,pos2,pos3,colorL); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos3,pos4,colorL); + addAALine(dl,pos3,pos4,colorL); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); pos1.x-=ImGui::CalcTextSize("1").x*0.5; @@ -239,11 +536,11 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos3,colorL); + addAALine(dl,pos1,pos3,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos3,colorL); + addAALine(dl,pos2,pos3,colorL); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos3,pos4,colorL); + addAALine(dl,pos3,pos4,colorL); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -267,11 +564,11 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos4,colorL); + addAALine(dl,pos1,pos4,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos3,colorL); + addAALine(dl,pos2,pos3,colorL); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos3,pos4,colorL); + addAALine(dl,pos3,pos4,colorL); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -295,11 +592,11 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); + addAALine(dl,pos1,pos2,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos4,colorL); + addAALine(dl,pos2,pos4,colorL); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos3,pos4,colorL); + addAALine(dl,pos3,pos4,colorL); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -324,13 +621,13 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); + addAALine(dl,pos1,pos2,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos3,pos4,colorL); + addAALine(dl,pos3,pos4,colorL); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos5,colorL); - dl->AddLine(pos4,pos5,colorL); + addAALine(dl,pos2,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -354,15 +651,15 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); - dl->AddLine(pos1,pos3,colorL); - dl->AddLine(pos1,pos4,colorL); + addAALine(dl,pos1,pos2,colorL); + addAALine(dl,pos1,pos3,colorL); + addAALine(dl,pos1,pos4,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos5,colorL); - dl->AddLine(pos3,pos5,colorL); - dl->AddLine(pos4,pos5,colorL); + addAALine(dl,pos2,pos5,colorL); + addAALine(dl,pos3,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -386,13 +683,13 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); + addAALine(dl,pos1,pos2,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos2,pos5,colorL); - dl->AddLine(pos3,pos5,colorL); - dl->AddLine(pos4,pos5,colorL); + addAALine(dl,pos2,pos5,colorL); + addAALine(dl,pos3,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -419,10 +716,10 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos5,colorL); - dl->AddLine(pos2,pos5,colorL); - dl->AddLine(pos3,pos5,colorL); - dl->AddLine(pos4,pos5,colorL); + addAALine(dl,pos1,pos5,colorL); + addAALine(dl,pos2,pos5,colorL); + addAALine(dl,pos3,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -447,7 +744,7 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.67,0.5)); dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); - dl->AddLine(pos1,pos2,colorL); + addAALine(dl,pos1,pos2,colorL); dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; @@ -475,13 +772,136 @@ void FurnaceGUI::drawAlgorithm(unsigned char alg, FurnaceGUIFMAlgs algType, cons } } break; + case FM_ALGS_4OP_OPL: + switch (alg) { + case 0: { // 1 > 2 > 3 > 4 + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.2,0.5)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.4,0.5)); + ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(0.6,0.5)); + ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.8,0.5)); + dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); + dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); + addAALine(dl,pos1,pos2,colorL); + dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos2,pos3,colorL); + dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos3,pos4,colorL); + dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); + + pos1.x-=ImGui::CalcTextSize("1").x*0.5; + pos2.x-=ImGui::CalcTextSize("2").x*0.5; + pos3.x-=ImGui::CalcTextSize("3").x*0.5; + pos4.x-=ImGui::CalcTextSize("4").x*0.5; + pos1.y-=ImGui::CalcTextSize("1").y+circleRadius; + pos2.y-=ImGui::CalcTextSize("2").y+circleRadius; + pos3.y-=ImGui::CalcTextSize("3").y+circleRadius; + pos4.y-=ImGui::CalcTextSize("4").y+circleRadius; + dl->AddText(pos1,color,"1"); + dl->AddText(pos2,color,"2"); + dl->AddText(pos3,color,"3"); + dl->AddText(pos4,color,"4"); + break; + } + case 1: { // 1 + (2 > 3 > 4) + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.4,0.3)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.2,0.7)); + ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(0.4,0.7)); + ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.6,0.7)); + ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.8,0.7)); + dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); + dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); + addAALine(dl,pos1,pos5,colorL); + dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos2,pos3,colorL); + dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos3,pos4,colorL); + dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); + + addAALine(dl,pos4,pos5,colorL); + + pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos3.x-=ImGui::CalcTextSize("3").x+circleRadius+3.0*dpiScale; + pos4.x-=ImGui::CalcTextSize("4").x*0.5; + pos1.y-=ImGui::CalcTextSize("1").y*0.5; + pos2.y-=ImGui::CalcTextSize("2").y*0.5; + pos3.y-=ImGui::CalcTextSize("3").y*0.5; + pos4.y-=ImGui::CalcTextSize("4").y+circleRadius; + dl->AddText(pos1,color,"1"); + dl->AddText(pos2,color,"2"); + dl->AddText(pos3,color,"3"); + dl->AddText(pos4,color,"4"); + break; + } + case 2: { // (1>2) + (3>4) + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.3)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.3)); + ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.7)); + ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.7)); + ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); + dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); + dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); + addAALine(dl,pos1,pos2,colorL); + dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); + dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos3,pos4,colorL); + dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos2,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); + + pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos3.x-=ImGui::CalcTextSize("3").x+circleRadius+3.0*dpiScale; + pos4.x-=ImGui::CalcTextSize("4").x+circleRadius+3.0*dpiScale; + pos1.y-=ImGui::CalcTextSize("1").y*0.5; + pos2.y-=ImGui::CalcTextSize("2").y*0.5; + pos3.y-=ImGui::CalcTextSize("3").y*0.5; + pos4.y-=ImGui::CalcTextSize("4").y*0.5; + dl->AddText(pos1,color,"1"); + dl->AddText(pos2,color,"2"); + dl->AddText(pos3,color,"3"); + dl->AddText(pos4,color,"4"); + break; + } + case 3: { // 1 + (2 > 3) + 4 + ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.25)); + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(0.25,0.5)); + ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.5)); + ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(0.5,0.75)); + ImVec2 pos5=ImLerp(rect.Min,rect.Max,ImVec2(0.75,0.5)); + dl->AddCircleFilled(pos1,4.0f*dpiScale+1.0f,color); + dl->AddCircle(pos1,6.0f*dpiScale+1.0f,color); + addAALine(dl,pos2,pos3,colorL); + dl->AddCircleFilled(pos2,4.0f*dpiScale+1.0f,color); + dl->AddCircleFilled(pos3,4.0f*dpiScale+1.0f,color); + dl->AddCircleFilled(pos4,4.0f*dpiScale+1.0f,color); + addAALine(dl,pos1,pos5,colorL); + addAALine(dl,pos3,pos5,colorL); + addAALine(dl,pos4,pos5,colorL); + + pos1.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos2.x-=ImGui::CalcTextSize("2").x+circleRadius+3.0*dpiScale; + pos3.x-=ImGui::CalcTextSize("3").x+circleRadius+3.0*dpiScale; + pos4.x-=ImGui::CalcTextSize("4").x+circleRadius+3.0*dpiScale; + pos1.y-=ImGui::CalcTextSize("1").y*0.5; + pos2.y-=ImGui::CalcTextSize("2").y*0.5; + pos3.y-=ImGui::CalcTextSize("3").y*0.5; + pos4.y-=ImGui::CalcTextSize("4").y*0.5; + dl->AddText(pos1,color,"1"); + dl->AddText(pos2,color,"2"); + dl->AddText(pos3,color,"3"); + dl->AddText(pos4,color,"4"); + break; + } + } + break; default: break; } } } -void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, unsigned char d2r, unsigned char rr, unsigned char sl, const ImVec2& size) { +void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, unsigned char d2r, unsigned char rr, unsigned char sl, float maxTl, float maxArDr, const ImVec2& size) { ImDrawList* dl=ImGui::GetWindowDrawList(); ImGuiWindow* window=ImGui::GetCurrentWindow(); @@ -499,8 +919,8 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); //calculate x positions - float arPos=float(31-ar)/31.0; //peak of AR, start of DR - float drPos=arPos+((sl/15.0)*(float(31-dr)/31.0)); //end of DR, start of D2R + float arPos=float(maxArDr-ar)/maxArDr; //peak of AR, start of DR + float drPos=arPos+((sl/15.0)*(float(maxArDr-dr)/maxArDr)); //end of DR, start of D2R float d2rPos=drPos+(((15.0-sl)/15.0)*(float(31.0-d2r)/31.0)); //End of D2R float rrPos=(float(15-rr)/15.0); //end of RR @@ -511,54 +931,53 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, rrPos/=1.0; ImVec2 pos1=ImLerp(rect.Min,rect.Max,ImVec2(0.0,1.0)); //the bottom corner - ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(arPos,(tl/127.0))); //peak of AR, start of DR - ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(drPos,(float)((tl/127.0)+(sl/15.0)-((tl/127.0)*(sl/15.0))))); //end of DR, start of D2R + ImVec2 pos2=ImLerp(rect.Min,rect.Max,ImVec2(arPos,(tl/maxTl))); //peak of AR, start of DR + ImVec2 pos3=ImLerp(rect.Min,rect.Max,ImVec2(drPos,(float)((tl/maxTl)+(sl/15.0)-((tl/maxTl)*(sl/15.0))))); //end of DR, start of D2R ImVec2 pos4=ImLerp(rect.Min,rect.Max,ImVec2(d2rPos,1.0)); //end of D2R - ImVec2 posRStart=ImLerp(rect.Min,rect.Max,ImVec2(0.0,(tl/127.0))); //release start + ImVec2 posRStart=ImLerp(rect.Min,rect.Max,ImVec2(0.0,(tl/maxTl))); //release start ImVec2 posREnd=ImLerp(rect.Min,rect.Max,ImVec2(rrPos,1.0));//release end - ImVec2 posSLineHEnd=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(float)((tl/127.0)+(sl/15.0)-((tl/127.0)*(sl/15.0))))); //sustain horizontal line end + ImVec2 posSLineHEnd=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(float)((tl/maxTl)+(sl/15.0)-((tl/maxTl)*(sl/15.0))))); //sustain horizontal line end ImVec2 posSLineVEnd=ImLerp(rect.Min,rect.Max,ImVec2(drPos,1.0)); //sustain vertical line end - ImVec2 posDecayRate0Pt=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(tl/127.0))); //Heght of the peak of AR, forever - ImVec2 posDecay2Rate0Pt=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(float)((tl/127.0)+(sl/15.0)-((tl/127.0)*(sl/15.0))))); //Heght of the peak of SR, forever + ImVec2 posDecayRate0Pt=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(tl/maxTl))); //Height of the peak of AR, forever + ImVec2 posDecay2Rate0Pt=ImLerp(rect.Min,rect.Max,ImVec2(1.0,(float)((tl/maxTl)+(sl/15.0)-((tl/maxTl)*(sl/15.0))))); //Height of the peak of SR, forever + //dl->Flags=ImDrawListFlags_AntiAliasedLines|ImDrawListFlags_AntiAliasedLinesUseTex; if (ar==0.0) { //if AR = 0, the envelope never starts dl->AddTriangleFilled(posRStart,posREnd,pos1,colorS); //draw release as shaded triangle behind everything - dl->AddLine(pos1,pos4,color); //draw line on ground - } - else if (dr==0.0 && sl!=0.0) { //if DR = 0 and SL is not 0, then the envelope stays at max volume forever + addAALine(dl,pos1,pos4,color); //draw line on ground + } else if (dr==0.0 && sl!=0.0) { //if DR = 0 and SL is not 0, then the envelope stays at max volume forever dl->AddTriangleFilled(posRStart,posREnd,pos1,colorS); //draw release as shaded triangle behind everything - //dl->AddLine(pos3,posSLineHEnd,colorS); //draw horiz line through sustain level - //dl->AddLine(pos3,posSLineVEnd,colorS); //draw vert. line through sustain level - dl->AddLine(pos1,pos2,color); //A - dl->AddLine(pos2,posDecayRate0Pt,color); //Line from A to end of graph - } - else if(d2r==0.0) { //if D2R = 0, the envelope stays at the sustain level forever + //addAALine(dl,pos3,posSLineHEnd,colorS); //draw horiz line through sustain level + //addAALine(dl,pos3,posSLineVEnd,colorS); //draw vert. line through sustain level + addAALine(dl,pos1,pos2,color); //A + addAALine(dl,pos2,posDecayRate0Pt,color); //Line from A to end of graph + } else if (d2r==0.0) { //if D2R = 0, the envelope stays at the sustain level forever dl->AddTriangleFilled(posRStart,posREnd,pos1,colorS); //draw release as shaded triangle behind everything - dl->AddLine(pos3,posSLineHEnd,colorS); //draw horiz line through sustain level - dl->AddLine(pos3,posSLineVEnd,colorS); //draw vert. line through sustain level - dl->AddLine(pos1,pos2,color); //A - dl->AddLine(pos2,pos3,color); //D - dl->AddLine(pos3,posDecay2Rate0Pt,color); //Line from D to end of graph - } - else { //draw graph normally + addAALine(dl,pos3,posSLineHEnd,colorS); //draw horiz line through sustain level + addAALine(dl,pos3,posSLineVEnd,colorS); //draw vert. line through sustain level + addAALine(dl,pos1,pos2,color); //A + addAALine(dl,pos2,pos3,color); //D + addAALine(dl,pos3,posDecay2Rate0Pt,color); //Line from D to end of graph + } else { //draw graph normally dl->AddTriangleFilled(posRStart,posREnd,pos1,colorS); //draw release as shaded triangle behind everything - dl->AddLine(pos3,posSLineHEnd,colorS); //draw horiz line through sustain level - dl->AddLine(pos3,posSLineVEnd,colorS); //draw vert. line through sustain level - dl->AddLine(pos1,pos2,color); //A - dl->AddLine(pos2,pos3,color); //D - dl->AddLine(pos3,pos4,color); //D2 + addAALine(dl,pos3,posSLineHEnd,colorS); //draw horiz line through sustain level + addAALine(dl,pos3,posSLineVEnd,colorS); //draw vert. line through sustain level + addAALine(dl,pos1,pos2,color); //A + addAALine(dl,pos2,pos3,color); //D + addAALine(dl,pos3,pos4,color); //D2 } + //dl->Flags^=ImDrawListFlags_AntiAliasedLines|ImDrawListFlags_AntiAliasedLinesUseTex; } } #define P(x) if (x) { \ - modified=true; \ + MARK_MODIFIED; \ e->notifyInsChange(curIns); \ } -#define PARAMETER modified=true; e->notifyInsChange(curIns); +#define PARAMETER MARK_MODIFIED; e->notifyInsChange(curIns); -#define NORMAL_MACRO(macro,macroLen,macroLoop,macroRel,macroMin,macroHeight,macroName,displayName,displayHeight,displayLoop,bitfield,bfVal,drawSlider,sliderVal,sliderLow,macroDispMin,bitOff,macroMode,macroColor,mmlStr,macroAMin,macroAMax,hoverFunc,blockMode) \ +#define NORMAL_MACRO(macro,macroMin,macroHeight,macroName,displayName,displayHeight,displayLoop,bitfield,bfVal,drawSlider,sliderVal,sliderLow,macroDispMin,bitOff,macroMode,macroModeMax,displayModeName,macroColor,mmlStr,macroAMin,macroAMax,hoverFunc,blockMode) \ ImGui::TableNextRow(); \ ImGui::TableNextColumn(); \ ImGui::Text("%s",displayName); \ @@ -568,26 +987,41 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, } \ if (displayLoop) { \ ImGui::SetNextItemWidth(lenAvail); \ - if (ImGui::InputScalar("##IMacroLen_" macroName,ImGuiDataType_U8,¯oLen,&_ONE,&_THREE)) { \ - if (macroLen>127) macroLen=127; \ + if (ImGui::InputScalar("##IMacroLen_" macroName,ImGuiDataType_U8,¯o.len,&_ONE,&_THREE)) { MARK_MODIFIED \ + if (macro.len>127) macro.len=127; \ } \ - if (macroMode!=NULL) { \ - ImGui::Checkbox("Fixed##IMacroMode_" macroName,macroMode); \ + if (macroMode) { \ + String modeName; \ + if (macro.mode>macroModeMax) { \ + modeName="none selected"; \ + } else { \ + modeName=displayModeName[macro.mode]; \ + } \ + if (ImGui::BeginCombo("TODO: Improve##IMacroMode_" macroName,modeName.c_str())) { \ + String id; \ + for (unsigned int i=0; i<=macroModeMax; i++) { \ + id=fmt::sprintf("%d: %s",i,displayModeName[i]); \ + if (ImGui::Selectable(id.c_str(),macro.mode==i)) { PARAMETER \ + macro.mode=i; \ + } \ + } \ + ImGui::EndCombo(); \ + } \ } \ } \ ImGui::TableNextColumn(); \ for (int j=0; j<256; j++) { \ - if (j+macroDragScroll>=macroLen) { \ + if (j+macroDragScroll>=macro.len) { \ asFloat[j]=0; \ asInt[j]=0; \ } else { \ - asFloat[j]=macro[j+macroDragScroll]+macroDispMin; \ - asInt[j]=macro[j+macroDragScroll]+macroDispMin+bitOff; \ + asFloat[j]=macro.val[j+macroDragScroll]+macroDispMin; \ + asInt[j]=macro.val[j+macroDragScroll]+macroDispMin+bitOff; \ } \ - if (j+macroDragScroll>=macroLen || (j+macroDragScroll>macroRel && macroLoop=macro.len || (j+macroDragScroll>macro.rel && macro.loop=macroLoop))|((macroRel!=-1 && (j+macroDragScroll)==macroRel)<<1); \ + loopIndicator[j]=((macro.loop!=-1 && (j+macroDragScroll)>=macro.loop))|((macro.rel!=-1 && (j+macroDragScroll)==macro.rel)<<1); \ } \ } \ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); \ @@ -595,7 +1029,7 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, if (bitfield) { \ PlotBitfield("##IMacro_" macroName,asInt,totalFit,0,bfVal,macroHeight,ImVec2(availableWidth,(displayLoop)?(displayHeight*dpiScale):(32.0f*dpiScale))); \ } else { \ - PlotCustom("##IMacro_" macroName,asFloat,totalFit,macroDragScroll,NULL,macroDispMin+macroMin,macroHeight+macroDispMin,ImVec2(availableWidth,(displayLoop)?(displayHeight*dpiScale):(32.0f*dpiScale)),sizeof(float),macroColor,macroLen-macroDragScroll,hoverFunc,blockMode); \ + PlotCustom("##IMacro_" macroName,asFloat,totalFit,macroDragScroll,NULL,macroDispMin+macroMin,macroHeight+macroDispMin,ImVec2(availableWidth,(displayLoop)?(displayHeight*dpiScale):(32.0f*dpiScale)),sizeof(float),macroColor,macro.len-macroDragScroll,hoverFunc,blockMode); \ } \ if (displayLoop && ImGui::IsItemClicked(ImGuiMouseButton_Left)) { \ macroDragStart=ImGui::GetItemRectMin(); \ @@ -608,46 +1042,46 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, macroDragInitialValue=false; \ macroDragLen=totalFit; \ macroDragActive=true; \ - macroDragTarget=macro; \ + macroDragTarget=macro.val; \ macroDragChar=false; \ processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); \ } \ if (displayLoop) { \ if (drawSlider) { \ ImGui::SameLine(); \ - ImGui::VSliderInt("##IArpMacroPos",ImVec2(20.0f*dpiScale,displayHeight*dpiScale),sliderVal,sliderLow,70); \ + CWVSliderInt("##IArpMacroPos",ImVec2(20.0f*dpiScale,displayHeight*dpiScale),sliderVal,sliderLow,70); \ } \ - PlotCustom("##IMacroLoop_" macroName,loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),macroColor,macroLen-macroDragScroll,¯oHoverLoop); \ + PlotCustom("##IMacroLoop_" macroName,loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),macroColor,macro.len-macroDragScroll,¯oHoverLoop); \ if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { \ macroLoopDragStart=ImGui::GetItemRectMin(); \ macroLoopDragAreaSize=ImVec2(availableWidth,12.0f*dpiScale); \ macroLoopDragLen=totalFit; \ if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ - macroLoopDragTarget=¯oRel; \ + macroLoopDragTarget=¯o.rel; \ } else { \ - macroLoopDragTarget=¯oLoop; \ + macroLoopDragTarget=¯o.loop; \ } \ macroLoopDragActive=true; \ processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); \ } \ if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { \ if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ - macroRel=-1; \ + macro.rel=-1; \ } else { \ - macroLoop=-1; \ + macro.loop=-1; \ } \ } \ ImGui::SetNextItemWidth(availableWidth); \ if (ImGui::InputText("##IMacroMML_" macroName,&mmlStr)) { \ - decodeMMLStr(mmlStr,macro,macroLen,macroLoop,macroAMin,(bitfield)?((1<127) macroLen=127; \ + if (ImGui::InputScalar("##IOPMacroLen_" #op macroName,ImGuiDataType_U8,¯o.len,&_ONE,&_THREE)) { MARK_MODIFIED \ + if (macro.len>127) macro.len=127; \ + } \ + if (macroMode) { \ + String modeName; \ + if (macro.mode>macroModeMax) { \ + modeName="none selected"; \ + } else { \ + modeName=displayModeName[macro.mode]; \ + } \ + if (ImGui::BeginCombo("TODO: Improve##IOPMacroMode_" macroName,modeName.c_str())) { \ + String id; \ + for (unsigned int i=0; i<=macroModeMax; i++) { \ + id=fmt::sprintf("%d: %s",i,displayModeName[i]); \ + if (ImGui::Selectable(id.c_str(),macro.mode==i)) { PARAMETER \ + macro.mode=i; \ + } \ + } \ + ImGui::EndCombo(); \ + } \ } \ } \ ImGui::TableNextColumn(); \ for (int j=0; j<256; j++) { \ - if (j+macroDragScroll>=macroLen) { \ + if (j+macroDragScroll>=macro.len) { \ asFloat[j]=0; \ asInt[j]=0; \ } else { \ - asFloat[j]=macro[j+macroDragScroll]; \ - asInt[j]=macro[j+macroDragScroll]; \ + asFloat[j]=macro.val[j+macroDragScroll]; \ + asInt[j]=macro.val[j+macroDragScroll]; \ } \ - if (j+macroDragScroll>=macroLen || (j+macroDragScroll>macroRel && macroLoop=macro.len || (j+macroDragScroll>macro.rel && macro.loop=macroLoop))|((macroRel!=-1 && (j+macroDragScroll)==macroRel)<<1); \ + loopIndicator[j]=((macro.loop!=-1 && (j+macroDragScroll)>=macro.loop))|((macro.rel!=-1 && (j+macroDragScroll)==macro.rel)<<1); \ } \ } \ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); \ @@ -681,7 +1133,7 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, if (bitfield) { \ PlotBitfield("##IOPMacro_" #op macroName,asInt,totalFit,0,bfVal,macroHeight,ImVec2(availableWidth,displayLoop?(displayHeight*dpiScale):(24*dpiScale))); \ } else { \ - PlotCustom("##IOPMacro_" #op macroName,asFloat,totalFit,macroDragScroll,NULL,0,macroHeight,ImVec2(availableWidth,displayLoop?(displayHeight*dpiScale):(24*dpiScale)),sizeof(float),uiColors[GUI_COLOR_MACRO_OTHER],macroLen-macroDragScroll); \ + PlotCustom("##IOPMacro_" #op macroName,asFloat,totalFit,macroDragScroll,NULL,0,macroHeight,ImVec2(availableWidth,displayLoop?(displayHeight*dpiScale):(24*dpiScale)),sizeof(float),uiColors[GUI_COLOR_MACRO_OTHER],macro.len-macroDragScroll); \ } \ if (displayLoop && ImGui::IsItemClicked(ImGuiMouseButton_Left)) { \ macroDragStart=ImGui::GetItemRectMin(); \ @@ -694,37 +1146,37 @@ void FurnaceGUI::drawFMEnv(unsigned char tl, unsigned char ar, unsigned char dr, macroDragInitialValue=false; \ macroDragLen=totalFit; \ macroDragActive=true; \ - macroDragCTarget=macro; \ - macroDragChar=true; \ + macroDragTarget=macro.val; \ + macroDragChar=false; \ processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); \ } \ if (displayLoop) { \ - PlotCustom("##IOPMacroLoop_" #op macroName,loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),uiColors[GUI_COLOR_MACRO_OTHER],macroLen-macroDragScroll,¯oHoverLoop); \ + PlotCustom("##IOPMacroLoop_" #op macroName,loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),uiColors[GUI_COLOR_MACRO_OTHER],macro.len-macroDragScroll,¯oHoverLoop); \ if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { \ macroLoopDragStart=ImGui::GetItemRectMin(); \ macroLoopDragAreaSize=ImVec2(availableWidth,8.0f*dpiScale); \ macroLoopDragLen=totalFit; \ if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ - macroLoopDragTarget=¯oRel; \ + macroLoopDragTarget=¯o.rel; \ } else { \ - macroLoopDragTarget=¯oLoop; \ + macroLoopDragTarget=¯o.loop; \ } \ macroLoopDragActive=true; \ processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); \ } \ if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { \ if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { \ - macroRel=-1; \ + macro.rel=-1; \ } else { \ - macroLoop=-1; \ + macro.loop=-1; \ } \ } \ ImGui::SetNextItemWidth(availableWidth); \ if (ImGui::InputText("##IOPMacroMML_" macroName,&mmlStr)) { \ - decodeMMLStr(mmlStr,macro,macroLen,macroLoop,0,bitfield?((1<127-totalFit) macroDragScroll=127-totalFit; \ } @@ -753,13 +1206,53 @@ if (ImGui::BeginTable("MacroSpace",2)) { \ ImGui::TableNextColumn(); \ ImGui::TableNextColumn(); \ ImGui::SetNextItemWidth(availableWidth); \ - if (ImGui::SliderInt("##MacroScroll",¯oDragScroll,0,127-totalFit,"")) { \ + if (CWSliderInt("##MacroScroll",¯oDragScroll,0,127-totalFit,"")) { \ if (macroDragScroll<0) macroDragScroll=0; \ if (macroDragScroll>127-totalFit) macroDragScroll=127-totalFit; \ } \ ImGui::EndTable(); \ } +#define DRUM_FREQ(name,db,df,prop) \ + ImGui::TableNextRow(); \ + ImGui::TableNextColumn(); \ + if (ins->type==DIV_INS_OPLL) { \ + block=(prop>>9)&7; \ + fNum=prop&511; \ + } else { \ + block=(prop>>10)&7; \ + fNum=prop&1023; \ + } \ + ImGui::Text(name); \ + ImGui::TableNextColumn(); \ + if (ImGui::InputInt(db,&block,1,1)) { \ + if (block<0) block=0; \ + if (block>7) block=7; \ + if (ins->type==DIV_INS_OPLL) { \ + prop=(block<<9)|fNum; \ + } else { \ + prop=(block<<10)|fNum; \ + } \ + } \ + ImGui::TableNextColumn(); \ + if (ImGui::InputInt(df,&fNum,1,1)) { \ + if (fNum<0) fNum=0; \ + if (ins->type==DIV_INS_OPLL) { \ + if (fNum>511) fNum=511; \ + prop=(block<<9)|fNum; \ + } else { \ + if (fNum>1023) fNum=1023; \ + prop=(block<<10)|fNum; \ + } \ + } + + +#define CENTER_TEXT(text) \ + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+0.5*(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(text).x)); + +#define CENTER_VSLIDER \ + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+0.5f*ImGui::GetContentRegionAvail().x-10.0f*dpiScale); + void FurnaceGUI::drawInsEdit() { if (nextWindow==GUI_WINDOW_INS_EDIT) { insEditOpen=true; @@ -773,12 +1266,59 @@ void FurnaceGUI::drawInsEdit() { ImGui::Text("no instrument selected"); } else { DivInstrument* ins=e->song.ins[curIns]; - ImGui::InputText("Name",&ins->name); - if (ins->type<0 || ins->type>23) ins->type=DIV_INS_FM; - int insType=ins->type; - if (ImGui::Combo("Type",&insType,insTypes,24,24)) { - ins->type=(DivInstrumentType)insType; + if (ImGui::BeginTable("InsProp",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + String insIndex=fmt::sprintf("%.2X",curIns); + if (ImGui::BeginCombo("##InsSelect",insIndex.c_str())) { + String name; + for (size_t i=0; isong.ins.size(); i++) { + name=fmt::sprintf("%.2X: %s##_INSS%d",i,e->song.ins[i]->name,i); + if (ImGui::Selectable(name.c_str(),curIns==(int)i)) { + curIns=i; + ins=e->song.ins[curIns]; + } + } + ImGui::EndCombo(); + } + + ImGui::TableNextColumn(); + ImGui::Text("Name"); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputText("##Name",&ins->name)) { + MARK_MODIFIED; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + // TODO: load replace + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##IELoad")) { + doAction(GUI_ACTION_INS_LIST_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FLOPPY_O "##IESave")) { + doAction(GUI_ACTION_INS_LIST_SAVE); + } + + ImGui::TableNextColumn(); + ImGui::Text("Type"); + + ImGui::TableNextColumn(); + if (ins->type>=DIV_INS_MAX) ins->type=DIV_INS_FM; + int insType=ins->type; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::Combo("##Type",&insType,insTypes,DIV_INS_MAX,DIV_INS_MAX)) { + ins->type=(DivInstrumentType)insType; + } + + ImGui::EndTable(); } + if (ImGui::BeginTabBar("insEditTab")) { if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPZ) { @@ -787,7 +1327,8 @@ void FurnaceGUI::drawInsEdit() { int asInt[256]; float loopIndicator[256]; int opCount=4; - if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL) opCount=2; + if (ins->type==DIV_INS_OPLL) opCount=2; + if (ins->type==DIV_INS_OPL) opCount=(ins->fm.ops==4)?4:2; if (ImGui::BeginTabItem("FM")) { if (ImGui::BeginTable("fmDetails",3,ImGuiTableFlags_SizingStretchSame)) { @@ -797,24 +1338,58 @@ void FurnaceGUI::drawInsEdit() { ImGui::TableNextRow(); switch (ins->type) { case DIV_INS_FM: - case DIV_INS_OPZ: ImGui::TableNextColumn(); - P(ImGui::SliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); - P(ImGui::SliderScalar(FM_NAME(FM_FMS),ImGuiDataType_U8,&ins->fm.fms,&_ZERO,&_SEVEN)); + P(CWSliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); rightClickable + P(CWSliderScalar(FM_NAME(FM_FMS),ImGuiDataType_U8,&ins->fm.fms,&_ZERO,&_SEVEN)); rightClickable ImGui::TableNextColumn(); - P(ImGui::SliderScalar(FM_NAME(FM_ALG),ImGuiDataType_U8,&ins->fm.alg,&_ZERO,&_SEVEN)); - P(ImGui::SliderScalar(FM_NAME(FM_AMS),ImGuiDataType_U8,&ins->fm.ams,&_ZERO,&_THREE)); + P(CWSliderScalar(FM_NAME(FM_ALG),ImGuiDataType_U8,&ins->fm.alg,&_ZERO,&_SEVEN)); rightClickable + P(CWSliderScalar(FM_NAME(FM_AMS),ImGuiDataType_U8,&ins->fm.ams,&_ZERO,&_THREE)); rightClickable ImGui::TableNextColumn(); drawAlgorithm(ins->fm.alg,FM_ALGS_4OP,ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); break; - case DIV_INS_OPL: + case DIV_INS_OPZ: + ImGui::TableNextColumn(); + P(CWSliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); rightClickable + P(CWSliderScalar(FM_NAME(FM_FMS),ImGuiDataType_U8,&ins->fm.fms,&_ZERO,&_SEVEN)); rightClickable + P(CWSliderScalar(FM_NAME(FM_FMS2),ImGuiDataType_U8,&ins->fm.fms2,&_ZERO,&_SEVEN)); rightClickable + ImGui::TableNextColumn(); + P(CWSliderScalar(FM_NAME(FM_ALG),ImGuiDataType_U8,&ins->fm.alg,&_ZERO,&_SEVEN)); rightClickable + P(CWSliderScalar(FM_NAME(FM_AMS),ImGuiDataType_U8,&ins->fm.ams,&_ZERO,&_THREE)); rightClickable + P(CWSliderScalar(FM_NAME(FM_AMS2),ImGuiDataType_U8,&ins->fm.ams2,&_ZERO,&_THREE)); rightClickable + ImGui::TableNextColumn(); + drawAlgorithm(ins->fm.alg,FM_ALGS_4OP,ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); + if (ImGui::Button("Send to TX81Z")) { + showError("Coming soon!"); + } + break; + case DIV_INS_OPL: { + bool fourOp=(ins->fm.ops==4); + bool drums=ins->fm.opllPreset==16; + int algMax=fourOp?3:1; + ImGui::TableNextColumn(); + ins->fm.alg&=algMax; + P(CWSliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); rightClickable + ImGui::BeginDisabled(ins->fm.opllPreset==16); + if (ImGui::Checkbox("4-op",&fourOp)) { PARAMETER + ins->fm.ops=fourOp?4:2; + } + ImGui::EndDisabled(); + ImGui::TableNextColumn(); + P(CWSliderScalar(FM_NAME(FM_ALG),ImGuiDataType_U8,&ins->fm.alg,&_ZERO,&algMax)); rightClickable + if (ImGui::Checkbox("Drums",&drums)) { PARAMETER + ins->fm.opllPreset=drums?16:0; + } + ImGui::TableNextColumn(); + drawAlgorithm(ins->fm.alg&algMax,fourOp?FM_ALGS_4OP_OPL:FM_ALGS_2OP_OPL,ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); + break; + } case DIV_INS_OPLL: { bool dc=ins->fm.fms; bool dm=ins->fm.ams; bool sus=ins->fm.alg; ImGui::TableNextColumn(); ImGui::BeginDisabled(ins->fm.opllPreset!=0); - P(ImGui::SliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); + P(CWSliderScalar(FM_NAME(FM_FB),ImGuiDataType_U8,&ins->fm.fb,&_ZERO,&_SEVEN)); rightClickable if (ImGui::Checkbox(FM_NAME(FM_DC),&dc)) { PARAMETER ins->fm.fms=dc; } @@ -847,195 +1422,658 @@ void FurnaceGUI::drawInsEdit() { ImGui::EndTable(); } - if (ins->type==DIV_INS_OPLL && ins->fm.opllPreset==16) { - ImGui::Text("the Drums patch is only there for compatibility.\nit is highly encouraged you use the OPLL (drums) system instead!"); + if ((ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL) && ins->fm.opllPreset==16) { + ins->fm.ops=2; + P(ImGui::Checkbox("Fixed frequency mode",&ins->fm.fixedDrums)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, drums will be set to the specified frequencies, ignoring the note."); + } + if (ins->fm.fixedDrums) { + int block=0; + int fNum=0; + if (ImGui::BeginTable("fixedDrumSettings",3)) { + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Drum"); + ImGui::TableNextColumn(); + ImGui::Text("Block"); + ImGui::TableNextColumn(); + ImGui::Text("FreqNum"); + + DRUM_FREQ("Kick","##DBlock0","##DFreq0",ins->fm.kickFreq); + DRUM_FREQ("Snare/Hi-hat","##DBlock1","##DFreq1",ins->fm.snareHatFreq); + DRUM_FREQ("Tom/Top","##DBlock2","##DFreq2",ins->fm.tomTopFreq); + ImGui::EndTable(); + } + } } bool willDisplayOps=true; if (ins->type==DIV_INS_OPLL && ins->fm.opllPreset!=0) willDisplayOps=false; if (!willDisplayOps && ins->type==DIV_INS_OPLL) { - P(ImGui::SliderScalar("Volume##TL",ImGuiDataType_U8,&ins->fm.op[1].tl,&_FIFTEEN,&_ZERO)); + ins->fm.op[1].tl&=15; + P(CWSliderScalar("Volume##TL",ImGuiDataType_U8,&ins->fm.op[1].tl,&_FIFTEEN,&_ZERO)); rightClickable } - if (willDisplayOps) if (ImGui::BeginTable("FMOperators",2,ImGuiTableFlags_SizingStretchSame)) { - for (int i=0; ifm.op[(opCount==4)?opOrder[i]:i]; - if ((i+1)&1) ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Separator(); - ImGui::PushID(fmt::sprintf("op%d",i).c_str()); - ImGui::Dummy(ImVec2(dpiScale,dpiScale)); - ImGui::Text("OP%d",i+1); + if (willDisplayOps) { + if (settings.fmLayout==0) { + int numCols=16; + if (ins->type==DIV_INS_OPL) numCols=13; + if (ins->type==DIV_INS_OPLL) numCols=12; + if (ins->type==DIV_INS_OPZ) numCols=19; + if (ImGui::BeginTable("FMOperators",numCols,ImGuiTableFlags_SizingStretchProp|ImGuiTableFlags_BordersH|ImGuiTableFlags_BordersOuterV)) { + // configure columns + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); // op name + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.05f); // ar + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.05f); // dr + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,0.05f); // sl + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch,0.05f); // d2r + } + ImGui::TableSetupColumn("c5",ImGuiTableColumnFlags_WidthStretch,0.05f); // rr + ImGui::TableSetupColumn("c6",ImGuiTableColumnFlags_WidthFixed); // -separator- + ImGui::TableSetupColumn("c7",ImGuiTableColumnFlags_WidthStretch,0.05f); // tl + ImGui::TableSetupColumn("c8",ImGuiTableColumnFlags_WidthStretch,0.05f); // rs/ksl + if (ins->type==DIV_INS_OPZ) { + ImGui::TableSetupColumn("c8z0",ImGuiTableColumnFlags_WidthStretch,0.05f); // egs + ImGui::TableSetupColumn("c8z1",ImGuiTableColumnFlags_WidthStretch,0.05f); // rev + } + ImGui::TableSetupColumn("c9",ImGuiTableColumnFlags_WidthStretch,0.05f); // mult - ImGui::SameLine(); + if (ins->type==DIV_INS_OPZ) { + ImGui::TableSetupColumn("c9z",ImGuiTableColumnFlags_WidthStretch,0.05f); // fine + } - bool amOn=op.am; - if (ImGui::Checkbox(FM_NAME(FM_AM),&amOn)) { PARAMETER - op.am=amOn; - } + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + ImGui::TableSetupColumn("c10",ImGuiTableColumnFlags_WidthStretch,0.05f); // dt + ImGui::TableSetupColumn("c11",ImGuiTableColumnFlags_WidthStretch,0.05f); // dt2 + } + ImGui::TableSetupColumn("c15",ImGuiTableColumnFlags_WidthFixed); // am - ImGui::SameLine(); + ImGui::TableSetupColumn("c12",ImGuiTableColumnFlags_WidthFixed); // -separator- + if (ins->type!=DIV_INS_OPLL) { + ImGui::TableSetupColumn("c13",ImGuiTableColumnFlags_WidthStretch,0.2f); // ssg/waveform + } + ImGui::TableSetupColumn("c14",ImGuiTableColumnFlags_WidthStretch,0.3f); // env - int maxTl=127; - if (ins->type==DIV_INS_OPLL) { - if (i==1) { - maxTl=15; + // header + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_AR)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_AR)); + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_DR)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_DR)); + if (settings.susPosition==0) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_SL)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_SL)); + } + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_D2R)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_D2R)); + } + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_RR)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_RR)); + if (settings.susPosition==1) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_SL)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_SL)); + } + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_TL)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_TL)); + ImGui::TableNextColumn(); + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + CENTER_TEXT(FM_SHORT_NAME(FM_RS)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_RS)); } else { - maxTl=63; + CENTER_TEXT(FM_SHORT_NAME(FM_KSL)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_KSL)); } - } - int maxArDr=(ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ)?31:15; - - bool ssgOn=op.ssgEnv&8; - bool ksrOn=op.ksr; - bool vibOn=op.vib; - unsigned char ssgEnv=op.ssgEnv&7; - if (ImGui::Checkbox((ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL)?FM_NAME(FM_EGS):"SSG On",&ssgOn)) { PARAMETER - op.ssgEnv=(op.ssgEnv&7)|(ssgOn<<3); - } - if (ins->type==DIV_INS_FM) { - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Only for Genesis and Neo Geo systems"); + if (ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_EGSHIFT)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_EGSHIFT)); + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_REV)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_REV)); } - } - - //52.0 controls vert scaling; default 96 - drawFMEnv(op.tl,op.ar,op.dr,op.d2r,op.rr,op.sl,ImVec2(ImGui::GetContentRegionAvail().x,52.0*dpiScale)); - //P(ImGui::SliderScalar(FM_NAME(FM_AR),ImGuiDataType_U8,&op.ar,&_ZERO,&_THIRTY_ONE)); - if (ImGui::BeginTable("opParams",2,ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.0); \ - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,0.0); \ - - ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##AR",ImGuiDataType_U8,&op.ar,&maxArDr,&_ZERO)); + CENTER_TEXT(FM_SHORT_NAME(FM_MULT)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_MULT)); + if (ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_SHORT_NAME(FM_FINE)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_FINE)); + } ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_AR)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##DR",ImGuiDataType_U8,&op.dr,&maxArDr,&_ZERO)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_DR)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##SL",ImGuiDataType_U8,&op.sl,&_FIFTEEN,&_ZERO)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_SL)); - if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { - ImGui::TableNextRow(); + CENTER_TEXT(FM_SHORT_NAME(FM_DT)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_DT)); ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##D2R",ImGuiDataType_U8,&op.d2r,&_THIRTY_ONE,&_ZERO)); + CENTER_TEXT(FM_SHORT_NAME(FM_DT2)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_DT2)); ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_D2R)); } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##RR",ImGuiDataType_U8,&op.rr,&_FIFTEEN,&_ZERO)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_RR)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##TL",ImGuiDataType_U8,&op.tl,&maxTl,&_ZERO)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_TL)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Separator(); - ImGui::TableNextColumn(); - ImGui::Separator(); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { - P(ImGui::SliderScalar("##RS",ImGuiDataType_U8,&op.rs,&_ZERO,&_THREE)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_RS)); + if (ins->type==DIV_INS_FM) { + CENTER_TEXT(FM_SHORT_NAME(FM_AM)); + ImGui::TextUnformatted(FM_SHORT_NAME(FM_AM)); } else { - P(ImGui::SliderScalar("##KSL",ImGuiDataType_U8,&op.ksl,&_ZERO,&_THREE)); + CENTER_TEXT("Other"); + ImGui::TextUnformatted("Other"); + } + ImGui::TableNextColumn(); + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPZ) { ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_KSL)); + CENTER_TEXT(FM_NAME(FM_WS)); + ImGui::TextUnformatted(FM_NAME(FM_WS)); + } else if (ins->type!=DIV_INS_OPLL) { + ImGui::TableNextColumn(); + CENTER_TEXT(FM_NAME(FM_SSG)); + ImGui::TextUnformatted(FM_NAME(FM_SSG)); + } + ImGui::TableNextColumn(); + CENTER_TEXT("Envelope"); + ImGui::TextUnformatted("Envelope"); + + float sliderHeight=32.0f*dpiScale; + + for (int i=0; ifm.op[(opCount==4)?opOrder[i]:i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + if (i==0) sliderHeight=(ImGui::GetContentRegionAvail().y/opCount)-ImGui::GetStyle().ItemSpacing.y; + + ImGui::PushID(fmt::sprintf("op%d",i).c_str()); + if (ins->type==DIV_INS_OPL && ins->fm.opllPreset==16) { + if (i==1) { + ImGui::Text("Kick"); + } else { + ImGui::Text("Env"); + } + } else { + ImGui::Text("OP%d",i+1); + } + + int maxTl=127; + if (ins->type==DIV_INS_OPLL) { + if (i==1) { + maxTl=15; + } else { + maxTl=63; + } + } + if (ins->type==DIV_INS_OPL) { + maxTl=63; + } + int maxArDr=(ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ)?31:15; + bool ssgOn=op.ssgEnv&8; + bool ksrOn=op.ksr; + bool vibOn=op.vib; + bool susOn=op.sus; + unsigned char ssgEnv=op.ssgEnv&7; + + ImGui::TableNextColumn(); + op.ar&=maxArDr; + CENTER_VSLIDER; + P(CWVSliderScalar("##AR",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.ar,&_ZERO,&maxArDr)); + + ImGui::TableNextColumn(); + op.dr&=maxArDr; + CENTER_VSLIDER; + P(CWVSliderScalar("##DR",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.dr,&_ZERO,&maxArDr)); + + if (settings.susPosition==0) { + ImGui::TableNextColumn(); + op.sl&=15; + CENTER_VSLIDER; + P(CWVSliderScalar("##SL",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.sl,&_FIFTEEN,&_ZERO)); + } + + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + op.d2r&=31; + CENTER_VSLIDER; + P(CWVSliderScalar("##D2R",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.d2r,&_ZERO,&_THIRTY_ONE)); + } + + ImGui::TableNextColumn(); + op.rr&=15; + CENTER_VSLIDER; + P(CWVSliderScalar("##RR",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.rr,&_ZERO,&_FIFTEEN)); + + if (settings.susPosition==1) { + ImGui::TableNextColumn(); + op.sl&=15; + CENTER_VSLIDER; + P(CWVSliderScalar("##SL",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.sl,&_FIFTEEN,&_ZERO)); + } + + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(4.0f*dpiScale,2.0f*dpiScale)); + + ImGui::TableNextColumn(); + op.tl&=maxTl; + CENTER_VSLIDER; + P(CWVSliderScalar("##TL",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.tl,&maxTl,&_ZERO)); + + ImGui::TableNextColumn(); + CENTER_VSLIDER; + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + P(CWVSliderScalar("##RS",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.rs,&_ZERO,&_THREE)); + } else { + P(CWVSliderScalar("##KSL",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.ksl,&_ZERO,&_THREE)); + } + + if (ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + CENTER_VSLIDER; + P(CWVSliderScalar("##EGS",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.ksl,&_ZERO,&_THREE)); + + ImGui::TableNextColumn(); + CENTER_VSLIDER; + P(CWVSliderScalar("##REV",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.dam,&_ZERO,&_SEVEN)); + } + + ImGui::TableNextColumn(); + CENTER_VSLIDER; + P(CWVSliderScalar("##MULT",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.mult,&_ZERO,&_FIFTEEN)); + + if (ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + CENTER_VSLIDER; + P(CWVSliderScalar("##FINE",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.dvb,&_ZERO,&_FIFTEEN)); + } + + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + int detune=(op.dt&7)-3; + ImGui::TableNextColumn(); + CENTER_VSLIDER; + if (CWVSliderInt("##DT",ImVec2(20.0f*dpiScale,sliderHeight),&detune,-3,4)) { PARAMETER + op.dt=detune+3; + } + + ImGui::TableNextColumn(); + CENTER_VSLIDER; + P(CWVSliderScalar("##DT2",ImVec2(20.0f*dpiScale,sliderHeight),ImGuiDataType_U8,&op.dt2,&_ZERO,&_THREE)); rightClickable + if (ImGui::IsItemHovered() && ins->type==DIV_INS_FM) { + ImGui::SetTooltip("Only on YM2151 (OPM)"); + } + + ImGui::TableNextColumn(); + bool amOn=op.am; + if (ins->type==DIV_INS_OPZ) { + bool egtOn=op.egt; + if (egtOn) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+0.5*(sliderHeight-ImGui::GetFrameHeight()*4.0-ImGui::GetStyle().ItemSpacing.y*3.0)); + } else { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+0.5*(sliderHeight-ImGui::GetFrameHeight()*2.0-ImGui::GetStyle().ItemSpacing.y*1.0)); + } + if (ImGui::Checkbox("AM",&amOn)) { PARAMETER + op.am=amOn; + } + if (ImGui::Checkbox("Fixed",&egtOn)) { PARAMETER + op.egt=egtOn; + } + if (egtOn) { + int block=op.dt; + int freqNum=(op.mult<<4)|(op.dvb&15); + if (ImGui::InputInt("Block",&block,1,1)) { + if (block<0) block=0; + if (block>7) block=7; + op.dt=block; + } + if (ImGui::InputInt("FreqNum",&freqNum,1,16)) { + if (freqNum<0) freqNum=0; + if (freqNum>255) freqNum=255; + op.mult=freqNum>>4; + op.dvb=freqNum&15; + } + } + } else { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+0.5*(sliderHeight-ImGui::GetFrameHeight())); + if (ImGui::Checkbox("##AM",&amOn)) { PARAMETER + op.am=amOn; + } + } + + if (ins->type!=DIV_INS_OPL && ins->type!=DIV_INS_OPZ) { + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(4.0f*dpiScale,2.0f*dpiScale)); + ImGui::TableNextColumn(); + ImGui::BeginDisabled(!ssgOn); + drawSSGEnv(op.ssgEnv&7,ImVec2(ImGui::GetContentRegionAvail().x,sliderHeight-ImGui::GetFrameHeightWithSpacing())); + ImGui::EndDisabled(); + if (ImGui::Checkbox("##SSGOn",&ssgOn)) { PARAMETER + op.ssgEnv=(op.ssgEnv&7)|(ssgOn<<3); + } + if (ins->type==DIV_INS_FM) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Only for OPN family chips"); + } + } + + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderScalar("##SSG",ImGuiDataType_U8,&ssgEnv,&_ZERO,&_SEVEN,ssgEnvTypes[ssgEnv])) { PARAMETER + op.ssgEnv=(op.ssgEnv&8)|(ssgEnv&7); + } + } + } else { + ImGui::TableNextColumn(); + bool amOn=op.am; + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+0.5*(sliderHeight-ImGui::GetFrameHeight()*4.0-ImGui::GetStyle().ItemSpacing.y*3.0)); + if (ImGui::Checkbox(FM_NAME(FM_AM),&amOn)) { PARAMETER + op.am=amOn; + } + if (ImGui::Checkbox(FM_NAME(FM_VIB),&vibOn)) { PARAMETER + op.vib=vibOn; + } + if (ImGui::Checkbox(FM_NAME(FM_KSR),&ksrOn)) { PARAMETER + op.ksr=ksrOn; + } + if (ins->type==DIV_INS_OPL) { + if (ImGui::Checkbox(FM_NAME(FM_SUS),&susOn)) { PARAMETER + op.sus=susOn; + } + } else if (ins->type==DIV_INS_OPLL) { + if (ImGui::Checkbox(FM_NAME(FM_EGS),&ssgOn)) { PARAMETER + op.ssgEnv=(op.ssgEnv&7)|(ssgOn<<3); + } + } + } + + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPZ) { + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(4.0f*dpiScale,2.0f*dpiScale)); + ImGui::TableNextColumn(); + + drawWaveform(op.ws&7,ins->type==DIV_INS_OPZ,ImVec2(ImGui::GetContentRegionAvail().x,sliderHeight-ImGui::GetFrameHeightWithSpacing())); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:oplWaveforms[op.ws&7])); rightClickable + if (ins->type==DIV_INS_OPL && ImGui::IsItemHovered()) { + ImGui::SetTooltip("OPL2/3 only (last 4 waveforms are OPL3 only)"); + } + } else if (ins->type==DIV_INS_OPLL) { + ImGui::TableNextColumn(); + ImGui::Dummy(ImVec2(4.0f*dpiScale,2.0f*dpiScale)); + } + + ImGui::TableNextColumn(); + drawFMEnv(op.tl&maxTl,op.ar&maxArDr,op.dr&maxArDr,(ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL)?((op.rr&15)*2):op.d2r&31,op.rr&15,op.sl&15,maxTl,maxArDr,ImVec2(ImGui::GetContentRegionAvail().x,sliderHeight)); + + ImGui::PopID(); } - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar(FM_NAME(FM_MULT),ImGuiDataType_U8,&op.mult,&_ZERO,&_FIFTEEN)); - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_MULT)); - - if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { - int detune=(op.dt&7)-3; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::SliderInt("##DT",&detune,-3,3)) { PARAMETER - op.dt=detune+3; - } - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_DT)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - P(ImGui::SliderScalar("##DT2",ImGuiDataType_U8,&op.dt2,&_ZERO,&_THREE)); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Only for Arcade system"); - } - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_DT2)); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::SliderScalar("##SSG",ImGuiDataType_U8,&ssgEnv,&_ZERO,&_SEVEN,ssgEnvTypes[ssgEnv])) { PARAMETER - op.ssgEnv=(op.ssgEnv&8)|(ssgEnv&7); - } - ImGui::TableNextColumn(); - ImGui::Text("%s",FM_NAME(FM_SSG)); - } - ImGui::EndTable(); } - - if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL) { - if (ImGui::Checkbox(FM_NAME(FM_VIB),&vibOn)) { PARAMETER - op.vib=vibOn; - } - ImGui::SameLine(); - if (ImGui::Checkbox(FM_NAME(FM_KSR),&ksrOn)) { PARAMETER - op.ksr=ksrOn; - } + } else { + int columns=2; + switch (settings.fmLayout) { + case 1: // 2x2 + columns=2; + break; + case 2: // 1x4 + columns=1; + break; + case 3: // 4x1 + columns=opCount; + break; } + if (ImGui::BeginTable("FMOperators",columns,ImGuiTableFlags_SizingStretchSame)) { + for (int i=0; ifm.op[(opCount==4)?opOrder[i]:i]; + if ((settings.fmLayout!=3 && ((i+1)&1)) || i==0 || settings.fmLayout==2) ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Separator(); + ImGui::PushID(fmt::sprintf("op%d",i).c_str()); + ImGui::Dummy(ImVec2(dpiScale,dpiScale)); + if (ins->type==DIV_INS_OPL && ins->fm.opllPreset==16) { + if (i==1) { + ImGui::Text("Envelope 2 (kick only)"); + } else { + ImGui::Text("Envelope"); + } + } else { + ImGui::Text("OP%d",i+1); + } - ImGui::PopID(); + ImGui::SameLine(); + + bool amOn=op.am; + if (ImGui::Checkbox(FM_NAME(FM_AM),&amOn)) { PARAMETER + op.am=amOn; + } + + int maxTl=127; + if (ins->type==DIV_INS_OPLL) { + if (i==1) { + maxTl=15; + } else { + maxTl=63; + } + } + if (ins->type==DIV_INS_OPL) { + maxTl=63; + } + int maxArDr=(ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ)?31:15; + + bool ssgOn=op.ssgEnv&8; + bool ksrOn=op.ksr; + bool vibOn=op.vib; + bool susOn=op.sus; // don't you make fun of this one + unsigned char ssgEnv=op.ssgEnv&7; + if (ins->type!=DIV_INS_OPL && ins->type!=DIV_INS_OPZ) { + ImGui::SameLine(); + if (ImGui::Checkbox((ins->type==DIV_INS_OPLL)?FM_NAME(FM_EGS):"SSG On",&ssgOn)) { PARAMETER + op.ssgEnv=(op.ssgEnv&7)|(ssgOn<<3); + } + if (ins->type==DIV_INS_FM) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Only for OPN family chips"); + } + } + } + + if (ins->type==DIV_INS_OPL) { + ImGui::SameLine(); + if (ImGui::Checkbox(FM_NAME(FM_SUS),&susOn)) { PARAMETER + op.sus=susOn; + } + } + + //52.0 controls vert scaling; default 96 + drawFMEnv(op.tl&maxTl,op.ar&maxArDr,op.dr&maxArDr,(ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL)?((op.rr&15)*2):op.d2r&31,op.rr&15,op.sl&15,maxTl,maxArDr,ImVec2(ImGui::GetContentRegionAvail().x,52.0*dpiScale)); + //P(CWSliderScalar(FM_NAME(FM_AR),ImGuiDataType_U8,&op.ar,&_ZERO,&_THIRTY_ONE)); rightClickable + if (ImGui::BeginTable("opParams",2,ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.0); \ + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,0.0); \ + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + op.ar&=maxArDr; + P(CWSliderScalar("##AR",ImGuiDataType_U8,&op.ar,&maxArDr,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_AR)); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + op.dr&=maxArDr; + P(CWSliderScalar("##DR",ImGuiDataType_U8,&op.dr,&maxArDr,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_DR)); + + if (settings.susPosition==0) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##SL",ImGuiDataType_U8,&op.sl,&_FIFTEEN,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_SL)); + } + + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##D2R",ImGuiDataType_U8,&op.d2r,&_THIRTY_ONE,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_D2R)); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##RR",ImGuiDataType_U8,&op.rr,&_FIFTEEN,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_RR)); + + if (settings.susPosition==1) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##SL",ImGuiDataType_U8,&op.sl,&_FIFTEEN,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_SL)); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + op.tl&=maxTl; + P(CWSliderScalar("##TL",ImGuiDataType_U8,&op.tl,&maxTl,&_ZERO)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_TL)); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Separator(); + ImGui::TableNextColumn(); + ImGui::Separator(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + P(CWSliderScalar("##RS",ImGuiDataType_U8,&op.rs,&_ZERO,&_THREE)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_RS)); + } else { + P(CWSliderScalar("##KSL",ImGuiDataType_U8,&op.ksl,&_ZERO,&_THREE)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_KSL)); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar(FM_NAME(FM_MULT),ImGuiDataType_U8,&op.mult,&_ZERO,&_FIFTEEN)); rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_MULT)); + + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + int detune=(op.dt&7)-3; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##DT",&detune,-3,4)) { PARAMETER + op.dt=detune+3; + } rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_DT)); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##DT2",ImGuiDataType_U8,&op.dt2,&_ZERO,&_THREE)); rightClickable + if (ImGui::IsItemHovered() && ins->type==DIV_INS_FM) { + ImGui::SetTooltip("Only on YM2151 (OPM)"); + } + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_DT2)); + + if (ins->type==DIV_INS_FM) { // OPN only + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderScalar("##SSG",ImGuiDataType_U8,&ssgEnv,&_ZERO,&_SEVEN,ssgEnvTypes[ssgEnv])) { PARAMETER + op.ssgEnv=(op.ssgEnv&8)|(ssgEnv&7); + } rightClickable + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_SSG)); + } + } + + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPZ) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + P(CWSliderScalar("##WS",ImGuiDataType_U8,&op.ws,&_ZERO,&_SEVEN,(ins->type==DIV_INS_OPZ)?opzWaveforms[op.ws&7]:oplWaveforms[op.ws&7])); rightClickable + if (ins->type==DIV_INS_OPL && ImGui::IsItemHovered()) { + ImGui::SetTooltip("OPL2/3 only (last 4 waveforms are OPL3 only)"); + } + ImGui::TableNextColumn(); + ImGui::Text("%s",FM_NAME(FM_WS)); + } + + ImGui::EndTable(); + } + + if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL) { + if (ImGui::Checkbox(FM_NAME(FM_VIB),&vibOn)) { PARAMETER + op.vib=vibOn; + } + ImGui::SameLine(); + if (ImGui::Checkbox(FM_NAME(FM_KSR),&ksrOn)) { PARAMETER + op.ksr=ksrOn; + } + } + + ImGui::PopID(); + } + ImGui::EndTable(); + } } - ImGui::EndTable(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("FM Macros")) { MACRO_BEGIN(0); - NORMAL_MACRO(ins->std.algMacro,ins->std.algMacroLen,ins->std.algMacroLoop,ins->std.algMacroRel,0,7,"alg",FM_NAME(FM_ALG),96,ins->std.algMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[0],0,7,NULL,false); - NORMAL_MACRO(ins->std.fbMacro,ins->std.fbMacroLen,ins->std.fbMacroLoop,ins->std.fbMacroRel,0,7,"fb",FM_NAME(FM_FB),96,ins->std.fbMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[1],0,7,NULL,false); - NORMAL_MACRO(ins->std.fmsMacro,ins->std.fmsMacroLen,ins->std.fmsMacroLoop,ins->std.fmsMacroRel,0,7,"fms",FM_NAME(FM_FMS),96,ins->std.fmsMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,7,NULL,false); - NORMAL_MACRO(ins->std.amsMacro,ins->std.amsMacroLen,ins->std.amsMacroLoop,ins->std.amsMacroRel,0,3,"ams",FM_NAME(FM_AMS),48,ins->std.amsMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[3],0,3,NULL,false); + if (ins->type==DIV_INS_OPLL) { + NORMAL_MACRO(ins->std.algMacro,0,1,"alg",FM_NAME(FM_SUS),32,ins->std.algMacro.open,true,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[0],0,1,NULL,false); + NORMAL_MACRO(ins->std.fbMacro,0,7,"fb",FM_NAME(FM_FB),96,ins->std.fbMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[1],0,7,NULL,false); + NORMAL_MACRO(ins->std.fmsMacro,0,1,"fms",FM_NAME(FM_DC),32,ins->std.fmsMacro.open,true,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,1,NULL,false); + NORMAL_MACRO(ins->std.amsMacro,0,1,"ams",FM_NAME(FM_DM),32,ins->std.amsMacro.open,true,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[3],0,1,NULL,false); + } else { + NORMAL_MACRO(ins->std.algMacro,0,7,"alg",FM_NAME(FM_ALG),96,ins->std.algMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[0],0,7,NULL,false); + NORMAL_MACRO(ins->std.fbMacro,0,7,"fb",FM_NAME(FM_FB),96,ins->std.fbMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[1],0,7,NULL,false); + if (ins->type!=DIV_INS_OPL) { + if (ins->type==DIV_INS_OPZ) { + // TODO: FMS2/AMS2 macros + NORMAL_MACRO(ins->std.fmsMacro,0,7,"fms",FM_NAME(FM_FMS),96,ins->std.fmsMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,7,NULL,false); + NORMAL_MACRO(ins->std.amsMacro,0,3,"ams",FM_NAME(FM_AMS),48,ins->std.amsMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,3,NULL,false); + } else { + NORMAL_MACRO(ins->std.fmsMacro,0,7,"fms",FM_NAME(FM_FMS),96,ins->std.fmsMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,7,NULL,false); + NORMAL_MACRO(ins->std.amsMacro,0,3,"ams",FM_NAME(FM_AMS),48,ins->std.amsMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[3],0,3,NULL,false); + } + } + } - NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,127,"ex1","AM Depth",128,ins->std.ex1MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,127,NULL,false); - NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,127,"ex2","PM Depth",128,ins->std.ex2MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,127,NULL,false); - NORMAL_MACRO(ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel,0,255,"ex3","LFO Speed",128,ins->std.ex3MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,255,NULL,false); - NORMAL_MACRO(ins->std.waveMacro,ins->std.waveMacroLen,ins->std.waveMacroLoop,ins->std.waveMacroRel,0,3,"wave","LFO Shape",48,ins->std.waveMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_WAVE],mmlString[7],0,3,¯oLFOWaves,false); + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ) { + NORMAL_MACRO(ins->std.ex1Macro,0,127,"ex1","AM Depth",128,ins->std.ex1Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,127,NULL,false); + NORMAL_MACRO(ins->std.ex2Macro,0,127,"ex2","PM Depth",128,ins->std.ex2Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[7],0,127,NULL,false); + NORMAL_MACRO(ins->std.ex3Macro,0,255,"ex3","LFO Speed",128,ins->std.ex3Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[8],0,255,NULL,false); + NORMAL_MACRO(ins->std.waveMacro,0,3,"wave","LFO Shape",48,ins->std.waveMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_WAVE],mmlString[9],0,3,¯oLFOWaves,false); + NORMAL_MACRO(ins->std.ex4Macro,0,4,"ex4","OpMask",128,ins->std.ex4Macro.open,true,fmOperatorBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[10],0,4,NULL,false); + } MACRO_END; ImGui::EndTabItem(); } @@ -1053,20 +2091,52 @@ void FurnaceGUI::drawInsEdit() { maxTl=63; } } + if (ins->type==DIV_INS_OPL) { + maxTl=63; + } int maxArDr=(ins->type==DIV_INS_FM || ins->type==DIV_INS_OPZ)?31:15; - OP_MACRO(ins->std.opMacros[ordi].tlMacro,ins->std.opMacros[ordi].tlMacroLen,ins->std.opMacros[ordi].tlMacroLoop,ins->std.opMacros[ordi].tlMacroRel,maxTl,ordi,"tl",FM_NAME(FM_TL),128,ins->std.opMacros[ordi].tlMacroOpen,false,NULL,mmlString[0]); - OP_MACRO(ins->std.opMacros[ordi].arMacro,ins->std.opMacros[ordi].arMacroLen,ins->std.opMacros[ordi].arMacroLoop,ins->std.opMacros[ordi].arMacroRel,maxArDr,ordi,"ar",FM_NAME(FM_AR),64,ins->std.opMacros[ordi].arMacroOpen,false,NULL,mmlString[1]); - OP_MACRO(ins->std.opMacros[ordi].drMacro,ins->std.opMacros[ordi].drMacroLen,ins->std.opMacros[ordi].drMacroLoop,ins->std.opMacros[ordi].drMacroRel,maxArDr,ordi,"dr",FM_NAME(FM_DR),64,ins->std.opMacros[ordi].drMacroOpen,false,NULL,mmlString[2]); - OP_MACRO(ins->std.opMacros[ordi].d2rMacro,ins->std.opMacros[ordi].d2rMacroLen,ins->std.opMacros[ordi].d2rMacroLoop,ins->std.opMacros[ordi].d2rMacroRel,31,ordi,"d2r",FM_NAME(FM_D2R),64,ins->std.opMacros[ordi].d2rMacroOpen,false,NULL,mmlString[3]); - OP_MACRO(ins->std.opMacros[ordi].rrMacro,ins->std.opMacros[ordi].rrMacroLen,ins->std.opMacros[ordi].rrMacroLoop,ins->std.opMacros[ordi].rrMacroRel,15,ordi,"rr",FM_NAME(FM_RR),64,ins->std.opMacros[ordi].rrMacroOpen,false,NULL,mmlString[4]); - OP_MACRO(ins->std.opMacros[ordi].slMacro,ins->std.opMacros[ordi].slMacroLen,ins->std.opMacros[ordi].slMacroLoop,ins->std.opMacros[ordi].slMacroRel,15,ordi,"sl",FM_NAME(FM_SL),64,ins->std.opMacros[ordi].slMacroOpen,false,NULL,mmlString[5]); - OP_MACRO(ins->std.opMacros[ordi].rsMacro,ins->std.opMacros[ordi].rsMacroLen,ins->std.opMacros[ordi].rsMacroLoop,ins->std.opMacros[ordi].rsMacroRel,3,ordi,"rs",FM_NAME(FM_RS),32,ins->std.opMacros[ordi].rsMacroOpen,false,NULL,mmlString[6]); - OP_MACRO(ins->std.opMacros[ordi].multMacro,ins->std.opMacros[ordi].multMacroLen,ins->std.opMacros[ordi].multMacroLoop,ins->std.opMacros[ordi].multMacroRel,15,ordi,"mult",FM_NAME(FM_MULT),64,ins->std.opMacros[ordi].multMacroOpen,false,NULL,mmlString[7]); - OP_MACRO(ins->std.opMacros[ordi].dtMacro,ins->std.opMacros[ordi].dtMacroLen,ins->std.opMacros[ordi].dtMacroLoop,ins->std.opMacros[ordi].dtMacroRel,7,ordi,"dt",FM_NAME(FM_DT),64,ins->std.opMacros[ordi].dtMacroOpen,false,NULL,mmlString[8]); - OP_MACRO(ins->std.opMacros[ordi].dt2Macro,ins->std.opMacros[ordi].dt2MacroLen,ins->std.opMacros[ordi].dt2MacroLoop,ins->std.opMacros[ordi].dt2MacroRel,3,ordi,"dt2",FM_NAME(FM_DT2),32,ins->std.opMacros[ordi].dt2MacroOpen,false,NULL,mmlString[9]); - OP_MACRO(ins->std.opMacros[ordi].amMacro,ins->std.opMacros[ordi].amMacroLen,ins->std.opMacros[ordi].amMacroLoop,ins->std.opMacros[ordi].amMacroRel,1,ordi,"am",FM_NAME(FM_AM),32,ins->std.opMacros[ordi].amMacroOpen,true,NULL,mmlString[10]); - OP_MACRO(ins->std.opMacros[ordi].ssgMacro,ins->std.opMacros[ordi].ssgMacroLen,ins->std.opMacros[ordi].ssgMacroLoop,ins->std.opMacros[ordi].ssgMacroRel,4,ordi,"ssg",FM_NAME(FM_SSG),64,ins->std.opMacros[ordi].ssgMacroOpen,true,ssgEnvBits,mmlString[11]); + if (ins->type==DIV_INS_OPL) { + OP_MACRO(ins->std.opMacros[ordi].tlMacro,maxTl,ordi,"tl",FM_NAME(FM_TL),128,ins->std.opMacros[ordi].tlMacro.open,false,NULL,false,0,macroDummyMode,mmlString[0]); + OP_MACRO(ins->std.opMacros[ordi].arMacro,maxArDr,ordi,"ar",FM_NAME(FM_AR),64,ins->std.opMacros[ordi].arMacro.open,false,NULL,false,0,macroDummyMode,mmlString[1]); + OP_MACRO(ins->std.opMacros[ordi].drMacro,maxArDr,ordi,"dr",FM_NAME(FM_DR),64,ins->std.opMacros[ordi].drMacro.open,false,NULL,false,0,macroDummyMode,mmlString[2]); + OP_MACRO(ins->std.opMacros[ordi].slMacro,15,ordi,"sl",FM_NAME(FM_SL),64,ins->std.opMacros[ordi].slMacro.open,false,NULL,false,0,macroDummyMode,mmlString[5]); + OP_MACRO(ins->std.opMacros[ordi].rrMacro,15,ordi,"rr",FM_NAME(FM_RR),64,ins->std.opMacros[ordi].rrMacro.open,false,NULL,false,0,macroDummyMode,mmlString[4]); + OP_MACRO(ins->std.opMacros[ordi].kslMacro,3,ordi,"ksl",FM_NAME(FM_KSL),32,ins->std.opMacros[ordi].kslMacro.open,false,NULL,false,0,macroDummyMode,mmlString[6]); + OP_MACRO(ins->std.opMacros[ordi].multMacro,15,ordi,"mult",FM_NAME(FM_MULT),64,ins->std.opMacros[ordi].multMacro.open,false,NULL,false,0,macroDummyMode,mmlString[7]); + OP_MACRO(ins->std.opMacros[ordi].wsMacro,7,ordi,"ws",FM_NAME(FM_WS),64,ins->std.opMacros[ordi].wsMacro.open,false,NULL,false,0,macroDummyMode,mmlString[8]); + + OP_MACRO(ins->std.opMacros[ordi].amMacro,1,ordi,"am",FM_NAME(FM_AM),32,ins->std.opMacros[ordi].amMacro.open,true,NULL,false,0,macroDummyMode,mmlString[9]); + OP_MACRO(ins->std.opMacros[ordi].vibMacro,1,ordi,"vib",FM_NAME(FM_VIB),32,ins->std.opMacros[ordi].vibMacro.open,true,NULL,false,0,macroDummyMode,mmlString[10]); + OP_MACRO(ins->std.opMacros[ordi].ksrMacro,1,ordi,"ksr",FM_NAME(FM_KSR),32,ins->std.opMacros[ordi].ksrMacro.open,true,NULL,false,0,macroDummyMode,mmlString[11]); + OP_MACRO(ins->std.opMacros[ordi].susMacro,1,ordi,"sus",FM_NAME(FM_SUS),32,ins->std.opMacros[ordi].susMacro.open,true,NULL,false,0,macroDummyMode,mmlString[12]); + } else if (ins->type==DIV_INS_OPLL) { + OP_MACRO(ins->std.opMacros[ordi].tlMacro,maxTl,ordi,"tl",FM_NAME(FM_TL),128,ins->std.opMacros[ordi].tlMacro.open,false,NULL,false,0,macroDummyMode,mmlString[0]); + OP_MACRO(ins->std.opMacros[ordi].arMacro,maxArDr,ordi,"ar",FM_NAME(FM_AR),64,ins->std.opMacros[ordi].arMacro.open,false,NULL,false,0,macroDummyMode,mmlString[1]); + OP_MACRO(ins->std.opMacros[ordi].drMacro,maxArDr,ordi,"dr",FM_NAME(FM_DR),64,ins->std.opMacros[ordi].drMacro.open,false,NULL,false,0,macroDummyMode,mmlString[2]); + OP_MACRO(ins->std.opMacros[ordi].slMacro,15,ordi,"sl",FM_NAME(FM_SL),64,ins->std.opMacros[ordi].slMacro.open,false,NULL,false,0,macroDummyMode,mmlString[5]); + OP_MACRO(ins->std.opMacros[ordi].rrMacro,15,ordi,"rr",FM_NAME(FM_RR),64,ins->std.opMacros[ordi].rrMacro.open,false,NULL,false,0,macroDummyMode,mmlString[4]); + OP_MACRO(ins->std.opMacros[ordi].kslMacro,3,ordi,"ksl",FM_NAME(FM_KSL),32,ins->std.opMacros[ordi].kslMacro.open,false,NULL,false,0,macroDummyMode,mmlString[6]); + OP_MACRO(ins->std.opMacros[ordi].multMacro,15,ordi,"mult",FM_NAME(FM_MULT),64,ins->std.opMacros[ordi].multMacro.open,false,NULL,false,0,macroDummyMode,mmlString[7]); + + OP_MACRO(ins->std.opMacros[ordi].amMacro,1,ordi,"am",FM_NAME(FM_AM),32,ins->std.opMacros[ordi].amMacro.open,true,NULL,false,0,macroDummyMode,mmlString[8]); + OP_MACRO(ins->std.opMacros[ordi].vibMacro,1,ordi,"vib",FM_NAME(FM_VIB),32,ins->std.opMacros[ordi].vibMacro.open,true,NULL,false,0,macroDummyMode,mmlString[9]); + OP_MACRO(ins->std.opMacros[ordi].ksrMacro,1,ordi,"ksr",FM_NAME(FM_KSR),32,ins->std.opMacros[ordi].ksrMacro.open,true,NULL,false,0,macroDummyMode,mmlString[10]); + OP_MACRO(ins->std.opMacros[ordi].egtMacro,1,ordi,"egt",FM_NAME(FM_EGS),32,ins->std.opMacros[ordi].egtMacro.open,true,NULL,false,0,macroDummyMode,mmlString[11]); + } else { + OP_MACRO(ins->std.opMacros[ordi].tlMacro,maxTl,ordi,"tl",FM_NAME(FM_TL),128,ins->std.opMacros[ordi].tlMacro.open,false,NULL,false,0,macroDummyMode,mmlString[0]); + OP_MACRO(ins->std.opMacros[ordi].arMacro,maxArDr,ordi,"ar",FM_NAME(FM_AR),64,ins->std.opMacros[ordi].arMacro.open,false,NULL,false,0,macroDummyMode,mmlString[1]); + OP_MACRO(ins->std.opMacros[ordi].drMacro,maxArDr,ordi,"dr",FM_NAME(FM_DR),64,ins->std.opMacros[ordi].drMacro.open,false,NULL,false,0,macroDummyMode,mmlString[2]); + OP_MACRO(ins->std.opMacros[ordi].d2rMacro,31,ordi,"d2r",FM_NAME(FM_D2R),64,ins->std.opMacros[ordi].d2rMacro.open,false,NULL,false,0,macroDummyMode,mmlString[3]); + OP_MACRO(ins->std.opMacros[ordi].rrMacro,15,ordi,"rr",FM_NAME(FM_RR),64,ins->std.opMacros[ordi].rrMacro.open,false,NULL,false,0,macroDummyMode,mmlString[4]); + OP_MACRO(ins->std.opMacros[ordi].slMacro,15,ordi,"sl",FM_NAME(FM_SL),64,ins->std.opMacros[ordi].slMacro.open,false,NULL,false,0,macroDummyMode,mmlString[5]); + OP_MACRO(ins->std.opMacros[ordi].rsMacro,3,ordi,"rs",FM_NAME(FM_RS),32,ins->std.opMacros[ordi].rsMacro.open,false,NULL,false,0,macroDummyMode,mmlString[6]); + OP_MACRO(ins->std.opMacros[ordi].multMacro,15,ordi,"mult",FM_NAME(FM_MULT),64,ins->std.opMacros[ordi].multMacro.open,false,NULL,false,0,macroDummyMode,mmlString[7]); + OP_MACRO(ins->std.opMacros[ordi].dtMacro,7,ordi,"dt",FM_NAME(FM_DT),64,ins->std.opMacros[ordi].dtMacro.open,false,NULL,false,0,macroDummyMode,mmlString[8]); + OP_MACRO(ins->std.opMacros[ordi].dt2Macro,3,ordi,"dt2",FM_NAME(FM_DT2),32,ins->std.opMacros[ordi].dt2Macro.open,false,NULL,false,0,macroDummyMode,mmlString[9]); + OP_MACRO(ins->std.opMacros[ordi].amMacro,1,ordi,"am",FM_NAME(FM_AM),32,ins->std.opMacros[ordi].amMacro.open,true,NULL,false,0,macroDummyMode,mmlString[10]); + OP_MACRO(ins->std.opMacros[ordi].ssgMacro,4,ordi,"ssg",FM_NAME(FM_SSG),64,ins->std.opMacros[ordi].ssgMacro.open,true,ssgEnvBits,false,0,macroDummyMode,mmlString[11]); + } MACRO_END; ImGui::PopID(); ImGui::EndTabItem(); @@ -1074,9 +2144,9 @@ void FurnaceGUI::drawInsEdit() { } } if (ins->type==DIV_INS_GB) if (ImGui::BeginTabItem("Game Boy")) { - P(ImGui::SliderScalar("Volume",ImGuiDataType_U8,&ins->gb.envVol,&_ZERO,&_FIFTEEN)); - P(ImGui::SliderScalar("Envelope Length",ImGuiDataType_U8,&ins->gb.envLen,&_ZERO,&_SEVEN)); - P(ImGui::SliderScalar("Sound Length",ImGuiDataType_U8,&ins->gb.soundLen,&_ZERO,&_SIXTY_FOUR,ins->gb.soundLen>63?"Infinity":"%d")); + 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 ImGui::Text("Envelope Direction:"); bool goesUp=ins->gb.envDir; @@ -1095,35 +2165,35 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_C64) if (ImGui::BeginTabItem("C64")) { ImGui::Text("Waveform"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.triOn)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.triOn)); if (ImGui::Button("tri")) { PARAMETER ins->c64.triOn=!ins->c64.triOn; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.sawOn)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.sawOn)); if (ImGui::Button("saw")) { PARAMETER ins->c64.sawOn=!ins->c64.sawOn; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.pulseOn)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.pulseOn)); if (ImGui::Button("pulse")) { PARAMETER ins->c64.pulseOn=!ins->c64.pulseOn; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.noiseOn)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.noiseOn)); if (ImGui::Button("noise")) { PARAMETER ins->c64.noiseOn=!ins->c64.noiseOn; } ImGui::PopStyleColor(); - P(ImGui::SliderScalar("Attack",ImGuiDataType_U8,&ins->c64.a,&_ZERO,&_FIFTEEN)); - P(ImGui::SliderScalar("Decay",ImGuiDataType_U8,&ins->c64.d,&_ZERO,&_FIFTEEN)); - P(ImGui::SliderScalar("Sustain",ImGuiDataType_U8,&ins->c64.s,&_ZERO,&_FIFTEEN)); - P(ImGui::SliderScalar("Release",ImGuiDataType_U8,&ins->c64.r,&_ZERO,&_FIFTEEN)); - P(ImGui::SliderScalar("Duty",ImGuiDataType_U16,&ins->c64.duty,&_ZERO,&_FOUR_THOUSAND_NINETY_FIVE)); + P(CWSliderScalar("Attack",ImGuiDataType_U8,&ins->c64.a,&_ZERO,&_FIFTEEN)); rightClickable + P(CWSliderScalar("Decay",ImGuiDataType_U8,&ins->c64.d,&_ZERO,&_FIFTEEN)); rightClickable + P(CWSliderScalar("Sustain",ImGuiDataType_U8,&ins->c64.s,&_ZERO,&_FIFTEEN)); rightClickable + P(CWSliderScalar("Release",ImGuiDataType_U8,&ins->c64.r,&_ZERO,&_FIFTEEN)); rightClickable + P(CWSliderScalar("Duty",ImGuiDataType_U16,&ins->c64.duty,&_ZERO,&_FOUR_THOUSAND_NINETY_FIVE)); rightClickable bool ringMod=ins->c64.ringMod; if (ImGui::Checkbox("Ring Modulation",&ringMod)) { PARAMETER @@ -1137,30 +2207,30 @@ void FurnaceGUI::drawInsEdit() { P(ImGui::Checkbox("Enable filter",&ins->c64.toFilter)); P(ImGui::Checkbox("Initialize filter",&ins->c64.initFilter)); - P(ImGui::SliderScalar("Cutoff",ImGuiDataType_U16,&ins->c64.cut,&_ZERO,&_TWO_THOUSAND_FORTY_SEVEN)); - P(ImGui::SliderScalar("Resonance",ImGuiDataType_U8,&ins->c64.res,&_ZERO,&_FIFTEEN)); + P(CWSliderScalar("Cutoff",ImGuiDataType_U16,&ins->c64.cut,&_ZERO,&_TWO_THOUSAND_FORTY_SEVEN)); rightClickable + P(CWSliderScalar("Resonance",ImGuiDataType_U8,&ins->c64.res,&_ZERO,&_FIFTEEN)); rightClickable ImGui::Text("Filter Mode"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.lp)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.lp)); if (ImGui::Button("low")) { PARAMETER ins->c64.lp=!ins->c64.lp; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.bp)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.bp)); if (ImGui::Button("band")) { PARAMETER ins->c64.bp=!ins->c64.bp; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.hp)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.hp)); if (ImGui::Button("high")) { PARAMETER ins->c64.hp=!ins->c64.hp; } ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button,ImVec4(0.2f,(ins->c64.ch3off)?0.6f:0.2f,0.2f,1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(ins->c64.ch3off)); if (ImGui::Button("ch3off")) { PARAMETER ins->c64.ch3off=!ins->c64.ch3off; } @@ -1188,15 +2258,234 @@ void FurnaceGUI::drawInsEdit() { } ImGui::EndCombo(); } + P(ImGui::Checkbox("Use sample map (does not work yet!)",&ins->amiga.useNoteMap)); + if (ins->amiga.useNoteMap) { + if (ImGui::BeginTable("NoteMap",3,ImGuiTableFlags_ScrollY|ImGuiTableFlags_Borders|ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableSetupScrollFreeze(0,1); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + ImGui::Text("Sample"); + ImGui::TableNextColumn(); + ImGui::Text("Frequency"); + for (int i=0; i<120; 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) { + sName="-- empty --"; + ins->amiga.noteMap[i]=-1; + } else { + sName=e->song.sample[ins->amiga.noteMap[i]]->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; + } + 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)); + } + } + 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; + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + } ImGui::EndTabItem(); } + if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem("Namco 163")) { + 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; + } + if (ImGui::InputInt("Offset##WAVEPOS",&ins->n163.wavePos,1,16)) { PARAMETER + if (ins->n163.wavePos<0) ins->n163.wavePos=0; + if (ins->n163.wavePos>255) ins->n163.wavePos=255; + } + if (ImGui::InputInt("Length##WAVELEN",&ins->n163.waveLen,4,16)) { PARAMETER + if (ins->n163.waveLen<0) ins->n163.waveLen=0; + if (ins->n163.waveLen>252) ins->n163.waveLen=252; + ins->n163.waveLen&=0xfc; + } + + bool preLoad=ins->n163.waveMode&0x1; + if (ImGui::Checkbox("Load waveform before playback",&preLoad)) { PARAMETER + ins->n163.waveMode=(ins->n163.waveMode&~0x1)|(preLoad?0x1:0); + } + bool waveMode=ins->n163.waveMode&0x2; + if (ImGui::Checkbox("Update waveforms into RAM when every waveform changes",&waveMode)) { PARAMETER + ins->n163.waveMode=(ins->n163.waveMode&~0x2)|(waveMode?0x2:0); + } + + ImGui::EndTabItem(); + } + if (ins->type==DIV_INS_FDS) if (ImGui::BeginTabItem("FDS")) { + float modTable[32]; + ImGui::Checkbox("Compatibility mode",&ins->fds.initModTableWithFirstWave); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("only use for compatibility with .dmf modules!\n- initializes modulation table with first wavetable\n- does not alter modulation parameters on instrument change"); + } + if (ImGui::InputInt("Modulation depth",&ins->fds.modDepth,1,32)) { + if (ins->fds.modDepth<0) ins->fds.modDepth=0; + if (ins->fds.modDepth>63) ins->fds.modDepth=63; + } + if (ImGui::InputInt("Modulation speed",&ins->fds.modSpeed,1,4)) { + if (ins->fds.modSpeed<0) ins->fds.modSpeed=0; + if (ins->fds.modSpeed>4095) ins->fds.modSpeed=4095; + } + ImGui::Text("Modulation table"); + for (int i=0; i<32; i++) { + modTable[i]=ins->fds.modTable[i]; + } + ImVec2 modTableSize=ImVec2(ImGui::GetContentRegionAvail().x,96.0f*dpiScale); + PlotCustom("ModTable",modTable,32,0,NULL,-4,3,modTableSize,sizeof(float),ImVec4(1.0f,1.0f,1.0f,1.0f),0,NULL,true); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + macroDragStart=ImGui::GetItemRectMin(); + macroDragAreaSize=modTableSize; + macroDragMin=-4; + macroDragMax=3; + macroDragBitOff=0; + macroDragBitMode=false; + macroDragInitialValueSet=false; + macroDragInitialValue=false; + macroDragLen=32; + macroDragActive=true; + macroDragCTarget=(unsigned char*)ins->fds.modTable; + macroDragChar=true; + processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); \ + } + ImGui::EndTabItem(); + } + if (ins->type==DIV_INS_GB || + ins->type==DIV_INS_AMIGA || + ins->type==DIV_INS_X1_010 || + ins->type==DIV_INS_N163 || + ins->type==DIV_INS_FDS || + ins->type==DIV_INS_SWAN || + ins->type==DIV_INS_PCE || + ins->type==DIV_INS_SCC) { + if (ImGui::BeginTabItem("Wavetable")) { + ImGui::Checkbox("Enable synthesizer",&ins->ws.enabled); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ins->ws.effect&0x80) { + if ((ins->ws.effect&0x7f)>=DIV_WS_DUAL_MAX) { + ins->ws.effect=0; + } + } else { + if ((ins->ws.effect&0x7f)>=DIV_WS_SINGLE_MAX) { + ins->ws.effect=0; + } + } + if (ImGui::BeginCombo("##WSEffect",(ins->ws.effect&0x80)?dualWSEffects[ins->ws.effect&0x7f]:singleWSEffects[ins->ws.effect&0x7f])) { + ImGui::Text("Single-waveform"); + ImGui::Indent(); + for (int i=0; iws.effect=i; + } + } + ImGui::Unindent(); + ImGui::Text("Dual-waveform"); + ImGui::Indent(); + for (int i=129; iws.effect=i; + } + } + ImGui::Unindent(); + ImGui::EndCombo(); + } + if (ImGui::BeginTable("WSPreview",2)) { + DivWavetable* wave1=e->getWave(ins->ws.wave1); + DivWavetable* wave2=e->getWave(ins->ws.wave2); + float wavePreview1[256]; + float wavePreview2[256]; + for (int i=0; ilen; i++) { + if (wave1->data[i]>wave1->max) { + wavePreview1[i]=wave1->max; + } else { + wavePreview1[i]=wave1->data[i]; + } + } + for (int i=0; ilen; i++) { + if (wave2->data[i]>wave2->max) { + wavePreview2[i]=wave2->max; + } else { + wavePreview2[i]=wave2->data[i]; + } + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImVec2 size1=ImVec2(ImGui::GetContentRegionAvail().x,64.0f*dpiScale); + PlotNoLerp("##WaveformP1",wavePreview1,wave1->len+1,0,NULL,0,wave1->max,size1); + ImGui::TableNextColumn(); + ImVec2 size2=ImVec2(ImGui::GetContentRegionAvail().x,64.0f*dpiScale); + PlotNoLerp("##WaveformP2",wavePreview2,wave2->len+1,0,NULL,0,wave2->max,size2); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Wave 1"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##SelWave1",&ins->ws.wave1,1,4)) { + if (ins->ws.wave1<0) ins->ws.wave1=0; + if (ins->ws.wave1>=(int)e->song.wave.size()) ins->ws.wave1=e->song.wave.size()-1; + } + ImGui::TableNextColumn(); + ImGui::Text("Wave 2"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##SelWave2",&ins->ws.wave2,1,4)) { + if (ins->ws.wave2<0) ins->ws.wave2=0; + if (ins->ws.wave2>=(int)e->song.wave.size()) ins->ws.wave2=e->song.wave.size()-1; + } + ImGui::EndTable(); + } + + ImGui::InputScalar("Update Rate",ImGuiDataType_U8,&ins->ws.rateDivider,&_ONE,&_SEVEN); + int speed=ins->ws.speed+1; + if (ImGui::InputInt("Speed",&speed,1,16)) { + if (speed<1) speed=1; + if (speed>256) speed=256; + ins->ws.speed=speed-1; + } + + ImGui::InputScalar("Amount",ImGuiDataType_U8,&ins->ws.param1,&_ONE,&_SEVEN); + + ImGui::Checkbox("Global",&ins->ws.global); + + ImGui::EndTabItem(); + } + } if (ImGui::BeginTabItem("Macros")) { float asFloat[256]; int asInt[256]; float loopIndicator[256]; const char* volumeLabel="Volume"; - int volMax=(ins->type==DIV_INS_PCE || ins->type==DIV_INS_AY8930)?31:15; + int volMax=15; int volMin=0; if (ins->type==DIV_INS_C64) { if (ins->c64.volIsCutoff) { @@ -1209,6 +2498,12 @@ void FurnaceGUI::drawInsEdit() { } } } + if ((ins->type==DIV_INS_PCE || ins->type==DIV_INS_AY8930)) { + volMax=31; + } + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_VERA || ins->type==DIV_INS_VRC6_SAW) { + volMax=63; + } if (ins->type==DIV_INS_AMIGA) { volMax=64; } @@ -1218,11 +2513,17 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_GB) { volMax=0; } + if (ins->type==DIV_INS_PET) { + volMax=1; + } + if (ins->type==DIV_INS_FDS) { + volMax=32; + } - bool arpMode=ins->std.arpMacroMode; + bool arpMode=ins->std.arpMacro.mode; const char* dutyLabel="Duty/Noise"; - int dutyMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?31:3; + int dutyMax=3; if (ins->type==DIV_INS_C64) { dutyLabel="Duty"; if (ins->c64.dutyIsAbs) { @@ -1234,89 +2535,159 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_FM) { dutyMax=32; } + if ((ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)) { + dutyMax=31; + } if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_FM) { dutyLabel="Noise Freq"; } - if (ins->type == DIV_INS_MIKEY) { - dutyLabel = "Duty/Int"; - dutyMax = 10; + if (ins->type==DIV_INS_MIKEY) { + dutyLabel="Duty/Int"; + dutyMax=10; } if (ins->type==DIV_INS_AY8930) { dutyMax=255; } - if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_PCE || ins->type==DIV_INS_AMIGA) { + if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SCC || ins->type==DIV_INS_PET || ins->type==DIV_INS_VIC) { dutyMax=0; } + if (ins->type==DIV_INS_PCE) { + dutyMax=1; + } + if (ins->type==DIV_INS_SWAN) { + dutyLabel="Noise"; + dutyMax=8; + } + if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL || ins->type==DIV_INS_VRC6_SAW || ins->type==DIV_INS_FDS) { + dutyMax=0; + } + if (ins->type==DIV_INS_VERA) { + dutyLabel="Duty"; + dutyMax=63; + } + if (ins->type==DIV_INS_N163) { + dutyLabel="Waveform pos."; + dutyMax=255; + } + if (ins->type==DIV_INS_VRC6) { + dutyLabel="Duty"; + dutyMax=7; + } bool dutyIsRel=(ins->type==DIV_INS_C64 && !ins->c64.dutyIsAbs); - int waveMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?3:63; + const char* waveLabel="Waveform"; + int waveMax=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_VERA)?3:63; bool bitMode=false; if (ins->type==DIV_INS_C64 || ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SAA1099) { bitMode=true; } - if (ins->type==DIV_INS_STD) waveMax=0; - if (ins->type==DIV_INS_TIA) waveMax=15; + if (ins->type==DIV_INS_STD || ins->type==DIV_INS_VRC6 || ins->type==DIV_INS_VRC6_SAW) waveMax=0; + if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_VIC || ins->type==DIV_INS_OPLL) waveMax=15; if (ins->type==DIV_INS_C64) waveMax=4; if (ins->type==DIV_INS_SAA1099) waveMax=2; - if (ins->type==DIV_INS_FM) waveMax=0; + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPZ) waveMax=0; if (ins->type==DIV_INS_MIKEY) waveMax=0; + if (ins->type==DIV_INS_PET) { + waveMax=8; + bitMode=true; + } - const char** waveNames=ayShapeBits; + if (ins->type==DIV_INS_OPLL) { + waveLabel="Patch"; + } + + const char** waveNames=NULL; + if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SAA1099) waveNames=ayShapeBits; if (ins->type==DIV_INS_C64) waveNames=c64ShapeBits; int ex1Max=(ins->type==DIV_INS_AY8930)?8:0; int ex2Max=(ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?4:0; + bool ex2Bit=true; if (ins->type==DIV_INS_C64) { ex1Max=4; ex2Max=15; } + if (ins->type==DIV_INS_X1_010) { + dutyMax=0; + ex1Max=7; + ex2Max=63; + ex2Bit=false; + } + if (ins->type==DIV_INS_N163) { + ex1Max=252; + ex2Max=2; + } + if (ins->type==DIV_INS_FDS) { + ex1Max=63; + ex2Max=4095; + } if (ins->type==DIV_INS_SAA1099) ex1Max=8; if (settings.macroView==0) { // modern view MACRO_BEGIN(28*dpiScale); if (volMax>0) { - NORMAL_MACRO(ins->std.volMacro,ins->std.volMacroLen,ins->std.volMacroLoop,ins->std.volMacroRel,volMin,volMax,"vol",volumeLabel,160,ins->std.volMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_VOLUME],mmlString[0],volMin,volMax,NULL,false); + NORMAL_MACRO(ins->std.volMacro,volMin,volMax,"vol",volumeLabel,160,ins->std.volMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_VOLUME],mmlString[0],volMin,volMax,NULL,false); } - NORMAL_MACRO(ins->std.arpMacro,ins->std.arpMacroLen,ins->std.arpMacroLoop,ins->std.arpMacroRel,arpMacroScroll,arpMacroScroll+24,"arp","Arpeggio",160,ins->std.arpMacroOpen,false,NULL,true,&arpMacroScroll,(arpMode?0:-80),0,0,&ins->std.arpMacroMode,uiColors[GUI_COLOR_MACRO_PITCH],mmlString[1],-92,94,(ins->std.arpMacroMode?(¯oHoverNote):NULL),true); + NORMAL_MACRO(ins->std.arpMacro,arpMacroScroll,arpMacroScroll+24,"arp","Arpeggio",160,ins->std.arpMacro.open,false,NULL,true,&arpMacroScroll,(arpMode?-60:-80),0,0,true,1,macroAbsoluteMode,uiColors[GUI_COLOR_MACRO_PITCH],mmlString[1],-92,94,(ins->std.arpMacro.mode?(¯oHoverNote):NULL),true); if (dutyMax>0) { - if (ins->type == DIV_INS_MIKEY) { - NORMAL_MACRO(ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacroOpen,true,mikeyFeedbackBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL,false); + if (ins->type==DIV_INS_MIKEY) { + NORMAL_MACRO(ins->std.dutyMacro,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacro.open,true,mikeyFeedbackBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL,false); + } else if (ins->type==DIV_INS_C64) { + NORMAL_MACRO(ins->std.dutyMacro,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacro.open,false,NULL,false,NULL,0,0,0,true,1,macroAbsoluteMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL,false); + } else { + NORMAL_MACRO(ins->std.dutyMacro,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL,false); } - else { - NORMAL_MACRO(ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL,false); - } } if (waveMax>0) { - NORMAL_MACRO(ins->std.waveMacro,ins->std.waveMacroLen,ins->std.waveMacroLoop,ins->std.waveMacroRel,0,waveMax,"wave","Waveform",bitMode?64:160,ins->std.waveMacroOpen,bitMode,waveNames,false,NULL,0,0,((ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?1:0),NULL,uiColors[GUI_COLOR_MACRO_WAVE],mmlString[3],0,waveMax,NULL,false); + NORMAL_MACRO(ins->std.waveMacro,0,waveMax,"wave",waveLabel,(bitMode && ins->type!=DIV_INS_PET)?64:160,ins->std.waveMacro.open,bitMode,waveNames,false,NULL,0,0,((ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?1:0),false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_WAVE],mmlString[3],0,waveMax,NULL,false); } if (ex1Max>0) { if (ins->type==DIV_INS_C64) { - NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Filter Mode",64,ins->std.ex1MacroOpen,true,filtModeBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Filter Mode",64,ins->std.ex1Macro.open,true,filtModeBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } else if (ins->type==DIV_INS_SAA1099) { - NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Envelope",160,ins->std.ex1MacroOpen,true,saaEnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Envelope",160,ins->std.ex1Macro.open,true,saaEnvBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + } else if (ins->type==DIV_INS_X1_010) { + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Envelope Mode",160,ins->std.ex1Macro.open,true,x1_010EnvBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + } else if (ins->type==DIV_INS_N163) { + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Waveform len.",160,ins->std.ex1Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + } else if (ins->type==DIV_INS_FDS) { + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Mod Depth",160,ins->std.ex1Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } else { - NORMAL_MACRO(ins->std.ex1Macro,ins->std.ex1MacroLen,ins->std.ex1MacroLoop,ins->std.ex1MacroRel,0,ex1Max,"ex1","Duty",160,ins->std.ex1MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); + NORMAL_MACRO(ins->std.ex1Macro,0,ex1Max,"ex1","Duty",160,ins->std.ex1Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[4],0,ex1Max,NULL,false); } } if (ex2Max>0) { if (ins->type==DIV_INS_C64) { - NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,ex2Max,"ex2","Resonance",64,ins->std.ex2MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); + NORMAL_MACRO(ins->std.ex2Macro,0,ex2Max,"ex2","Resonance",64,ins->std.ex2Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); + } else if (ins->type==DIV_INS_N163) { + NORMAL_MACRO(ins->std.ex2Macro,0,ex2Max,"ex2","Waveform update",64,ins->std.ex2Macro.open,true,n163UpdateBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); + } else if (ins->type==DIV_INS_FDS) { + NORMAL_MACRO(ins->std.ex2Macro,0,ex2Max,"ex2","Mod Speed",160,ins->std.ex2Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); } else { - NORMAL_MACRO(ins->std.ex2Macro,ins->std.ex2MacroLen,ins->std.ex2MacroLoop,ins->std.ex2MacroRel,0,ex2Max,"ex2","Envelope",64,ins->std.ex2MacroOpen,true,ayEnvBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); + NORMAL_MACRO(ins->std.ex2Macro,0,ex2Max,"ex2","Envelope",ex2Bit?64:160,ins->std.ex2Macro.open,ex2Bit,ayEnvBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[5],0,ex2Max,NULL,false); } } if (ins->type==DIV_INS_C64) { - NORMAL_MACRO(ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel,0,2,"ex3","Special",32,ins->std.ex3MacroOpen,true,c64SpecialBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,2,NULL,false); + NORMAL_MACRO(ins->std.ex3Macro,0,2,"ex3","Special",32,ins->std.ex3Macro.open,true,c64SpecialBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,2,NULL,false); } - if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930) { - NORMAL_MACRO(ins->std.ex3Macro,ins->std.ex3MacroLen,ins->std.ex3MacroLoop,ins->std.ex3MacroRel,0,15,"ex3","AutoEnv Num",96,ins->std.ex3MacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,15,NULL,false); - NORMAL_MACRO(ins->std.algMacro,ins->std.algMacroLen,ins->std.algMacroLoop,ins->std.algMacroRel,0,15,"alg","AutoEnv Den",96,ins->std.algMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[7],0,15,NULL,false); + if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_X1_010) { + NORMAL_MACRO(ins->std.ex3Macro,0,15,"ex3","AutoEnv Num",96,ins->std.ex3Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,15,NULL,false); + NORMAL_MACRO(ins->std.algMacro,0,15,"alg","AutoEnv Den",96,ins->std.algMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[7],0,15,NULL,false); } if (ins->type==DIV_INS_AY8930) { // oh my i am running out of macros - NORMAL_MACRO(ins->std.fbMacro,ins->std.fbMacroLen,ins->std.fbMacroLoop,ins->std.fbMacroRel,0,8,"fb","Noise AND Mask",96,ins->std.fbMacroOpen,true,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[8],0,8,NULL,false); - NORMAL_MACRO(ins->std.fmsMacro,ins->std.fmsMacroLen,ins->std.fmsMacroLoop,ins->std.fmsMacroRel,0,8,"fms","Noise OR Mask",96,ins->std.fmsMacroOpen,true,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[9],0,8,NULL,false); + NORMAL_MACRO(ins->std.fbMacro,0,8,"fb","Noise AND Mask",96,ins->std.fbMacro.open,true,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[8],0,8,NULL,false); + NORMAL_MACRO(ins->std.fmsMacro,0,8,"fms","Noise OR Mask",96,ins->std.fmsMacro.open,true,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[9],0,8,NULL,false); + } + if (ins->type==DIV_INS_N163) { + NORMAL_MACRO(ins->std.ex3Macro,0,255,"ex3","Waveform to Load",160,ins->std.ex3Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,255,NULL,false); + NORMAL_MACRO(ins->std.algMacro,0,255,"alg","Wave pos. to Load",160,ins->std.algMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[7],0,255,NULL,false); + NORMAL_MACRO(ins->std.fbMacro,0,252,"fb","Wave len. to Load",160,ins->std.fbMacro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[8],0,252,NULL,false); + NORMAL_MACRO(ins->std.fmsMacro,0,2,"fms","Waveform load",64,ins->std.fmsMacro.open,true,n163UpdateBits,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[9],0,2,NULL,false); + } + if (ins->type==DIV_INS_FDS) { + NORMAL_MACRO(ins->std.ex3Macro,0,127,"ex3","Mod Position",160,ins->std.ex3Macro.open,false,NULL,false,NULL,0,0,0,false,0,macroDummyMode,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[6],0,2,NULL,false); } MACRO_END; @@ -1332,87 +2703,87 @@ void FurnaceGUI::drawInsEdit() { } else { ImGui::Text("Volume Macro"); } - for (int i=0; istd.volMacroLen; i++) { + for (int i=0; istd.volMacro.len; i++) { if (ins->type==DIV_INS_C64 && ins->c64.volIsCutoff && !ins->c64.filterIsAbs) { - asFloat[i]=ins->std.volMacro[i]-18; + asFloat[i]=ins->std.volMacro.val[i]-18; } else { - asFloat[i]=ins->std.volMacro[i]; + asFloat[i]=ins->std.volMacro.val[i]; } - loopIndicator[i]=(ins->std.volMacroLoop!=-1 && i>=ins->std.volMacroLoop); + loopIndicator[i]=(ins->std.volMacro.loop!=-1 && i>=ins->std.volMacro.loop); } macroDragScroll=0; if (volMax>0) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - ImGui::PlotHistogram("##IVolMacro",asFloat,ins->std.volMacroLen,0,NULL,volMin,volMax,ImVec2(400.0f*dpiScale,200.0f*dpiScale)); + ImGui::PlotHistogram("##IVolMacro",asFloat,ins->std.volMacro.len,0,NULL,volMin,volMax,ImVec2(400.0f*dpiScale,200.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); macroDragAreaSize=ImVec2(400.0f*dpiScale,200.0f*dpiScale); macroDragMin=volMin; macroDragMax=volMax; - macroDragLen=ins->std.volMacroLen; + macroDragLen=ins->std.volMacro.len; macroDragActive=true; - macroDragTarget=ins->std.volMacro; + macroDragTarget=ins->std.volMacro.val; macroDragChar=false; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } - ImGui::PlotHistogram("##IVolMacroLoop",loopIndicator,ins->std.volMacroLen,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); + ImGui::PlotHistogram("##IVolMacro.loop",loopIndicator,ins->std.volMacro.len,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroLoopDragStart=ImGui::GetItemRectMin(); macroLoopDragAreaSize=ImVec2(400.0f*dpiScale,16.0f*dpiScale); - macroLoopDragLen=ins->std.volMacroLen; - macroLoopDragTarget=&ins->std.volMacroLoop; + macroLoopDragLen=ins->std.volMacro.len; + macroLoopDragTarget=&ins->std.volMacro.loop; macroLoopDragActive=true; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - ins->std.volMacroLoop=-1; + ins->std.volMacro.loop=-1; } ImGui::PopStyleVar(); - if (ImGui::InputScalar("Length##IVolMacroL",ImGuiDataType_U8,&ins->std.volMacroLen,&_ONE,&_THREE)) { - if (ins->std.volMacroLen>127) ins->std.volMacroLen=127; + if (ImGui::InputScalar("Length##IVolMacroL",ImGuiDataType_U8,&ins->std.volMacro.len,&_ONE,&_THREE)) { + if (ins->std.volMacro.len>127) ins->std.volMacro.len=127; } } // arp macro ImGui::Separator(); ImGui::Text("Arpeggio Macro"); - for (int i=0; istd.arpMacroLen; i++) { - asFloat[i]=ins->std.arpMacro[i]; - loopIndicator[i]=(ins->std.arpMacroLoop!=-1 && i>=ins->std.arpMacroLoop); + for (int i=0; istd.arpMacro.len; i++) { + asFloat[i]=ins->std.arpMacro.val[i]; + loopIndicator[i]=(ins->std.arpMacro.loop!=-1 && i>=ins->std.arpMacro.loop); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - ImGui::PlotHistogram("##IArpMacro",asFloat,ins->std.arpMacroLen,0,NULL,arpMode?arpMacroScroll:(arpMacroScroll-12),arpMacroScroll+(arpMode?24:12),ImVec2(400.0f*dpiScale,200.0f*dpiScale)); + ImGui::PlotHistogram("##IArpMacro",asFloat,ins->std.arpMacro.len,0,NULL,arpMode?arpMacroScroll:(arpMacroScroll-12),arpMacroScroll+(arpMode?24:12),ImVec2(400.0f*dpiScale,200.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); macroDragAreaSize=ImVec2(400.0f*dpiScale,200.0f*dpiScale); macroDragMin=arpMacroScroll; macroDragMax=arpMacroScroll+24; - macroDragLen=ins->std.arpMacroLen; + macroDragLen=ins->std.arpMacro.len; macroDragActive=true; - macroDragTarget=ins->std.arpMacro; + macroDragTarget=ins->std.arpMacro.val; macroDragChar=false; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } ImGui::SameLine(); - ImGui::VSliderInt("##IArpMacroPos",ImVec2(20.0f*dpiScale,200.0f*dpiScale),&arpMacroScroll,arpMode?0:-80,70); - ImGui::PlotHistogram("##IArpMacroLoop",loopIndicator,ins->std.arpMacroLen,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); + CWVSliderInt("##IArpMacroPos",ImVec2(20.0f*dpiScale,200.0f*dpiScale),&arpMacroScroll,arpMode?0:-80,70); + ImGui::PlotHistogram("##IArpMacro.loop",loopIndicator,ins->std.arpMacro.len,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroLoopDragStart=ImGui::GetItemRectMin(); macroLoopDragAreaSize=ImVec2(400.0f*dpiScale,16.0f*dpiScale); - macroLoopDragLen=ins->std.arpMacroLen; - macroLoopDragTarget=&ins->std.arpMacroLoop; + macroLoopDragLen=ins->std.arpMacro.len; + macroLoopDragTarget=&ins->std.arpMacro.loop; macroLoopDragActive=true; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - ins->std.arpMacroLoop=-1; + ins->std.arpMacro.loop=-1; } ImGui::PopStyleVar(); - if (ImGui::InputScalar("Length##IArpMacroL",ImGuiDataType_U8,&ins->std.arpMacroLen,&_ONE,&_THREE)) { - if (ins->std.arpMacroLen>127) ins->std.arpMacroLen=127; + if (ImGui::InputScalar("Length##IArpMacroL",ImGuiDataType_U8,&ins->std.arpMacro.len,&_ONE,&_THREE)) { + if (ins->std.arpMacro.len>127) ins->std.arpMacro.len=127; } if (ImGui::Checkbox("Fixed",&arpMode)) { - ins->std.arpMacroMode=arpMode; + ins->std.arpMacro.mode=arpMode; if (arpMode) { if (arpMacroScroll<0) arpMacroScroll=0; } @@ -1434,39 +2805,39 @@ void FurnaceGUI::drawInsEdit() { ImGui::Text("Duty/Noise Mode Macro"); } } - for (int i=0; istd.dutyMacroLen; i++) { - asFloat[i]=ins->std.dutyMacro[i]-(dutyIsRel?12:0); - loopIndicator[i]=(ins->std.dutyMacroLoop!=-1 && i>=ins->std.dutyMacroLoop); + for (int i=0; istd.dutyMacro.len; i++) { + asFloat[i]=ins->std.dutyMacro.val[i]-(dutyIsRel?12:0); + loopIndicator[i]=(ins->std.dutyMacro.loop!=-1 && i>=ins->std.dutyMacro.loop); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - ImGui::PlotHistogram("##IDutyMacro",asFloat,ins->std.dutyMacroLen,0,NULL,dutyIsRel?-12:0,dutyMax-(dutyIsRel?12:0),ImVec2(400.0f*dpiScale,200.0f*dpiScale)); + ImGui::PlotHistogram("##IDutyMacro",asFloat,ins->std.dutyMacro.len,0,NULL,dutyIsRel?-12:0,dutyMax-(dutyIsRel?12:0),ImVec2(400.0f*dpiScale,200.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); macroDragAreaSize=ImVec2(400.0f*dpiScale,200.0f*dpiScale); macroDragMin=0; macroDragMax=dutyMax; - macroDragLen=ins->std.dutyMacroLen; + macroDragLen=ins->std.dutyMacro.len; macroDragActive=true; - macroDragTarget=ins->std.dutyMacro; + macroDragTarget=ins->std.dutyMacro.val; macroDragChar=false; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } - ImGui::PlotHistogram("##IDutyMacroLoop",loopIndicator,ins->std.dutyMacroLen,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); + ImGui::PlotHistogram("##IDutyMacro.loop",loopIndicator,ins->std.dutyMacro.len,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroLoopDragStart=ImGui::GetItemRectMin(); macroLoopDragAreaSize=ImVec2(400.0f*dpiScale,16.0f*dpiScale); - macroLoopDragLen=ins->std.dutyMacroLen; - macroLoopDragTarget=&ins->std.dutyMacroLoop; + macroLoopDragLen=ins->std.dutyMacro.len; + macroLoopDragTarget=&ins->std.dutyMacro.loop; macroLoopDragActive=true; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - ins->std.dutyMacroLoop=-1; + ins->std.dutyMacro.loop=-1; } ImGui::PopStyleVar(); - if (ImGui::InputScalar("Length##IDutyMacroL",ImGuiDataType_U8,&ins->std.dutyMacroLen,&_ONE,&_THREE)) { - if (ins->std.dutyMacroLen>127) ins->std.dutyMacroLen=127; + if (ImGui::InputScalar("Length##IDutyMacroL",ImGuiDataType_U8,&ins->std.dutyMacro.len,&_ONE,&_THREE)) { + if (ins->std.dutyMacro.len>127) ins->std.dutyMacro.len=127; } } @@ -1474,24 +2845,24 @@ void FurnaceGUI::drawInsEdit() { if (waveMax>0) { ImGui::Separator(); ImGui::Text("Waveform Macro"); - for (int i=0; istd.waveMacroLen; i++) { - asFloat[i]=ins->std.waveMacro[i]; + for (int i=0; istd.waveMacro.len; i++) { + asFloat[i]=ins->std.waveMacro.val[i]; if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930) { - asInt[i]=ins->std.waveMacro[i]+1; + asInt[i]=ins->std.waveMacro.val[i]+1; } else { - asInt[i]=ins->std.waveMacro[i]; + asInt[i]=ins->std.waveMacro.val[i]; } - loopIndicator[i]=(ins->std.waveMacroLoop!=-1 && i>=ins->std.waveMacroLoop); + loopIndicator[i]=(ins->std.waveMacro.loop!=-1 && i>=ins->std.waveMacro.loop); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); ImVec2 areaSize=ImVec2(400.0f*dpiScale,200.0f*dpiScale); if (ins->type==DIV_INS_C64 || ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SAA1099) { areaSize=ImVec2(400.0f*dpiScale,waveMax*32.0f*dpiScale); - PlotBitfield("##IWaveMacro",asInt,ins->std.waveMacroLen,0,(ins->type==DIV_INS_C64)?c64ShapeBits:ayShapeBits,waveMax,areaSize); + PlotBitfield("##IWaveMacro",asInt,ins->std.waveMacro.len,0,(ins->type==DIV_INS_C64)?c64ShapeBits:ayShapeBits,waveMax,areaSize); bitMode=true; } else { - ImGui::PlotHistogram("##IWaveMacro",asFloat,ins->std.waveMacroLen,0,NULL,0,waveMax,areaSize); + ImGui::PlotHistogram("##IWaveMacro",asFloat,ins->std.waveMacro.len,0,NULL,0,waveMax,areaSize); } if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); @@ -1502,27 +2873,27 @@ void FurnaceGUI::drawInsEdit() { macroDragBitMode=bitMode; macroDragInitialValueSet=false; macroDragInitialValue=false; - macroDragLen=ins->std.waveMacroLen; + macroDragLen=ins->std.waveMacro.len; macroDragActive=true; - macroDragTarget=ins->std.waveMacro; + macroDragTarget=ins->std.waveMacro.val; macroDragChar=false; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } - ImGui::PlotHistogram("##IWaveMacroLoop",loopIndicator,ins->std.waveMacroLen,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); + ImGui::PlotHistogram("##IWaveMacro.loop",loopIndicator,ins->std.waveMacro.len,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroLoopDragStart=ImGui::GetItemRectMin(); macroLoopDragAreaSize=ImVec2(400.0f*dpiScale,16.0f*dpiScale); - macroLoopDragLen=ins->std.waveMacroLen; - macroLoopDragTarget=&ins->std.waveMacroLoop; + macroLoopDragLen=ins->std.waveMacro.len; + macroLoopDragTarget=&ins->std.waveMacro.loop; macroLoopDragActive=true; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - ins->std.waveMacroLoop=-1; + ins->std.waveMacro.loop=-1; } ImGui::PopStyleVar(); - if (ImGui::InputScalar("Length##IWaveMacroL",ImGuiDataType_U8,&ins->std.waveMacroLen,&_ONE,&_THREE)) { - if (ins->std.waveMacroLen>127) ins->std.waveMacroLen=127; + if (ImGui::InputScalar("Length##IWaveMacroL",ImGuiDataType_U8,&ins->std.waveMacro.len,&_ONE,&_THREE)) { + if (ins->std.waveMacro.len>127) ins->std.waveMacro.len=127; } } @@ -1534,39 +2905,39 @@ void FurnaceGUI::drawInsEdit() { } else { ImGui::Text("Extra 1 Macro"); } - for (int i=0; istd.ex1MacroLen; i++) { - asFloat[i]=ins->std.ex1Macro[i]; - loopIndicator[i]=(ins->std.ex1MacroLoop!=-1 && i>=ins->std.ex1MacroLoop); + for (int i=0; istd.ex1Macro.len; i++) { + asFloat[i]=ins->std.ex1Macro.val[i]; + loopIndicator[i]=(ins->std.ex1Macro.loop!=-1 && i>=ins->std.ex1Macro.loop); } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - ImGui::PlotHistogram("##IEx1Macro",asFloat,ins->std.ex1MacroLen,0,NULL,0,ex1Max,ImVec2(400.0f*dpiScale,200.0f*dpiScale)); + ImGui::PlotHistogram("##IEx1Macro",asFloat,ins->std.ex1Macro.len,0,NULL,0,ex1Max,ImVec2(400.0f*dpiScale,200.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroDragStart=ImGui::GetItemRectMin(); macroDragAreaSize=ImVec2(400.0f*dpiScale,200.0f*dpiScale); macroDragMin=0; macroDragMax=ex1Max; - macroDragLen=ins->std.ex1MacroLen; + macroDragLen=ins->std.ex1Macro.len; macroDragActive=true; - macroDragTarget=ins->std.ex1Macro; + macroDragTarget=ins->std.ex1Macro.val; macroDragChar=false; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } - ImGui::PlotHistogram("##IEx1MacroLoop",loopIndicator,ins->std.ex1MacroLen,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); + ImGui::PlotHistogram("##IEx1Macro.loop",loopIndicator,ins->std.ex1Macro.len,0,NULL,0,1,ImVec2(400.0f*dpiScale,16.0f*dpiScale)); if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { macroLoopDragStart=ImGui::GetItemRectMin(); macroLoopDragAreaSize=ImVec2(400.0f*dpiScale,16.0f*dpiScale); - macroLoopDragLen=ins->std.ex1MacroLen; - macroLoopDragTarget=&ins->std.ex1MacroLoop; + macroLoopDragLen=ins->std.ex1Macro.len; + macroLoopDragTarget=&ins->std.ex1Macro.loop; macroLoopDragActive=true; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - ins->std.ex1MacroLoop=-1; + ins->std.ex1Macro.loop=-1; } ImGui::PopStyleVar(); - if (ImGui::InputScalar("Length##IEx1MacroL",ImGuiDataType_U8,&ins->std.ex1MacroLen,&_ONE,&_THREE)) { - if (ins->std.ex1MacroLen>127) ins->std.ex1MacroLen=127; + if (ImGui::InputScalar("Length##IEx1MacroL",ImGuiDataType_U8,&ins->std.ex1Macro.len,&_ONE,&_THREE)) { + if (ins->std.ex1Macro.len>127) ins->std.ex1Macro.len=127; } } } @@ -1579,149 +2950,3 @@ void FurnaceGUI::drawInsEdit() { if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_EDIT; ImGui::End(); } - -#undef P -#undef PARAMETER - -void FurnaceGUI::drawWaveList() { - if (nextWindow==GUI_WINDOW_WAVE_LIST) { - waveListOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!waveListOpen) return; - float wavePreview[256]; - if (ImGui::Begin("Wavetables",&waveListOpen)) { - if (ImGui::Button(ICON_FA_PLUS "##WaveAdd")) { - doAction(GUI_ACTION_WAVE_LIST_ADD); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FILES_O "##WaveClone")) { - doAction(GUI_ACTION_WAVE_LIST_DUPLICATE); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FOLDER_OPEN "##WaveLoad")) { - doAction(GUI_ACTION_WAVE_LIST_OPEN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FLOPPY_O "##WaveSave")) { - doAction(GUI_ACTION_WAVE_LIST_SAVE); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("WaveUp",ImGuiDir_Up)) { - doAction(GUI_ACTION_WAVE_LIST_UP); - } - ImGui::SameLine(); - if (ImGui::ArrowButton("WaveDown",ImGuiDir_Down)) { - doAction(GUI_ACTION_WAVE_LIST_DOWN); - } - ImGui::SameLine(); - if (ImGui::Button(ICON_FA_TIMES "##WaveDelete")) { - doAction(GUI_ACTION_WAVE_LIST_DELETE); - } - ImGui::Separator(); - if (ImGui::BeginTable("WaveListScroll",1,ImGuiTableFlags_ScrollY)) { - for (int i=0; i<(int)e->song.wave.size(); i++) { - DivWavetable* wave=e->song.wave[i]; - for (int i=0; ilen; i++) { - wavePreview[i]=wave->data[i]; - } - if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::Selectable(fmt::sprintf("%d##_WAVE%d\n",i,i).c_str(),curWave==i)) { - curWave=i; - } - if (ImGui::IsItemHovered()) { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - waveEditOpen=true; - } - } - ImGui::SameLine(); - PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,wave->max); - } - ImGui::EndTable(); - } - } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_WAVE_LIST; - ImGui::End(); -} - -void FurnaceGUI::drawWaveEdit() { - if (nextWindow==GUI_WINDOW_WAVE_EDIT) { - waveEditOpen=true; - ImGui::SetNextWindowFocus(); - nextWindow=GUI_WINDOW_NOTHING; - } - if (!waveEditOpen) return; - float wavePreview[256]; - ImGui::SetNextWindowSizeConstraints(ImVec2(450.0f*dpiScale,300.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); - if (ImGui::Begin("Wavetable Editor",&waveEditOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { - if (curWave<0 || curWave>=(int)e->song.wave.size()) { - ImGui::Text("no wavetable selected"); - } else { - DivWavetable* wave=e->song.wave[curWave]; - ImGui::Text("Width"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a width of 32 on Game Boy and PC Engine.\nany other widths will be scaled during playback."); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(128.0f*dpiScale); - 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); - modified=true; - } - ImGui::SameLine(); - ImGui::Text("Height"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("use a height of:\n- 15 for Game Boy\n- 31 for PC Engine\nany other heights will be scaled during playback."); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(128.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); - modified=true; - } - for (int i=0; ilen; i++) { - wavePreview[i]=wave->data[i]; - } - if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); //wavetable text input size found here - if (ImGui::InputText("##MMLWave",&mmlStringW)) { - decodeMMLStrW(mmlStringW,wave->data,wave->len,wave->max); - } - if (!ImGui::IsItemActive()) { - encodeMMLStr(mmlStringW,wave->data,wave->len,-1,-1); - } - - 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; - ImGui::End(); -} - diff --git a/src/gui/intConst.cpp b/src/gui/intConst.cpp index dae78ff10..9c7f53b94 100644 --- a/src/gui/intConst.cpp +++ b/src/gui/intConst.cpp @@ -29,6 +29,7 @@ const int _THIRTY_ONE=31; const int _SIXTY_FOUR=64; const int _ONE_HUNDRED=100; const int _ONE_HUNDRED_TWENTY_SEVEN=127; +const int _TWO_HUNDRED_FIFTY_FIVE=255; const int _TWO_THOUSAND_FORTY_SEVEN=2047; const int _FOUR_THOUSAND_NINETY_FIVE=4095; const int _MINUS_ONE_HUNDRED_TWENTY_SEVEN=-127; diff --git a/src/gui/intConst.h b/src/gui/intConst.h index 2618ec652..c6b13b9af 100644 --- a/src/gui/intConst.h +++ b/src/gui/intConst.h @@ -31,6 +31,7 @@ extern const int _THIRTY_ONE; extern const int _SIXTY_FOUR; extern const int _ONE_HUNDRED; extern const int _ONE_HUNDRED_TWENTY_SEVEN; +extern const int _TWO_HUNDRED_FIFTY_FIVE; extern const int _TWO_THOUSAND_FORTY_SEVEN; extern const int _FOUR_THOUSAND_NINETY_FIVE; extern const int _MINUS_ONE_HUNDRED_TWENTY_SEVEN; diff --git a/src/gui/log.cpp b/src/gui/log.cpp new file mode 100644 index 000000000..c6c4e2a86 --- /dev/null +++ b/src/gui/log.cpp @@ -0,0 +1,77 @@ +#include "gui.h" +#include "../ta-log.h" +#include + +const char* logLevels[5]={ + "ERROR", + "warning", + "info", + "debug", + "trace" +}; + +FurnaceGUIColors logColors[5]={ + GUI_COLOR_LOGLEVEL_ERROR, + GUI_COLOR_LOGLEVEL_WARNING, + GUI_COLOR_LOGLEVEL_INFO, + GUI_COLOR_LOGLEVEL_DEBUG, + GUI_COLOR_LOGLEVEL_TRACE +}; + +void FurnaceGUI::drawLog() { + if (nextWindow==GUI_WINDOW_LOG) { + logOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!logOpen) return; + if (ImGui::Begin("Log Viewer",&logOpen)) { + ImGui::Checkbox("Follow",&followLog); + ImGui::SameLine(); + ImGui::Text("Level"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::Combo("##LogLevel",&logLevel,logLevels,5); + if (ImGui::BeginTable("LogView",3,ImGuiTableFlags_ScrollY|ImGuiTableFlags_BordersInnerV)) { + ImGui::PushFont(patFont); + + float timeChars=ImGui::CalcTextSize("00:00:00").x; + float levelChars=ImGui::CalcTextSize("warning").x; + + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,timeChars); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,levelChars); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableSetupScrollFreeze(0,1); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("time"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("level"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("message"); + + int pos=logPosition; + for (int i=0; i>4)-8; + int chan=where.type&15; + int data1=where.data[0]; + int data2=where.data[1]; + if (type<0) return 0; + if (map[type]==NULL) return 0; + + int** chanMap=map[type][chan]; + if (chanMap==NULL) { + chanMap=map[type][16]; + if (chanMap==NULL) return 0; + } + + int* dataMap=chanMap[data1]; + if (dataMap==NULL) { + dataMap=chanMap[128]; + if (dataMap==NULL) return 0; + } + + int ret=dataMap[data2]; + if (ret==0) { + ret=dataMap[128]; + if (ret==0) { // maybe this is not the correct mapping + dataMap=chanMap[128]; + if (dataMap==NULL) { + chanMap=map[type][16]; + if (chanMap==NULL) return 0; + + dataMap=chanMap[data1]; + if (dataMap==NULL) { + dataMap=chanMap[128]; + if (dataMap==NULL) return 0; + } + + ret=dataMap[data2]; + if (ret==0) { + ret=dataMap[128]; + } + } else { + ret=dataMap[data2]; + if (ret==0) { + ret=dataMap[128]; + } + } + } + } + + return ret; +} + +#define UNDERSTAND_OPTION(x) if (optionNameS==#x) { \ + x=std::stoi(optionValueS); \ +} + +#define UNDERSTAND_FLOAT_OPTION(x) if (optionNameS==#x) { \ + x=std::stof(optionValueS); \ +} + +#define UNDERSTAND_ARRAY_OPTION(x,yMax) if (optionNameS==#x) { \ + if (optionIndex<0 || optionIndex>=yMax) { \ + logW("MIDI map array option %d out of range (0-%d) at line %d: %s",optionIndex,yMax,curLine,line); \ + break; \ + } \ + x[optionIndex]=std::stoi(optionValueS); \ +} + +bool MIDIMap::read(String path) { + char line[4096]; + int curLine=1; + FILE* f=fopen(path.c_str(),"rb"); + if (f==NULL) { + if (errno!=ENOENT) { + logE("error while loading MIDI mapping! %s",strerror(errno)); + } + return false; + } + + binds.clear(); + while (fgets(line,4096,f)) { + char* nlPos=strrchr(line,'\n'); + if (nlPos!=NULL) *nlPos=0; + if (strstr(line,"aOption")==line) { + char optionName[256]; + int optionIndex=-1; + char optionValue[256]; + String optionNameS, optionValueS; + + int result=sscanf(line,"aOption %255s %d %255s",optionName,&optionIndex,optionValue); + if (result!=3) { + logW("MIDI map garbage data at line %d: %s",curLine,line); + break; + } + + optionNameS=optionName; + optionValueS=optionValue; + + try { + UNDERSTAND_ARRAY_OPTION(valueInputSpecificStyle,18) else + UNDERSTAND_ARRAY_OPTION(valueInputSpecificMSB,18) else + UNDERSTAND_ARRAY_OPTION(valueInputSpecificLSB,18) else + UNDERSTAND_ARRAY_OPTION(valueInputSpecificSingle,18) else { + logW("MIDI map unknown array option %s at line %d: %s",optionName,curLine,line); + } + } catch (std::out_of_range& e) { + logW("MIDI map invalid value %s for array option %s at line %d: %s",optionValue,optionName,curLine,line); + } catch (std::invalid_argument& e) { + logW("MIDI map invalid value %s for array option %s at line %d: %s",optionValue,optionName,curLine,line); + } + + curLine++; + continue; + } + if (strstr(line,"option")==line) { + char optionName[256]; + char optionValue[256]; + String optionNameS, optionValueS; + int result=sscanf(line,"option %255s %255s",optionName,optionValue); + if (result!=2) { + logW("MIDI map garbage data at line %d: %s",curLine,line); + break; + } + + optionNameS=optionName; + optionValueS=optionValue; + + try { + UNDERSTAND_OPTION(noteInput) else + UNDERSTAND_OPTION(volInput) else + UNDERSTAND_OPTION(rawVolume) else + UNDERSTAND_OPTION(polyInput) else + UNDERSTAND_OPTION(directChannel) else + UNDERSTAND_OPTION(programChange) else + UNDERSTAND_OPTION(midiClock) else + UNDERSTAND_OPTION(midiTimeCode) else + UNDERSTAND_OPTION(valueInputStyle) else + UNDERSTAND_OPTION(valueInputControlMSB) else + UNDERSTAND_OPTION(valueInputControlLSB) else + UNDERSTAND_OPTION(valueInputControlSingle) else + UNDERSTAND_FLOAT_OPTION(volExp) else { + logW("MIDI map unknown option %s at line %d: %s",optionName,curLine,line); + } + } catch (std::out_of_range& e) { + logW("MIDI map invalid value %s for option %s at line %d: %s",optionValue,optionName,curLine,line); + } catch (std::invalid_argument& e) { + logW("MIDI map invalid value %s for option %s at line %d: %s",optionValue,optionName,curLine,line); + } + + curLine++; + continue; + } + + char bindAction[256]; + MIDIBind bind; + int result=sscanf(line,"%d %d %d %d %255s",&bind.type,&bind.channel,&bind.data1,&bind.data2,bindAction); + if (result!=5 || result==EOF) { + logW("MIDI map garbage data at line %d: %s",curLine,line); + break; + } + + bool foundAction=false; + for (int i=0; i15) continue; + if (i.channel<0 || i.channel>16) continue; + if (i.data1<0 || i.data1>128) continue; + if (i.data2<0 || i.data2>128) continue; + + if (map[i.type-8]==NULL) { + map[i.type-8]=new int**[17]; + memset(map[i.type-8],0,sizeof(int**)*17); + } + if (map[i.type-8][i.channel]==NULL) { + map[i.type-8][i.channel]=new int*[129]; + memset(map[i.type-8][i.channel],0,sizeof(int*)*129); + } + if (map[i.type-8][i.channel][i.data1]==NULL) { + map[i.type-8][i.channel][i.data1]=new int[129]; + memset(map[i.type-8][i.channel][i.data1],0,sizeof(int)*129); + } + + map[i.type-8][i.channel][i.data1][i.data2]=i.action; + logD("MIDI mapping %d %d %d %d to %d",i.type-8,i.channel,i.data1,i.data2,i.action); + } +} diff --git a/src/gui/mixer.cpp b/src/gui/mixer.cpp new file mode 100644 index 000000000..5bb3694e5 --- /dev/null +++ b/src/gui/mixer.cpp @@ -0,0 +1,59 @@ +/** + * 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 "intConst.h" + +void FurnaceGUI::drawMixer() { + if (nextWindow==GUI_WINDOW_MIXER) { + mixerOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!mixerOpen) return; + ImGui::SetNextWindowSizeConstraints(ImVec2(400.0f*dpiScale,200.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (ImGui::Begin("Mixer",&mixerOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { + char id[32]; + if (ImGui::SliderFloat("Master Volume",&e->song.masterVol,0,3,"%.2fx")) { + if (e->song.masterVol<0) e->song.masterVol=0; + if (e->song.masterVol>3) e->song.masterVol=3; + } rightClickable + for (int i=0; isong.systemLen; i++) { + snprintf(id,31,"MixS%d",i); + bool doInvert=e->song.systemVol[i]&128; + signed char vol=e->song.systemVol[i]&127; + ImGui::PushID(id); + ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i])); + ImGui::SameLine(ImGui::GetWindowWidth()-(82.0f*dpiScale)); + if (ImGui::Checkbox("Invert",&doInvert)) { + e->song.systemVol[i]^=128; + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(50.0f*dpiScale)); + if (CWSliderScalar("Volume",ImGuiDataType_S8,&vol,&_ZERO,&_ONE_HUNDRED_TWENTY_SEVEN)) { + e->song.systemVol[i]=(e->song.systemVol[i]&128)|vol; + } rightClickable + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-(50.0f*dpiScale)); + CWSliderScalar("Panning",ImGuiDataType_S8,&e->song.systemPan[i],&_MINUS_ONE_HUNDRED_TWENTY_SEVEN,&_ONE_HUNDRED_TWENTY_SEVEN); rightClickable + + ImGui::PopID(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_MIXER; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/newSong.cpp b/src/gui/newSong.cpp new file mode 100644 index 000000000..d6188b3e5 --- /dev/null +++ b/src/gui/newSong.cpp @@ -0,0 +1,90 @@ +/** + * 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" + +void FurnaceGUI::drawNewSong() { + bool accepted=false; + + ImGui::PushFont(bigFont); + ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize("Choose a System!").x)*0.5); + ImGui::Text("Choose a System!"); + ImGui::PopFont(); + + if (ImGui::BeginTable("sysPicker",2)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0f); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0f); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Categories"); + ImGui::TableNextColumn(); + ImGui::Text("Systems"); + + ImGui::TableNextRow(); + + // CATEGORIES + ImGui::TableNextColumn(); + int index=0; + for (FurnaceGUISysCategory& i: sysCategories) { + if (ImGui::Selectable(i.name,newSongCategory==index,ImGuiSelectableFlags_DontClosePopups)) { \ + newSongCategory=index; + } + index++; + } + + // SYSTEMS + ImGui::TableNextColumn(); + if (ImGui::BeginTable("Systems",1,ImGuiTableFlags_BordersInnerV|ImGuiTableFlags_ScrollY)) { + for (FurnaceGUISysDef& i: sysCategories[newSongCategory].systems) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(i.name,false,ImGuiSelectableFlags_DontClosePopups)) { + nextDesc=i.definition.data(); + accepted=true; + } + } + ImGui::EndTable(); + } + + ImGui::EndTable(); + } + + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + + if (accepted) { + e->createNew(nextDesc); + undoHist.clear(); + redoHist.clear(); + curFileName=""; + modified=false; + curNibble=false; + orderNibble=false; + orderCursor=-1; + samplePos=0; + updateSampleTex=true; + selStart=SelectionPoint(); + selEnd=SelectionPoint(); + cursor=SelectionPoint(); + updateWindowTitle(); + ImGui::CloseCurrentPopup(); + } +} \ No newline at end of file diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index 94191ec04..945f551d7 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -39,7 +39,10 @@ void FurnaceGUI::drawOrders() { for (int i=0; igetTotalChannelCount(); i++) { if (e->song.chanShow[i]) displayChans++; } - if (ImGui::BeginTable("OrdersTable",1+displayChans,ImGuiTableFlags_SizingStretchSame|ImGuiTableFlags_ScrollX|ImGuiTableFlags_ScrollY)) { + ImGui::PushFont(patFont); + bool tooSmall=((displayChans+1)>((ImGui::GetContentRegionAvail().x)/(ImGui::CalcTextSize("AA").x+2.0*ImGui::GetStyle().ItemInnerSpacing.x))); + ImGui::PopFont(); + if (ImGui::BeginTable("OrdersTable",1+displayChans,(tooSmall?ImGuiTableFlags_SizingFixedFit:ImGuiTableFlags_SizingStretchSame)|ImGuiTableFlags_ScrollX|ImGuiTableFlags_ScrollY)) { ImGui::PushFont(patFont); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,prevSpacing); ImGui::TableSetupScrollFreeze(1,1); @@ -52,7 +55,7 @@ void FurnaceGUI::drawOrders() { } ImGui::TableNextRow(0,lineHeight); ImGui::TableNextColumn(); - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ROW_INDEX]); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); for (int i=0; igetTotalChannelCount(); i++) { if (!e->song.chanShow[i]) continue; ImGui::TableNextColumn(); @@ -61,9 +64,9 @@ void FurnaceGUI::drawOrders() { ImGui::PopStyleColor(); for (int i=0; isong.ordersLen; i++) { ImGui::TableNextRow(0,lineHeight); - if (oldOrder1==i) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,0x40ffffff); + if (oldOrder1==i) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_ORDER_ACTIVE])); ImGui::TableNextColumn(); - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ROW_INDEX]); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); bool highlightLoop=(i>=loopOrder && i<=loopEnd); if (highlightLoop) ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,ImGui::GetColorU32(uiColors[GUI_COLOR_SONG_LOOP])); if (settings.orderRowsBase==1) { @@ -75,6 +78,10 @@ void FurnaceGUI::drawOrders() { e->setOrder(i); curNibble=false; orderCursor=-1; + + if (orderEditMode==0) { + handleUnimportant; + } } ImGui::PopStyleColor(); for (int j=0; jgetTotalChannelCount(); j++) { @@ -86,17 +93,21 @@ void FurnaceGUI::drawOrders() { } else {*/ snprintf(selID,4096,"%.2X##O_%.2x_%.2x",e->song.orders.ord[j][i],j,i); //} + + ImGui::PushStyleColor(ImGuiCol_Text,(curOrder==i || e->song.orders.ord[j][i]==e->song.orders.ord[j][curOrder])?uiColors[GUI_COLOR_ORDER_SIMILAR]:uiColors[GUI_COLOR_ORDER_INACTIVE]); if (ImGui::Selectable(selID,(orderEditMode!=0 && curOrder==i && orderCursor==j))) { if (curOrder==i) { if (orderEditMode==0) { prepareUndo(GUI_UNDO_CHANGE_ORDER); - if (changeAllOrders) { - for (int k=0; kgetTotalChannelCount(); k++) { - if (e->song.orders.ord[k][i]<0x7f) e->song.orders.ord[k][i]++; + e->lockSave([this,i,j]() { + if (changeAllOrders) { + for (int k=0; kgetTotalChannelCount(); k++) { + if (e->song.orders.ord[k][i]<0xff) e->song.orders.ord[k][i]++; + } + } else { + if (e->song.orders.ord[j][i]<0xff) e->song.orders.ord[j][i]++; } - } else { - if (e->song.orders.ord[j][i]<0x7f) e->song.orders.ord[j][i]++; - } + }); e->walkSong(loopOrder,loopRow,loopEnd); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { @@ -111,7 +122,12 @@ void FurnaceGUI::drawOrders() { curNibble=false; } } + + if (orderEditMode==0) { + handleUnimportant; + } } + ImGui::PopStyleColor(); if (!pat->name.empty() && ImGui::IsItemHovered()) { ImGui::SetTooltip("%s",pat->name.c_str()); } @@ -119,13 +135,15 @@ void FurnaceGUI::drawOrders() { if (curOrder==i) { if (orderEditMode==0) { prepareUndo(GUI_UNDO_CHANGE_ORDER); - if (changeAllOrders) { - for (int k=0; kgetTotalChannelCount(); k++) { - if (e->song.orders.ord[k][i]>0) e->song.orders.ord[k][i]--; + e->lockSave([this,i,j]() { + if (changeAllOrders) { + for (int k=0; kgetTotalChannelCount(); k++) { + if (e->song.orders.ord[k][i]>0) e->song.orders.ord[k][i]--; + } + } else { + if (e->song.orders.ord[j][i]>0) e->song.orders.ord[j][i]--; } - } else { - if (e->song.orders.ord[j][i]>0) e->song.orders.ord[j][i]--; - } + }); e->walkSong(loopOrder,loopRow,loopEnd); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { @@ -148,21 +166,21 @@ void FurnaceGUI::drawOrders() { ImGui::EndTable(); } ImGui::NextColumn(); - if (ImGui::Button(ICON_FA_PLUS)) { + if (ImGui::Button(ICON_FA_PLUS)) { handleUnimportant // add order row (new) doAction(GUI_ACTION_ORDERS_ADD); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Add new order"); } - if (ImGui::Button(ICON_FA_MINUS)) { + if (ImGui::Button(ICON_FA_MINUS)) { handleUnimportant // remove this order row doAction(GUI_ACTION_ORDERS_REMOVE); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Remove order"); - } - if (ImGui::Button(ICON_FA_FILES_O)) { + } + if (ImGui::Button(ICON_FA_FILES_O)) { handleUnimportant // duplicate order row doAction(GUI_ACTION_ORDERS_DUPLICATE); } @@ -172,21 +190,21 @@ void FurnaceGUI::drawOrders() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Duplicate order (right-click to deep clone)"); } - if (ImGui::Button(ICON_FA_ANGLE_UP)) { + if (ImGui::Button(ICON_FA_ANGLE_UP)) { handleUnimportant // move order row up doAction(GUI_ACTION_ORDERS_MOVE_UP); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Move order up"); } - if (ImGui::Button(ICON_FA_ANGLE_DOWN)) { + if (ImGui::Button(ICON_FA_ANGLE_DOWN)) { handleUnimportant // move order row down doAction(GUI_ACTION_ORDERS_MOVE_DOWN); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Move order down"); } - if (ImGui::Button(ICON_FA_ANGLE_DOUBLE_DOWN)) { + if (ImGui::Button(ICON_FA_ANGLE_DOUBLE_DOWN)) { handleUnimportant // duplicate order row at end doAction(GUI_ACTION_ORDERS_DUPLICATE_END); } @@ -196,7 +214,7 @@ void FurnaceGUI::drawOrders() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Duplicate order at end of song (right-click to deep clone)"); } - if (ImGui::Button(changeAllOrders?ICON_FA_LINK"##ChangeAll":ICON_FA_CHAIN_BROKEN"##ChangeAll")) { + if (ImGui::Button(changeAllOrders?ICON_FA_LINK"##ChangeAll":ICON_FA_CHAIN_BROKEN"##ChangeAll")) { handleUnimportant // whether to change one or all orders in a row changeAllOrders=!changeAllOrders; } @@ -217,7 +235,7 @@ void FurnaceGUI::drawOrders() { } else { orderEditModeLabel=ICON_FA_MOUSE_POINTER "##OrderEditMode"; } - if (ImGui::Button(orderEditModeLabel)) { + if (ImGui::Button(orderEditModeLabel)) { handleUnimportant orderEditMode++; if (orderEditMode>3) orderEditMode=0; curNibble=false; @@ -238,4 +256,4 @@ void FurnaceGUI::drawOrders() { if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_ORDERS; oldOrder1=e->getOrder(); ImGui::End(); -} \ No newline at end of file +} diff --git a/src/gui/osc.cpp b/src/gui/osc.cpp new file mode 100644 index 000000000..c0ce978cf --- /dev/null +++ b/src/gui/osc.cpp @@ -0,0 +1,184 @@ +/** + * 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 "imgui_internal.h" +#include +#include + +void FurnaceGUI::readOsc() { + int writePos=e->oscWritePos; + int readPos=e->oscReadPos; + int avail=0; + int total=0; + if (writePos>=readPos) { + avail=writePos-readPos; + } else { + avail=writePos-readPos+32768; + } + if (oscTotal==0) { + oscTotal=ImGui::GetIO().DeltaTime*e->getAudioDescGot().rate; + } else { + oscTotal=(oscTotal+(int)round(ImGui::GetIO().DeltaTime*e->getAudioDescGot().rate))>>1; + } + int bias=avail-oscTotal-e->getAudioDescGot().bufsize; + if (bias<0) bias=0; + total=oscTotal+(bias>>6); + if (total>avail) total=avail; + //printf("total: %d. avail: %d bias: %d\n",total,avail,bias); + for (int i=0; i<512; i++) { + int pos=(readPos+(i*total/512))&0x7fff; + oscValues[i]=(e->oscBuf[0][pos]+e->oscBuf[1][pos])*0.5f; + } + + float peakDecay=0.05f*60.0f*ImGui::GetIO().DeltaTime; + for (int i=0; i<2; i++) { + peak[i]*=1.0-peakDecay; + if (peak[i]<0.0001) peak[i]=0.0; + float newPeak=peak[i]; + for (int j=0; joscBuf[i][pos])>newPeak) { + newPeak=fabs(e->oscBuf[i][pos]); + } + } + peak[i]+=(newPeak-peak[i])*0.9; + } + + readPos=(readPos+total)&0x7fff; + e->oscReadPos=readPos; +} + +void FurnaceGUI::drawOsc() { + if (nextWindow==GUI_WINDOW_OSCILLOSCOPE) { + oscOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!oscOpen) return; + ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (settings.oscTakesEntireWindow) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing,ImVec2(0,0)); + } + if (ImGui::Begin("Oscilloscope",&oscOpen)) { + if (oscZoomSlider) { + if (ImGui::VSliderFloat("##OscZoom",ImVec2(20.0f*dpiScale,ImGui::GetContentRegionAvail().y),&oscZoom,0.5,2.0)) { + if (oscZoom<0.5) oscZoom=0.5; + if (oscZoom>2.0) oscZoom=2.0; + } + ImGui::SameLine(); + } + + ImDrawList* dl=ImGui::GetWindowDrawList(); + ImGuiWindow* window=ImGui::GetCurrentWindow(); + + ImVec2 waveform[512]; + ImVec2 size=ImGui::GetContentRegionAvail(); + + ImVec2 minArea=window->DC.CursorPos; + ImVec2 maxArea=ImVec2( + minArea.x+size.x, + minArea.y+size.y + ); + ImRect rect=ImRect(minArea,maxArea); + ImRect inRect=rect; + inRect.Min.x+=dpiScale; + inRect.Min.y+=dpiScale; + inRect.Max.x-=dpiScale; + inRect.Max.y-=dpiScale; + ImGuiStyle& style=ImGui::GetStyle(); + ImU32 color=ImGui::GetColorU32(isClipping?uiColors[GUI_COLOR_OSC_WAVE_PEAK]:uiColors[GUI_COLOR_OSC_WAVE]); + ImU32 borderColor=ImGui::GetColorU32(uiColors[GUI_COLOR_OSC_BORDER]); + ImU32 refColor=ImGui::GetColorU32(uiColors[GUI_COLOR_OSC_REF]); + ImGui::ItemSize(size,style.FramePadding.y); + if (ImGui::ItemAdd(rect,ImGui::GetID("wsDisplay"))) { + // https://github.com/ocornut/imgui/issues/3710 + const int v0 = dl->VtxBuffer.Size; + dl->AddRectFilled(inRect.Min,inRect.Max,0xffffffff,settings.oscRoundedCorners?(8.0f*dpiScale):0.0f); + const int v1 = dl->VtxBuffer.Size; + + for (int i=v0; iVtxBuffer.Data[i]; + ImVec4 col0=uiColors[GUI_COLOR_OSC_BG1]; + ImVec4 col1=uiColors[GUI_COLOR_OSC_BG3]; + ImVec4 col2=uiColors[GUI_COLOR_OSC_BG2]; + ImVec4 col3=uiColors[GUI_COLOR_OSC_BG4]; + + float shadeX=(v->pos.x-rect.Min.x)/(rect.Max.x-rect.Min.x); + float shadeY=(v->pos.y-rect.Min.y)/(rect.Max.y-rect.Min.y); + if (shadeX<0.0f) shadeX=0.0f; + if (shadeX>1.0f) shadeX=1.0f; + if (shadeY<0.0f) shadeY=0.0f; + if (shadeY>1.0f) shadeY=1.0f; + + col0.x+=(col2.x-col0.x)*shadeX; + col0.y+=(col2.y-col0.y)*shadeX; + col0.z+=(col2.z-col0.z)*shadeX; + col0.w+=(col2.w-col0.w)*shadeX; + + col1.x+=(col3.x-col1.x)*shadeX; + col1.y+=(col3.y-col1.y)*shadeX; + col1.z+=(col3.z-col1.z)*shadeX; + col1.w+=(col3.w-col1.w)*shadeX; + + col0.x+=(col1.x-col0.x)*shadeY; + col0.y+=(col1.y-col0.y)*shadeY; + col0.z+=(col1.z-col0.z)*shadeY; + col0.w+=(col1.w-col0.w)*shadeY; + + ImVec4 conv=ImGui::ColorConvertU32ToFloat4(v->col); + col0.x*=conv.x; + col0.y*=conv.y; + col0.z*=conv.z; + col0.w*=conv.w; + + v->col=ImGui::ColorConvertFloat4ToU32(col0); + } + + dl->AddLine( + ImLerp(rect.Min,rect.Max,ImVec2(0.0f,0.5f)), + ImLerp(rect.Min,rect.Max,ImVec2(0.0f,0.5f)), + refColor, + dpiScale + ); + + for (size_t i=0; i<512; i++) { + float x=(float)i/512.0f; + float y=oscValues[i]*oscZoom; + if (y<-0.5f) y=-0.5f; + if (y>0.5f) y=0.5f; + waveform[i]=ImLerp(rect.Min,rect.Max,ImVec2(x,0.5f-y)); + } + dl->AddPolyline(waveform,512,color,ImDrawFlags_None,dpiScale); + if (settings.oscBorder) { + dl->AddRect(inRect.Min,inRect.Max,borderColor,settings.oscRoundedCorners?(8.0f*dpiScale):0.0f,0,1.5f*dpiScale); + } + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + oscZoomSlider=!oscZoomSlider; + } + } + if (settings.oscTakesEntireWindow) { + ImGui::PopStyleVar(3); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_OSCILLOSCOPE; + ImGui::End(); +} diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index c6c78ba8d..0decef970 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -17,9 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include #define _USE_MATH_DEFINES #include "gui.h" +#include "../ta-log.h" #include "imgui_internal.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" @@ -45,7 +45,7 @@ const FurnaceGUIColors fxColors[16]={ GUI_COLOR_PATTERN_EFFECT_SPEED, // 0F }; -const FurnaceGUIColors extFxColors[16]={ +const FurnaceGUIColors extFxColors[32]={ GUI_COLOR_PATTERN_EFFECT_MISC, // E0 GUI_COLOR_PATTERN_EFFECT_PITCH, // E1 GUI_COLOR_PATTERN_EFFECT_PITCH, // E2 @@ -62,6 +62,22 @@ const FurnaceGUIColors extFxColors[16]={ GUI_COLOR_PATTERN_EFFECT_TIME, // ED GUI_COLOR_PATTERN_EFFECT_SONG, // EE GUI_COLOR_PATTERN_EFFECT_SONG, // EF + GUI_COLOR_PATTERN_EFFECT_SPEED, // F0 + GUI_COLOR_PATTERN_EFFECT_PITCH, // F1 + GUI_COLOR_PATTERN_EFFECT_PITCH, // F2 + GUI_COLOR_PATTERN_EFFECT_VOLUME, // F3 + GUI_COLOR_PATTERN_EFFECT_VOLUME, // F4 + GUI_COLOR_PATTERN_EFFECT_INVALID, // F5 + GUI_COLOR_PATTERN_EFFECT_INVALID, // F6 + GUI_COLOR_PATTERN_EFFECT_INVALID, // F7 + GUI_COLOR_PATTERN_EFFECT_VOLUME, // F8 + GUI_COLOR_PATTERN_EFFECT_VOLUME, // F9 + GUI_COLOR_PATTERN_EFFECT_VOLUME, // FA + GUI_COLOR_PATTERN_EFFECT_INVALID, // FB + GUI_COLOR_PATTERN_EFFECT_INVALID, // FC + GUI_COLOR_PATTERN_EFFECT_INVALID, // FD + GUI_COLOR_PATTERN_EFFECT_INVALID, // FE + GUI_COLOR_PATTERN_EFFECT_SONG, // FF }; inline float randRange(float min, float max) { @@ -69,7 +85,7 @@ inline float randRange(float min, float max) { } // draw a pattern row -inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord) { +inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord, const DivPattern** patCache) { static char id[32]; bool selectedRow=(i>=sel1.y && i<=sel2.y); ImGui::TableNextRow(0,lineHeight); @@ -87,12 +103,24 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int return; } bool isPushing=false; + ImVec4 activeColor=uiColors[GUI_COLOR_PATTERN_ACTIVE]; + ImVec4 inactiveColor=uiColors[GUI_COLOR_PATTERN_INACTIVE]; + ImVec4 rowIndexColor=uiColors[GUI_COLOR_PATTERN_ROW_INDEX]; + if (e->song.hilightB>0 && !(i%e->song.hilightB)) { + activeColor=uiColors[GUI_COLOR_PATTERN_ACTIVE_HI2]; + inactiveColor=uiColors[GUI_COLOR_PATTERN_INACTIVE_HI2]; + rowIndexColor=uiColors[GUI_COLOR_PATTERN_ROW_INDEX_HI2]; + } else if (e->song.hilightA>0 && !(i%e->song.hilightA)) { + activeColor=uiColors[GUI_COLOR_PATTERN_ACTIVE_HI1]; + inactiveColor=uiColors[GUI_COLOR_PATTERN_INACTIVE_HI1]; + rowIndexColor=uiColors[GUI_COLOR_PATTERN_ROW_INDEX_HI1]; + } // check overflow highlight if (settings.overflowHighlight) { if (edit && cursor.y==i) { ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_EDITING])); } else if (isPlaying && oldRow==i) { - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,0x40ffffff); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_PLAY_HEAD])); } else if (e->song.hilightB>0 && !(i%e->song.hilightB)) { ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_HI_2])); } else if (e->song.hilightA>0 && !(i%e->song.hilightA)) { @@ -103,7 +131,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int if (edit && cursor.y==i) { ImGui::PushStyleColor(ImGuiCol_Header,ImGui::GetColorU32(uiColors[GUI_COLOR_EDITING])); } else if (isPlaying && oldRow==i) { - ImGui::PushStyleColor(ImGuiCol_Header,0x40ffffff); + ImGui::PushStyleColor(ImGuiCol_Header,ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_PLAY_HEAD])); } else if (e->song.hilightB>0 && !(i%e->song.hilightB)) { ImGui::PushStyleColor(ImGuiCol_Header,ImGui::GetColorU32(uiColors[GUI_COLOR_PATTERN_HI_2])); } else if (e->song.hilightA>0 && !(i%e->song.hilightA)) { @@ -114,9 +142,9 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int } // row number if (settings.patRowsBase==1) { - ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX]," %.2X ",i); + ImGui::TextColored(rowIndexColor," %.2X ",i); } else { - ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX],"%3d ",i); + ImGui::TextColored(rowIndexColor,"%3d ",i); } // for each column for (int j=0; jgetMaxVolumeChan(j); if (chanVolMax<1) chanVolMax=1; - DivPattern* pat=e->song.pat[j].getPattern(e->song.orders.ord[j][ord],true); + const DivPattern* pat=patCache[j]; ImGui::TableNextColumn(); patChanX[j]=ImGui::GetCursorPosX(); @@ -142,14 +170,12 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int bool cursorIns=(cursor.y==i && cursor.xCoarse==j && cursor.xFine==1); bool cursorVol=(cursor.y==i && cursor.xCoarse==j && cursor.xFine==2); - - // note sprintf(id,"%s##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,uiColors[GUI_COLOR_PATTERN_INACTIVE]); + ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ACTIVE]); + ImGui::PushStyleColor(ImGuiCol_Text,activeColor); } if (cursorNote) { ImGui::PushStyleColor(ImGuiCol_Header,uiColors[GUI_COLOR_PATTERN_CURSOR]); @@ -175,10 +201,19 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int if (!e->song.chanCollapse[j]) { // instrument if (pat->data[i][2]==-1) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INACTIVE]); + ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); sprintf(id,"..##PI_%d_%d",i,j); } else { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); + if (pat->data[i][2]<0 || pat->data[i][2]>=e->song.insLen) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS_ERROR]); + } else { + DivInstrumentType t=e->song.ins[pat->data[i][2]]->type; + if (t!=DIV_INS_AMIGA && t!=e->getPreferInsType(j)) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS_WARN]); + } else { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); + } + } sprintf(id,"%.2X##PI_%d_%d",pat->data[i][2],i,j); } ImGui::SameLine(0.0f,0.0f); @@ -205,7 +240,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int // volume if (pat->data[i][3]==-1) { sprintf(id,"..##PV_%d_%d",i,j); - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INACTIVE]); + ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { int volColor=(pat->data[i][3]*127)/chanVolMax; if (volColor>127) volColor=127; @@ -245,27 +280,35 @@ 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); - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INACTIVE]); + ImGui::PushStyleColor(ImGuiCol_Text,inactiveColor); } else { - sprintf(id,"%.2X##PE%d_%d_%d",pat->data[i][index],k,i,j); - if (pat->data[i][index]<0x10) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[fxColors[pat->data[i][index]]]); - } else if (pat->data[i][index]<0x20) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]); - } else if (pat->data[i][index]<0x30) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY]); - } else if (pat->data[i][index]<0x48) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]); - } else if (pat->data[i][index]<0xc0) { + if (pat->data[i][index]>0xff) { + sprintf(id,"??##PE%d_%d_%d",k,i,j); ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); - } else if (pat->data[i][index]<0xd0) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SPEED]); - } else if (pat->data[i][index]<0xe0) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); - } else if (pat->data[i][index]<0xf0) { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[extFxColors[pat->data[i][index]-0xe0]]); } else { - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); + const unsigned char data=pat->data[i][index]; + sprintf(id,"%.2X##PE%d_%d_%d",data,k,i,j); + if (data<0x10) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[fxColors[data]]); + } else if (data<0x20) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]); + } else if (data<0x30) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY]); + } else if (data<0x48) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]); + } else if (data<0x90) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); + } else if (data<0xa0) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_MISC]); + } else if (data<0xc0) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); + } else if (data<0xd0) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_SPEED]); + } else if (data<0xe0) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_INVALID]); + } else { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[extFxColors[data-0xe0]]); + } } } ImGui::SameLine(0.0f,0.0f); @@ -325,6 +368,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int } void FurnaceGUI::drawPattern() { + //int delta0=SDL_GetPerformanceCounter(); if (nextWindow==GUI_WINDOW_PATTERN) { patternOpen=true; ImGui::SetNextWindowFocus(); @@ -332,6 +376,7 @@ void FurnaceGUI::drawPattern() { } if (!patternOpen) return; + bool inhibitMenu=false; float scrollX=0; if (e->isPlaying() && followPattern) cursor.y=oldRow; @@ -357,7 +402,7 @@ void FurnaceGUI::drawPattern() { sel2.xFine^=sel1.xFine; } ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0.0f,0.0f)); - if (ImGui::Begin("Pattern",&patternOpen)) { + if (ImGui::Begin("Pattern",&patternOpen,settings.avoidRaisingPattern?ImGuiWindowFlags_NoBringToFrontOnFocus:0)) { //ImGui::SetWindowSize(ImVec2(scrW*dpiScale,scrH*dpiScale)); patWindowPos=ImGui::GetWindowPos(); patWindowSize=ImGui::GetWindowSize(); @@ -367,6 +412,7 @@ void FurnaceGUI::drawPattern() { oldOrder=e->getOrder(); int chans=e->getTotalChannelCount(); int displayChans=0; + const DivPattern* patCache[DIV_MAX_CHANS]; for (int i=0; isong.chanShow[i]) displayChans++; } @@ -411,6 +457,7 @@ void FurnaceGUI::drawPattern() { } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { fancyPattern=!fancyPattern; + inhibitMenu=true; e->enableCommandStream(fancyPattern); e->getCommandStream(cmdStream); cmdStream.clear(); @@ -448,10 +495,16 @@ void FurnaceGUI::drawPattern() { keyHit[i]=0.2; e->keyHit[i]=false; } - chanHead.x*=0.25+keyHit[i]; chanHead.y*=0.25+keyHit[i]; chanHead.z*=0.25+keyHit[i]; - chanHeadActive.x*=0.8; chanHeadActive.y*=0.8; chanHeadActive.z*=0.8; - chanHeadHover.x*=0.4+keyHit[i]; chanHeadHover.y*=0.4+keyHit[i]; chanHeadHover.z*=0.4+keyHit[i]; - keyHit[i]-=0.02; + if (settings.guiColorsBase) { + chanHead.x*=1.0-keyHit[i]; chanHead.y*=1.0-keyHit[i]; chanHead.z*=1.0-keyHit[i]; + chanHeadActive.x*=0.5; chanHeadActive.y*=0.5; chanHeadActive.z*=0.5; + chanHeadHover.x*=0.9-keyHit[i]; chanHeadHover.y*=0.9-keyHit[i]; chanHeadHover.z*=0.9-keyHit[i]; + } else { + chanHead.x*=0.25+keyHit[i]; chanHead.y*=0.25+keyHit[i]; chanHead.z*=0.25+keyHit[i]; + chanHeadActive.x*=0.8; chanHeadActive.y*=0.8; chanHeadActive.z*=0.8; + chanHeadHover.x*=0.4+keyHit[i]; chanHeadHover.y*=0.4+keyHit[i]; chanHeadHover.z*=0.4+keyHit[i]; + } + keyHit[i]-=0.02*60.0*ImGui::GetIO().DeltaTime; if (keyHit[i]<0) keyHit[i]=0; ImGui::PushStyleColor(ImGuiCol_Header,chanHead); ImGui::PushStyleColor(ImGuiCol_HeaderActive,chanHeadActive); @@ -475,6 +528,7 @@ void FurnaceGUI::drawPattern() { if (muted) ImGui::PopStyleColor(); ImGui::PopStyleColor(3); if (settings.soloAction!=2) if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + inhibitMenu=true; e->toggleSolo(i); } if (extraChannelButtons==2) { @@ -544,8 +598,11 @@ void FurnaceGUI::drawPattern() { // previous pattern ImGui::BeginDisabled(); if (settings.viewPrevPattern) { + if ((ord-1)>=0) for (int i=0; isong.pat[i].getPattern(e->song.orders.ord[i][ord-1],true); + } for (int i=0; isong.patLen+i-dummyRows+1,e->isPlaying(),lineHeight,chans,ord-1); + patternRow(e->song.patLen+i-dummyRows+1,e->isPlaying(),lineHeight,chans,ord-1,patCache); } } else { for (int i=0; isong.pat[i].getPattern(e->song.orders.ord[i][ord],true); + } for (int i=0; isong.patLen; i++) { - patternRow(i,e->isPlaying(),lineHeight,chans,ord); + patternRow(i,e->isPlaying(),lineHeight,chans,ord,patCache); } // next pattern ImGui::BeginDisabled(); if (settings.viewPrevPattern) { + if ((ord+1)song.ordersLen) for (int i=0; isong.pat[i].getPattern(e->song.orders.ord[i][ord+1],true); + } for (int i=0; i<=dummyRows; i++) { - patternRow(i,e->isPlaying(),lineHeight,chans,ord+1); + patternRow(i,e->isPlaying(),lineHeight,chans,ord+1,patCache); } } else { for (int i=0; i<=dummyRows; i++) { @@ -717,6 +780,8 @@ void FurnaceGUI::drawPattern() { } } + float frameTime=ImGui::GetIO().DeltaTime*60.0f; + // note slides ImVec2 arrowPoints[7]; if (e->isPlaying()) for (int i=0; iAddPolyline(arrowPoints,7,ImGui::GetColorU32(col),ImDrawFlags_None,5.0f*dpiScale); } } - patChanSlideY[i]+=((ch->portaNote<=ch->note)?-8:8)*dpiScale; + patChanSlideY[i]+=((ch->portaNote<=ch->note)?-8:8)*dpiScale*frameTime; if (width>0) { if (patChanSlideY[i]<0) { patChanSlideY[i]=-fmod(-patChanSlideY[i],width*0.7); @@ -778,7 +843,7 @@ void FurnaceGUI::drawPattern() { ImDrawList* fdl=ImGui::GetForegroundDrawList(); for (size_t i=0; i255) part.life=255; fdl->AddText( iconFont, @@ -799,7 +864,16 @@ void FurnaceGUI::drawPattern() { ImGui::PopFont(); } ImGui::PopStyleVar(); + if (patternOpen) { + if (!inhibitMenu && ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("patternActionMenu"); + if (ImGui::BeginPopup("patternActionMenu",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { + editOptions(false); + ImGui::EndPopup(); + } + } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_PATTERN; ImGui::End(); + //int delta1=SDL_GetPerformanceCounter(); + //logV("render time: %dµs",(delta1-delta0)/(SDL_GetPerformanceFrequency()/1000000)); } diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp new file mode 100644 index 000000000..d3a3d35ef --- /dev/null +++ b/src/gui/piano.cpp @@ -0,0 +1,46 @@ +/** + * 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 "guiConst.h" + +void FurnaceGUI::drawPiano() { + if (nextWindow==GUI_WINDOW_PIANO) { + pianoOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!pianoOpen) return; + if (ImGui::Begin("Piano",&pianoOpen)) { + for (int i=0; igetTotalChannelCount(); i++) { + DivChannelState* cs=e->getChanState(i); + if (cs->keyOn) { + const char* noteName=NULL; + if (cs->note<-60 || cs->note>120) { + noteName="???"; + } else { + noteName=noteNames[cs->note+60]; + } + ImGui::Text("%d: %s",i,noteName); + } + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_PIANO; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/plot_nolerp.cpp b/src/gui/plot_nolerp.cpp index 1c1cf94ad..c8970c953 100644 --- a/src/gui/plot_nolerp.cpp +++ b/src/gui/plot_nolerp.cpp @@ -332,6 +332,8 @@ int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_gett scale_max = v_max; } + if (blockMode) scale_max+=1.0f; + ImU32 bgColor=ImGui::GetColorU32(ImVec4(color.x,color.y,color.z,color.w*0.15)); ImGui::RenderFrame(frame_bb.Min, frame_bb.Max, ImGui::GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); @@ -372,7 +374,7 @@ int PlotCustomEx(ImGuiPlotType plot_type, const char* label, float (*values_gett float v0 = values_getter(data, (0) % values_count); float t0 = 0.0f; ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle - float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + (blockMode?(scale_min-0.5):scale_min) * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands const ImU32 col_base = ImGui::GetColorU32(color); const ImU32 col_hovered = ImGui::GetColorU32(color); diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp new file mode 100644 index 000000000..e7b08081c --- /dev/null +++ b/src/gui/presets.cpp @@ -0,0 +1,755 @@ +/** + * 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" + +// add system configurations here. +// every entry is written in the following format: +// cat.systems.push_back(FurnaceGUISysDef( +// "System Name", { +// DIV_SYSTEM_???, Volume, Panning, Flags, +// DIV_SYSTEM_???, Volume, Panning, Flags, +// ... +// 0 +// } +// )); + +void FurnaceGUI::initSystemPresets() { + FurnaceGUISysCategory cat; + + cat=FurnaceGUISysCategory("FM"); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2612", { + DIV_SYSTEM_YM2612, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2612 (extended channel 3)", { + DIV_SYSTEM_YM2612_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2151", { + DIV_SYSTEM_YM2151, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2610", { + DIV_SYSTEM_YM2610_FULL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2610 (extended channel 2)", { + DIV_SYSTEM_YM2610_FULL_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2610B", { + DIV_SYSTEM_YM2610B, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2610B (extended channel 3)", { + DIV_SYSTEM_YM2610B_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2413", { + DIV_SYSTEM_OPLL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM2413 (drums mode)", { + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM3526", { + DIV_SYSTEM_OPL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM3526 (drums mode)", { + DIV_SYSTEM_OPL_DRUMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM3812", { + DIV_SYSTEM_OPL2, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YM3812 (drums mode)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YMF262", { + DIV_SYSTEM_OPL3, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YMF262 (drums mode)", { + DIV_SYSTEM_OPL3_DRUMS, 64, 0, 0, + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("Square"); + cat.systems.push_back(FurnaceGUISysDef( + "TI SN76489", { + DIV_SYSTEM_SMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "AY-3-8910", { + DIV_SYSTEM_AY8910, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Philips SAA1099", { + DIV_SYSTEM_SAA1099, 64, 0, 0, + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("Sample"); + cat.systems.push_back(FurnaceGUISysDef( + "Amiga", { + DIV_SYSTEM_AMIGA, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "SegaPCM", { + DIV_SYSTEM_SEGAPCM, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Capcom QSound", { + DIV_SYSTEM_QSOUND, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta/Allumer X1-010", { + DIV_SYSTEM_X1_010, 64, 0, 0, + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("Game consoles"); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Genesis", { + DIV_SYSTEM_YM2612, 64, 0, 0, + DIV_SYSTEM_SMS, 24, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Genesis (extended channel 3)", { + DIV_SYSTEM_YM2612_EXT, 64, 0, 0, + DIV_SYSTEM_SMS, 24, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Master System", { + DIV_SYSTEM_SMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Master System (with FM expansion)", { + DIV_SYSTEM_SMS, 64, 0, 0, + DIV_SYSTEM_OPLL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Master System (with FM expansion in drums mode)", { + DIV_SYSTEM_SMS, 64, 0, 0, + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Game Boy", { + DIV_SYSTEM_GB, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NEC PC Engine/TurboGrafx-16", { + DIV_SYSTEM_PCE, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES", { + DIV_SYSTEM_NES, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Konami VRC6", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_VRC6, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Konami VRC7", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_VRC7, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with MMC5", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_MMC5, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Sunsoft 5B", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 38, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Namco 163", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_N163, 64, 0, 112, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Family Noraebang", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_OPLL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Family Noraebang (drums mode)", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Famicom Disk System", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_FDS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Mattel Intellivision", { + DIV_SYSTEM_AY8910, 64, 0, 48, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Vectrex", { + DIV_SYSTEM_AY8910, 64, 0, 4, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo AES", { + DIV_SYSTEM_YM2610_FULL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo AES (extended channel 2)", { + DIV_SYSTEM_YM2610_FULL_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Atari 2600/7800", { + DIV_SYSTEM_TIA, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Atari Lynx", { + DIV_SYSTEM_LYNX, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "WonderSwan", { + DIV_SYSTEM_SWAN, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Gamate", { + DIV_SYSTEM_AY8910, 64, 0, 73, + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("Computers"); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore PET", { + DIV_SYSTEM_PET, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore VIC-20", { + DIV_SYSTEM_VIC20, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID)", { + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID)", { + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID + Sound Expander)", { + DIV_SYSTEM_OPL, 64, 0, 0, + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID + Sound Expander with drums mode)", { + DIV_SYSTEM_OPL_DRUMS, 64, 0, 0, + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID + Sound Expander)", { + DIV_SYSTEM_OPL, 64, 0, 0, + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID + Sound Expander with drums mode)", { + DIV_SYSTEM_OPL_DRUMS, 64, 0, 0, + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Amiga", { + DIV_SYSTEM_AMIGA, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX", { + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + SFG-01", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + MSX-MUSIC", { + DIV_SYSTEM_OPLL, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "MSX + MSX-MUSIC (drums mode)", { + DIV_SYSTEM_OPLL_DRUMS, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 16, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "ZX Spectrum (48K)", { + DIV_SYSTEM_AY8910, 64, 0, 2, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "ZX Spectrum (128K)", { + DIV_SYSTEM_AY8910, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Amstrad CPC", { + DIV_SYSTEM_AY8910, 64, 0, 5, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "SAM Coupé", { + DIV_SYSTEM_SAA1099, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "BBC Micro", { + DIV_SYSTEM_SMS, 64, 0, 6, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC (barebones)", { + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Covox Sound Master", { + DIV_SYSTEM_AY8930, 64, 0, 3, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + SSI 2001", { + DIV_SYSTEM_C64_6581, 64, 0, 2, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Game Blaster", { + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + AdLib/Sound Blaster", { + DIV_SYSTEM_OPL2, 64, 0, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + AdLib/Sound Blaster (drums mode)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster w/Game Blaster Compatible", { + DIV_SYSTEM_OPL2, 64, 0, 0, + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster w/Game Blaster Compatible (drums mode)", { + DIV_SYSTEM_OPL2_DRUMS, 64, 0, 0, + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_SAA1099, 64, 0, 1, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster Pro", { + DIV_SYSTEM_OPL2, 64, -127, 0, + DIV_SYSTEM_OPL2, 64, 127, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster Pro (drums mode)", { + DIV_SYSTEM_OPL2_DRUMS, 64, -127, 0, + DIV_SYSTEM_OPL2_DRUMS, 64, 127, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster Pro 2", { + DIV_SYSTEM_OPL3, 64, 0, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + Sound Blaster Pro 2 (drums mode)", { + DIV_SYSTEM_OPL3_DRUMS, 64, 0, 0, + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + PC-FXGA", { + DIV_SYSTEM_PCE, 64, 0, 0, // HuC6230 (WSG from HuC6280 but with built in 2 OKI ADPCM playback engines) + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "PC + SAAYM", { + DIV_SYSTEM_YM2151, 64, 0, 0, // 3.58MHz or 4MHz selectable via jumper + DIV_SYSTEM_SAA1099, 64, 0, 1, // 7.16MHz or 8MHz selectable via jumper + DIV_SYSTEM_SAA1099, 64, 0, 1, // "" + DIV_SYSTEM_PCSPKR, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sharp X1", { + DIV_SYSTEM_AY8910, 64, 0, 3, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sharp X1 + FM Addon", { + DIV_SYSTEM_AY8910, 64, 0, 3, + DIV_SYSTEM_YM2151, 64, 0, 2, + 0 + } + )); + /* + cat.systems.push_back(FurnaceGUISysDef( + "Sharp X68000", { + DIV_SYSTEM_YM2151, 64, 0, 2, + DIV_SYSTEM_MSM6258, 64, 0, 0, + 0 + } + ));*/ + cat.systems.push_back(FurnaceGUISysDef( + "Commander X16", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_VERA, 64, 0, 0, + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("Arcade systems"); + cat.systems.push_back(FurnaceGUISysDef( + "Bally Midway MCR", { + DIV_SYSTEM_AY8910, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Kyugo", { + DIV_SYSTEM_AY8910, 64, 0, 4, + DIV_SYSTEM_AY8910, 64, 0, 4, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega OutRun/X Board", { + DIV_SYSTEM_YM2151, 64, 0, 2, + DIV_SYSTEM_SEGAPCM, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo MVS", { + DIV_SYSTEM_YM2610_FULL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo MVS (extended channel 2)", { + DIV_SYSTEM_YM2610_FULL_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Taito Arcade", { + DIV_SYSTEM_YM2610B, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Taito Arcade (extended channel 3)", { + DIV_SYSTEM_YM2610B_EXT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Capcom CPS-2 (QSound)", { + DIV_SYSTEM_QSOUND, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta 1", { + DIV_SYSTEM_X1_010, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta 1 + FM addon", { + DIV_SYSTEM_YM2612, 64, 0, 2, // Discrete YM3438 + DIV_SYSTEM_X1_010, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Seta 2", { + DIV_SYSTEM_X1_010, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Konami Bubble System", { + DIV_SYSTEM_AY8910, 64, 0, 0, + DIV_SYSTEM_AY8910, 64, 0, 0, + DIV_SYSTEM_BUBSYS_WSG, 64, 0, 0, + // VLM5030 exists but not used for music at all + 0 + } + )); + sysCategories.push_back(cat); + + cat=FurnaceGUISysCategory("DefleMask-compatible"); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Genesis", { + DIV_SYSTEM_YM2612, 64, 0, 0, + DIV_SYSTEM_SMS, 24, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Genesis (extended channel 3)", { + DIV_SYSTEM_YM2612_EXT, 64, 0, 0, + DIV_SYSTEM_SMS, 24, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Master System", { + DIV_SYSTEM_SMS, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Sega Master System (with FM expansion)", { + DIV_SYSTEM_SMS, 64, 0, 0, + DIV_SYSTEM_OPLL, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Game Boy", { + DIV_SYSTEM_GB, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NEC PC Engine/TurboGrafx-16", { + DIV_SYSTEM_PCE, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES", { + DIV_SYSTEM_NES, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Konami VRC7", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_VRC7, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (6581 SID)", { + DIV_SYSTEM_C64_6581, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Commodore 64 (8580 SID)", { + DIV_SYSTEM_C64_8580, 64, 0, 1, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Arcade (YM2151 and SegaPCM)", { + DIV_SYSTEM_YM2151, 64, 0, 0, + DIV_SYSTEM_SEGAPCM_COMPAT, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo CD", { + DIV_SYSTEM_YM2610, 64, 0, 0, + 0 + } + )); + cat.systems.push_back(FurnaceGUISysDef( + "Neo Geo CD (extended channel 2)", { + DIV_SYSTEM_YM2610_EXT, 64, 0, 0, + 0 + } + )); + sysCategories.push_back(cat); +} diff --git a/src/gui/regView.cpp b/src/gui/regView.cpp new file mode 100644 index 000000000..9755e4a83 --- /dev/null +++ b/src/gui/regView.cpp @@ -0,0 +1,71 @@ +/** + * 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" + +void FurnaceGUI::drawRegView() { + if (nextWindow==GUI_WINDOW_REGISTER_VIEW) { + channelsOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!regViewOpen) return; + if (ImGui::Begin("Register View",®ViewOpen)) { + for (int i=0; isong.systemLen; i++) { + ImGui::Text("%d. %s",i+1,getSystemName(e->song.system[i])); + int size=0; + int depth=8; + unsigned char* regPool=e->getRegisterPool(i,size,depth); + unsigned short* regPoolW=(unsigned short*)regPool; + if (regPool==NULL) { + ImGui::Text("- no register pool available"); + } else { + ImGui::PushFont(patFont); + if (ImGui::BeginTable("Memory",17)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + for (int i=0; i<16; i++) { + ImGui::TableNextColumn(); + ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX]," %X",i); + } + for (int i=0; i<=((size-1)>>4); i++) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(uiColors[GUI_COLOR_PATTERN_ROW_INDEX],"%.2X",i*16); + for (int j=0; j<16; j++) { + ImGui::TableNextColumn(); + if (i*16+j>=size) continue; + if (depth == 8) { + ImGui::Text("%.2x",regPool[i*16+j]); + } else if (depth == 16) { + ImGui::Text("%.4x",regPoolW[i*16+j]); + } else { + ImGui::Text("??"); + } + } + } + ImGui::EndTable(); + } + ImGui::PopFont(); + } + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REGISTER_VIEW; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp new file mode 100644 index 000000000..04af4cab3 --- /dev/null +++ b/src/gui/sampleEdit.cpp @@ -0,0 +1,873 @@ +/** + * 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 +#include +#include +#include "../ta-log.h" +#include "IconsFontAwesome4.h" +#include "misc/cpp/imgui_stdlib.h" +#include +#include "guiConst.h" +#include "sampleUtil.h" +#include "util.h" + +void FurnaceGUI::drawSampleEdit() { + if (nextWindow==GUI_WINDOW_SAMPLE_EDIT) { + sampleEditOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!sampleEditOpen) return; + if (ImGui::Begin("Sample Editor",&sampleEditOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { + if (curSample<0 || curSample>=(int)e->song.sample.size()) { + ImGui::Text("no sample selected"); + } else { + DivSample* sample=e->song.sample[curSample]; + String sampleType="Invalid"; + if (sample->depth<17) { + if (sampleDepths[sample->depth]!=NULL) { + sampleType=sampleDepths[sample->depth]; + } + } + ImGui::Text("Name"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputText("##SampleName",&sample->name)) { + MARK_MODIFIED; + } + + if (ImGui::BeginTable("SampleProps",4,ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Type"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::BeginCombo("##SampleType",sampleType.c_str())) { + for (int i=0; i<17; i++) { + if (sampleDepths[i]==NULL) continue; + if (ImGui::Selectable(sampleDepths[i])) { + sample->prepareUndo(true); + sample->depth=i; + e->renderSamplesP(); + updateSampleTex=true; + MARK_MODIFIED; + } + } + ImGui::EndCombo(); + } + + ImGui::TableNextColumn(); + ImGui::Text("Rate (Hz)"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##SampleRate",&sample->rate,10,200)) { MARK_MODIFIED + if (sample->rate<100) sample->rate=100; + if (sample->rate>96000) sample->rate=96000; + } + + ImGui::TableNextColumn(); + ImGui::Text("C-4 (Hz)"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##SampleCenter",&sample->centerRate,10,200)) { MARK_MODIFIED + if (sample->centerRate<100) sample->centerRate=100; + if (sample->centerRate>65535) sample->centerRate=65535; + } + + ImGui::TableNextColumn(); + bool doLoop=(sample->loopStart>=0); + if (ImGui::Checkbox("Loop",&doLoop)) { MARK_MODIFIED + if (doLoop) { + sample->loopStart=0; + } else { + sample->loopStart=-1; + } + updateSampleTex=true; + } + if (doLoop) { + 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) { + sample->loopStart=0; + } + updateSampleTex=true; + } + } + ImGui::EndTable(); + } + + /* + if (ImGui::Button("Apply")) { + e->renderSamplesP(); + } + ImGui::SameLine(); + */ + ImGui::Separator(); + + ImGui::BeginDisabled(sample->depth!=8 && sample->depth!=16); + + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(!sampleDragMode)); + if (ImGui::Button(ICON_FA_I_CURSOR "##SSelect")) { + sampleDragMode=false; + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Edit mode: Select"); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button,TOGGLE_COLOR(sampleDragMode)); + if (ImGui::Button(ICON_FA_PENCIL "##SDraw")) { + sampleDragMode=true; + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Edit mode: Draw"); + } + ImGui::SameLine(); + ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); + ImGui::SameLine(); + ImGui::Button(ICON_FA_ARROWS_H "##SResize"); + if (ImGui::IsItemClicked()) { + resizeSize=sample->samples; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Resize"); + } + if (openSampleResizeOpt) { + openSampleResizeOpt=false; + ImGui::OpenPopup("SResizeOpt"); + } + if (ImGui::BeginPopupContextItem("SResizeOpt",ImGuiPopupFlags_MouseButtonLeft)) { + if (ImGui::InputInt("Samples",&resizeSize,1,64)) { + if (resizeSize<0) resizeSize=0; + if (resizeSize>16777215) resizeSize=16777215; + } + if (ImGui::Button("Resize")) { + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + if (!sample->resize(resizeSize)) { + showError("couldn't resize! make sure your sample is 8 or 16-bit."); + } + e->renderSamples(); + }); + updateSampleTex=true; + sampleSelStart=-1; + sampleSelEnd=-1; + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } else { + resizeSize=sample->samples; + } + ImGui::SameLine(); + ImGui::Button(ICON_FA_EXPAND "##SResample"); + if (ImGui::IsItemClicked()) { + resampleTarget=sample->rate; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Resample"); + } + if (openSampleResampleOpt) { + openSampleResampleOpt=false; + ImGui::OpenPopup("SResampleOpt"); + } + if (ImGui::BeginPopupContextItem("SResampleOpt",ImGuiPopupFlags_MouseButtonLeft)) { + ImGui::Text("Rate"); + if (ImGui::InputDouble("##SRRate",&resampleTarget,1.0,50.0,"%g")) { + if (resampleTarget<0) resampleTarget=0; + if (resampleTarget>96000) resampleTarget=96000; + } + ImGui::SameLine(); + if (ImGui::Button("0.5x")) { + resampleTarget*=0.5; + } + ImGui::SameLine(); + if (ImGui::Button("==")) { + resampleTarget=sample->rate; + } + ImGui::SameLine(); + if (ImGui::Button("2.0x")) { + resampleTarget*=2.0; + } + double factor=resampleTarget/(double)sample->rate; + if (ImGui::InputDouble("Factor",&factor,0.125,0.5,"%g")) { + resampleTarget=(double)sample->rate*factor; + if (resampleTarget<0) resampleTarget=0; + if (resampleTarget>96000) resampleTarget=96000; + } + ImGui::Combo("Filter",&resampleStrat,resampleStrats,6); + if (ImGui::Button("Resample")) { + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + if (!sample->resample(resampleTarget,resampleStrat)) { + showError("couldn't resample! make sure your sample is 8 or 16-bit."); + } + e->renderSamples(); + }); + updateSampleTex=true; + sampleSelStart=-1; + sampleSelEnd=-1; + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } else { + resampleTarget=sample->rate; + } + ImGui::SameLine(); + ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_UNDO "##SUndo")) { + doUndoSample(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Undo"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_REPEAT "##SRedo")) { + doRedoSample(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Redo"); + } + ImGui::SameLine(); + ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); + ImGui::SameLine(); + ImGui::Button(ICON_FA_VOLUME_UP "##SAmplify"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Amplify"); + } + if (openSampleAmplifyOpt) { + openSampleAmplifyOpt=false; + ImGui::OpenPopup("SAmplifyOpt"); + } + if (ImGui::BeginPopupContextItem("SAmplifyOpt",ImGuiPopupFlags_MouseButtonLeft)) { + ImGui::Text("Volume"); + if (ImGui::InputFloat("##SRVolume",&lifyVol,10.0,50.0,"%g%%")) { + if (amplifyVol<0) amplifyVol=0; + if (amplifyVol>10000) amplifyVol=10000; + } + ImGui::SameLine(); + ImGui::Text("(%.1fdB)",20.0*log10(amplifyVol/100.0f)); + if (ImGui::Button("Apply")) { + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + float vol=amplifyVol/100.0f; + + if (sample->depth==16) { + 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) { + for (unsigned int i=start; idata8[i]*vol; + if (val<-128) val=-128; + if (val>127) val=127; + sample->data8[i]=val; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROWS_V "##SNormalize")) { + doAction(GUI_ACTION_SAMPLE_NORMALIZE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Normalize"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_UP "##SFadeIn")) { + doAction(GUI_ACTION_SAMPLE_FADE_IN); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Fade in"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ARROW_DOWN "##SFadeOut")) { + doAction(GUI_ACTION_SAMPLE_FADE_OUT); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Fade out"); + } + ImGui::SameLine(); + ImGui::Button(ICON_FA_ADJUST "##SInsertSilence"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Insert silence"); + } + if (openSampleSilenceOpt) { + openSampleSilenceOpt=false; + ImGui::OpenPopup("SSilenceOpt"); + } + if (ImGui::BeginPopupContextItem("SSilenceOpt",ImGuiPopupFlags_MouseButtonLeft)) { + if (ImGui::InputInt("Samples",&silenceSize,1,64)) { + if (silenceSize<0) silenceSize=0; + if (silenceSize>16777215) silenceSize=16777215; + } + if (ImGui::Button("Resize")) { + int pos=(sampleSelStart==-1 || sampleSelStart==sampleSelEnd)?sample->samples:sampleSelStart; + sample->prepareUndo(true); + e->lockEngine([this,sample,pos]() { + if (!sample->insert(pos,silenceSize)) { + showError("couldn't insert! make sure your sample is 8 or 16-bit."); + } + e->renderSamples(); + }); + updateSampleTex=true; + sampleSelStart=pos; + sampleSelEnd=pos+silenceSize; + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_ERASER "##SSilence")) { + doAction(GUI_ACTION_SAMPLE_SILENCE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply silence"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_TIMES "##SDelete")) { + doAction(GUI_ACTION_SAMPLE_DELETE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_CROP "##STrim")) { + doAction(GUI_ACTION_SAMPLE_TRIM); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Trim"); + } + ImGui::SameLine(); + ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_BACKWARD "##SReverse")) { + doAction(GUI_ACTION_SAMPLE_REVERSE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Reverse"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_SORT_AMOUNT_ASC "##SInvert")) { + doAction(GUI_ACTION_SAMPLE_INVERT); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Invert"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_LEVEL_DOWN "##SSign")) { + doAction(GUI_ACTION_SAMPLE_SIGN); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Signed/unsigned exchange"); + } + ImGui::SameLine(); + ImGui::Button(ICON_FA_INDUSTRY "##SFilter"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply filter"); + } + if (openSampleFilterOpt) { + openSampleFilterOpt=false; + ImGui::OpenPopup("SFilterOpt"); + } + if (ImGui::BeginPopupContextItem("SFilterOpt",ImGuiPopupFlags_MouseButtonLeft)) { + float lowP=sampleFilterL*100.0f; + float bandP=sampleFilterB*100.0f; + float highP=sampleFilterH*100.0f; + float resP=sampleFilterRes*100.0f; + ImGui::Text("Cutoff:"); + if (ImGui::SliderFloat("From",&sampleFilterCutStart,0.0f,sample->rate*0.5,"%.0fHz")) { + if (sampleFilterCutStart<0.0) sampleFilterCutStart=0.0; + if (sampleFilterCutStart>sample->rate*0.5) sampleFilterCutStart=sample->rate*0.5; + } + if (ImGui::SliderFloat("To",&sampleFilterCutEnd,0.0f,sample->rate*0.5,"%.0fHz")) { + if (sampleFilterCutEnd<0.0) sampleFilterCutEnd=0.0; + if (sampleFilterCutEnd>sample->rate*0.5) sampleFilterCutEnd=sample->rate*0.5; + } + ImGui::Separator(); + if (ImGui::SliderFloat("Resonance",&resP,0.0f,99.0f,"%.1f%%")) { + sampleFilterRes=resP/100.0f; + if (sampleFilterRes<0.0f) sampleFilterRes=0.0f; + if (sampleFilterRes>0.99f) sampleFilterRes=0.99f; + } + ImGui::Text("Power"); + ImGui::SameLine(); + if (ImGui::RadioButton("1x",sampleFilterPower==1)) { + sampleFilterPower=1; + } + ImGui::SameLine(); + if (ImGui::RadioButton("2x",sampleFilterPower==2)) { + sampleFilterPower=2; + } + ImGui::SameLine(); + if (ImGui::RadioButton("3x",sampleFilterPower==3)) { + sampleFilterPower=3; + } + ImGui::Separator(); + if (ImGui::SliderFloat("Low-pass",&lowP,0.0f,100.0f,"%.1f%%")) { + sampleFilterL=lowP/100.0f; + if (sampleFilterL<0.0f) sampleFilterL=0.0f; + if (sampleFilterL>1.0f) sampleFilterL=1.0f; + } + if (ImGui::SliderFloat("Band-pass",&bandP,0.0f,100.0f,"%.1f%%")) { + sampleFilterB=bandP/100.0f; + if (sampleFilterB<0.0f) sampleFilterB=0.0f; + if (sampleFilterB>1.0f) sampleFilterB=1.0f; + } + if (ImGui::SliderFloat("High-pass",&highP,0.0f,100.0f,"%.1f%%")) { + sampleFilterH=highP/100.0f; + if (sampleFilterH<0.0f) sampleFilterH=0.0f; + if (sampleFilterH>1.0f) sampleFilterH=1.0f; + } + + if (ImGui::Button("Apply")) { + sample->prepareUndo(true); + e->lockEngine([this,sample]() { + SAMPLE_OP_BEGIN; + float res=1.0-pow(sampleFilterRes,0.5f); + float low=0; + float band=0; + float high=0; + + double power=(sampleFilterCutStart>sampleFilterCutEnd)?0.5:2.0; + + if (sample->depth==16) { + for (unsigned int i=start; irate))*M_PI); + + for (int j=0; jdata16[i])-low-(res*band); + band=cut*high+band; + } + + float val=low*sampleFilterL+band*sampleFilterB+high*sampleFilterH; + if (val<-32768) val=-32768; + if (val>32767) val=32767; + sample->data16[i]=val; + } + } else if (sample->depth==8) { + for (unsigned int i=start; irate))*M_PI); + + for (int j=0; jdata8[i])-low-(res*band); + band=cut*high+band; + } + + float val=low*sampleFilterL+band*sampleFilterB+high*sampleFilterH; + if (val<-128) val=-128; + if (val>127) val=127; + sample->data8[i]=val; + } + } + + updateSampleTex=true; + + e->renderSamples(); + }); + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + ImGui::Dummy(ImVec2(4.0*dpiScale,dpiScale)); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_VOLUME_UP "##PreviewSample")) { + e->previewSample(curSample); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Preview sample"); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_VOLUME_OFF "##StopSample")) { + e->stopSamplePreview(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Stop sample preview"); + } + + ImGui::SameLine(); + double zoomPercent=100.0/sampleZoom; + ImGui::Text("Zoom"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f*dpiScale); + if (ImGui::InputDouble("##SZoom",&zoomPercent,5.0,20.0,"%g%%")) { + if (zoomPercent>10000.0) zoomPercent=10000.0; + if (zoomPercent<1.0) zoomPercent=1.0; + sampleZoom=100.0/zoomPercent; + if (sampleZoom<0.01) sampleZoom=0.01; + sampleZoomAuto=false; + updateSampleTex=true; + } + ImGui::SameLine(); + if (sampleZoomAuto) { + if (ImGui::Button("100%")) { + sampleZoom=1.0; + sampleZoomAuto=false; + updateSampleTex=true; + } + } else { + if (ImGui::Button("Auto")) { + sampleZoomAuto=true; + updateSampleTex=true; + } + } + + ImGui::Separator(); + + ImVec2 avail=ImGui::GetContentRegionAvail(); + avail.y-=ImGui::GetFontSize()+ImGui::GetStyle().ItemSpacing.y+ImGui::GetStyle().ScrollbarSize; + int availX=avail.x; + int availY=avail.y; + + + if (sampleZoomAuto) { + samplePos=0; + if (sample->samples<1 || avail.x<=0) { + sampleZoom=1.0; + } else { + sampleZoom=(double)sample->samples/avail.x; + } + if (sampleZoom!=prevSampleZoom) { + prevSampleZoom=sampleZoom; + updateSampleTex=true; + } + } + + if (sampleTex==NULL || sampleTexW!=avail.x || sampleTexH!=avail.y) { + if (sampleTex!=NULL) { + SDL_DestroyTexture(sampleTex); + sampleTex=NULL; + } + if (avail.x>=1 && avail.y>=1) { + logD("recreating sample texture."); + sampleTex=SDL_CreateTexture(sdlRend,SDL_PIXELFORMAT_ABGR8888,SDL_TEXTUREACCESS_STREAMING,avail.x,avail.y); + sampleTexW=avail.x; + sampleTexH=avail.y; + if (sampleTex==NULL) { + logE("error while creating sample texture! %s",SDL_GetError()); + } else { + updateSampleTex=true; + } + } + } + + if (sampleTex!=NULL) { + if (updateSampleTex) { + unsigned int* data=NULL; + int pitch=0; + logD("updating sample texture."); + if (SDL_LockTexture(sampleTex,NULL,(void**)&data,&pitch)!=0) { + logE("error while locking sample texture! %s",SDL_GetError()); + } else { + ImU32 bgColor=ImGui::GetColorU32(ImGuiCol_FrameBg); + ImU32 bgColorLoop=ImAlphaBlendColors(bgColor,ImGui::GetColorU32(ImGuiCol_FrameBgHovered,0.5)); + ImU32 lineColor=ImGui::GetColorU32(ImGuiCol_PlotLines); + ImU32 centerLineColor=ImAlphaBlendColors(bgColor,ImGui::GetColorU32(ImGuiCol_PlotLines,0.25)); + for (int i=0; iloopStart>=0 && sample->loopStart<(int)sample->samples && ((j+samplePos)*sampleZoom)>sample->loopStart) { + data[i*availX+j]=bgColorLoop; + } else { + data[i*availX+j]=bgColor; + } + } + } + if (availY>0) { + for (int i=availX*(availY>>1); i>1)); i++) { + data[i]=centerLineColor; + } + } + unsigned int xCoarse=samplePos; + unsigned int xFine=0; + unsigned int xAdvanceCoarse=sampleZoom; + unsigned int xAdvanceFine=fmod(sampleZoom,1.0)*16777216; + for (unsigned int i=0; i<(unsigned int)availX; i++) { + if (xCoarse>=sample->samples) break; + int y1, y2; + int totalAdvance=0; + if (sample->depth==8) { + y1=((unsigned char)sample->data8[xCoarse]^0x80)*availY/256; + } else { + y1=((unsigned short)sample->data16[xCoarse]^0x8000)*availY/65536; + } + xFine+=xAdvanceFine; + if (xFine>=16777216) { + xFine-=16777216; + totalAdvance++; + } + totalAdvance+=xAdvanceCoarse; + if (xCoarse>=sample->samples) break; + do { + if (sample->depth==8) { + y2=((unsigned char)sample->data8[xCoarse]^0x80)*availY/256; + } else { + y2=((unsigned short)sample->data16[xCoarse]^0x8000)*availY/65536; + } + if (y1>y2) { + y2^=y1; + y1^=y2; + y2^=y1; + } + if (y1<0) y1=0; + if (y1>=availY) y1=availY-1; + if (y2<0) y2=0; + if (y2>=availY) y2=availY-1; + for (int j=y1; j<=y2; j++) { + data[i+availX*(availY-j-1)]=lineColor; + } + if (totalAdvance>0) xCoarse++; + } while ((totalAdvance--)>0); + } + SDL_UnlockTexture(sampleTex); + } + updateSampleTex=false; + } + + ImGui::ImageButton(sampleTex,avail,ImVec2(0,0),ImVec2(1,1),0); + + ImVec2 rectMin=ImGui::GetItemRectMin(); + ImVec2 rectMax=ImGui::GetItemRectMax(); + ImVec2 rectSize=ImGui::GetItemRectSize(); + + if (ImGui::IsItemClicked()) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + sampleDragActive=false; + sampleSelStart=0; + sampleSelEnd=sample->samples; + } else { + if (sample->samples>0 && (sample->depth==16 || sample->depth==8)) { + sampleDragStart=rectMin; + sampleDragAreaSize=rectSize; + sampleDrag16=(sample->depth==16); + sampleDragTarget=(sample->depth==16)?((void*)sample->data16):((void*)sample->data8); + sampleDragLen=sample->samples; + sampleDragActive=true; + sampleSelStart=-1; + sampleSelEnd=-1; + if (sampleDragMode) sample->prepareUndo(true); + processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); + } + } + } + + if (!sampleDragMode && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + ImGui::OpenPopup("SRightClick"); + } + + if (ImGui::BeginPopup("SRightClick",ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_SAMPLE_CUT))) { + doAction(GUI_ACTION_SAMPLE_CUT); + } + if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_SAMPLE_COPY))) { + doAction(GUI_ACTION_SAMPLE_COPY); + } + if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_SAMPLE_PASTE))) { + doAction(GUI_ACTION_SAMPLE_PASTE); + } + if (ImGui::MenuItem("paste (replace)",BIND_FOR(GUI_ACTION_SAMPLE_PASTE_REPLACE))) { + doAction(GUI_ACTION_SAMPLE_PASTE_REPLACE); + } + if (ImGui::MenuItem("paste (mix)",BIND_FOR(GUI_ACTION_SAMPLE_PASTE_MIX))) { + doAction(GUI_ACTION_SAMPLE_PASTE_MIX); + } + if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_SAMPLE_SELECT_ALL))) { + doAction(GUI_ACTION_SAMPLE_SELECT_ALL); + } + ImGui::EndPopup(); + } + + String statusBar=sampleDragMode?"Draw":"Select"; + bool drawSelection=false; + + if (!sampleDragMode) { + if (sampleSelStart>=0 && sampleSelEnd>=0) { + int start=sampleSelStart; + int end=sampleSelEnd; + if (start>end) { + start^=end; + end^=start; + start^=end; + } + statusBar+=fmt::sprintf(" (%d-%d)",start,end); + drawSelection=true; + } + } + + if (ImGui::IsItemHovered()) { + int posX=-1; + int posY=0; + ImVec2 pos=ImGui::GetMousePos(); + pos.x-=rectMin.x; + pos.y-=rectMin.y; + + if (sampleZoom>0) { + posX=samplePos+pos.x*sampleZoom; + if (posX>(int)sample->samples) posX=-1; + } + posY=(0.5-pos.y/rectSize.y)*((sample->depth==8)?255:32767); + if (posX>=0) { + statusBar+=fmt::sprintf(" | (%d, %d)",posX,posY); + } + } + + if (drawSelection) { + int start=sampleSelStart; + int end=sampleSelEnd; + if (start>end) { + start^=end; + end^=start; + start^=end; + } + ImDrawList* dl=ImGui::GetWindowDrawList(); + ImVec2 p1=rectMin; + p1.x+=(start-samplePos)/sampleZoom; + + ImVec2 p2=ImVec2(rectMin.x+(end-samplePos)/sampleZoom,rectMax.y); + ImVec4 boundColor=uiColors[GUI_COLOR_ACCENT_PRIMARY]; + ImVec4 selColor=uiColors[GUI_COLOR_ACCENT_SECONDARY]; + boundColor.w*=0.5; + selColor.w*=0.25; + + if (p1.xrectMax.x) p1.x=rectMax.x; + + if (p2.xrectMax.x) p2.x=rectMax.x; + + dl->AddRectFilled(p1,p2,ImGui::GetColorU32(selColor)); + dl->AddLine(ImVec2(p1.x,p1.y),ImVec2(p1.x,p2.y),ImGui::GetColorU32(boundColor)); + if (start!=end) { + dl->AddLine(ImVec2(p2.x,p1.y),ImVec2(p2.x,p2.y),ImGui::GetColorU32(boundColor)); + } + } + + ImS64 scrollV=samplePos; + ImS64 availV=round(rectSize.x*sampleZoom); + ImS64 contentsV=MAX(sample->samples,MAX(availV,1)); + + if (ImGui::ScrollbarEx(ImRect(ImVec2(rectMin.x,rectMax.y),ImVec2(rectMax.x,rectMax.y+ImGui::GetStyle().ScrollbarSize)),ImGui::GetID("sampleScroll"),ImGuiAxis_X,&scrollV,availV,contentsV,0)) { + if (!sampleZoomAuto && samplePos!=scrollV) { + samplePos=scrollV; + updateSampleTex=true; + } + } + + if (sample->depth!=8 && sample->depth!=16) { + statusBar="Non-8/16-bit samples cannot be edited without prior conversion."; + } + + ImGui::EndDisabled(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+ImGui::GetStyle().ScrollbarSize); + ImGui::Text("%s",statusBar.c_str()); + } + + /* + bool considerations=false; + ImGui::Text("notes:"); + if (sample->loopStart>=0) { + considerations=true; + ImGui::Text("- sample won't loop on Neo Geo ADPCM-A and X1-010"); + if (sample->loopStart&1) { + ImGui::Text("- sample loop start will be aligned to the nearest even sample on Amiga"); + } + if (sample->loopStart>0) { + ImGui::Text("- sample loop start will be ignored on Neo Geo ADPCM-B"); + } + } + if (sample->samples&1) { + considerations=true; + ImGui::Text("- sample length will be aligned to the nearest even sample on Amiga"); + } + if (sample->samples&511) { + considerations=true; + ImGui::Text("- sample length will be aligned and padded to 512 sample units on Neo Geo ADPCM."); + } + if (sample->samples&4095) { + considerations=true; + ImGui::Text("- sample length will be aligned and padded to 4096 sample units on X1-010."); + } + if (sample->samples>65535) { + considerations=true; + ImGui::Text("- maximum sample length on Sega PCM and QSound is 65536 samples"); + } + if (sample->samples>131071) { + considerations=true; + ImGui::Text("- maximum sample length on X1-010 is 131072 samples"); + } + if (sample->samples>2097151) { + considerations=true; + ImGui::Text("- maximum sample length on Neo Geo ADPCM is 2097152 samples"); + } + if (!considerations) { + ImGui::Text("- none"); + }*/ + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SAMPLE_EDIT; + ImGui::End(); +} + +void FurnaceGUI::doUndoSample() { + if (!sampleEditOpen) return; + if (curSample<0 || curSample>=(int)e->song.sample.size()) return; + DivSample* sample=e->song.sample[curSample]; + e->lockEngine([this,sample]() { + if (sample->undo()==2) { + e->renderSamples(); + updateSampleTex=true; + } + }); +} + +void FurnaceGUI::doRedoSample() { + if (!sampleEditOpen) return; + if (curSample<0 || curSample>=(int)e->song.sample.size()) return; + DivSample* sample=e->song.sample[curSample]; + e->lockEngine([this,sample]() { + if (sample->redo()==2) { + e->renderSamples(); + updateSampleTex=true; + } + }); +} diff --git a/src/gui/sampleUtil.h b/src/gui/sampleUtil.h new file mode 100644 index 000000000..981a56046 --- /dev/null +++ b/src/gui/sampleUtil.h @@ -0,0 +1,31 @@ +/** + * 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 SAMPLE_OP_BEGIN \ + unsigned int start=0; \ + unsigned int end=sample->samples; \ + if (sampleSelStart!=-1 && sampleSelEnd!=-1 && sampleSelStart!=sampleSelEnd) { \ + start=sampleSelStart; \ + end=sampleSelEnd; \ + if (start>end) { \ + start^=end; \ + end^=start; \ + start^=end; \ + } \ + } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 52faa7b95..87b798572 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -18,20 +18,17 @@ */ #include "gui.h" +#include "fonts.h" #include "../ta-log.h" +#include "../fileutils.h" #include "util.h" +#include "guiConst.h" +#include "ImGuiFileDialog.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" -#include #include #include -#ifdef __APPLE__ -#define FURKMOD_CMD FURKMOD_META -#else -#define FURKMOD_CMD FURKMOD_CTRL -#endif - #define DEFAULT_NOTE_KEYS "5:7;6:4;7:3;8:16;10:6;11:8;12:24;13:10;16:11;17:9;18:26;19:28;20:12;21:17;22:1;23:19;24:23;25:5;26:14;27:2;28:21;29:0;30:100;31:13;32:15;34:18;35:20;36:22;38:25;39:27;43:100;46:101;47:29;48:31;53:102;" const char* mainFonts[]={ @@ -79,6 +76,67 @@ const char* saaCores[]={ "SAASound" }; +const char* valueInputStyles[]={ + "Disabled/custom", + "Two octaves (0 is C-4, F is D#5)", + "Raw (note number is value)", + "Two octaves alternate (lower keys are 0-9, upper keys are A-F)", + "Use dual control change (one for each nibble)", + "Use 14-bit control change", + "Use single control change (imprecise)" +}; + +const char* valueSInputStyles[]={ + "Disabled/custom", + "Use dual control change (one for each nibble)", + "Use 14-bit control change", + "Use single control change (imprecise)" +}; + +const char* messageTypes[]={ + "--select--", + "???", + "???", + "???", + "???", + "???", + "???", + "???", + "Note Off", + "Note On", + "Aftertouch", + "Control", + "Program", + "ChanPressure", + "Pitch Bend", + "SysEx" +}; + +const char* messageChannels[]={ + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "Any" +}; + +const char* specificControls[18]={ + "Instrument", + "Volume", + "Effect 1 type", + "Effect 1 value", + "Effect 2 type", + "Effect 2 value", + "Effect 3 type", + "Effect 3 value", + "Effect 4 type", + "Effect 4 value", + "Effect 5 type", + "Effect 5 value", + "Effect 6 type", + "Effect 6 value", + "Effect 7 type", + "Effect 7 value", + "Effect 8 type", + "Effect 8 value" +}; + #define SAMPLE_RATE_SELECTABLE(x) \ if (ImGui::Selectable(#x,settings.audioRate==x)) { \ settings.audioRate=x; \ @@ -90,7 +148,9 @@ const char* saaCores[]={ } #define UI_COLOR_CONFIG(what,label) \ - ImGui::ColorEdit4(label "##CC_" #what,(float*)&uiColors[what]); + if (ImGui::ColorEdit4(label "##CC_" #what,(float*)&uiColors[what])) { \ + applyUISettings(false); \ + } #define KEYBIND_CONFIG_BEGIN(id) \ if (ImGui::BeginTable(id,2)) { @@ -99,16 +159,28 @@ const char* saaCores[]={ ImGui::EndTable(); \ } -#define UI_KEYBIND_CONFIG(what,label) \ +#define UI_KEYBIND_CONFIG(what) \ ImGui::TableNextRow(); \ ImGui::TableNextColumn(); \ - ImGui::Text(label); \ + ImGui::TextUnformatted(guiActions[what].friendlyName); \ ImGui::TableNextColumn(); \ if (ImGui::Button(fmt::sprintf("%s##KC_" #what,(bindSetPending && bindSetTarget==what)?"Press key...":getKeyName(actionKeys[what])).c_str())) { \ promptKey(what); \ } \ if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) actionKeys[what]=0; +String stripName(String what) { + String ret; + for (char& i: what) { + if ((i>='A' && i<='Z') || (i>='a' && i<='z') || (i>='0' && i<='9')) { + ret+=i; + } else { + ret+='-'; + } + } + return ret; +} + void FurnaceGUI::promptKey(int which) { bindSetTarget=which; bindSetActive=true; @@ -136,6 +208,19 @@ void FurnaceGUI::drawSettings() { if (ImGui::Begin("Settings",NULL,ImGuiWindowFlags_NoDocking)) { if (ImGui::BeginTabBar("settingsTab")) { if (ImGui::BeginTabItem("General")) { + ImGui::Text("Workspace layout"); + if (ImGui::Button("Import")) { + openFileDialog(GUI_FILE_IMPORT_LAYOUT); + } + ImGui::SameLine(); + if (ImGui::Button("Export")) { + openFileDialog(GUI_FILE_EXPORT_LAYOUT); + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + showWarning("Are you sure you want to reset the workspace layout?",GUI_WARN_RESET_LAYOUT); + } + ImGui::Separator(); ImGui::Text("Toggle channel solo on:"); if (ImGui::RadioButton("Right-click or double-click##soloA",settings.soloAction==0)) { settings.soloAction=0; @@ -157,16 +242,46 @@ void FurnaceGUI::drawSettings() { settings.stepOnDelete=stepOnDeleteB; } + bool effectDeletionAltersValueB=settings.effectDeletionAltersValue; + if (ImGui::Checkbox("Delete effect value when deleting effect",&effectDeletionAltersValueB)) { + settings.effectDeletionAltersValue=effectDeletionAltersValueB; + } + + bool stepOnInsertB=settings.stepOnInsert; + if (ImGui::Checkbox("Move cursor by edit step on insert (push)",&stepOnInsertB)) { + settings.stepOnInsert=stepOnInsertB; + } + + bool cursorPastePosB=settings.cursorPastePos; + if (ImGui::Checkbox("Move cursor to end of clipboard content when pasting",&cursorPastePosB)) { + settings.cursorPastePos=cursorPastePosB; + } + bool allowEditDockingB=settings.allowEditDocking; if (ImGui::Checkbox("Allow docking editors",&allowEditDockingB)) { settings.allowEditDocking=allowEditDockingB; } + bool avoidRaisingPatternB=settings.avoidRaisingPattern; + if (ImGui::Checkbox("Don't raise pattern editor on click",&avoidRaisingPatternB)) { + settings.avoidRaisingPattern=avoidRaisingPatternB; + } + + bool insFocusesPatternB=settings.insFocusesPattern; + if (ImGui::Checkbox("Focus pattern editor when selecting instrument",&insFocusesPatternB)) { + settings.insFocusesPattern=insFocusesPatternB; + } + bool restartOnFlagChangeB=settings.restartOnFlagChange; if (ImGui::Checkbox("Restart song when changing system properties",&restartOnFlagChangeB)) { settings.restartOnFlagChange=restartOnFlagChangeB; } + bool sysFileDialogB=settings.sysFileDialog; + if (ImGui::Checkbox("Use system file picker",&sysFileDialogB)) { + settings.sysFileDialog=sysFileDialogB; + } + ImGui::Text("Wrap pattern cursor horizontally:"); if (ImGui::RadioButton("No##wrapH0",settings.wrapHorizontal==0)) { settings.wrapHorizontal=0; @@ -196,9 +311,21 @@ void FurnaceGUI::drawSettings() { if (ImGui::RadioButton("Move by Edit Step##cmk1",settings.scrollStep==1)) { settings.scrollStep=1; } + + ImGui::Text("Effect input cursor behavior:"); + if (ImGui::RadioButton("Move down##eicb0",settings.effectCursorDir==0)) { + settings.effectCursorDir=0; + } + if (ImGui::RadioButton("Move to effect value (otherwise move down)##eicb1",settings.effectCursorDir==1)) { + settings.effectCursorDir=1; + } + if (ImGui::RadioButton("Move to effect value/next effect and wrap around##eicb2",settings.effectCursorDir==2)) { + settings.effectCursorDir=2; + } + ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Audio")) { + if (ImGui::BeginTabItem("Audio/MIDI")) { ImGui::Text("Backend"); ImGui::SameLine(); ImGui::Combo("##Backend",&settings.audioEngine,audioBackends,2); @@ -259,8 +386,268 @@ void FurnaceGUI::drawSettings() { TAAudioDesc& audioWant=e->getAudioDescWant(); TAAudioDesc& audioGot=e->getAudioDescGot(); - ImGui::Text("want: %d samples @ %.0fHz\n",audioWant.bufsize,audioWant.rate); - ImGui::Text("got: %d samples @ %.0fHz\n",audioGot.bufsize,audioGot.rate); + ImGui::Text("want: %d samples @ %.0fHz",audioWant.bufsize,audioWant.rate); + ImGui::Text("got: %d samples @ %.0fHz",audioGot.bufsize,audioGot.rate); + + ImGui::Separator(); + + ImGui::Text("MIDI input"); + ImGui::SameLine(); + String midiInName=settings.midiInDevice.empty()?"":settings.midiInDevice; + bool hasToReloadMidi=false; + if (ImGui::BeginCombo("##MidiInDevice",midiInName.c_str())) { + if (ImGui::Selectable("",settings.midiInDevice.empty())) { + settings.midiInDevice=""; + hasToReloadMidi=true; + } + for (String& i: e->getMidiIns()) { + if (ImGui::Selectable(i.c_str(),i==settings.midiInDevice)) { + settings.midiInDevice=i; + hasToReloadMidi=true; + } + } + ImGui::EndCombo(); + } + + if (hasToReloadMidi) { + midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); + midiMap.compile(); + } + + ImGui::Text("MIDI output"); + ImGui::SameLine(); + String midiOutName=settings.midiOutDevice.empty()?"":settings.midiOutDevice; + if (ImGui::BeginCombo("##MidiOutDevice",midiOutName.c_str())) { + if (ImGui::Selectable("",settings.midiOutDevice.empty())) { + settings.midiOutDevice=""; + } + for (String& i: e->getMidiIns()) { + if (ImGui::Selectable(i.c_str(),i==settings.midiOutDevice)) { + settings.midiOutDevice=i; + } + } + ImGui::EndCombo(); + } + + if (ImGui::TreeNode("MIDI input settings")) { + ImGui::Checkbox("Note input",&midiMap.noteInput); + ImGui::Checkbox("Velocity input",&midiMap.volInput); + // TODO + //ImGui::Checkbox("Use raw velocity value (don't map from linear to log)",&midiMap.rawVolume); + //ImGui::Checkbox("Polyphonic/chord input",&midiMap.polyInput); + ImGui::Checkbox("Map MIDI channels to direct channels",&midiMap.directChannel); + ImGui::Checkbox("Program change is instrument selection",&midiMap.programChange); + //ImGui::Checkbox("Listen to MIDI clock",&midiMap.midiClock); + //ImGui::Checkbox("Listen to MIDI time code",&midiMap.midiTimeCode); + ImGui::Combo("Value input style",&midiMap.valueInputStyle,valueInputStyles,7); + if (midiMap.valueInputStyle>3) { + if (midiMap.valueInputStyle==6) { + if (ImGui::InputInt("Control##valueCCS",&midiMap.valueInputControlSingle,1,16)) { + if (midiMap.valueInputControlSingle<0) midiMap.valueInputControlSingle=0; + if (midiMap.valueInputControlSingle>127) midiMap.valueInputControlSingle=127; + } + } else { + if (ImGui::InputInt((midiMap.valueInputStyle==4)?"CC of upper nibble##valueCC1":"MSB CC##valueCC1",&midiMap.valueInputControlMSB,1,16)) { + if (midiMap.valueInputControlMSB<0) midiMap.valueInputControlMSB=0; + if (midiMap.valueInputControlMSB>127) midiMap.valueInputControlMSB=127; + } + if (ImGui::InputInt((midiMap.valueInputStyle==4)?"CC of lower nibble##valueCC2":"LSB CC##valueCC2",&midiMap.valueInputControlLSB,1,16)) { + if (midiMap.valueInputControlLSB<0) midiMap.valueInputControlLSB=0; + if (midiMap.valueInputControlLSB>127) midiMap.valueInputControlLSB=127; + } + } + } + if (ImGui::TreeNode("Per-column control change")) { + for (int i=0; i<18; i++) { + ImGui::PushID(i); + ImGui::Combo(specificControls[i],&midiMap.valueInputSpecificStyle[i],valueSInputStyles,4); + if (midiMap.valueInputSpecificStyle[i]>0) { + ImGui::Indent(); + if (midiMap.valueInputSpecificStyle[i]==3) { + if (ImGui::InputInt("Control##valueCCS",&midiMap.valueInputSpecificSingle[i],1,16)) { + if (midiMap.valueInputSpecificSingle[i]<0) midiMap.valueInputSpecificSingle[i]=0; + if (midiMap.valueInputSpecificSingle[i]>127) midiMap.valueInputSpecificSingle[i]=127; + } + } else { + if (ImGui::InputInt((midiMap.valueInputSpecificStyle[i]==4)?"CC of upper nibble##valueCC1":"MSB CC##valueCC1",&midiMap.valueInputSpecificMSB[i],1,16)) { + if (midiMap.valueInputSpecificMSB[i]<0) midiMap.valueInputSpecificMSB[i]=0; + if (midiMap.valueInputSpecificMSB[i]>127) midiMap.valueInputSpecificMSB[i]=127; + } + if (ImGui::InputInt((midiMap.valueInputSpecificStyle[i]==4)?"CC of lower nibble##valueCC2":"LSB CC##valueCC2",&midiMap.valueInputSpecificLSB[i],1,16)) { + if (midiMap.valueInputSpecificLSB[i]<0) midiMap.valueInputSpecificLSB[i]=0; + if (midiMap.valueInputSpecificLSB[i]>127) midiMap.valueInputSpecificLSB[i]=127; + } + } + ImGui::Unindent(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + if (ImGui::SliderFloat("Volume curve",&midiMap.volExp,0.01,8.0,"%.2f")) { + if (midiMap.volExp<0.01) midiMap.volExp=0.01; + if (midiMap.volExp>8.0) midiMap.volExp=8.0; + } rightClickable + float curve[128]; + for (int i=0; i<128; i++) { + curve[i]=(int)(pow((double)i/127.0,midiMap.volExp)*127.0); + } + ImGui::PlotLines("##VolCurveDisplay",curve,128,0,"Volume curve",0.0,127.0,ImVec2(200.0f*dpiScale,200.0f*dpiScale)); + + ImGui::Text("Actions:"); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLUS "##AddAction")) { + midiMap.binds.push_back(MIDIBind()); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_EXTERNAL_LINK "##AddLearnAction")) { + midiMap.binds.push_back(MIDIBind()); + learning=midiMap.binds.size()-1; + } + if (learning!=-1) { + ImGui::SameLine(); + ImGui::Text("(learning! press a button or move a slider/knob/something on your device.)"); + } + + if (ImGui::BeginTable("MIDIActions",7)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.2); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.1); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.3); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,0.2); + ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch,0.5); + ImGui::TableSetupColumn("c5",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c6",ImGuiTableColumnFlags_WidthFixed); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Type"); + ImGui::TableNextColumn(); + ImGui::Text("Channel"); + ImGui::TableNextColumn(); + ImGui::Text("Note/Control"); + ImGui::TableNextColumn(); + ImGui::Text("Velocity/Value"); + ImGui::TableNextColumn(); + ImGui::Text("Action"); + ImGui::TableNextColumn(); + ImGui::Text("Learn"); + ImGui::TableNextColumn(); + ImGui::Text("Remove"); + + for (size_t i=0; i3.0f) settings.dpiScale=3.0f; - } + } rightClickable } ImGui::Text("Main font"); ImGui::SameLine(); @@ -323,6 +710,19 @@ void FurnaceGUI::drawSettings() { if (settings.patFontSize>96) settings.patFontSize=96; } + bool loadJapaneseB=settings.loadJapanese; + if (ImGui::Checkbox("Display Japanese characters",&loadJapaneseB)) { + settings.loadJapanese=loadJapaneseB; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Only toggle this option if you have enough graphics memory.\n" + "This is a temporary solution until dynamic font atlas is implemented in Dear ImGui.\n\n" + "このオプションは、十分なグラフィックメモリがある場合にのみ切り替えてください。\n" + "これは、Dear ImGuiにダイナミックフォントアトラスが実装されるまでの一時的な解決策です。" + ); + } + ImGui::Separator(); ImGui::Text("Orders row number format:"); @@ -354,6 +754,30 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); + ImGui::Text("Title bar:"); + if (ImGui::RadioButton("Furnace##tbar0",settings.titleBarInfo==0)) { + settings.titleBarInfo=0; + updateWindowTitle(); + } + if (ImGui::RadioButton("Song Name - Furnace##tbar1",settings.titleBarInfo==1)) { + settings.titleBarInfo=1; + updateWindowTitle(); + } + if (ImGui::RadioButton("file_name.fur - Furnace##tbar2",settings.titleBarInfo==2)) { + settings.titleBarInfo=2; + updateWindowTitle(); + } + if (ImGui::RadioButton("/path/to/file.fur - Furnace##tbar3",settings.titleBarInfo==3)) { + settings.titleBarInfo=3; + updateWindowTitle(); + } + + bool titleBarSysB=settings.titleBarSys; + if (ImGui::Checkbox("Display system name on title bar",&titleBarSysB)) { + settings.titleBarSys=titleBarSysB; + updateWindowTitle(); + } + ImGui::Text("Status bar:"); if (ImGui::RadioButton("Cursor details##sbar0",settings.statusDisplay==0)) { settings.statusDisplay=0; @@ -382,11 +806,38 @@ void FurnaceGUI::drawSettings() { settings.controlLayout=3; } + ImGui::Text("FM parameter editor layout:"); + if (ImGui::RadioButton("Modern##fml0",settings.fmLayout==0)) { + settings.fmLayout=0; + } + if (ImGui::RadioButton("Compact (2x2, classic)##fml1",settings.fmLayout==1)) { + settings.fmLayout=1; + } + if (ImGui::RadioButton("Compact (1x4)##fml2",settings.fmLayout==2)) { + settings.fmLayout=2; + } + if (ImGui::RadioButton("Compact (4x1)##fml3",settings.fmLayout==3)) { + settings.fmLayout=3; + } + + ImGui::Text("Position of Sustain in FM editor:"); + if (ImGui::RadioButton("Between Decay and Sustain Rate##susp0",settings.susPosition==0)) { + settings.susPosition=0; + } + if (ImGui::RadioButton("After Release Rate##susp1",settings.susPosition==1)) { + settings.susPosition=1; + } + bool macroViewB=settings.macroView; - if (ImGui::Checkbox("Classic macro view (standard macros only)",¯oViewB)) { + if (ImGui::Checkbox("Classic macro view (standard macros only; deprecated!)",¯oViewB)) { settings.macroView=macroViewB; } + bool unifiedDataViewB=settings.unifiedDataView; + if (ImGui::Checkbox("Unified instrument/wavetable/sample list",&unifiedDataViewB)) { + settings.unifiedDataView=unifiedDataViewB; + } + bool chipNamesB=settings.chipNames; if (ImGui::Checkbox("Use chip names instead of system names",&chipNamesB)) { settings.chipNames=chipNamesB; @@ -423,8 +874,67 @@ void FurnaceGUI::drawSettings() { ImGui::Separator(); + bool roundedWindowsB=settings.roundedWindows; + if (ImGui::Checkbox("Rounded window corners",&roundedWindowsB)) { + settings.roundedWindows=roundedWindowsB; + } + + bool roundedButtonsB=settings.roundedButtons; + if (ImGui::Checkbox("Rounded buttons",&roundedButtonsB)) { + settings.roundedButtons=roundedButtonsB; + } + + bool roundedMenusB=settings.roundedMenus; + if (ImGui::Checkbox("Rounded menu corners",&roundedMenusB)) { + settings.roundedMenus=roundedMenusB; + } + + bool frameBordersB=settings.frameBorders; + if (ImGui::Checkbox("Borders around widgets",&frameBordersB)) { + settings.frameBorders=frameBordersB; + } + + ImGui::Separator(); + + ImGui::Text("Oscilloscope settings:"); + + bool oscRoundedCornersB=settings.oscRoundedCorners; + if (ImGui::Checkbox("Rounded corners",&oscRoundedCornersB)) { + settings.oscRoundedCorners=oscRoundedCornersB; + } + + bool oscTakesEntireWindowB=settings.oscTakesEntireWindow; + if (ImGui::Checkbox("Fill entire window",&oscTakesEntireWindowB)) { + settings.oscTakesEntireWindow=oscTakesEntireWindowB; + } + + bool oscBorderB=settings.oscBorder; + if (ImGui::Checkbox("Border",&oscBorderB)) { + settings.oscBorder=oscBorderB; + } + + ImGui::Separator(); + if (ImGui::TreeNode("Color scheme")) { + if (ImGui::Button("Import")) { + openFileDialog(GUI_FILE_IMPORT_COLORS); + } + ImGui::SameLine(); + if (ImGui::Button("Export")) { + openFileDialog(GUI_FILE_EXPORT_COLORS); + } + ImGui::SameLine(); + if (ImGui::Button("Reset defaults")) { + showWarning("Are you sure you want to reset the color scheme?",GUI_WARN_RESET_COLORS); + } if (ImGui::TreeNode("General")) { + ImGui::Text("Color scheme type:"); + if (ImGui::RadioButton("Dark##gcb0",settings.guiColorsBase==0)) { + settings.guiColorsBase=0; + } + if (ImGui::RadioButton("Light##gcb1",settings.guiColorsBase==1)) { + settings.guiColorsBase=1; + } UI_COLOR_CONFIG(GUI_COLOR_BACKGROUND,"Background"); UI_COLOR_CONFIG(GUI_COLOR_FRAME_BACKGROUND,"Window background"); UI_COLOR_CONFIG(GUI_COLOR_MODAL_BACKDROP,"Modal backdrop"); @@ -432,17 +942,52 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_TEXT,"Text"); UI_COLOR_CONFIG(GUI_COLOR_ACCENT_PRIMARY,"Primary"); UI_COLOR_CONFIG(GUI_COLOR_ACCENT_SECONDARY,"Secondary"); + UI_COLOR_CONFIG(GUI_COLOR_BORDER,"Border"); + UI_COLOR_CONFIG(GUI_COLOR_BORDER_SHADOW,"Border shadow"); + UI_COLOR_CONFIG(GUI_COLOR_TOGGLE_ON,"Toggle on"); + UI_COLOR_CONFIG(GUI_COLOR_TOGGLE_OFF,"Toggle off"); UI_COLOR_CONFIG(GUI_COLOR_EDITING,"Editing"); UI_COLOR_CONFIG(GUI_COLOR_SONG_LOOP,"Song loop"); UI_COLOR_CONFIG(GUI_COLOR_PLAYBACK_STAT,"Playback status"); ImGui::TreePop(); } + if (ImGui::TreeNode("File Picker (built-in)")) { + UI_COLOR_CONFIG(GUI_COLOR_FILE_DIR,"Directory"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_SONG_NATIVE,"Song (native)"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_SONG_IMPORT,"Song (import)"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_INSTR,"Instrument"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_AUDIO,"Audio"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_WAVE,"Wavetable"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_VGM,"VGM"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_FONT,"Font"); + UI_COLOR_CONFIG(GUI_COLOR_FILE_OTHER,"Other"); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Oscilloscope")) { + UI_COLOR_CONFIG(GUI_COLOR_OSC_BORDER,"Border"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_BG1,"Background (top-left)"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_BG2,"Background (top-right)"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_BG3,"Background (bottom-left)"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_BG4,"Background (bottom-right)"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_WAVE,"Waveform"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_WAVE_PEAK,"Waveform (clip)"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_REF,"Reference"); + UI_COLOR_CONFIG(GUI_COLOR_OSC_GUIDE,"Guide"); + ImGui::TreePop(); + } if (ImGui::TreeNode("Volume Meter")) { UI_COLOR_CONFIG(GUI_COLOR_VOLMETER_LOW,"Low"); UI_COLOR_CONFIG(GUI_COLOR_VOLMETER_HIGH,"High"); UI_COLOR_CONFIG(GUI_COLOR_VOLMETER_PEAK,"Clip"); ImGui::TreePop(); } + if (ImGui::TreeNode("Orders")) { + UI_COLOR_CONFIG(GUI_COLOR_ORDER_ROW_INDEX,"Order number"); + UI_COLOR_CONFIG(GUI_COLOR_ORDER_ACTIVE,"Current order background"); + UI_COLOR_CONFIG(GUI_COLOR_ORDER_SIMILAR,"Similar patterns"); + UI_COLOR_CONFIG(GUI_COLOR_ORDER_INACTIVE,"Inactive patterns"); + ImGui::TreePop(); + } if (ImGui::TreeNode("Macro Editor")) { UI_COLOR_CONFIG(GUI_COLOR_MACRO_VOLUME,"Volume"); UI_COLOR_CONFIG(GUI_COLOR_MACRO_PITCH,"Pitch"); @@ -464,6 +1009,7 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_VIC,"VIC"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_PET,"PET"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_VRC6,"VRC6"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_VRC6_SAW,"VRC6 (saw)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_OPLL,"FM (OPLL)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_OPL,"FM (OPL)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_FDS,"FDS"); @@ -475,6 +1021,8 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_BEEPER,"PC Beeper"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_SWAN,"WonderSwan"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_MIKEY,"Lynx"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_VERA,"VERA"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_X1_010,"X1-010"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,"Other/Unknown"); ImGui::TreePop(); } @@ -489,6 +1037,7 @@ void FurnaceGUI::drawSettings() { ImGui::TreePop(); } if (ImGui::TreeNode("Pattern")) { + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_PLAY_HEAD,"Playhead"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_CURSOR,"Cursor"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_CURSOR_HOVER,"Cursor (hovered)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_CURSOR_ACTIVE,"Cursor (clicked)"); @@ -498,9 +1047,17 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_PATTERN_HI_1,"Highlight 1"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_HI_2,"Highlight 2"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ROW_INDEX,"Row number"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ROW_INDEX_HI1,"Row number (highlight 1)"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ROW_INDEX_HI2,"Row number (highlight 2)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ACTIVE,"Note"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ACTIVE_HI1,"Note (highlight 1)"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_ACTIVE_HI2,"Note (highlight 2)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INACTIVE,"Blank"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INACTIVE_HI1,"Blank (highlight 1)"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INACTIVE_HI2,"Blank (highlight 2)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INS,"Instrument"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INS_WARN,"Instrument (invalid type)"); + UI_COLOR_CONFIG(GUI_COLOR_PATTERN_INS_ERROR,"Instrument (out of range)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_VOLUME_MIN,"Volume (0%)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_VOLUME_HALF,"Volume (50%)"); UI_COLOR_CONFIG(GUI_COLOR_PATTERN_VOLUME_MAX,"Volume (100%)"); @@ -517,38 +1074,58 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_EE_VALUE,"External command output"); ImGui::TreePop(); } + if (ImGui::TreeNode("Log Viewer")) { + UI_COLOR_CONFIG(GUI_COLOR_LOGLEVEL_ERROR,"Log level: Error"); + UI_COLOR_CONFIG(GUI_COLOR_LOGLEVEL_WARNING,"Log level: Warning"); + UI_COLOR_CONFIG(GUI_COLOR_LOGLEVEL_INFO,"Log level: Info"); + UI_COLOR_CONFIG(GUI_COLOR_LOGLEVEL_DEBUG,"Log level: Debug"); + UI_COLOR_CONFIG(GUI_COLOR_LOGLEVEL_TRACE,"Log level: Trace/Verbose"); + ImGui::TreePop(); + } ImGui::TreePop(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Keyboard")) { + if (ImGui::Button("Import")) { + openFileDialog(GUI_FILE_IMPORT_KEYBINDS); + } + ImGui::SameLine(); + if (ImGui::Button("Export")) { + openFileDialog(GUI_FILE_EXPORT_KEYBINDS); + } + ImGui::SameLine(); + if (ImGui::Button("Reset defaults")) { + showWarning("Are you sure you want to reset the keyboard settings?",GUI_WARN_RESET_KEYBINDS); + } if (ImGui::TreeNode("Global hotkeys")) { KEYBIND_CONFIG_BEGIN("keysGlobal"); - UI_KEYBIND_CONFIG(GUI_ACTION_OPEN,"Open file"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAVE,"Save file"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAVE_AS,"Save as"); - UI_KEYBIND_CONFIG(GUI_ACTION_UNDO,"Undo"); - UI_KEYBIND_CONFIG(GUI_ACTION_REDO,"Redo"); - UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_TOGGLE,"Play/Stop (toggle)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PLAY,"Play"); - UI_KEYBIND_CONFIG(GUI_ACTION_STOP,"Stop"); - UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_REPEAT,"Play (repeat pattern)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_CURSOR,"Play from cursor"); - UI_KEYBIND_CONFIG(GUI_ACTION_STEP_ONE,"Step row"); - UI_KEYBIND_CONFIG(GUI_ACTION_OCTAVE_UP,"Octave up"); - UI_KEYBIND_CONFIG(GUI_ACTION_OCTAVE_DOWN,"Octave down"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_UP,"Previous instrument"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_DOWN,"Next instrument"); - UI_KEYBIND_CONFIG(GUI_ACTION_STEP_UP,"Increase edit step"); - UI_KEYBIND_CONFIG(GUI_ACTION_STEP_DOWN,"Decrease edit step"); - UI_KEYBIND_CONFIG(GUI_ACTION_TOGGLE_EDIT,"Toggle edit mode"); - UI_KEYBIND_CONFIG(GUI_ACTION_METRONOME,"Metronome"); - UI_KEYBIND_CONFIG(GUI_ACTION_REPEAT_PATTERN,"Toggle repeat pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_FOLLOW_ORDERS,"Follow orders"); - UI_KEYBIND_CONFIG(GUI_ACTION_FOLLOW_PATTERN,"Follow pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PANIC,"Panic"); + UI_KEYBIND_CONFIG(GUI_ACTION_OPEN); + UI_KEYBIND_CONFIG(GUI_ACTION_OPEN_BACKUP); + UI_KEYBIND_CONFIG(GUI_ACTION_SAVE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAVE_AS); + UI_KEYBIND_CONFIG(GUI_ACTION_UNDO); + UI_KEYBIND_CONFIG(GUI_ACTION_REDO); + UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_TOGGLE); + UI_KEYBIND_CONFIG(GUI_ACTION_PLAY); + UI_KEYBIND_CONFIG(GUI_ACTION_STOP); + UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_REPEAT); + UI_KEYBIND_CONFIG(GUI_ACTION_PLAY_CURSOR); + UI_KEYBIND_CONFIG(GUI_ACTION_STEP_ONE); + UI_KEYBIND_CONFIG(GUI_ACTION_OCTAVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_OCTAVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_STEP_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_STEP_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_TOGGLE_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_METRONOME); + UI_KEYBIND_CONFIG(GUI_ACTION_REPEAT_PATTERN); + UI_KEYBIND_CONFIG(GUI_ACTION_FOLLOW_ORDERS); + UI_KEYBIND_CONFIG(GUI_ACTION_FOLLOW_PATTERN); + UI_KEYBIND_CONFIG(GUI_ACTION_PANIC); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -556,31 +1133,32 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Window activation")) { KEYBIND_CONFIG_BEGIN("keysWindow"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_EDIT_CONTROLS,"Edit Controls"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_ORDERS,"Orders"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_INS_LIST,"Instrument List"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_INS_EDIT,"Instrument Editor"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SONG_INFO,"Song Information"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_PATTERN,"Pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_WAVE_LIST,"Wavetable List"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_WAVE_EDIT,"Wavetable Editor"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SAMPLE_LIST,"Sample List"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SAMPLE_EDIT,"Sample Editor"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_ABOUT,"About"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SETTINGS,"Settings"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_MIXER,"Mixer"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_DEBUG,"Debug Menu"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_OSCILLOSCOPE,"Oscilloscope"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_VOL_METER,"Volume Meter"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_STATS,"Statistics"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_COMPAT_FLAGS,"Compatibility Flags"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_PIANO,"Piano"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_NOTES,"Song Comments"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_CHANNELS,"Channels"); - UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_REGISTER_VIEW,"Register View"); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_EDIT_CONTROLS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_ORDERS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_INS_LIST); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_INS_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SONG_INFO); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_PATTERN); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_WAVE_LIST); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_WAVE_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SAMPLE_LIST); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SAMPLE_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_ABOUT); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_SETTINGS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_MIXER); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_DEBUG); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_OSCILLOSCOPE); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_VOL_METER); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_STATS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_COMPAT_FLAGS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_PIANO); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_NOTES); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_CHANNELS); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_REGISTER_VIEW); + UI_KEYBIND_CONFIG(GUI_ACTION_WINDOW_LOG); - UI_KEYBIND_CONFIG(GUI_ACTION_COLLAPSE_WINDOW,"Collapse/expand current window"); - UI_KEYBIND_CONFIG(GUI_ACTION_CLOSE_WINDOW,"Close current window"); + UI_KEYBIND_CONFIG(GUI_ACTION_COLLAPSE_WINDOW); + UI_KEYBIND_CONFIG(GUI_ACTION_CLOSE_WINDOW); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -671,49 +1249,62 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Pattern")) { KEYBIND_CONFIG_BEGIN("keysPattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NOTE_UP,"Transpose (semitone up)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NOTE_DOWN,"Transpose (semitone down"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_OCTAVE_UP,"Transpose (octave up)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_OCTAVE_DOWN,"Transpose (octave down)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECT_ALL,"Select all"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CUT,"Cut"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COPY,"Copy"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE,"Paste"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP,"Move cursor up"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN,"Move cursor down"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_LEFT,"Move cursor left"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_RIGHT,"Move cursor right"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP_ONE,"Move cursor up by one (override Edit Step)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN_ONE,"Move cursor down by one (override Edit Step)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL,"Move cursor to previous channel"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL,"Move cursor to next channel"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL,"Move cursor to previous channel (overflow)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL,"Move cursor to next channel (overflow)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_BEGIN,"Move cursor to beginning of pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_END,"Move cursor to end of pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP_COARSE,"Move cursor up (coarse)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN_COARSE,"Move cursor down (coarse)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP,"Expand selection upwards"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN,"Expand selection downwards"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_LEFT,"Expand selection to the left"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_RIGHT,"Expand selection to the right"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP_ONE,"Expand selection upwards by one (override Edit Step)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN_ONE,"Expand selection downwards by one (override Edit Step)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_BEGIN,"Expand selection to beginning of pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_END,"Expand selection to end of pattern"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP_COARSE,"Expand selection upwards (coarse)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN_COARSE,"Expand selection downwards (coarse)"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_DELETE,"Delete"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PULL_DELETE,"Pull delete"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INSERT,"Insert"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_MUTE_CURSOR,"Mute channel at cursor"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SOLO_CURSOR,"Solo channel at cursor"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_UNMUTE_ALL,"Unmute all channels"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NEXT_ORDER,"Go to next order"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PREV_ORDER,"Go to previous order"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COLLAPSE,"Collapse channel at cursor"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INCREASE_COLUMNS,"Increase effect columns"); - UI_KEYBIND_CONFIG(GUI_ACTION_PAT_DECREASE_COLUMNS,"Decrease effect columns"); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NOTE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NOTE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_OCTAVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_OCTAVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECT_ALL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CUT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COPY); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE_MIX); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE_MIX_BG); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE_FLOOD); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PASTE_OVERFLOW); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_LEFT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_RIGHT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP_ONE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN_ONE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_BEGIN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_END); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_UP_COARSE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_CURSOR_DOWN_COARSE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_LEFT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_RIGHT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP_ONE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN_ONE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_BEGIN); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_END); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_UP_COARSE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SELECTION_DOWN_COARSE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PULL_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INSERT); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_MUTE_CURSOR); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_SOLO_CURSOR); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_UNMUTE_ALL); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_NEXT_ORDER); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_PREV_ORDER); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COLLAPSE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INCREASE_COLUMNS); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_DECREASE_COLUMNS); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INTERPOLATE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_FADE); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_INVERT_VALUES); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_FLIP_SELECTION); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_COLLAPSE_ROWS); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_EXPAND_ROWS); + UI_KEYBIND_CONFIG(GUI_ACTION_PAT_LATCH); + + // TODO: collapse/expand pattern and song KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -721,16 +1312,16 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Instrument list")) { KEYBIND_CONFIG_BEGIN("keysInsList"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_ADD,"Add"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DUPLICATE,"Duplicate"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_OPEN,"Open"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_SAVE,"Save"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_MOVE_UP,"Move up"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_MOVE_DOWN,"Move down"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DELETE,"Delete"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_EDIT,"Edit"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_UP,"Cursor up"); - UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DOWN,"Cursor down"); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_ADD); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DUPLICATE); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_OPEN); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_SAVE); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_MOVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_MOVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_INS_LIST_DOWN); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -738,16 +1329,16 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Wavetable list")) { KEYBIND_CONFIG_BEGIN("keysWaveList"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_ADD,"Add"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DUPLICATE,"Duplicate"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_OPEN,"Open"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_SAVE,"Save"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_MOVE_UP,"Move up"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_MOVE_DOWN,"Move down"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DELETE,"Delete"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_EDIT,"Edit"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_UP,"Cursor up"); - UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DOWN,"Cursor down"); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_ADD); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DUPLICATE); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_OPEN); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_SAVE); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_MOVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_MOVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_WAVE_LIST_DOWN); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -755,18 +1346,18 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Sample list")) { KEYBIND_CONFIG_BEGIN("keysSampleList"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_ADD,"Add"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DUPLICATE,"Duplicate"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_OPEN,"Open"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_SAVE,"Save"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_MOVE_UP,"Move up"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN,"Move down"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DELETE,"Delete"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_EDIT,"Edit"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_UP,"Cursor up"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DOWN,"Cursor down"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_PREVIEW,"Preview"); - UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW,"Stop preview"); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_ADD); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DUPLICATE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_OPEN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_SAVE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_MOVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_EDIT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_PREVIEW); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -774,23 +1365,57 @@ void FurnaceGUI::drawSettings() { if (ImGui::TreeNode("Orders")) { KEYBIND_CONFIG_BEGIN("keysOrders"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_UP,"Previous order"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DOWN,"Next order"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_LEFT,"Cursor left"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_RIGHT,"Cursor right"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_INCREASE,"Increase value"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DECREASE,"Decrease value"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_EDIT_MODE,"Switch edit mode"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_LINK,"Toggle alter entire row"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_ADD,"Add"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DUPLICATE,"Duplicate"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DEEP_CLONE,"Deep clone"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DUPLICATE_END,"Duplicate to end of song"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DEEP_CLONE_END,"Deep clone to end of song"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_REMOVE,"Remove"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_MOVE_UP,"Move up"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_MOVE_DOWN,"Move down"); - UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_REPLAY,"Replay"); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_LEFT); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_RIGHT); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_INCREASE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DECREASE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_EDIT_MODE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_LINK); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_ADD); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DUPLICATE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DEEP_CLONE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DUPLICATE_END); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_DEEP_CLONE_END); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_REMOVE); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_MOVE_UP); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_MOVE_DOWN); + UI_KEYBIND_CONFIG(GUI_ACTION_ORDERS_REPLAY); + + KEYBIND_CONFIG_END; + ImGui::TreePop(); + } + if (ImGui::TreeNode("Sample editor")) { + KEYBIND_CONFIG_BEGIN("keysSampleEdit"); + + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_SELECT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_DRAW); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_CUT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_COPY); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_PASTE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_PASTE_REPLACE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_PASTE_MIX); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_SELECT_ALL); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_RESIZE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_RESAMPLE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_AMPLIFY); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_NORMALIZE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_FADE_IN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_FADE_OUT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_INSERT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_SILENCE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_DELETE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_TRIM); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_REVERSE); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_INVERT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_SIGN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_FILTER); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_PREVIEW); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_STOP_PREVIEW); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_ZOOM_IN); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_ZOOM_OUT); + UI_KEYBIND_CONFIG(GUI_ACTION_SAMPLE_ZOOM_AUTO); KEYBIND_CONFIG_END; ImGui::TreePop(); @@ -814,8 +1439,13 @@ void FurnaceGUI::drawSettings() { ImGui::End(); } -#define LOAD_KEYBIND(x,y) \ - actionKeys[x]=e->getConfInt("keybind_" #x,y); +#define clampSetting(x,minV,maxV) \ + if (xmaxV) { \ + x=maxV; \ + } void FurnaceGUI::syncSettings() { settings.mainFontSize=e->getConfInt("mainFontSize",18); @@ -823,12 +1453,14 @@ void FurnaceGUI::syncSettings() { settings.iconSize=e->getConfInt("iconSize",16); settings.audioEngine=(e->getConfString("audioEngine","SDL")=="SDL")?1:0; settings.audioDevice=e->getConfString("audioDevice",""); + settings.midiInDevice=e->getConfString("midiInDevice",""); + settings.midiOutDevice=e->getConfString("midiOutDevice",""); settings.audioQuality=e->getConfInt("audioQuality",0); settings.audioBufSize=e->getConfInt("audioBufSize",1024); settings.audioRate=e->getConfInt("audioRate",44100); settings.arcadeCore=e->getConfInt("arcadeCore",0); settings.ym2612Core=e->getConfInt("ym2612Core",0); - settings.saaCore=e->getConfInt("saaCore",0); + settings.saaCore=e->getConfInt("saaCore",1); settings.mainFont=e->getConfInt("mainFont",0); settings.patFont=e->getConfInt("patFont",0); settings.mainFontPath=e->getConfString("mainFontPath",""); @@ -844,174 +1476,106 @@ void FurnaceGUI::syncSettings() { settings.allowEditDocking=e->getConfInt("allowEditDocking",0); settings.chipNames=e->getConfInt("chipNames",0); settings.overflowHighlight=e->getConfInt("overflowHighlight",0); - if (settings.fmNames<0 || settings.fmNames>2) settings.fmNames=0; settings.partyTime=e->getConfInt("partyTime",0); settings.germanNotation=e->getConfInt("germanNotation",0); settings.stepOnDelete=e->getConfInt("stepOnDelete",0); settings.scrollStep=e->getConfInt("scrollStep",0); settings.sysSeparators=e->getConfInt("sysSeparators",1); settings.forceMono=e->getConfInt("forceMono",0); - settings.controlLayout=e->getConfInt("controlLayout",0); + settings.controlLayout=e->getConfInt("controlLayout",3); settings.restartOnFlagChange=e->getConfInt("restartOnFlagChange",1); settings.statusDisplay=e->getConfInt("statusDisplay",0); settings.dpiScale=e->getConfFloat("dpiScale",0.0f); settings.viewPrevPattern=e->getConfInt("viewPrevPattern",1); + settings.guiColorsBase=e->getConfInt("guiColorsBase",0); + settings.avoidRaisingPattern=e->getConfInt("avoidRaisingPattern",0); + 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.roundedWindows=e->getConfInt("roundedWindows",1); + settings.roundedButtons=e->getConfInt("roundedButtons",1); + settings.roundedMenus=e->getConfInt("roundedMenus",0); + settings.loadJapanese=e->getConfInt("loadJapanese",0); + settings.fmLayout=e->getConfInt("fmLayout",0); + settings.susPosition=e->getConfInt("susPosition",0); + settings.effectCursorDir=e->getConfInt("effectCursorDir",1); + settings.cursorPastePos=e->getConfInt("cursorPastePos",1); + settings.titleBarInfo=e->getConfInt("titleBarInfo",1); + settings.titleBarSys=e->getConfInt("titleBarSys",1); + settings.frameBorders=e->getConfInt("frameBorders",0); + settings.effectDeletionAltersValue=e->getConfInt("effectDeletionAltersValue",1); + settings.oscRoundedCorners=e->getConfInt("oscRoundedCorners",1); + settings.oscTakesEntireWindow=e->getConfInt("oscTakesEntireWindow",0); + settings.oscBorder=e->getConfInt("oscBorder",1); + + clampSetting(settings.mainFontSize,2,96); + clampSetting(settings.patFontSize,2,96); + clampSetting(settings.iconSize,2,48); + clampSetting(settings.audioEngine,0,1); + clampSetting(settings.audioQuality,0,1); + clampSetting(settings.audioBufSize,32,4096); + clampSetting(settings.audioRate,8000,384000); + clampSetting(settings.arcadeCore,0,1); + clampSetting(settings.ym2612Core,0,1); + clampSetting(settings.saaCore,0,1); + clampSetting(settings.mainFont,0,6); + clampSetting(settings.patFont,0,6); + clampSetting(settings.patRowsBase,0,1); + clampSetting(settings.orderRowsBase,0,1); + clampSetting(settings.soloAction,0,2); + clampSetting(settings.pullDeleteBehavior,0,1); + clampSetting(settings.wrapHorizontal,0,2); + clampSetting(settings.wrapVertical,0,2); + clampSetting(settings.macroView,0,1); + clampSetting(settings.fmNames,0,2); + clampSetting(settings.allowEditDocking,0,1); + clampSetting(settings.chipNames,0,1); + clampSetting(settings.overflowHighlight,0,1); + clampSetting(settings.partyTime,0,1); + clampSetting(settings.germanNotation,0,1); + clampSetting(settings.stepOnDelete,0,1); + clampSetting(settings.scrollStep,0,1); + clampSetting(settings.sysSeparators,0,1); + clampSetting(settings.forceMono,0,1); + clampSetting(settings.controlLayout,0,3); + clampSetting(settings.statusDisplay,0,3); + clampSetting(settings.dpiScale,0.0f,4.0f); + clampSetting(settings.viewPrevPattern,0,1); + clampSetting(settings.guiColorsBase,0,1); + clampSetting(settings.avoidRaisingPattern,0,1); + clampSetting(settings.insFocusesPattern,0,1); + clampSetting(settings.stepOnInsert,0,1); + clampSetting(settings.unifiedDataView,0,1); + clampSetting(settings.sysFileDialog,0,1); + clampSetting(settings.roundedWindows,0,1); + clampSetting(settings.roundedButtons,0,1); + clampSetting(settings.roundedMenus,0,1); + clampSetting(settings.loadJapanese,0,1); + clampSetting(settings.fmLayout,0,3); + clampSetting(settings.susPosition,0,1); + clampSetting(settings.effectCursorDir,0,2); + clampSetting(settings.cursorPastePos,0,1); + clampSetting(settings.titleBarInfo,0,3); + clampSetting(settings.titleBarSys,0,1); + clampSetting(settings.frameBorders,0,1); + clampSetting(settings.effectDeletionAltersValue,0,1); // keybinds - LOAD_KEYBIND(GUI_ACTION_OPEN,FURKMOD_CMD|SDLK_o); - LOAD_KEYBIND(GUI_ACTION_SAVE,FURKMOD_CMD|SDLK_s); - LOAD_KEYBIND(GUI_ACTION_SAVE_AS,FURKMOD_CMD|FURKMOD_SHIFT|SDLK_s); - LOAD_KEYBIND(GUI_ACTION_UNDO,FURKMOD_CMD|SDLK_z); - LOAD_KEYBIND(GUI_ACTION_REDO,FURKMOD_CMD|SDLK_y); - LOAD_KEYBIND(GUI_ACTION_PLAY_TOGGLE,SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_PLAY,0); - LOAD_KEYBIND(GUI_ACTION_STOP,0); - LOAD_KEYBIND(GUI_ACTION_PLAY_REPEAT,0); - LOAD_KEYBIND(GUI_ACTION_PLAY_CURSOR,FURKMOD_SHIFT|SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_STEP_ONE,FURKMOD_CMD|SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_OCTAVE_UP,SDLK_KP_MULTIPLY); - LOAD_KEYBIND(GUI_ACTION_OCTAVE_DOWN,SDLK_KP_DIVIDE); - LOAD_KEYBIND(GUI_ACTION_INS_UP,FURKMOD_SHIFT|SDLK_KP_DIVIDE); - LOAD_KEYBIND(GUI_ACTION_INS_DOWN,FURKMOD_SHIFT|SDLK_KP_MULTIPLY); - LOAD_KEYBIND(GUI_ACTION_STEP_UP,FURKMOD_CMD|SDLK_KP_MULTIPLY); - LOAD_KEYBIND(GUI_ACTION_STEP_DOWN,FURKMOD_CMD|SDLK_KP_DIVIDE); - LOAD_KEYBIND(GUI_ACTION_TOGGLE_EDIT,SDLK_SPACE); - LOAD_KEYBIND(GUI_ACTION_METRONOME,FURKMOD_CMD|SDLK_m); - LOAD_KEYBIND(GUI_ACTION_REPEAT_PATTERN,0); - LOAD_KEYBIND(GUI_ACTION_FOLLOW_ORDERS,0); - LOAD_KEYBIND(GUI_ACTION_FOLLOW_PATTERN,0); - LOAD_KEYBIND(GUI_ACTION_PANIC,SDLK_F12); - - LOAD_KEYBIND(GUI_ACTION_WINDOW_EDIT_CONTROLS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_ORDERS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_INS_LIST,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_INS_EDIT,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_SONG_INFO,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_PATTERN,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_WAVE_LIST,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_WAVE_EDIT,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_SAMPLE_LIST,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_SAMPLE_EDIT,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_ABOUT,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_SETTINGS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_MIXER,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_DEBUG,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_OSCILLOSCOPE,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_VOL_METER,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_STATS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_COMPAT_FLAGS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_PIANO,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_NOTES,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_CHANNELS,0); - LOAD_KEYBIND(GUI_ACTION_WINDOW_REGISTER_VIEW,0); - - LOAD_KEYBIND(GUI_ACTION_COLLAPSE_WINDOW,0); - LOAD_KEYBIND(GUI_ACTION_CLOSE_WINDOW,FURKMOD_SHIFT|SDLK_ESCAPE); - - LOAD_KEYBIND(GUI_ACTION_PAT_NOTE_UP,FURKMOD_CMD|SDLK_F2); - LOAD_KEYBIND(GUI_ACTION_PAT_NOTE_DOWN,FURKMOD_CMD|SDLK_F1); - LOAD_KEYBIND(GUI_ACTION_PAT_OCTAVE_UP,FURKMOD_CMD|SDLK_F4); - LOAD_KEYBIND(GUI_ACTION_PAT_OCTAVE_DOWN,FURKMOD_CMD|SDLK_F3); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECT_ALL,FURKMOD_CMD|SDLK_a); - LOAD_KEYBIND(GUI_ACTION_PAT_CUT,FURKMOD_CMD|SDLK_x); - LOAD_KEYBIND(GUI_ACTION_PAT_COPY,FURKMOD_CMD|SDLK_c); - LOAD_KEYBIND(GUI_ACTION_PAT_PASTE,FURKMOD_CMD|SDLK_v); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_UP,SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN,SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_LEFT,SDLK_LEFT); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_RIGHT,SDLK_RIGHT); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_UP_ONE,FURKMOD_SHIFT|SDLK_HOME); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN_ONE,FURKMOD_SHIFT|SDLK_END); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL,0); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL,0); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL,0); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL,0); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_BEGIN,SDLK_HOME); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_END,SDLK_END); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_UP_COARSE,SDLK_PAGEUP); - LOAD_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN_COARSE,SDLK_PAGEDOWN); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_UP,FURKMOD_SHIFT|SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN,FURKMOD_SHIFT|SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_LEFT,FURKMOD_SHIFT|SDLK_LEFT); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_RIGHT,FURKMOD_SHIFT|SDLK_RIGHT); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_UP_ONE,0); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN_ONE,0); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_BEGIN,0); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_END,0); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_UP_COARSE,FURKMOD_SHIFT|SDLK_PAGEUP); - LOAD_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN_COARSE,FURKMOD_SHIFT|SDLK_PAGEDOWN); - LOAD_KEYBIND(GUI_ACTION_PAT_DELETE,SDLK_DELETE); - LOAD_KEYBIND(GUI_ACTION_PAT_PULL_DELETE,SDLK_BACKSPACE); - LOAD_KEYBIND(GUI_ACTION_PAT_INSERT,SDLK_INSERT); - LOAD_KEYBIND(GUI_ACTION_PAT_MUTE_CURSOR,FURKMOD_ALT|SDLK_F9); - LOAD_KEYBIND(GUI_ACTION_PAT_SOLO_CURSOR,FURKMOD_ALT|SDLK_F10); - LOAD_KEYBIND(GUI_ACTION_PAT_UNMUTE_ALL,FURKMOD_ALT|FURKMOD_SHIFT|SDLK_F9); - LOAD_KEYBIND(GUI_ACTION_PAT_NEXT_ORDER,0); - LOAD_KEYBIND(GUI_ACTION_PAT_PREV_ORDER,0); - LOAD_KEYBIND(GUI_ACTION_PAT_COLLAPSE,0); - LOAD_KEYBIND(GUI_ACTION_PAT_INCREASE_COLUMNS,0); - LOAD_KEYBIND(GUI_ACTION_PAT_DECREASE_COLUMNS,0); - - LOAD_KEYBIND(GUI_ACTION_INS_LIST_ADD,SDLK_INSERT); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_DUPLICATE,FURKMOD_CMD|SDLK_d); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_OPEN,0); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_SAVE,0); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_MOVE_UP,FURKMOD_SHIFT|SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_MOVE_DOWN,FURKMOD_SHIFT|SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_DELETE,0); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_EDIT,FURKMOD_SHIFT|SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_UP,SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_INS_LIST_DOWN,SDLK_DOWN); - - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_ADD,SDLK_INSERT); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_DUPLICATE,FURKMOD_CMD|SDLK_d); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_OPEN,0); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_SAVE,0); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_MOVE_UP,FURKMOD_SHIFT|SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_MOVE_DOWN,FURKMOD_SHIFT|SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_DELETE,0); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_EDIT,FURKMOD_SHIFT|SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_UP,SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_WAVE_LIST_DOWN,SDLK_DOWN); - - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_ADD,SDLK_INSERT); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_DUPLICATE,FURKMOD_CMD|SDLK_d); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_OPEN,0); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_SAVE,0); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_MOVE_UP,FURKMOD_SHIFT|SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN,FURKMOD_SHIFT|SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_DELETE,0); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_EDIT,FURKMOD_SHIFT|SDLK_RETURN); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_UP,SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_DOWN,SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_PREVIEW,0); - LOAD_KEYBIND(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW,0); - - LOAD_KEYBIND(GUI_ACTION_ORDERS_UP,SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DOWN,SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_ORDERS_LEFT,SDLK_LEFT); - LOAD_KEYBIND(GUI_ACTION_ORDERS_RIGHT,SDLK_RIGHT); - LOAD_KEYBIND(GUI_ACTION_ORDERS_INCREASE,0); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DECREASE,0); - LOAD_KEYBIND(GUI_ACTION_ORDERS_EDIT_MODE,0); - LOAD_KEYBIND(GUI_ACTION_ORDERS_LINK,FURKMOD_CMD|SDLK_l); - LOAD_KEYBIND(GUI_ACTION_ORDERS_ADD,SDLK_INSERT); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DUPLICATE,FURKMOD_CMD|SDLK_d); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DEEP_CLONE,FURKMOD_CMD|FURKMOD_SHIFT|SDLK_d); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DUPLICATE_END,FURKMOD_CMD|SDLK_e); - LOAD_KEYBIND(GUI_ACTION_ORDERS_DEEP_CLONE_END,FURKMOD_CMD|FURKMOD_SHIFT|SDLK_e); - LOAD_KEYBIND(GUI_ACTION_ORDERS_REMOVE,SDLK_DELETE); - LOAD_KEYBIND(GUI_ACTION_ORDERS_MOVE_UP,FURKMOD_SHIFT|SDLK_UP); - LOAD_KEYBIND(GUI_ACTION_ORDERS_MOVE_DOWN,FURKMOD_SHIFT|SDLK_DOWN); - LOAD_KEYBIND(GUI_ACTION_ORDERS_REPLAY,0); + for (int i=0; igetConfInt(String("keybind_GUI_ACTION_")+String(guiActions[i].name),guiActions[i].defaultBind); + } decodeKeyMap(noteKeys,e->getConfString("noteKeys",DEFAULT_NOTE_KEYS)); parseKeybinds(); -} -#define PUT_UI_COLOR(source) e->setConf(#source,(int)ImGui::GetColorU32(uiColors[source])); -#define SAVE_KEYBIND(x) e->setConf("keybind_" #x,actionKeys[x]); + midiMap.read(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); + midiMap.compile(); + + e->setMidiDirect(midiMap.directChannel); +} void FurnaceGUI::commitSettings() { e->setConf("mainFontSize",settings.mainFontSize); @@ -1019,6 +1583,8 @@ void FurnaceGUI::commitSettings() { e->setConf("iconSize",settings.iconSize); e->setConf("audioEngine",String(audioBackends[settings.audioEngine])); e->setConf("audioDevice",settings.audioDevice); + e->setConf("midiInDevice",settings.midiInDevice); + e->setConf("midiOutDevice",settings.midiOutDevice); e->setConf("audioQuality",settings.audioQuality); e->setConf("audioBufSize",settings.audioBufSize); e->setConf("audioRate",settings.audioRate); @@ -1051,232 +1617,46 @@ void FurnaceGUI::commitSettings() { e->setConf("statusDisplay",settings.statusDisplay); e->setConf("dpiScale",settings.dpiScale); e->setConf("viewPrevPattern",settings.viewPrevPattern); + e->setConf("guiColorsBase",settings.guiColorsBase); + e->setConf("avoidRaisingPattern",settings.avoidRaisingPattern); + e->setConf("insFocusesPattern",settings.insFocusesPattern); + e->setConf("stepOnInsert",settings.stepOnInsert); + e->setConf("unifiedDataView",settings.unifiedDataView); + e->setConf("sysFileDialog",settings.sysFileDialog); + e->setConf("roundedWindows",settings.roundedWindows); + e->setConf("roundedButtons",settings.roundedButtons); + e->setConf("roundedMenus",settings.roundedMenus); + e->setConf("loadJapanese",settings.loadJapanese); + e->setConf("fmLayout",settings.fmLayout); + e->setConf("susPosition",settings.susPosition); + e->setConf("effectCursorDir",settings.effectCursorDir); + e->setConf("cursorPastePos",settings.cursorPastePos); + e->setConf("titleBarInfo",settings.titleBarInfo); + e->setConf("titleBarSys",settings.titleBarSys); + e->setConf("frameBorders",settings.frameBorders); + e->setConf("effectDeletionAltersValue",settings.effectDeletionAltersValue); + e->setConf("oscRoundedCorners",settings.oscRoundedCorners); + e->setConf("oscTakesEntireWindow",settings.oscTakesEntireWindow); + e->setConf("oscBorder",settings.oscBorder); - PUT_UI_COLOR(GUI_COLOR_BACKGROUND); - PUT_UI_COLOR(GUI_COLOR_FRAME_BACKGROUND); - PUT_UI_COLOR(GUI_COLOR_MODAL_BACKDROP); - PUT_UI_COLOR(GUI_COLOR_HEADER); - PUT_UI_COLOR(GUI_COLOR_TEXT); - PUT_UI_COLOR(GUI_COLOR_ACCENT_PRIMARY); - PUT_UI_COLOR(GUI_COLOR_ACCENT_SECONDARY); - PUT_UI_COLOR(GUI_COLOR_EDITING); - PUT_UI_COLOR(GUI_COLOR_SONG_LOOP); - PUT_UI_COLOR(GUI_COLOR_VOLMETER_LOW); - PUT_UI_COLOR(GUI_COLOR_VOLMETER_HIGH); - PUT_UI_COLOR(GUI_COLOR_VOLMETER_PEAK); - PUT_UI_COLOR(GUI_COLOR_MACRO_VOLUME); - PUT_UI_COLOR(GUI_COLOR_MACRO_PITCH); - PUT_UI_COLOR(GUI_COLOR_MACRO_OTHER); - PUT_UI_COLOR(GUI_COLOR_MACRO_WAVE); - PUT_UI_COLOR(GUI_COLOR_INSTR_FM); - PUT_UI_COLOR(GUI_COLOR_INSTR_STD); - PUT_UI_COLOR(GUI_COLOR_INSTR_GB); - PUT_UI_COLOR(GUI_COLOR_INSTR_C64); - PUT_UI_COLOR(GUI_COLOR_INSTR_AMIGA); - PUT_UI_COLOR(GUI_COLOR_INSTR_PCE); - PUT_UI_COLOR(GUI_COLOR_INSTR_AY); - PUT_UI_COLOR(GUI_COLOR_INSTR_AY8930); - PUT_UI_COLOR(GUI_COLOR_INSTR_TIA); - PUT_UI_COLOR(GUI_COLOR_INSTR_SAA1099); - PUT_UI_COLOR(GUI_COLOR_INSTR_VIC); - PUT_UI_COLOR(GUI_COLOR_INSTR_PET); - PUT_UI_COLOR(GUI_COLOR_INSTR_VRC6); - PUT_UI_COLOR(GUI_COLOR_INSTR_OPLL); - PUT_UI_COLOR(GUI_COLOR_INSTR_OPL); - PUT_UI_COLOR(GUI_COLOR_INSTR_FDS); - PUT_UI_COLOR(GUI_COLOR_INSTR_VBOY); - PUT_UI_COLOR(GUI_COLOR_INSTR_N163); - PUT_UI_COLOR(GUI_COLOR_INSTR_SCC); - PUT_UI_COLOR(GUI_COLOR_INSTR_OPZ); - PUT_UI_COLOR(GUI_COLOR_INSTR_POKEY); - PUT_UI_COLOR(GUI_COLOR_INSTR_BEEPER); - PUT_UI_COLOR(GUI_COLOR_INSTR_SWAN); - PUT_UI_COLOR(GUI_COLOR_INSTR_MIKEY); - PUT_UI_COLOR(GUI_COLOR_INSTR_UNKNOWN); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_FM); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_PULSE); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_NOISE); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_PCM); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_WAVE); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_OP); - PUT_UI_COLOR(GUI_COLOR_CHANNEL_MUTED); - PUT_UI_COLOR(GUI_COLOR_PATTERN_CURSOR); - PUT_UI_COLOR(GUI_COLOR_PATTERN_CURSOR_HOVER); - PUT_UI_COLOR(GUI_COLOR_PATTERN_CURSOR_ACTIVE); - PUT_UI_COLOR(GUI_COLOR_PATTERN_SELECTION); - PUT_UI_COLOR(GUI_COLOR_PATTERN_SELECTION_HOVER); - PUT_UI_COLOR(GUI_COLOR_PATTERN_SELECTION_ACTIVE); - PUT_UI_COLOR(GUI_COLOR_PATTERN_HI_1); - PUT_UI_COLOR(GUI_COLOR_PATTERN_HI_2); - PUT_UI_COLOR(GUI_COLOR_PATTERN_ROW_INDEX); - PUT_UI_COLOR(GUI_COLOR_PATTERN_ACTIVE); - PUT_UI_COLOR(GUI_COLOR_PATTERN_INACTIVE); - PUT_UI_COLOR(GUI_COLOR_PATTERN_INS); - PUT_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_MIN); - PUT_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_HALF); - PUT_UI_COLOR(GUI_COLOR_PATTERN_VOLUME_MAX); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_INVALID); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_PITCH); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_VOLUME); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_PANNING); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SONG); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_TIME); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SPEED); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY); - PUT_UI_COLOR(GUI_COLOR_PATTERN_EFFECT_MISC); - PUT_UI_COLOR(GUI_COLOR_EE_VALUE); - PUT_UI_COLOR(GUI_COLOR_PLAYBACK_STAT); + // colors + for (int i=0; isetConf(guiColors[i].name,(int)ImGui::ColorConvertFloat4ToU32(uiColors[i])); + } - SAVE_KEYBIND(GUI_ACTION_OPEN); - SAVE_KEYBIND(GUI_ACTION_SAVE); - SAVE_KEYBIND(GUI_ACTION_SAVE_AS); - SAVE_KEYBIND(GUI_ACTION_UNDO); - SAVE_KEYBIND(GUI_ACTION_REDO); - SAVE_KEYBIND(GUI_ACTION_PLAY_TOGGLE); - SAVE_KEYBIND(GUI_ACTION_PLAY); - SAVE_KEYBIND(GUI_ACTION_STOP); - SAVE_KEYBIND(GUI_ACTION_PLAY_REPEAT); - SAVE_KEYBIND(GUI_ACTION_PLAY_CURSOR); - SAVE_KEYBIND(GUI_ACTION_STEP_ONE); - SAVE_KEYBIND(GUI_ACTION_OCTAVE_UP); - SAVE_KEYBIND(GUI_ACTION_OCTAVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_INS_UP); - SAVE_KEYBIND(GUI_ACTION_INS_DOWN); - SAVE_KEYBIND(GUI_ACTION_STEP_UP); - SAVE_KEYBIND(GUI_ACTION_STEP_DOWN); - SAVE_KEYBIND(GUI_ACTION_TOGGLE_EDIT); - SAVE_KEYBIND(GUI_ACTION_METRONOME); - SAVE_KEYBIND(GUI_ACTION_REPEAT_PATTERN); - SAVE_KEYBIND(GUI_ACTION_FOLLOW_ORDERS); - SAVE_KEYBIND(GUI_ACTION_FOLLOW_PATTERN); - SAVE_KEYBIND(GUI_ACTION_PANIC); + // keybinds + for (int i=0; isetConf(String("keybind_GUI_ACTION_")+String(guiActions[i].name),actionKeys[i]); + } - SAVE_KEYBIND(GUI_ACTION_WINDOW_EDIT_CONTROLS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_ORDERS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_INS_LIST); - SAVE_KEYBIND(GUI_ACTION_WINDOW_INS_EDIT); - SAVE_KEYBIND(GUI_ACTION_WINDOW_SONG_INFO); - SAVE_KEYBIND(GUI_ACTION_WINDOW_PATTERN); - SAVE_KEYBIND(GUI_ACTION_WINDOW_WAVE_LIST); - SAVE_KEYBIND(GUI_ACTION_WINDOW_WAVE_EDIT); - SAVE_KEYBIND(GUI_ACTION_WINDOW_SAMPLE_LIST); - SAVE_KEYBIND(GUI_ACTION_WINDOW_SAMPLE_EDIT); - SAVE_KEYBIND(GUI_ACTION_WINDOW_ABOUT); - SAVE_KEYBIND(GUI_ACTION_WINDOW_SETTINGS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_MIXER); - SAVE_KEYBIND(GUI_ACTION_WINDOW_DEBUG); - SAVE_KEYBIND(GUI_ACTION_WINDOW_OSCILLOSCOPE); - SAVE_KEYBIND(GUI_ACTION_WINDOW_VOL_METER); - SAVE_KEYBIND(GUI_ACTION_WINDOW_STATS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_COMPAT_FLAGS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_PIANO); - SAVE_KEYBIND(GUI_ACTION_WINDOW_NOTES); - SAVE_KEYBIND(GUI_ACTION_WINDOW_CHANNELS); - SAVE_KEYBIND(GUI_ACTION_WINDOW_REGISTER_VIEW); - - SAVE_KEYBIND(GUI_ACTION_COLLAPSE_WINDOW); - SAVE_KEYBIND(GUI_ACTION_CLOSE_WINDOW); - - SAVE_KEYBIND(GUI_ACTION_PAT_NOTE_UP); - SAVE_KEYBIND(GUI_ACTION_PAT_NOTE_DOWN); - SAVE_KEYBIND(GUI_ACTION_PAT_OCTAVE_UP); - SAVE_KEYBIND(GUI_ACTION_PAT_OCTAVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECT_ALL); - SAVE_KEYBIND(GUI_ACTION_PAT_CUT); - SAVE_KEYBIND(GUI_ACTION_PAT_COPY); - SAVE_KEYBIND(GUI_ACTION_PAT_PASTE); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_UP); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_LEFT); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_RIGHT); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_UP_ONE); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN_ONE); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_LEFT_CHANNEL); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_RIGHT_CHANNEL); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_NEXT_CHANNEL); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_PREVIOUS_CHANNEL); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_BEGIN); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_END); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_UP_COARSE); - SAVE_KEYBIND(GUI_ACTION_PAT_CURSOR_DOWN_COARSE); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_UP); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_LEFT); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_RIGHT); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_UP_ONE); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN_ONE); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_BEGIN); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_END); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_UP_COARSE); - SAVE_KEYBIND(GUI_ACTION_PAT_SELECTION_DOWN_COARSE); - SAVE_KEYBIND(GUI_ACTION_PAT_DELETE); - SAVE_KEYBIND(GUI_ACTION_PAT_PULL_DELETE); - SAVE_KEYBIND(GUI_ACTION_PAT_INSERT); - SAVE_KEYBIND(GUI_ACTION_PAT_MUTE_CURSOR); - SAVE_KEYBIND(GUI_ACTION_PAT_SOLO_CURSOR); - SAVE_KEYBIND(GUI_ACTION_PAT_UNMUTE_ALL); - SAVE_KEYBIND(GUI_ACTION_PAT_NEXT_ORDER); - SAVE_KEYBIND(GUI_ACTION_PAT_PREV_ORDER); - SAVE_KEYBIND(GUI_ACTION_PAT_COLLAPSE); - SAVE_KEYBIND(GUI_ACTION_PAT_INCREASE_COLUMNS); - SAVE_KEYBIND(GUI_ACTION_PAT_DECREASE_COLUMNS); - - SAVE_KEYBIND(GUI_ACTION_INS_LIST_ADD); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_DUPLICATE); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_OPEN); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_SAVE); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_MOVE_UP); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_MOVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_DELETE); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_EDIT); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_UP); - SAVE_KEYBIND(GUI_ACTION_INS_LIST_DOWN); - - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_ADD); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_DUPLICATE); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_OPEN); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_SAVE); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_MOVE_UP); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_MOVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_DELETE); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_EDIT); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_UP); - SAVE_KEYBIND(GUI_ACTION_WAVE_LIST_DOWN); - - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_ADD); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_DUPLICATE); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_OPEN); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_SAVE); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_MOVE_UP); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_MOVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_DELETE); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_EDIT); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_UP); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_DOWN); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_PREVIEW); - SAVE_KEYBIND(GUI_ACTION_SAMPLE_LIST_STOP_PREVIEW); - - SAVE_KEYBIND(GUI_ACTION_ORDERS_UP); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DOWN); - SAVE_KEYBIND(GUI_ACTION_ORDERS_LEFT); - SAVE_KEYBIND(GUI_ACTION_ORDERS_RIGHT); - SAVE_KEYBIND(GUI_ACTION_ORDERS_INCREASE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DECREASE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_EDIT_MODE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_LINK); - SAVE_KEYBIND(GUI_ACTION_ORDERS_ADD); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DUPLICATE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DEEP_CLONE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DUPLICATE_END); - SAVE_KEYBIND(GUI_ACTION_ORDERS_DEEP_CLONE_END); - SAVE_KEYBIND(GUI_ACTION_ORDERS_REMOVE); - SAVE_KEYBIND(GUI_ACTION_ORDERS_MOVE_UP); - SAVE_KEYBIND(GUI_ACTION_ORDERS_MOVE_DOWN); - SAVE_KEYBIND(GUI_ACTION_ORDERS_REPLAY); + parseKeybinds(); e->setConf("noteKeys",encodeKeyMap(noteKeys)); + midiMap.compile(); + midiMap.write(e->getConfigPath()+DIR_SEPARATOR_STR+"midiIn_"+stripName(settings.midiInDevice)+".cfg"); + e->saveConf(); if (!e->switchMaster()) { @@ -1289,14 +1669,595 @@ void FurnaceGUI::commitSettings() { ImGui_ImplSDLRenderer_DestroyFontsTexture(); if (!ImGui::GetIO().Fonts->Build()) { - logE("error while building font atlas!\n"); + logE("error while building font atlas!"); showError("error while loading fonts! please check your settings."); ImGui::GetIO().Fonts->Clear(); mainFont=ImGui::GetIO().Fonts->AddFontDefault(); patFont=mainFont; ImGui_ImplSDLRenderer_DestroyFontsTexture(); if (!ImGui::GetIO().Fonts->Build()) { - logE("error again while building font atlas!\n"); + logE("error again while building font atlas!"); } } } + +bool FurnaceGUI::importColors(String path) { + FILE* f=ps_fopen(path.c_str(),"rb"); + if (f==NULL) { + logW("error while opening color file for import: %s",strerror(errno)); + return false; + } + resetColors(); + char line[4096]; + while (!feof(f)) { + String key=""; + String value=""; + bool keyOrValue=false; + if (fgets(line,4095,f)==NULL) { + break; + } + for (char* i=line; *i; i++) { + if (*i=='\n') continue; + if (keyOrValue) { + value+=*i; + } else { + if (*i=='=') { + keyOrValue=true; + } else { + key+=*i; + } + } + } + if (keyOrValue) { + // unoptimal + const char* cs=key.c_str(); + bool found=false; + for (int i=0; i>1)) { + fclose(f); + return false; + } + if (len<1) { + if (len==0) { + logE("that file is empty!"); + lastError="file is empty"; + } else { + perror("tell error"); + } + fclose(f); + return false; + } + unsigned char* file=new unsigned char[len]; + if (fseek(f,0,SEEK_SET)<0) { + perror("size error"); + lastError=fmt::sprintf("on get size: %s",strerror(errno)); + fclose(f); + delete[] file; + return false; + } + if (fread(file,1,(size_t)len,f)!=(size_t)len) { + perror("read error"); + lastError=fmt::sprintf("on read: %s",strerror(errno)); + fclose(f); + delete[] file; + return false; + } + fclose(f); + + ImGui::LoadIniSettingsFromMemory((const char*)file,len); + delete[] file; + return true; +} + +bool FurnaceGUI::exportLayout(String path) { + FILE* f=ps_fopen(path.c_str(),"wb"); + if (f==NULL) { + logW("error while opening layout file for export: %s",strerror(errno)); + return false; + } + size_t dataSize=0; + const char* data=ImGui::SaveIniSettingsToMemory(&dataSize); + if (fwrite(data,1,dataSize,f)!=dataSize) { + logW("error while exporting layout: %s",strerror(errno)); + } + fclose(f); + return true; +} + +void FurnaceGUI::resetColors() { + for (int i=0; i=0.5f) dpiScale=settings.dpiScale; + + // colors + if (updateFonts) { + for (int i=0; igetConfInt(guiColors[i].name,guiColors[i].defaultColor)); + } + } + + for (int i=0; i<64; i++) { + ImVec4 col1=uiColors[GUI_COLOR_PATTERN_VOLUME_MIN]; + ImVec4 col2=uiColors[GUI_COLOR_PATTERN_VOLUME_HALF]; + ImVec4 col3=uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]; + volColors[i]=ImVec4(col1.x+((col2.x-col1.x)*float(i)/64.0f), + col1.y+((col2.y-col1.y)*float(i)/64.0f), + col1.z+((col2.z-col1.z)*float(i)/64.0f), + 1.0f); + volColors[i+64]=ImVec4(col2.x+((col3.x-col2.x)*float(i)/64.0f), + col2.y+((col3.y-col2.y)*float(i)/64.0f), + col2.z+((col3.z-col2.z)*float(i)/64.0f), + 1.0f); + } + + float hue, sat, val; + + ImVec4 primaryActive=uiColors[GUI_COLOR_ACCENT_PRIMARY]; + ImVec4 primaryHover, primary; + primaryHover.w=primaryActive.w; + primary.w=primaryActive.w; + ImGui::ColorConvertRGBtoHSV(primaryActive.x,primaryActive.y,primaryActive.z,hue,sat,val); + if (settings.guiColorsBase) { + primary=primaryActive; + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.9,primaryHover.x,primaryHover.y,primaryHover.z); + ImGui::ColorConvertHSVtoRGB(hue,sat,val*0.5,primaryActive.x,primaryActive.y,primaryActive.z); + } else { + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.5,primaryHover.x,primaryHover.y,primaryHover.z); + ImGui::ColorConvertHSVtoRGB(hue,sat*0.8,val*0.35,primary.x,primary.y,primary.z); + } + + ImVec4 secondaryActive=uiColors[GUI_COLOR_ACCENT_SECONDARY]; + ImVec4 secondaryHover, secondary, secondarySemiActive; + secondarySemiActive.w=secondaryActive.w; + secondaryHover.w=secondaryActive.w; + secondary.w=secondaryActive.w; + ImGui::ColorConvertRGBtoHSV(secondaryActive.x,secondaryActive.y,secondaryActive.z,hue,sat,val); + if (settings.guiColorsBase) { + secondary=secondaryActive; + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.7,secondarySemiActive.x,secondarySemiActive.y,secondarySemiActive.z); + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.9,secondaryHover.x,secondaryHover.y,secondaryHover.z); + ImGui::ColorConvertHSVtoRGB(hue,sat,val*0.5,secondaryActive.x,secondaryActive.y,secondaryActive.z); + } else { + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.75,secondarySemiActive.x,secondarySemiActive.y,secondarySemiActive.z); + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.5,secondaryHover.x,secondaryHover.y,secondaryHover.z); + ImGui::ColorConvertHSVtoRGB(hue,sat*0.9,val*0.25,secondary.x,secondary.y,secondary.z); + } + + + sty.Colors[ImGuiCol_WindowBg]=uiColors[GUI_COLOR_FRAME_BACKGROUND]; + sty.Colors[ImGuiCol_ModalWindowDimBg]=uiColors[GUI_COLOR_MODAL_BACKDROP]; + sty.Colors[ImGuiCol_Text]=uiColors[GUI_COLOR_TEXT]; + + sty.Colors[ImGuiCol_Button]=primary; + sty.Colors[ImGuiCol_ButtonHovered]=primaryHover; + sty.Colors[ImGuiCol_ButtonActive]=primaryActive; + sty.Colors[ImGuiCol_Tab]=primary; + sty.Colors[ImGuiCol_TabHovered]=secondaryHover; + sty.Colors[ImGuiCol_TabActive]=secondarySemiActive; + sty.Colors[ImGuiCol_TabUnfocused]=primary; + sty.Colors[ImGuiCol_TabUnfocusedActive]=primaryHover; + sty.Colors[ImGuiCol_Header]=secondary; + sty.Colors[ImGuiCol_HeaderHovered]=secondaryHover; + sty.Colors[ImGuiCol_HeaderActive]=secondaryActive; + sty.Colors[ImGuiCol_ResizeGrip]=secondary; + sty.Colors[ImGuiCol_ResizeGripHovered]=secondaryHover; + sty.Colors[ImGuiCol_ResizeGripActive]=secondaryActive; + sty.Colors[ImGuiCol_FrameBg]=secondary; + sty.Colors[ImGuiCol_FrameBgHovered]=secondaryHover; + sty.Colors[ImGuiCol_FrameBgActive]=secondaryActive; + sty.Colors[ImGuiCol_SliderGrab]=primaryActive; + sty.Colors[ImGuiCol_SliderGrabActive]=primaryActive; + sty.Colors[ImGuiCol_TitleBgActive]=primary; + sty.Colors[ImGuiCol_CheckMark]=primaryActive; + sty.Colors[ImGuiCol_TextSelectedBg]=secondaryHover; + sty.Colors[ImGuiCol_PlotHistogram]=uiColors[GUI_COLOR_MACRO_OTHER]; + sty.Colors[ImGuiCol_PlotHistogramHovered]=uiColors[GUI_COLOR_MACRO_OTHER]; + sty.Colors[ImGuiCol_Border]=uiColors[GUI_COLOR_BORDER]; + sty.Colors[ImGuiCol_BorderShadow]=uiColors[GUI_COLOR_BORDER_SHADOW]; + + if (settings.roundedWindows) sty.WindowRounding=8.0f; + if (settings.roundedButtons) { + sty.FrameRounding=6.0f; + sty.GrabRounding=6.0f; + } + if (settings.roundedMenus) sty.PopupRounding=8.0f; + + if (settings.frameBorders) { + sty.FrameBorderSize=1.0f; + } else { + sty.FrameBorderSize=0.0f; + } + + sty.ScaleAllSizes(dpiScale); + + ImGui::GetStyle()=sty; + + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_PITCH]; + pitchGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_ACTIVE]; + noteGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_PANNING]; + panGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_INS]; + insGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=volColors[i/2]; + volGrad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_PRIMARY]; + sysCmd1Grad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + for (int i=0; i<256; i++) { + ImVec4& base=uiColors[GUI_COLOR_PATTERN_EFFECT_SYS_SECONDARY]; + sysCmd2Grad[i]=ImGui::GetColorU32(ImVec4(base.x,base.y,base.z,((float)i/255.0f)*base.w)); + } + + if (updateFonts) { + // set to 800 for now due to problems with unifont + static const ImWchar upTo800[]={0x20,0x7e,0xa0,0x800,0}; + ImFontGlyphRangesBuilder range; + ImVector outRange; + + range.AddRanges(upTo800); + if (settings.loadJapanese) { + range.AddRanges(ImGui::GetIO().Fonts->GetGlyphRangesJapanese()); + } + // I'm terribly sorry + range.UsedChars[0x80>>5]=0; + + range.BuildRanges(&outRange); + if (fontRange!=NULL) delete[] fontRange; + fontRange=new ImWchar[outRange.size()]; + int index=0; + for (ImWchar& i: outRange) { + fontRange[index++]=i; + } + + if (settings.mainFont<0 || settings.mainFont>6) settings.mainFont=0; + if (settings.patFont<0 || settings.patFont>6) settings.patFont=0; + + if (settings.mainFont==6 && settings.mainFontPath.empty()) { + logW("UI font path is empty! reverting to default font"); + settings.mainFont=0; + } + if (settings.patFont==6 && settings.patFontPath.empty()) { + logW("pattern font path is empty! reverting to default font"); + settings.patFont=0; + } + + ImFontConfig fc1; + fc1.MergeMode=true; + + if (settings.mainFont==6) { // custom font + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(settings.mainFontPath.c_str(),e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + logW("could not load UI font! reverting to default font"); + settings.mainFont=0; + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + logE("could not load UI font! falling back to Proggy Clean."); + mainFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + } else if (settings.mainFont==5) { // system font + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_1,e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_2,e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_FONT_PATH_3,e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + logW("could not load UI font! reverting to default font"); + settings.mainFont=0; + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + logE("could not load UI font! falling back to Proggy Clean."); + mainFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + } + } + } else { + if ((mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFont[settings.mainFont],builtinFontLen[settings.mainFont],e->getConfInt("mainFontSize",18)*dpiScale,NULL,fontRange))==NULL) { + logE("could not load UI font! falling back to Proggy Clean."); + mainFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + + // two fallback fonts + mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_liberationSans_compressed_data,font_liberationSans_compressed_size,e->getConfInt("mainFontSize",18)*dpiScale,&fc1,fontRange); + mainFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_unifont_compressed_data,font_unifont_compressed_size,e->getConfInt("mainFontSize",18)*dpiScale,&fc1,fontRange); + + ImFontConfig fc; + fc.MergeMode=true; + fc.GlyphMinAdvanceX=e->getConfInt("iconSize",16)*dpiScale; + static const ImWchar fontRangeIcon[]={ICON_MIN_FA,ICON_MAX_FA,0}; + if ((iconFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(iconFont_compressed_data,iconFont_compressed_size,e->getConfInt("iconSize",16)*dpiScale,&fc,fontRangeIcon))==NULL) { + logE("could not load icon font!"); + } + if (settings.mainFontSize==settings.patFontSize && settings.patFont<5 && builtinFontM[settings.patFont]==builtinFont[settings.mainFont]) { + logD("using main font for pat font."); + patFont=mainFont; + } else { + if (settings.patFont==6) { // custom font + if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(settings.patFontPath.c_str(),e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + logW("could not load pattern font! reverting to default font"); + settings.patFont=0; + if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + logE("could not load pattern font! falling back to Proggy Clean."); + patFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + } else if (settings.patFont==5) { // system font + if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_1,e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_2,e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + if ((patFont=ImGui::GetIO().Fonts->AddFontFromFileTTF(SYSTEM_PAT_FONT_PATH_3,e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + logW("could not load pattern font! reverting to default font"); + settings.patFont=0; + if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + logE("could not load pattern font! falling back to Proggy Clean."); + patFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + } + } + } else { + if ((patFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(builtinFontM[settings.patFont],builtinFontMLen[settings.patFont],e->getConfInt("patFontSize",18)*dpiScale,NULL,upTo800))==NULL) { + logE("could not load pattern font!"); + patFont=ImGui::GetIO().Fonts->AddFontDefault(); + } + } + } + if ((bigFont=ImGui::GetIO().Fonts->AddFontFromMemoryCompressedTTF(font_plexSans_compressed_data,font_plexSans_compressed_size,40*dpiScale))==NULL) { + logE("could not load big UI font!"); + } + + mainFont->FallbackChar='?'; + mainFont->DotChar='.'; + } + + // TODO: allow changing these colors. + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeDir,"",uiColors[GUI_COLOR_FILE_DIR],ICON_FA_FOLDER_O); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByTypeFile,"",uiColors[GUI_COLOR_FILE_OTHER],ICON_FA_FILE_O); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fur",uiColors[GUI_COLOR_FILE_SONG_NATIVE],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fui",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fuw",uiColors[GUI_COLOR_FILE_WAVE],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmf",uiColors[GUI_COLOR_FILE_SONG_NATIVE],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmp",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmw",uiColors[GUI_COLOR_FILE_WAVE],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".wav",uiColors[GUI_COLOR_FILE_AUDIO],ICON_FA_FILE_AUDIO_O); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".vgm",uiColors[GUI_COLOR_FILE_VGM],ICON_FA_FILE_AUDIO_O); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttf",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".otf",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttc",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT); + + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".mod",uiColors[GUI_COLOR_FILE_SONG_IMPORT],ICON_FA_FILE); + + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".tfi",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".vgi",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".s3i",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".sbi",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".fti",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".bti",uiColors[GUI_COLOR_FILE_INSTR],ICON_FA_FILE); + + if (updateFonts) { + if (fileDialog!=NULL) delete fileDialog; + fileDialog=new FurnaceGUIFileDialog(settings.sysFileDialog); + } +} diff --git a/src/gui/songInfo.cpp b/src/gui/songInfo.cpp new file mode 100644 index 000000000..a2f788c35 --- /dev/null +++ b/src/gui/songInfo.cpp @@ -0,0 +1,176 @@ +/** + * 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 "intConst.h" + +void FurnaceGUI::drawSongInfo() { + if (nextWindow==GUI_WINDOW_SONG_INFO) { + songInfoOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!songInfoOpen) return; + if (ImGui::Begin("Song Information",&songInfoOpen)) { + if (ImGui::BeginTable("NameAuthor",2,ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Name"); + ImGui::TableNextColumn(); + float avail=ImGui::GetContentRegionAvail().x; + ImGui::SetNextItemWidth(avail); + if (ImGui::InputText("##Name",&e->song.name)) { MARK_MODIFIED + updateWindowTitle(); + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Author"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputText("##Author",&e->song.author)) { + MARK_MODIFIED; + } + ImGui::EndTable(); + } + + if (ImGui::BeginTable("OtherProps",3,ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.0); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("TimeBase"); + ImGui::TableNextColumn(); + float avail=ImGui::GetContentRegionAvail().x; + ImGui::SetNextItemWidth(avail); + unsigned char realTB=e->song.timeBase+1; + if (ImGui::InputScalar("##TimeBase",ImGuiDataType_U8,&realTB,&_ONE,&_THREE)) { MARK_MODIFIED + if (realTB<1) realTB=1; + if (realTB>16) realTB=16; + e->song.timeBase=realTB-1; + } + ImGui::TableNextColumn(); + ImGui::Text("%.2f BPM",calcBPM(e->song.speed1,e->song.speed2,e->song.hz)); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Speed"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->song.speed1,&_ONE,&_THREE)) { MARK_MODIFIED + if (e->song.speed1<1) e->song.speed1=1; + if (e->isPlaying()) play(); + } + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->song.speed2,&_ONE,&_THREE)) { MARK_MODIFIED + if (e->song.speed2<1) e->song.speed2=1; + if (e->isPlaying()) play(); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Highlight"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputScalar("##Highlight1",ImGuiDataType_U8,&e->song.hilightA,&_ONE,&_THREE)) { + MARK_MODIFIED; + } + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + if (ImGui::InputScalar("##Highlight2",ImGuiDataType_U8,&e->song.hilightB,&_ONE,&_THREE)) { + MARK_MODIFIED; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Pattern Length"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + int patLen=e->song.patLen; + if (ImGui::InputInt("##PatLength",&patLen,1,3)) { MARK_MODIFIED + if (patLen<1) patLen=1; + if (patLen>256) patLen=256; + e->song.patLen=patLen; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Song Length"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + int ordLen=e->song.ordersLen; + if (ImGui::InputInt("##OrdLength",&ordLen,1,3)) { MARK_MODIFIED + if (ordLen<1) ordLen=1; + if (ordLen>256) ordLen=256; + e->song.ordersLen=ordLen; + if (e->getOrder()>=ordLen) { + e->setOrder(ordLen-1); + } + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable(tempoView?"Base Tempo##TempoOrHz":"Tick Rate##TempoOrHz")) { + tempoView=!tempoView; + } + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(avail); + float setHz=tempoView?e->song.hz*2.5:e->song.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>999) setHz=999; + e->setSongRate(setHz,setHz<52); + } + if (tempoView) { + ImGui::TableNextColumn(); + ImGui::Text("= %gHz",e->song.hz); + } else { + if (e->song.hz>=49.98 && e->song.hz<=50.02) { + ImGui::TableNextColumn(); + ImGui::Text("PAL"); + } + if (e->song.hz>=59.9 && e->song.hz<=60.11) { + ImGui::TableNextColumn(); + ImGui::Text("NTSC"); + } + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Tuning (A-4)"); + ImGui::TableNextColumn(); + float tune=e->song.tuning; + ImGui::SetNextItemWidth(avail); + if (ImGui::InputFloat("##Tuning",&tune,1.0f,3.0f,"%g")) { MARK_MODIFIED + if (tune<220.0f) tune=220.0f; + if (tune>880.0f) tune=880.0f; + e->song.tuning=tune; + } + ImGui::EndTable(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SONG_INFO; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/songNotes.cpp b/src/gui/songNotes.cpp new file mode 100644 index 000000000..7c7cbdaa9 --- /dev/null +++ b/src/gui/songNotes.cpp @@ -0,0 +1,37 @@ +/** + * 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" + +// NOTE: please don't ask me to enable text wrap. +// Dear ImGui doesn't have that feature. D: +void FurnaceGUI::drawNotes() { + if (nextWindow==GUI_WINDOW_NOTES) { + notesOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!notesOpen) return; + if (ImGui::Begin("Song Comments",¬esOpen)) { + ImGui::InputTextMultiline("##SongNotes",&e->song.notes,ImGui::GetContentRegionAvail()); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_NOTES; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/stats.cpp b/src/gui/stats.cpp new file mode 100644 index 000000000..274be2eda --- /dev/null +++ b/src/gui/stats.cpp @@ -0,0 +1,50 @@ +/** + * 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 + +void FurnaceGUI::drawStats() { + if (nextWindow==GUI_WINDOW_STATS) { + statsOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!statsOpen) return; + if (ImGui::Begin("Statistics",&statsOpen)) { + String adpcmAUsage=fmt::sprintf("%d/16384KB",e->adpcmAMemLen/1024); + String adpcmBUsage=fmt::sprintf("%d/16384KB",e->adpcmBMemLen/1024); + String qsoundUsage=fmt::sprintf("%d/16384KB",e->qsoundMemLen/1024); + String x1_010Usage=fmt::sprintf("%d/1024KB",e->x1_010MemLen/1024); + ImGui::Text("ADPCM-A"); + ImGui::SameLine(); + ImGui::ProgressBar(((float)e->adpcmAMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmAUsage.c_str()); + ImGui::Text("ADPCM-B"); + ImGui::SameLine(); + ImGui::ProgressBar(((float)e->adpcmBMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),adpcmBUsage.c_str()); + ImGui::Text("QSound"); + ImGui::SameLine(); + ImGui::ProgressBar(((float)e->qsoundMemLen)/16777216.0f,ImVec2(-FLT_MIN,0),qsoundUsage.c_str()); + ImGui::Text("X1-010"); + ImGui::SameLine(); + ImGui::ProgressBar(((float)e->x1_010MemLen)/1048576.0f,ImVec2(-FLT_MIN,0),x1_010Usage.c_str()); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_STATS; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp new file mode 100644 index 000000000..40cc67a35 --- /dev/null +++ b/src/gui/sysConf.cpp @@ -0,0 +1,403 @@ +/** + * 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" + +void FurnaceGUI::drawSysConf(int i) { + unsigned int flags=e->song.systemFlags[i]; + bool restart=settings.restartOnFlagChange; + bool sysPal=flags&1; + switch (e->song.system[i]) { + case DIV_SYSTEM_YM2612: + case DIV_SYSTEM_YM2612_EXT: { + if (ImGui::RadioButton("NTSC (7.67MHz)",(flags&3)==0)) { + e->setSysFlags(i,(flags&0x80000000)|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (7.61MHz)",(flags&3)==1)) { + e->setSysFlags(i,(flags&0x80000000)|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("FM Towns (8MHz)",(flags&3)==2)) { + e->setSysFlags(i,(flags&0x80000000)|2,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("AtGames Genesis (6.13MHz)",(flags&3)==3)) { + e->setSysFlags(i,(flags&0x80000000)|3,restart); + updateWindowTitle(); + } + bool ladder=flags&0x80000000; + if (ImGui::Checkbox("Enable DAC distortion",&ladder)) { + e->setSysFlags(i,(flags&(~0x80000000))|(ladder?0x80000000:0),restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_SMS: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("NTSC (3.58MHz)",(flags&3)==0)) { + e->setSysFlags(i,(flags&(~3))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (3.55MHz)",(flags&3)==1)) { + e->setSysFlags(i,(flags&(~3))|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("BBC Micro (4MHz)",(flags&3)==2)) { + e->setSysFlags(i,(flags&(~3))|2,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Half NTSC (1.79MHz)",(flags&3)==3)) { + e->setSysFlags(i,(flags&(~3))|3,restart); + updateWindowTitle(); + } + ImGui::Text("Chip type:"); + if (ImGui::RadioButton("Sega VDP/Master System",((flags>>2)&3)==0)) { + e->setSysFlags(i,(flags&(~12))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("TI SN76489",((flags>>2)&3)==1)) { + e->setSysFlags(i,(flags&(~12))|4,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("TI SN76489 with Atari-like short noise",((flags>>2)&3)==2)) { + e->setSysFlags(i,(flags&(~12))|8,restart); + updateWindowTitle(); + } + /*if (ImGui::RadioButton("Game Gear",(flags>>2)==3)) { + e->setSysFlags(i,(flags&3)|12); + }*/ + + bool noPhaseReset=flags&16; + if (ImGui::Checkbox("Disable noise period change phase reset",&noPhaseReset)) { + e->setSysFlags(i,(flags&(~16))|(noPhaseReset<<4),restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_OPLL: + case DIV_SYSTEM_OPLL_DRUMS: + case DIV_SYSTEM_VRC7: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("NTSC (3.58MHz)",(flags&15)==0)) { + e->setSysFlags(i,(flags&(~15))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (3.55MHz)",(flags&15)==1)) { + e->setSysFlags(i,(flags&(~15))|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("BBC Micro (4MHz)",(flags&15)==2)) { + e->setSysFlags(i,(flags&(~15))|2,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Half NTSC (1.79MHz)",(flags&15)==3)) { + e->setSysFlags(i,(flags&(~15))|3,restart); + updateWindowTitle(); + } + if (e->song.system[i]!=DIV_SYSTEM_VRC7) { + ImGui::Text("Patch set:"); + if (ImGui::RadioButton("Yamaha YM2413",((flags>>4)&15)==0)) { + e->setSysFlags(i,(flags&(~0xf0))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Yamaha YMF281",((flags>>4)&15)==1)) { + e->setSysFlags(i,(flags&(~0xf0))|0x10,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Yamaha YM2423",((flags>>4)&15)==2)) { + e->setSysFlags(i,(flags&(~0xf0))|0x20,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Konami VRC7",((flags>>4)&15)==3)) { + e->setSysFlags(i,(flags&(~0xf0))|0x30,restart); + updateWindowTitle(); + } + } + break; + } + case DIV_SYSTEM_YM2151: + if (ImGui::RadioButton("NTSC/X16 (3.58MHz)",flags==0)) { + e->setSysFlags(i,0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (3.55MHz)",flags==1)) { + e->setSysFlags(i,1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("X1/X68000 (4MHz)",flags==2)) { + e->setSysFlags(i,2,restart); + updateWindowTitle(); + } + break; + case DIV_SYSTEM_NES: + case DIV_SYSTEM_VRC6: + case DIV_SYSTEM_FDS: + case DIV_SYSTEM_MMC5: + if (ImGui::RadioButton("NTSC (1.79MHz)",flags==0)) { + e->setSysFlags(i,0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (1.67MHz)",flags==1)) { + e->setSysFlags(i,1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Dendy (1.77MHz)",flags==2)) { + e->setSysFlags(i,2,restart); + updateWindowTitle(); + } + break; + case DIV_SYSTEM_C64_8580: + case DIV_SYSTEM_C64_6581: + if (ImGui::RadioButton("NTSC (1.02MHz)",flags==0)) { + e->setSysFlags(i,0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (0.99MHz)",flags==1)) { + e->setSysFlags(i,1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("SSI 2001 (0.89MHz)",flags==2)) { + e->setSysFlags(i,2,restart); + updateWindowTitle(); + } + break; + case DIV_SYSTEM_AY8910: + case DIV_SYSTEM_AY8930: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("1.79MHz (ZX Spectrum NTSC/MSX)",(flags&15)==0)) { + e->setSysFlags(i,(flags&(~15))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1.77MHz (ZX Spectrum)",(flags&15)==1)) { + e->setSysFlags(i,(flags&(~15))|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1.75MHz (ZX Spectrum)",(flags&15)==2)) { + e->setSysFlags(i,(flags&(~15))|2,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("2MHz (Atari ST/Sharp X1)",(flags&15)==3)) { + e->setSysFlags(i,(flags&(~15))|3,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1.5MHz (Vectrex)",(flags&15)==4)) { + e->setSysFlags(i,(flags&(~15))|4,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1MHz (Amstrad CPC)",(flags&15)==5)) { + e->setSysFlags(i,(flags&(~15))|5,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("0.89MHz (Sunsoft 5B)",(flags&15)==6)) { + e->setSysFlags(i,(flags&(~15))|6,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1.67MHz (?)",(flags&15)==7)) { + e->setSysFlags(i,(flags&(~15))|7,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("0.83MHz (Sunsoft 5B on PAL)",(flags&15)==8)) { + e->setSysFlags(i,(flags&(~15))|8,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("1.10MHz (Gamate/VIC-20 PAL)",(flags&15)==9)) { + e->setSysFlags(i,(flags&(~15))|9,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("2^21Hz (Game Boy)",(flags&15)==10)) { + e->setSysFlags(i,(flags&(~15))|10,restart); + updateWindowTitle(); + } + if (e->song.system[i]==DIV_SYSTEM_AY8910) { + ImGui::Text("Chip type:"); + if (ImGui::RadioButton("AY-3-8910",(flags&0x30)==0)) { + e->setSysFlags(i,(flags&(~0x30))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("YM2149(F)",(flags&0x30)==16)) { + e->setSysFlags(i,(flags&(~0x30))|16,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Sunsoft 5B",(flags&0x30)==32)) { + e->setSysFlags(i,(flags&(~0x30))|32,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("AY-3-8914",(flags&0x30)==48)) { + e->setSysFlags(i,(flags&(~0x30))|48,restart); + updateWindowTitle(); + } + } + bool stereo=flags&0x40; + ImGui::BeginDisabled((flags&0x30)==32); + if (ImGui::Checkbox("Stereo##_AY_STEREO",&stereo)) { + e->setSysFlags(i,(flags&(~0x40))|(stereo?0x40:0),restart); + updateWindowTitle(); + } + ImGui::EndDisabled(); + break; + } + case DIV_SYSTEM_SAA1099: + if (ImGui::RadioButton("SAM Coupé (8MHz)",flags==0)) { + e->setSysFlags(i,0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("NTSC (7.15MHz)",flags==1)) { + e->setSysFlags(i,1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (7.09MHz)",flags==2)) { + e->setSysFlags(i,2,restart); + updateWindowTitle(); + } + break; + case DIV_SYSTEM_AMIGA: { + ImGui::Text("Stereo separation:"); + int stereoSep=(flags>>8)&127; + if (CWSliderInt("##StereoSep",&stereoSep,0,127)) { + if (stereoSep<0) stereoSep=0; + if (stereoSep>127) stereoSep=127; + e->setSysFlags(i,(flags&(~0x7f00))|((stereoSep&127)<<8),restart); + updateWindowTitle(); + } rightClickable + if (ImGui::RadioButton("Amiga 500 (OCS)",(flags&2)==0)) { + e->setSysFlags(i,flags&(~2),restart); + } + if (ImGui::RadioButton("Amiga 1200 (AGA)",(flags&2)==2)) { + e->setSysFlags(i,(flags&(~2))|2,restart); + } + sysPal=flags&1; + if (ImGui::Checkbox("PAL",&sysPal)) { + e->setSysFlags(i,(flags&(~1))|(unsigned int)sysPal,restart); + updateWindowTitle(); + } + bool bypassLimits=flags&4; + if (ImGui::Checkbox("Bypass frequency limits",&bypassLimits)) { + e->setSysFlags(i,(flags&(~4))|(bypassLimits<<2),restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_PCSPKR: { + ImGui::Text("Speaker type:"); + if (ImGui::RadioButton("Unfiltered",(flags&3)==0)) { + e->setSysFlags(i,(flags&(~3))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Cone",(flags&3)==1)) { + e->setSysFlags(i,(flags&(~3))|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Piezo",(flags&3)==2)) { + e->setSysFlags(i,(flags&(~3))|2,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Use system beeper (Linux only!)",(flags&3)==3)) { + e->setSysFlags(i,(flags&(~3))|3,restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_QSOUND: { + ImGui::Text("Echo delay:"); + int echoBufSize=2725 - (flags & 4095); + if (CWSliderInt("##EchoBufSize",&echoBufSize,0,2725)) { + if (echoBufSize<0) echoBufSize=0; + if (echoBufSize>2725) echoBufSize=2725; + e->setSysFlags(i,(flags & ~4095) | ((2725 - echoBufSize) & 4095),restart); + updateWindowTitle(); + } rightClickable + ImGui::Text("Echo feedback:"); + int echoFeedback=(flags>>12)&255; + if (CWSliderInt("##EchoFeedback",&echoFeedback,0,255)) { + if (echoFeedback<0) echoFeedback=0; + if (echoFeedback>255) echoFeedback=255; + e->setSysFlags(i,(flags & ~(255 << 12)) | ((echoFeedback & 255) << 12),restart); + updateWindowTitle(); + } rightClickable + break; + } + case DIV_SYSTEM_X1_010: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("16MHz (Seta 1)",(flags&15)==0)) { + e->setSysFlags(i,(flags&(~15))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("16.67MHz (Seta 2)",(flags&15)==1)) { + e->setSysFlags(i,(flags&(~15))|1,restart); + updateWindowTitle(); + } + bool x1_010Stereo=flags&16; + if (ImGui::Checkbox("Stereo",&x1_010Stereo)) { + e->setSysFlags(i,(flags&(~16))|(x1_010Stereo<<4),restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_N163: { + ImGui::Text("Clock rate:"); + if (ImGui::RadioButton("NTSC (1.79MHz)",(flags&15)==0)) { + e->setSysFlags(i,(flags&(~15))|0,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("PAL (1.67MHz)",(flags&15)==1)) { + e->setSysFlags(i,(flags&(~15))|1,restart); + updateWindowTitle(); + } + if (ImGui::RadioButton("Dendy (1.77MHz)",(flags&15)==2)) { + e->setSysFlags(i,(flags&(~15))|2,restart); + updateWindowTitle(); + } + ImGui::Text("Initial channel limit:"); + int initialChannelLimit=((flags>>4)&7)+1; + if (CWSliderInt("##InitialChannelLimit",&initialChannelLimit,1,8)) { + if (initialChannelLimit<1) initialChannelLimit=1; + if (initialChannelLimit>8) initialChannelLimit=8; + e->setSysFlags(i,(flags & ~(7 << 4)) | (((initialChannelLimit-1) & 7) << 4),restart); + updateWindowTitle(); + } rightClickable + bool n163Multiplex=flags&128; + if (ImGui::Checkbox("Disable hissing",&n163Multiplex)) { + e->setSysFlags(i,(flags&(~128))|(n163Multiplex<<7),restart); + updateWindowTitle(); + } + break; + } + case DIV_SYSTEM_GB: + case DIV_SYSTEM_SWAN: + case DIV_SYSTEM_VERA: + case DIV_SYSTEM_BUBSYS_WSG: + case DIV_SYSTEM_YM2610: + case DIV_SYSTEM_YM2610_EXT: + case DIV_SYSTEM_YM2610_FULL: + case DIV_SYSTEM_YM2610_FULL_EXT: + case DIV_SYSTEM_YM2610B: + case DIV_SYSTEM_YM2610B_EXT: + case DIV_SYSTEM_YMU759: + case DIV_SYSTEM_PET: + ImGui::Text("nothing to configure"); + break; + default: + if (ImGui::Checkbox("PAL",&sysPal)) { + e->setSysFlags(i,sysPal,restart); + updateWindowTitle(); + } + break; + } +} diff --git a/src/gui/volMeter.cpp b/src/gui/volMeter.cpp new file mode 100644 index 000000000..7be3464e9 --- /dev/null +++ b/src/gui/volMeter.cpp @@ -0,0 +1,103 @@ +/** + * 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 "imgui_internal.h" + +void FurnaceGUI::drawVolMeter() { + if (nextWindow==GUI_WINDOW_VOL_METER) { + volMeterOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!volMeterOpen) return; + if (--isClipping<0) isClipping=0; + ImGui::SetNextWindowSizeConstraints(ImVec2(6.0f*dpiScale,6.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing,ImVec2(0,0)); + if (ImGui::Begin("Volume Meter",&volMeterOpen)) { + ImDrawList* dl=ImGui::GetWindowDrawList(); + bool aspectRatio=(ImGui::GetWindowSize().x/ImGui::GetWindowSize().y)>1.0; + + ImVec2 minArea=ImVec2( + ImGui::GetWindowPos().x+ImGui::GetCursorPos().x, + ImGui::GetWindowPos().y+ImGui::GetCursorPos().y + ); + ImVec2 maxArea=ImVec2( + ImGui::GetWindowPos().x+ImGui::GetCursorPos().x+ImGui::GetContentRegionAvail().x, + ImGui::GetWindowPos().y+ImGui::GetCursorPos().y+ImGui::GetContentRegionAvail().y + ); + ImRect rect=ImRect(minArea,maxArea); + ImGuiStyle& style=ImGui::GetStyle(); + ImGui::ItemSize(ImVec2(4.0f,4.0f),style.FramePadding.y); + ImU32 lowColor=ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_LOW]); + if (ImGui::ItemAdd(rect,ImGui::GetID("volMeter"))) { + ImGui::RenderFrame(rect.Min,rect.Max,ImGui::GetColorU32(ImGuiCol_FrameBg),true,style.FrameRounding); + for (int i=0; i<2; i++) { + float logPeak=(20*log10(peak[i])/36.0); + if (logPeak==NAN) logPeak=0.0; + if (logPeak<-1.0) logPeak=-1.0; + if (logPeak>0.0) { + isClipping=8; + logPeak=0.0; + } + logPeak+=1.0; + ImU32 highColor=ImGui::GetColorU32( + ImLerp(uiColors[GUI_COLOR_VOLMETER_LOW],uiColors[GUI_COLOR_VOLMETER_HIGH],logPeak) + ); + ImRect s; + if (aspectRatio) { + s=ImRect( + ImLerp(rect.Min,rect.Max,ImVec2(0,float(i)*0.5)), + ImLerp(rect.Min,rect.Max,ImVec2(logPeak,float(i+1)*0.5)) + ); + if (i==0) s.Max.y-=dpiScale; + if (isClipping) { + dl->AddRectFilled(s.Min,s.Max,ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_PEAK])); + } else { + dl->AddRectFilledMultiColor(s.Min,s.Max,lowColor,highColor,highColor,lowColor); + } + } else { + s=ImRect( + ImLerp(rect.Min,rect.Max,ImVec2(float(i)*0.5,1.0-logPeak)), + ImLerp(rect.Min,rect.Max,ImVec2(float(i+1)*0.5,1.0)) + ); + if (i==0) s.Max.x-=dpiScale; + if (isClipping) { + dl->AddRectFilled(s.Min,s.Max,ImGui::GetColorU32(uiColors[GUI_COLOR_VOLMETER_PEAK])); + } else { + dl->AddRectFilledMultiColor(s.Min,s.Max,highColor,highColor,lowColor,lowColor); + } + } + } + if (ImGui::IsItemHovered()) { + if (aspectRatio) { + ImGui::SetTooltip("%.1fdB",36*((ImGui::GetMousePos().x-ImGui::GetItemRectMin().x)/(rect.Max.x-rect.Min.x)-1.0)); + } else { + ImGui::SetTooltip("%.1fdB",-(36+36*((ImGui::GetMousePos().y-ImGui::GetItemRectMin().y)/(rect.Max.y-rect.Min.y)-1.0))); + } + } + } + } + ImGui::PopStyleVar(4); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_VOL_METER; + ImGui::End(); +} \ No newline at end of file diff --git a/src/gui/waveEdit.cpp b/src/gui/waveEdit.cpp new file mode 100644 index 000000000..a9efe512e --- /dev/null +++ b/src/gui/waveEdit.cpp @@ -0,0 +1,127 @@ +/** + * 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 "plot_nolerp.h" +#include "IconsFontAwesome4.h" +#include "misc/cpp/imgui_stdlib.h" +#include + +void FurnaceGUI::drawWaveEdit() { + if (nextWindow==GUI_WINDOW_WAVE_EDIT) { + waveEditOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!waveEditOpen) return; + float wavePreview[256]; + ImGui::SetNextWindowSizeConstraints(ImVec2(450.0f*dpiScale,300.0f*dpiScale),ImVec2(scrW*dpiScale,scrH*dpiScale)); + if (ImGui::Begin("Wavetable Editor",&waveEditOpen,settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking)) { + 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]; + 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(128.0f*dpiScale); + 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::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(128.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; + } + for (int i=0; ilen; i++) { + if (wave->data[i]>wave->max) wave->data[i]=wave->max; + wavePreview[i]=wave->data[i]; + } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + 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); + } + 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; + ImGui::End(); +} \ No newline at end of file diff --git a/src/log.cpp b/src/log.cpp index c8dd030e7..7f7fae8a9 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -21,6 +21,28 @@ int logLevel=LOGLEVEL_INFO; +std::atomic logPosition; + +LogEntry logEntries[TA_LOG_SIZE]; + +static constexpr unsigned int TA_LOG_MASK=TA_LOG_SIZE-1; + +int logV(const char* format, ...) { + va_list va; + int ret; + if (logLevelgetFinalBuf(),1,w->size(),f); fclose(f); } else { - logE("could not open file! %s\n",strerror(errno)); + logE("could not open file! %s",strerror(errno)); } w->finish(); delete w; } else { - logE("could not write VGM!\n"); + logE("could not write VGM!"); } } if (outName!="") { @@ -389,7 +392,7 @@ int main(int argc, char** argv) { } if (consoleMode) { - logI("playing...\n"); + logI("playing..."); e.play(); #ifdef HAVE_GUI SDL_Event ev; @@ -415,7 +418,7 @@ int main(int argc, char** argv) { if (!g.init()) return 1; if (displayEngineFailError) { - logE("displaying engine fail error.\n"); + logE("displaying engine fail error."); g.showError("error while initializing audio!"); } @@ -424,13 +427,13 @@ int main(int argc, char** argv) { } g.loop(); - logI("closing GUI.\n"); + logI("closing GUI."); g.finish(); #else - logE("GUI requested but GUI not compiled!\n"); + logE("GUI requested but GUI not compiled!"); #endif - logI("stopping engine.\n"); + logI("stopping engine."); e.quit(); return 0; } diff --git a/src/ta-log.h b/src/ta-log.h index c7e8b803d..dd26dd94f 100644 --- a/src/ta-log.h +++ b/src/ta-log.h @@ -21,16 +21,62 @@ #define _TA_LOG_H #include #include +#include +#include +#include +#include #define LOGLEVEL_ERROR 0 #define LOGLEVEL_WARN 1 #define LOGLEVEL_INFO 2 #define LOGLEVEL_DEBUG 3 +#define LOGLEVEL_TRACE 4 + +// this has to be a power of 2 +#define TA_LOG_SIZE 2048 extern int logLevel; -int logD(const char* format, ...); -int logI(const char* format, ...); -int logW(const char* format, ...); -int logE(const char* format, ...); +extern std::atomic logPosition; + +struct LogEntry { + int loglevel; + struct tm time; + std::string text; + bool ready; + LogEntry(): + loglevel(0), + ready(false) {} +}; + +int writeLog(int level, const char* msg, fmt::printf_args& args); + +extern LogEntry logEntries[TA_LOG_SIZE]; + +template int logV(const char* msg, const T&... args) { + fmt::printf_args a=fmt::make_printf_args(args...); + return writeLog(LOGLEVEL_TRACE,msg,a); +} + +template int logD(const char* msg, const T&... args) { + fmt::printf_args a=fmt::make_printf_args(args...); + return writeLog(LOGLEVEL_DEBUG,msg,a); +} + +template int logI(const char* msg, const T&... args) { + fmt::printf_args a=fmt::make_printf_args(args...); + return writeLog(LOGLEVEL_INFO,msg,a); +} + +template int logW(const char* msg, const T&... args) { + fmt::printf_args a=fmt::make_printf_args(args...); + return writeLog(LOGLEVEL_WARN,msg,a); +} + +template int logE(const char* msg, const T&... args) { + fmt::printf_args a=fmt::make_printf_args(args...); + return writeLog(LOGLEVEL_ERROR,msg,a); +} + +void initLog(); #endif diff --git a/src/ta-utils.h b/src/ta-utils.h index db876f43f..98b184f31 100644 --- a/src/ta-utils.h +++ b/src/ta-utils.h @@ -30,8 +30,10 @@ typedef SSIZE_T ssize_t; #ifdef _WIN32 #define DIR_SEPARATOR '\\' +#define DIR_SEPARATOR_STR "\\" #else #define DIR_SEPARATOR '/' +#define DIR_SEPARATOR_STR "/" #endif typedef std::string String;