Merge branch 'master' of https://github.com/tildearrow/furnace into ymf289b
54
.github/workflows/build.yml
vendored
|
@ -82,7 +82,7 @@ jobs:
|
|||
package_ext=".dmg"
|
||||
else
|
||||
package_name="${package_name}-Linux-${{ matrix.config.arch }}"
|
||||
package_ext=".AppImage"
|
||||
package_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
echo "Package identifier: ${package_name}"
|
||||
|
@ -128,10 +128,7 @@ jobs:
|
|||
librtmidi-dev \
|
||||
libsndfile1-dev \
|
||||
zlib1g-dev \
|
||||
libjack-jackd2-dev \
|
||||
appstream
|
||||
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" || wget "https://tildearrow.org/storage/furnace/ci/appimagetool-x86_64.AppImage"
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
libjack-jackd2-dev
|
||||
|
||||
- name: Install Dependencies [Linux armhf]
|
||||
if: ${{ runner.os == 'Linux' && matrix.config.compiler != 'mingw' && matrix.config.arch == 'armhf' }}
|
||||
|
@ -151,9 +148,6 @@ jobs:
|
|||
libsndfile1-dev:armhf \
|
||||
zlib1g-dev:armhf \
|
||||
libjack-jackd2-dev:armhf
|
||||
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" || wget "https://tildearrow.org/storage/furnace/ci/appimagetool-x86_64.AppImage"
|
||||
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-armhf" || wget "https://tildearrow.org/storage/furnace/ci/runtime-armhf"
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
ls /usr/arm-linux-gnueabihf/lib
|
||||
|
||||
- name: Configure (System Libraries)
|
||||
|
@ -243,6 +237,7 @@ jobs:
|
|||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \
|
||||
-DWARNINGS_ARE_ERRORS=${USE_WAE} \
|
||||
-DWITH_DEMOS=OFF -DWITH_INSTRUMENTS=OFF -DWITH_WAVETABLES=OFF \
|
||||
"${CMAKE_EXTRA_ARGS[@]}"
|
||||
|
||||
- name: Build
|
||||
|
@ -300,24 +295,37 @@ jobs:
|
|||
# strip -s build/furnace
|
||||
#fi
|
||||
|
||||
mkdir -p target/furnace.AppDir
|
||||
make -C ${PWD}/build DESTDIR=${PWD}/target/furnace.AppDir install
|
||||
pushd target
|
||||
mkdir -p target/furnace
|
||||
make -C ${PWD}/build DESTDIR=${PWD}/target/furnace install
|
||||
pushd target/furnace
|
||||
|
||||
cp -v ../../res/logo.png .DirIcon
|
||||
|
||||
cd usr
|
||||
|
||||
mv bin/furnace ..
|
||||
rmdir bin
|
||||
|
||||
rm -r share/applications
|
||||
rm -r share/doc
|
||||
rm -r share/icons
|
||||
rm -r share/licenses
|
||||
rm -r share/metainfo
|
||||
|
||||
rmdir share
|
||||
|
||||
cd ..
|
||||
|
||||
cp ../../LICENSE .
|
||||
cp ../../README.md .
|
||||
cp -r ../../papers papers
|
||||
rmdir usr
|
||||
|
||||
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
|
||||
|
||||
if [ '${{ matrix.config.arch }}' == 'armhf' ]; then
|
||||
../appimagetool-x86_64.AppImage --runtime-file=../runtime-armhf furnace.AppDir
|
||||
else
|
||||
../appimagetool-x86_64.AppImage furnace.AppDir
|
||||
fi
|
||||
mv Furnace-*.AppImage ../${{ steps.package-identify.outputs.filename }}
|
||||
popd
|
||||
cd target
|
||||
|
||||
tar -zcv -f ../${{ steps.package-identify.outputs.filename }} furnace
|
||||
|
||||
- name: Upload artifact
|
||||
if: ${{ github.repository == 'tildearrow/furnace' && github.ref_name == 'master' }}
|
||||
|
|
2
.gitignore
vendored
|
@ -15,6 +15,8 @@ test/songs/
|
|||
test/delta/
|
||||
test/result/
|
||||
test/assert_delta
|
||||
android/local.properties
|
||||
android/.idea/
|
||||
android/.gradle/
|
||||
android/app/build/
|
||||
android/app/.cxx/
|
||||
|
|
1
.gitmodules
vendored
|
@ -1,6 +1,7 @@
|
|||
[submodule "extern/SDL"]
|
||||
path = extern/SDL
|
||||
url = https://github.com/libsdl-org/SDL.git
|
||||
branch = release-2.28.x
|
||||
[submodule "extern/libsndfile"]
|
||||
path = extern/libsndfile
|
||||
url = https://github.com/libsndfile/libsndfile.git
|
||||
|
|
103
CMakeLists.txt
|
@ -54,12 +54,34 @@ else()
|
|||
set(WITH_JACK_DEFAULT OFF)
|
||||
endif()
|
||||
|
||||
set(WITH_RENDER_SDL_DEFAULT ON)
|
||||
if (APPLE)
|
||||
set(WITH_RENDER_OPENGL_DEFAULT OFF)
|
||||
else()
|
||||
set(WITH_RENDER_OPENGL_DEFAULT ON)
|
||||
endif()
|
||||
if (WIN32)
|
||||
set(WITH_RENDER_DX11_DEFAULT ON)
|
||||
else()
|
||||
set(WITH_RENDER_DX11_DEFAULT OFF)
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
set(USE_GLES_DEFAULT ON)
|
||||
else()
|
||||
set(USE_GLES_DEFAULT OFF)
|
||||
endif()
|
||||
|
||||
option(BUILD_GUI "Build the tracker (disable to build only a headless player)" ${BUILD_GUI_DEFAULT})
|
||||
option(USE_RTMIDI "Build with MIDI support using RtMidi." ${USE_RTMIDI_DEFAULT})
|
||||
option(USE_SDL2 "Build with SDL2. Required to build with GUI." ${USE_SDL2_DEFAULT})
|
||||
option(USE_SNDFILE "Build with libsndfile. Required in order to work with audio files." ${USE_SNDFILE_DEFAULT})
|
||||
option(USE_BACKWARD "Use backward-cpp to print a backtrace on crash/abort." ${USE_BACKWARD_DEFAULT})
|
||||
option(WITH_JACK "Whether to build with JACK support. Auto-detects if JACK is available" ${WITH_JACK_DEFAULT})
|
||||
option(WITH_RENDER_SDL "Whether to build with the SDL_Renderer render backend." ${WITH_RENDER_SDL_DEFAULT})
|
||||
option(WITH_RENDER_OPENGL "Whether to build with the OpenGL render backend." ${WITH_RENDER_OPENGL_DEFAULT})
|
||||
option(WITH_RENDER_DX11 "Whether to build with the DirectX 11 render backend." ${WITH_RENDER_DX11_DEFAULT})
|
||||
option(USE_GLES "Use OpenGL ES for the OpenGL render backend." ${USE_GLES_DEFAULT})
|
||||
option(SYSTEM_FFTW "Use a system-installed version of FFTW instead of the vendored one" OFF)
|
||||
option(SYSTEM_FMT "Use a system-installed version of fmt instead of the vendored one" OFF)
|
||||
option(SYSTEM_LIBSNDFILE "Use a system-installed version of libsndfile instead of the vendored one" OFF)
|
||||
|
@ -92,12 +114,19 @@ set(DEPENDENCIES_LIBRARY_DIRS "")
|
|||
set(DEPENDENCIES_LINK_OPTIONS "")
|
||||
set(DEPENDENCIES_LEGACY_LDFLAGS "")
|
||||
|
||||
if (BUILD_GUI)
|
||||
if (BUILD_GUI AND WITH_RENDER_SDL)
|
||||
set(SYSTEM_SDL_MIN_VER 2.0.18)
|
||||
else()
|
||||
set(SYSTEM_SDL_MIN_VER 2.0.0)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
# support Windows XP
|
||||
if (SUPPORT_XP)
|
||||
add_compile_definitions("_WIN32_WINNT=0x0501")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
list(APPEND DEPENDENCIES_INCLUDE_DIRS "extern/SAASound/include")
|
||||
list(APPEND DEPENDENCIES_INCLUDE_DIRS "extern/vgsound_emu-modified")
|
||||
|
||||
|
@ -278,6 +307,12 @@ else()
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if (BUILD_GUI)
|
||||
if (NOT WITH_RENDER_SDL AND NOT WITH_RENDER_OPENGL AND NOT WITH_RENDER_DX11)
|
||||
message(FATAL_ERROR "No render backends selected!")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(AUDIO_SOURCES
|
||||
src/audio/abstract.cpp
|
||||
src/audio/midi.cpp
|
||||
|
@ -420,6 +455,8 @@ src/engine/platform/sound/c64_fp/WaveformCalculator.cpp
|
|||
src/engine/platform/sound/c64_fp/WaveformGenerator.cpp
|
||||
src/engine/platform/sound/c64_fp/resample/SincResampler.cpp
|
||||
|
||||
src/engine/platform/sound/c64_d/dsid.c
|
||||
|
||||
src/engine/platform/sound/tia/AudioChannel.cpp
|
||||
src/engine/platform/sound/tia/Audio.cpp
|
||||
|
||||
|
@ -551,11 +588,15 @@ src/engine/platform/k007232.cpp
|
|||
src/engine/platform/ga20.cpp
|
||||
src/engine/platform/sm8521.cpp
|
||||
src/engine/platform/pv1000.cpp
|
||||
src/engine/platform/k053260.cpp
|
||||
src/engine/platform/pcmdac.cpp
|
||||
src/engine/platform/dummy.cpp
|
||||
|
||||
src/engine/export/abstract.cpp
|
||||
src/engine/export/amigaValidation.cpp
|
||||
|
||||
src/engine/effect/abstract.cpp
|
||||
src/engine/effect/dummy.cpp
|
||||
)
|
||||
|
||||
if (USE_SNDFILE)
|
||||
|
@ -577,13 +618,15 @@ extern/imgui_patched/imgui.cpp
|
|||
extern/imgui_patched/imgui_draw.cpp
|
||||
extern/imgui_patched/imgui_tables.cpp
|
||||
extern/imgui_patched/imgui_widgets.cpp
|
||||
extern/imgui_patched/backends/imgui_impl_sdlrenderer.cpp
|
||||
extern/imgui_patched/backends/imgui_impl_sdl.cpp
|
||||
extern/imgui_patched/backends/imgui_impl_sdl2.cpp
|
||||
extern/imgui_patched/misc/cpp/imgui_stdlib.cpp
|
||||
extern/igfd/ImGuiFileDialog.cpp
|
||||
|
||||
src/gui/plot_nolerp.cpp
|
||||
|
||||
src/gui/render.cpp
|
||||
src/gui/render/abstract.cpp
|
||||
|
||||
src/gui/font_exo.cpp
|
||||
src/gui/font_liberationSans.cpp
|
||||
src/gui/font_mononoki.cpp
|
||||
|
@ -653,6 +696,7 @@ src/gui/subSongs.cpp
|
|||
src/gui/sysConf.cpp
|
||||
src/gui/sysEx.cpp
|
||||
src/gui/sysManager.cpp
|
||||
src/gui/sysPartNumber.cpp
|
||||
src/gui/sysPicker.cpp
|
||||
src/gui/tutorial.cpp
|
||||
src/gui/util.cpp
|
||||
|
@ -672,6 +716,47 @@ if (APPLE)
|
|||
list(APPEND GUI_SOURCES extern/nfd-modified/src/nfd_cocoa.mm)
|
||||
endif()
|
||||
|
||||
if (WITH_RENDER_SDL)
|
||||
list(APPEND GUI_SOURCES src/gui/render/renderSDL.cpp)
|
||||
list(APPEND GUI_SOURCES extern/imgui_patched/backends/imgui_impl_sdlrenderer2.cpp)
|
||||
list(APPEND DEPENDENCIES_DEFINES HAVE_RENDER_SDL)
|
||||
message(STATUS "UI render backend: SDL_Renderer")
|
||||
endif()
|
||||
|
||||
if (WITH_RENDER_OPENGL)
|
||||
list(APPEND GUI_SOURCES src/gui/render/renderGL.cpp)
|
||||
list(APPEND GUI_SOURCES extern/imgui_patched/backends/imgui_impl_opengl3.cpp)
|
||||
list(APPEND DEPENDENCIES_DEFINES HAVE_RENDER_GL)
|
||||
if (USE_GLES)
|
||||
list(APPEND DEPENDENCIES_DEFINES USE_GLES)
|
||||
list(APPEND DEPENDENCIES_DEFINES IMGUI_IMPL_OPENGL_ES2)
|
||||
endif()
|
||||
if (WIN32)
|
||||
list(APPEND DEPENDENCIES_LIBRARIES opengl32)
|
||||
elseif(USE_GLES)
|
||||
list(APPEND DEPENDENCIES_LIBRARIES GLESv2)
|
||||
else()
|
||||
list(APPEND DEPENDENCIES_LIBRARIES GL)
|
||||
endif()
|
||||
message(STATUS "UI render backend: OpenGL")
|
||||
endif()
|
||||
|
||||
if (WITH_RENDER_DX11)
|
||||
if (WIN32)
|
||||
if (SUPPORT_XP)
|
||||
message(FATAL_ERROR "SUPPORT_XP is on. cannot enable DirectX 11 backend.")
|
||||
else()
|
||||
list(APPEND GUI_SOURCES src/gui/render/renderDX11.cpp)
|
||||
list(APPEND GUI_SOURCES extern/imgui_patched/backends/imgui_impl_dx11.cpp)
|
||||
list(APPEND DEPENDENCIES_DEFINES HAVE_RENDER_DX11)
|
||||
list(APPEND DEPENDENCIES_LIBRARIES d3d11)
|
||||
message(STATUS "UI render backend: DirectX 11")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "DirectX 11 render backend only for Windows!")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
CHECK_INCLUDE_FILE(sys/io.h SYS_IO_FOUND)
|
||||
CHECK_INCLUDE_FILE(linux/input.h LINUX_INPUT_FOUND)
|
||||
|
@ -722,7 +807,6 @@ if (BUILD_GUI)
|
|||
list(APPEND USED_SOURCES ${GUI_SOURCES})
|
||||
list(APPEND DEPENDENCIES_INCLUDE_DIRS
|
||||
extern/imgui_patched
|
||||
extern/imgui_conf
|
||||
extern/imgui_patched/backends
|
||||
extern/IconFontCppHeaders
|
||||
extern/igfd
|
||||
|
@ -743,10 +827,6 @@ if (WIN32)
|
|||
if (NOT MSVC)
|
||||
list(APPEND DEPENDENCIES_LIBRARIES -static)
|
||||
endif()
|
||||
# support Windows XP
|
||||
if (SUPPORT_XP)
|
||||
list(APPEND DEPENDENCIES_DEFINES "_WIN32_WINNT=0x0501")
|
||||
endif()
|
||||
elseif (APPLE)
|
||||
find_library(COCOA Cocoa REQUIRED)
|
||||
list(APPEND DEPENDENCIES_LIBRARIES ${COCOA})
|
||||
|
@ -783,7 +863,7 @@ endif()
|
|||
string(REPLACE ";" " " WARNING_FLAGS_STRING "${WARNING_FLAGS}")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS_STRING}")
|
||||
if (WARNINGS_ARE_ERRORS)
|
||||
message(WARNING
|
||||
message(STATUS
|
||||
"Treating all warnings in furnace's C++ code as errors! "
|
||||
"Please report any errors you encounter on the bug tracker."
|
||||
)
|
||||
|
@ -798,7 +878,7 @@ else()
|
|||
endif()
|
||||
|
||||
target_include_directories(furnace SYSTEM PRIVATE ${DEPENDENCIES_INCLUDE_DIRS})
|
||||
target_compile_definitions(furnace PRIVATE ${DEPENDENCIES_DEFINES} IMGUI_USER_CONFIG="imconfig_fur.h")
|
||||
target_compile_definitions(furnace PRIVATE ${DEPENDENCIES_DEFINES})
|
||||
target_compile_options(furnace PRIVATE ${DEPENDENCIES_COMPILE_OPTIONS})
|
||||
target_link_libraries(furnace PRIVATE ${DEPENDENCIES_LIBRARIES})
|
||||
if (PKG_CONFIG_FOUND AND (SYSTEM_FMT OR SYSTEM_LIBSNDFILE OR SYSTEM_ZLIB OR SYSTEM_SDL2 OR SYSTEM_RTMIDI OR WITH_JACK))
|
||||
|
@ -820,7 +900,8 @@ if (NOT ANDROID OR TERMUX)
|
|||
install(TARGETS furnace RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(FILES res/furnace.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES res/furnace.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||
install(DIRECTORY papers DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||
install(DIRECTORY doc DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||
install(DIRECTORY papers DESTINATION ${CMAKE_INSTALL_DOCDIR}/other)
|
||||
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/furnace)
|
||||
if (WITH_DEMOS)
|
||||
install(DIRECTORY demos DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace)
|
||||
|
|
|
@ -81,6 +81,10 @@ additional guidelines:
|
|||
- on a switch block, **always** put `default` last and not in any other position.
|
||||
- I have fear of some C/C++ compilers ignoring the rest of cases upon hitting default.
|
||||
|
||||
## Do NOT Force-Push after submitting Pull Request
|
||||
|
||||
if you do so, your pull request will be closed.
|
||||
|
||||
## Demo Songs
|
||||
|
||||
just put your demo song in `demos/`! be noted there are some guidelines:
|
||||
|
|
131
README.md
|
@ -1,17 +1,19 @@
|
|||
# Furnace (chiptune tracker)
|
||||
|
||||
![screenshot](papers/screenshot2.png)
|
||||
![screenshot](papers/screenshot3.png)
|
||||
|
||||
the biggest multi-system chiptune tracker ever made!
|
||||
|
||||
[downloads](#downloads) | [discussion/help](#quick-references) | [developer info](#developer-info) | [unofficial packages](#unofficial-packages) | [FAQ](#frequently-asked-questions)
|
||||
[downloads](#downloads) | [discussion/help](#quick-references) | [developer info](#developer-info) | [Unix/Linux packages](#packages) | [FAQ](#frequently-asked-questions)
|
||||
|
||||
---
|
||||
## downloads
|
||||
|
||||
check out the [Releases](https://github.com/tildearrow/furnace/releases) page. available for Windows, macOS and Linux (AppImage).
|
||||
check out the [Releases](https://github.com/tildearrow/furnace/releases) page. available for Windows, macOS and Linux.
|
||||
|
||||
[see here](https://nightly.link/tildearrow/furnace/workflows/build/master) for unstable developer builds.
|
||||
for other operating systems, you may [build the source](#developer-info).
|
||||
|
||||
[see here](https://nightly.link/tildearrow/furnace/workflows/build/master) for the latest unstable build.
|
||||
|
||||
## features
|
||||
|
||||
|
@ -66,9 +68,12 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
|
|||
- Family Noraebang (OPLL)
|
||||
- SID (6581/8580) used in Commodore 64
|
||||
- Mikey used in Atari Lynx
|
||||
- ZX Spectrum beeper (SFX-like engine)
|
||||
- ZX Spectrum beeper
|
||||
- SFX-like engine
|
||||
- QuadTone engine
|
||||
- Pokémon Mini
|
||||
- Commodore PET
|
||||
- Casio PV-1000
|
||||
- TIA used in Atari 2600
|
||||
- POKEY used in Atari 8-bit computers
|
||||
- Game Boy
|
||||
|
@ -76,17 +81,19 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
|
|||
- modern/fantasy:
|
||||
- Commander X16 VERA
|
||||
- tildearrow Sound Unit
|
||||
- Generic PCM DAC
|
||||
- mix and match sound chips!
|
||||
- over 200 ready to use presets from computers, game consoles and arcade boards...
|
||||
- ...or create your own - up to 32 of them or a total of 128 channels!
|
||||
- DefleMask compatibility
|
||||
- loads .dmf modules from all versions (beta 1 to 1.1.7)
|
||||
- loads .dmf modules from all versions (beta 1 to 1.1.9)
|
||||
- saves .dmf modules - both modern and legacy
|
||||
- Furnace doubles as a module downgrader
|
||||
- loads/saves .dmp instruments and .dmw wavetables as well
|
||||
- clean-room design (guesswork and ABX tests only, no decompilation involved)
|
||||
- some bug/quirk implementation for increased playback accuracy through compatibility flags
|
||||
- VGM export
|
||||
- ZSM export for Commander X16
|
||||
- modular layout that you may adapt to your needs
|
||||
- audio file export - entire song, per chip or per channel
|
||||
- quality emulation cores (Nuked, MAME, SameBoy, Mednafen PCE, NSFplay, puNES, reSID, Stella, SAASound, vgsound_emu and ymfm)
|
||||
|
@ -116,16 +123,17 @@ check out the [Releases](https://github.com/tildearrow/furnace/releases) page. a
|
|||
---
|
||||
# quick references
|
||||
|
||||
- **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**: see the [Discussions](https://github.com/tildearrow/furnace/discussions) section, the [official Revolt](https://rvlt.gg/GRPS6tmc) or the [official Discord server](https://discord.gg/EfrwT2wq7z).
|
||||
- **help**: check out the [documentation](doc/README.md). it's about 80% complete.
|
||||
|
||||
## unofficial packages
|
||||
## packages
|
||||
|
||||
[![Packaging status](https://repology.org/badge/tiny-repos/furnace.svg)](https://repology.org/project/furnace/versions)
|
||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/furnace.svg)](https://repology.org/project/furnace/versions)
|
||||
|
||||
some people have provided packages for Unix/Unix-like distributions. here's a list.
|
||||
- **Arch Linux**: [furnace](https://archlinux.org/packages/community/x86_64/furnace/) is now in the community repo!
|
||||
- **FreeBSD**: [a package in ports](https://www.freshports.org/audio/furnace/) is available courtesy of ehaupt.
|
||||
|
||||
- **Arch Linux**: [furnace](https://archlinux.org/packages/extra/x86_64/furnace/) is in the official repositories.
|
||||
- **FreeBSD**: [a package in ports](https://www.freshports.org/audio/furnace/) is available courtesy of ehaupt (warning: 0.5.8!).
|
||||
- **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.
|
||||
|
||||
|
@ -141,7 +149,9 @@ if you can't download these artifacts (because GitHub requires you to be logged
|
|||
## dependencies
|
||||
|
||||
- CMake
|
||||
- Git (for cloning the repository)
|
||||
- JACK (optional, macOS/Linux only)
|
||||
- a C/C++ compiler (e.g. Visual Studio or MinGW on Windows, Xcode (the command-line tools are enough) on macOS or GCC on Linux)
|
||||
|
||||
if building under Windows or macOS, no additional dependencies are required.
|
||||
otherwise, you may also need the following:
|
||||
|
@ -150,6 +160,7 @@ otherwise, you may also need the following:
|
|||
- libx11
|
||||
- libasound
|
||||
- libGL
|
||||
- any other libraries which may be used by SDL
|
||||
|
||||
some Linux distributions (e.g. Ubuntu or openSUSE) will require you to install the `-dev` versions of these.
|
||||
|
||||
|
@ -178,10 +189,32 @@ from the developer tools command prompt:
|
|||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
```
|
||||
|
||||
then open the solution file in Visual Studio and build.
|
||||
|
||||
alternatively, do:
|
||||
|
||||
```
|
||||
msbuild ALL_BUILD.vcxproj
|
||||
```
|
||||
|
||||
### macOS and Linux
|
||||
### Windows using MinGW
|
||||
|
||||
setting up MinGW is a bit more complicated. two benefits are a faster, hotter Furnace, and Windows XP support.
|
||||
|
||||
however, one huge drawback is lack of backtrace support, so you'll have to use gdb when diagnosing a crash.
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G "MinGW Makefiles" ..
|
||||
mingw32-make
|
||||
```
|
||||
|
||||
you may use "MSYS Makefiles" instead, depending on how you installed MinGW.
|
||||
|
||||
### macOS, Linux and other Unix/Unix-like
|
||||
|
||||
```
|
||||
mkdir build
|
||||
|
@ -189,7 +222,16 @@ cd build
|
|||
cmake ..
|
||||
make
|
||||
```
|
||||
Alternatively, build scripts are provided in the `scripts/` folder in the root of the repository.
|
||||
|
||||
on macOS you may do the following instead:
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G Xcode ..
|
||||
```
|
||||
|
||||
...and then load the project on Xcode or type `xcodebuild`.
|
||||
|
||||
### CMake options
|
||||
|
||||
|
@ -218,8 +260,21 @@ Available options:
|
|||
| `WITH_INSTRUMENTS` | `ON` | Install demo instruments on `make install` |
|
||||
| `WITH_WAVETABLES` | `ON` | Install wavetables on `make install` |
|
||||
|
||||
## CMake Error
|
||||
|
||||
if it says something about a missing subdirectory in `extern`, then either:
|
||||
|
||||
1. you didn't set up submodules, or
|
||||
2. you downloaded the source as a .zip or .tar.gz. don't do this.
|
||||
|
||||
if 1, you may run `git submodule update --init --recursive`. this will initialize submodules.
|
||||
|
||||
if 2, clone this repo.
|
||||
|
||||
## console usage
|
||||
|
||||
(note: if on Windows, type `furnace.exe` instead, or `Debug\furnace.exe` on MSVC)
|
||||
|
||||
```
|
||||
./furnace
|
||||
```
|
||||
|
@ -238,25 +293,19 @@ 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.**
|
||||
**note that console mode may not work correctly on Windows. you may have to quit using the Task Manager.**
|
||||
|
||||
---
|
||||
# frequently asked questions
|
||||
|
||||
> woah! 50 sound chips?! I can't believe it!
|
||||
|
||||
yup, it's real.
|
||||
|
||||
> where's the manual?
|
||||
|
||||
see [papers/](papers/doc/README.md). it's kind of incomplete, but at least the sound chips section is there.
|
||||
|
||||
> it doesn't open under macOS!
|
||||
|
||||
this is due to Apple's application signing policy. a workaround is to right click on the Furnace app icon and select Open.
|
||||
|
||||
> it says "Furnace" is damaged and can't be opened!
|
||||
|
||||
**as of Monterey, this workaround no longer works (especially on ARM).** yeah, Apple has decided to be strict on the matter.
|
||||
if you happen to be on that version, use this workaround instead (on a Terminal):
|
||||
if you happen to be on that version (or later), use this workaround instead (on a Terminal):
|
||||
|
||||
```
|
||||
xattr -d com.apple.quarantine /path/to/Furnace.app
|
||||
|
@ -266,24 +315,25 @@ xattr -d com.apple.quarantine /path/to/Furnace.app
|
|||
|
||||
you may need to log out and/or reboot after doing this.
|
||||
|
||||
> where's the manual?
|
||||
|
||||
it is in [doc/](doc/README.md).
|
||||
|
||||
> is there a tutorial?
|
||||
|
||||
sadly, the in-program tutorial isn't ready yet. however, [a video tutorial is available on YouTube](https://youtube.com/playlist?list=PLCELB6AsTZUnwv0PC5AAGHjvg47F44YQ1), made by Spinning Square Waves.
|
||||
|
||||
> I've lost my song!
|
||||
|
||||
Furnace keeps backups of the songs you've worked on before. go to **file > restore backup**.
|
||||
|
||||
> .spc export?
|
||||
|
||||
**not yet!** coming in 0.7 though, eventually...
|
||||
|
||||
> how do I use C64 absolute filter/duty?
|
||||
> ROM export?
|
||||
|
||||
on Instrument Editor in the C64 tab there are two options to toggle these.
|
||||
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`.)
|
||||
|
||||
> how do I use PCM on a PCM-capable chip?
|
||||
|
||||
two possibilities:
|
||||
- the recommended way is by creating the "Sample" type instrument and assigning a sample to it.
|
||||
- otherwise you may employ the DefleMask-compatible method, using `17xx` effect.
|
||||
**not yet!** coming in 0.7 though, eventually...
|
||||
|
||||
> my .dmf song sounds odd at a certain point
|
||||
|
||||
|
@ -293,10 +343,6 @@ Furnace's .dmf compatibility isn't perfect and it's mostly because DefleMask doe
|
|||
|
||||
you should only save as .dmf if you're really sure, because the DefleMask format has several limitations. save in Furnace song format instead (.fur).
|
||||
|
||||
> how do I solo channels?
|
||||
|
||||
right click on the channel name.
|
||||
|
||||
---
|
||||
# footnotes
|
||||
|
||||
|
@ -309,4 +355,5 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|||
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.
|
||||
|
||||
|
||||
despite the fact this program works with the .dmf, .dmp and .dmw file formats (besides its native .fur format), it is NOT affiliated with Delek or DefleMask in any way, nor it is a replacement for the original program.
|
||||
Furnace is NOT affiliated with Delek or DefleMask in any form, regardless of its ability to load and save the .dmf, .dmp and .dmw file formats.
|
||||
additionally, Furnace does not intend to replace DefleMask, nor any other program.
|
||||
|
|
10
TODO.md
|
@ -1,16 +1,12 @@
|
|||
# to-do for 0.6pre5
|
||||
# to-do for 0.6pre7
|
||||
|
||||
- tutorial
|
||||
- tutorial?
|
||||
- ease-of-use improvements... ideas:
|
||||
- preset compat flags
|
||||
- setting to toggle the Choose a System screen on new project
|
||||
- maybe reduced set of presets for the sake of simplicity
|
||||
- a more preferable highlight/drag system
|
||||
- some speed/intuitive workflow improvements that go a long way
|
||||
- Had a hard time finding the docs on github and in Furnace's folder.
|
||||
- make .pdf manual out of papers/doc/
|
||||
- you're going too fast; please slow down
|
||||
- make .pdf manual out of doc/
|
||||
- break compatibility if it relieves complexity
|
||||
- ins/wave/sample organization (folders and all)
|
||||
- multi-key binds
|
||||
- bug fixes
|
||||
|
|
|
@ -15,11 +15,11 @@ android {
|
|||
}
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 143
|
||||
versionName "0.6pre4"
|
||||
versionCode 162
|
||||
versionName "0.6pre7"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static"
|
||||
arguments "-DANDROID_APP_PLATFORM=android-21", "-DANDROID_STL=c++_static", "-DWARNINGS_ARE_ERRORS=ON"
|
||||
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.tildearrow.furnace"
|
||||
android:versionCode="143"
|
||||
android:versionName="0.6pre4"
|
||||
android:versionCode="162"
|
||||
android:versionName="0.6pre7"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!-- OpenGL ES 2.0 -->
|
||||
|
|
|
@ -186,7 +186,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
|
|||
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||
private BluetoothGatt connectGatt(boolean managed) {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
try {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -251,6 +251,8 @@ public class HIDDeviceManager {
|
|||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2c22, // Qanba
|
||||
0x2dc8, // 8BitDo
|
||||
0x9886, // ASTRO Gaming
|
||||
};
|
||||
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
|
@ -271,14 +273,16 @@ public class HIDDeviceManager {
|
|||
final int XB1_IFACE_SUBCLASS = 71;
|
||||
final int XB1_IFACE_PROTOCOL = 208;
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
0x1532, // Razer Wildcat
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2dc8, /* 8BitDo */
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
};
|
||||
|
||||
|
@ -353,13 +357,13 @@ public class HIDDeviceManager {
|
|||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT <= 30 &&
|
||||
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18)) {
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||
return;
|
||||
}
|
||||
|
@ -573,7 +577,7 @@ public class HIDDeviceManager {
|
|||
try {
|
||||
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||
int flags;
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
flags = FLAG_MUTABLE;
|
||||
} else {
|
||||
flags = 0;
|
||||
|
|
|
@ -52,7 +52,7 @@ class HIDDeviceUSB implements HIDDevice {
|
|||
@Override
|
||||
public String getSerialNumber() {
|
||||
String result = null;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
try {
|
||||
result = mDevice.getSerialNumber();
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class HIDDeviceUSB implements HIDDevice {
|
|||
@Override
|
||||
public String getManufacturerName() {
|
||||
String result = null;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
result = mDevice.getManufacturerName();
|
||||
}
|
||||
if (result == null) {
|
||||
|
@ -86,7 +86,7 @@ class HIDDeviceUSB implements HIDDevice {
|
|||
@Override
|
||||
public String getProductName() {
|
||||
String result = null;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
result = mDevice.getProductName();
|
||||
}
|
||||
if (result == null) {
|
||||
|
|
|
@ -29,6 +29,7 @@ public class SDL {
|
|||
|
||||
// This function stores the current activity (SDL or not)
|
||||
public static void setContext(Context context) {
|
||||
SDLAudioManager.setContext(context);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,9 @@ import android.content.pm.PackageManager;
|
|||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
@ -37,11 +33,8 @@ import android.view.Display;
|
|||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
|
@ -51,6 +44,7 @@ import android.view.inputmethod.EditorInfo;
|
|||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
@ -65,6 +59,9 @@ import java.util.Locale;
|
|||
*/
|
||||
public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
|
||||
private static final String TAG = "SDL";
|
||||
private static final int SDL_MAJOR_VERSION = 2;
|
||||
private static final int SDL_MINOR_VERSION = 28;
|
||||
private static final int SDL_MICRO_VERSION = 0;
|
||||
/*
|
||||
// Display InputType.SOURCE/CLASS of events and devices
|
||||
//
|
||||
|
@ -96,7 +93,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
|
||||
s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
tst = InputDevice.SOURCE_BLUETOOTH_STYLUS;
|
||||
if ((s & tst) == tst) src += " BLUETOOTH_STYLUS";
|
||||
s2 &= ~tst;
|
||||
|
@ -110,7 +107,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
if ((s & tst) == tst) src += " GAMEPAD";
|
||||
s2 &= ~tst;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
tst = InputDevice.SOURCE_HDMI;
|
||||
if ((s & tst) == tst) src += " HDMI";
|
||||
s2 &= ~tst;
|
||||
|
@ -149,7 +146,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
if ((s & tst) == tst) src += " TOUCHSCREEN";
|
||||
s2 &= ~tst;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
tst = InputDevice.SOURCE_TOUCH_NAVIGATION;
|
||||
if ((s & tst) == tst) src += " TOUCH_NAVIGATION";
|
||||
s2 &= ~tst;
|
||||
|
@ -173,7 +170,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
*/
|
||||
|
||||
public static boolean mIsResumedCalled, mHasFocus;
|
||||
public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24);
|
||||
public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */);
|
||||
|
||||
// Cursor types
|
||||
// private static final int SDL_SYSTEM_CURSOR_NONE = -1;
|
||||
|
@ -213,7 +210,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
// Main components
|
||||
protected static SDLActivity mSingleton;
|
||||
protected static SDLSurface mSurface;
|
||||
protected static View mTextEdit;
|
||||
protected static DummyEdit mTextEdit;
|
||||
protected static boolean mScreenKeyboardShown;
|
||||
protected static ViewGroup mLayout;
|
||||
protected static SDLClipboardHandler mClipboardHandler;
|
||||
|
@ -227,9 +224,9 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
|
||||
protected static SDLGenericMotionListener_API12 getMotionListener() {
|
||||
if (mMotionListener == null) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
|
||||
mMotionListener = new SDLGenericMotionListener_API26();
|
||||
} else if (Build.VERSION.SDK_INT >= 24) {
|
||||
} else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
mMotionListener = new SDLGenericMotionListener_API24();
|
||||
} else {
|
||||
mMotionListener = new SDLGenericMotionListener_API12();
|
||||
|
@ -315,6 +312,10 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
mCurrentNativeState = NativeState.INIT;
|
||||
}
|
||||
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new SDLSurface(context);
|
||||
}
|
||||
|
||||
// Setup
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -344,8 +345,18 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
errorMsgBrokenLib = e.getMessage();
|
||||
}
|
||||
|
||||
if (mBrokenLibraries)
|
||||
{
|
||||
if (!mBrokenLibraries) {
|
||||
String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." +
|
||||
String.valueOf(SDL_MINOR_VERSION) + "." +
|
||||
String.valueOf(SDL_MICRO_VERSION);
|
||||
String version = nativeGetVersion();
|
||||
if (!version.equals(expected_version)) {
|
||||
mBrokenLibraries = true;
|
||||
errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")";
|
||||
}
|
||||
}
|
||||
|
||||
if (mBrokenLibraries) {
|
||||
mSingleton = this;
|
||||
AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
|
||||
dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
|
||||
|
@ -382,7 +393,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
mHIDDeviceManager = HIDDeviceManager.acquire(this);
|
||||
|
||||
// Set up the surface
|
||||
mSurface = new SDLSurface(getApplication());
|
||||
mSurface = createSDLSurface(this);
|
||||
|
||||
mLayout = new RelativeLayout(this);
|
||||
mLayout.addView(mSurface);
|
||||
|
@ -393,7 +404,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
|
||||
mCurrentLocale = getContext().getResources().getConfiguration().locale;
|
||||
} else {
|
||||
mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0);
|
||||
|
@ -577,6 +588,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
mHIDDeviceManager = null;
|
||||
}
|
||||
|
||||
SDLAudioManager.release(this);
|
||||
|
||||
if (SDLActivity.mBrokenLibraries) {
|
||||
super.onDestroy();
|
||||
return;
|
||||
|
@ -755,7 +768,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
}
|
||||
break;
|
||||
case COMMAND_CHANGE_WINDOW_STYLE:
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||
if (context instanceof Activity) {
|
||||
Window window = ((Activity) context).getWindow();
|
||||
if (window != null) {
|
||||
|
@ -830,7 +843,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
msg.obj = data;
|
||||
boolean result = commandHandler.sendMessage(msg);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||
if (command == COMMAND_CHANGE_WINDOW_STYLE) {
|
||||
// Ensure we don't return until the resize has actually happened,
|
||||
// or 500ms have passed.
|
||||
|
@ -886,6 +899,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
}
|
||||
|
||||
// C functions we call
|
||||
public static native String nativeGetVersion();
|
||||
public static native int nativeSetupJNI();
|
||||
public static native int nativeRunMain(String library, String function, Object arguments);
|
||||
public static native void nativeLowMemory();
|
||||
|
@ -957,15 +971,18 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
/* If set, hint "explicitly controls which UI orientations are allowed". */
|
||||
if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) {
|
||||
orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
} else if (hint.contains("LandscapeRight")) {
|
||||
orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
} else if (hint.contains("LandscapeLeft")) {
|
||||
orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
} else if (hint.contains("LandscapeRight")) {
|
||||
orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
|
||||
}
|
||||
|
||||
if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) {
|
||||
/* exact match to 'Portrait' to distinguish with PortraitUpsideDown */
|
||||
boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait");
|
||||
|
||||
if (contains_Portrait && hint.contains("PortraitUpsideDown")) {
|
||||
orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
|
||||
} else if (hint.contains("Portrait")) {
|
||||
} else if (contains_Portrait) {
|
||||
orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
} else if (hint.contains("PortraitUpsideDown")) {
|
||||
orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
|
||||
|
@ -1078,7 +1095,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
// thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result,
|
||||
// we should stick to relative mode.
|
||||
//
|
||||
if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
|
||||
if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1168,7 +1185,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static boolean isDeXMode() {
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
|
@ -1220,8 +1237,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
}
|
||||
|
||||
// This method is called by SDLControllerManager's API 26 Generic Motion Handler.
|
||||
public static View getContentView()
|
||||
{
|
||||
public static View getContentView() {
|
||||
return mLayout;
|
||||
}
|
||||
|
||||
|
@ -1292,6 +1308,77 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE;
|
||||
}
|
||||
|
||||
public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) {
|
||||
int deviceId = event.getDeviceId();
|
||||
int source = event.getSource();
|
||||
|
||||
if (source == InputDevice.SOURCE_UNKNOWN) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device != null) {
|
||||
source = device.getSources();
|
||||
}
|
||||
}
|
||||
|
||||
// if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
||||
// } else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
||||
// }
|
||||
|
||||
// Dispatch the different events depending on where they come from
|
||||
// Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
|
||||
// So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
|
||||
//
|
||||
// Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
|
||||
// SOURCE_JOYSTICK, while its key events arrive from the keyboard source
|
||||
// So, retrieve the device itself and check all of its sources
|
||||
if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
|
||||
// Note that we process events with specific key codes here
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (isTextInputEvent(event)) {
|
||||
if (ic != null) {
|
||||
ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
} else {
|
||||
SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
}
|
||||
}
|
||||
onNativeKeyDown(keyCode);
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
onNativeKeyUp(keyCode);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
|
||||
// on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
|
||||
// they are ignored here because sending them as mouse input to SDL is messy
|
||||
if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
case KeyEvent.ACTION_UP:
|
||||
// mark the event as handled or it will be handled by system
|
||||
// handling KEYCODE_BACK by system will call onBackPressed()
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
|
@ -1535,7 +1622,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
private final Runnable rehideSystemUi = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||
int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
|
@ -1588,7 +1675,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
|
||||
++mLastCursorID;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
try {
|
||||
mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY));
|
||||
} catch (Exception e) {
|
||||
|
@ -1604,7 +1691,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void destroyCustomCursor(int cursorID) {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
try {
|
||||
mCursors.remove(cursorID);
|
||||
} catch (Exception e) {
|
||||
|
@ -1618,7 +1705,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
*/
|
||||
public static boolean setCustomCursor(int cursorID) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
try {
|
||||
mSurface.setPointerIcon(mCursors.get(cursorID));
|
||||
} catch (Exception e) {
|
||||
|
@ -1673,7 +1760,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
cursor_type = 1002; //PointerIcon.TYPE_HAND;
|
||||
break;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
try {
|
||||
mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type));
|
||||
} catch (Exception e) {
|
||||
|
@ -1687,7 +1774,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static void requestPermission(String permission, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
nativePermissionResult(requestCode, true);
|
||||
return;
|
||||
}
|
||||
|
@ -1716,7 +1803,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
i.setData(Uri.parse(url));
|
||||
|
||||
int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
|
||||
} else {
|
||||
flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
|
||||
|
@ -1809,455 +1896,6 @@ class SDLMain implements Runnable {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
public boolean mIsSurfaceReady;
|
||||
|
||||
// Startup
|
||||
public SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
public void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
public void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
public Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
try
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip in MultiWindow.
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
||||
Log.v("SDL", "Don't skip in Multi-Window");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
|
||||
int deviceId = event.getDeviceId();
|
||||
int source = event.getSource();
|
||||
|
||||
if (source == InputDevice.SOURCE_UNKNOWN) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device != null) {
|
||||
source = device.getSources();
|
||||
}
|
||||
}
|
||||
|
||||
// if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
||||
// } else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
||||
// }
|
||||
|
||||
// Dispatch the different events depending on where they come from
|
||||
// Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
|
||||
// So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
|
||||
//
|
||||
// Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
|
||||
// SOURCE_JOYSTICK, while its key events arrive from the keyboard source
|
||||
// So, retrieve the device itself and check all of its sources
|
||||
if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
|
||||
// Note that we process events with specific key codes here
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (SDLActivity.isTextInputEvent(event)) {
|
||||
SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
}
|
||||
SDLActivity.onNativeKeyDown(keyCode);
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
SDLActivity.onNativeKeyUp(keyCode);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
|
||||
// on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
|
||||
// they are ignored here because sending them as mouse input to SDL is messy
|
||||
if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
case KeyEvent.ACTION_UP:
|
||||
// mark the event as handled or it will be handled by system
|
||||
// handling KEYCODE_BACK by system will call onBackPressed()
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerFingerId;
|
||||
int i = -1;
|
||||
float x,y,p;
|
||||
|
||||
/*
|
||||
* Prevent id to be -1, since it's used in SDL internal for synthetic events
|
||||
* Appears when using Android emulator, eg:
|
||||
* adb shell input mouse tap 100 100
|
||||
* adb shell input touchscreen tap 100 100
|
||||
*/
|
||||
if (touchDevId < 0) {
|
||||
touchDevId -= 1;
|
||||
}
|
||||
|
||||
// 12290 = Samsung DeX mode desktop mouse
|
||||
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
||||
// 0x2 = SOURCE_CLASS_POINTER
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||
int mouseButton = 1;
|
||||
try {
|
||||
Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
||||
if (object != null) {
|
||||
mouseButton = (Integer) object;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event);
|
||||
y = motionListener.getEventY(event);
|
||||
|
||||
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
||||
} else {
|
||||
switch(action) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Primary pointer up/down, the index is always zero
|
||||
i = 0;
|
||||
/* fallthrough */
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
// Non primary pointer up/down
|
||||
if (i == -1) {
|
||||
i = event.getActionIndex();
|
||||
}
|
||||
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
public void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
mSensorManager.registerListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME, null);
|
||||
} else {
|
||||
mSensorManager.unregisterListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newOrientation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newOrientation != SDLActivity.mCurrentOrientation) {
|
||||
SDLActivity.mCurrentOrientation = newOrientation;
|
||||
SDLActivity.onNativeOrientationChanged(newOrientation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* This is a fake invisible editor view that receives the input and defines the
|
||||
* pan&scan region
|
||||
*/
|
||||
|
@ -2278,21 +1916,7 @@ class DummyEdit extends View implements View.OnKeyListener {
|
|||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
/*
|
||||
* This handles the hardware keyboard input
|
||||
*/
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (SDLActivity.isTextInputEvent(event)) {
|
||||
ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
||||
return true;
|
||||
}
|
||||
SDLActivity.onNativeKeyDown(keyCode);
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
SDLActivity.onNativeKeyUp(keyCode);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -2316,9 +1940,10 @@ class DummyEdit extends View implements View.OnKeyListener {
|
|||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
ic = new SDLInputConnection(this, true);
|
||||
|
||||
outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
| EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_FLAG_MULTI_LINE;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
|
||||
return ic;
|
||||
}
|
||||
|
@ -2326,9 +1951,17 @@ class DummyEdit extends View implements View.OnKeyListener {
|
|||
|
||||
class SDLInputConnection extends BaseInputConnection {
|
||||
|
||||
protected EditText mEditText;
|
||||
protected String mCommittedText = "";
|
||||
|
||||
public SDLInputConnection(View targetView, boolean fullEditor) {
|
||||
super(targetView, fullEditor);
|
||||
mEditText = new EditText(SDL.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditText.getEditableText();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2351,68 +1984,31 @@ class SDLInputConnection extends BaseInputConnection {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
|
||||
/* Generate backspaces for the text we're going to replace */
|
||||
final Editable content = getEditable();
|
||||
if (content != null) {
|
||||
int a = getComposingSpanStart(content);
|
||||
int b = getComposingSpanEnd(content);
|
||||
if (a == -1 || b == -1) {
|
||||
a = Selection.getSelectionStart(content);
|
||||
b = Selection.getSelectionEnd(content);
|
||||
if (!super.commitText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
if (a < 0) a = 0;
|
||||
if (b < 0) b = 0;
|
||||
if (b < a) {
|
||||
int tmp = a;
|
||||
a = b;
|
||||
b = tmp;
|
||||
}
|
||||
int backspaces = (b - a);
|
||||
|
||||
for (int i = 0; i < backspaces; i++) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
nativeGenerateScancodeForUnichar(c);
|
||||
}
|
||||
|
||||
SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
|
||||
|
||||
return super.commitText(text, newCursorPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
|
||||
nativeSetComposingText(text.toString(), newCursorPosition);
|
||||
|
||||
return super.setComposingText(text, newCursorPosition);
|
||||
if (!super.setComposingText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public native void nativeGenerateScancodeForUnichar(char c);
|
||||
|
||||
public native void nativeSetComposingText(String text, int newCursorPosition);
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
|
||||
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
|
@ -2421,9 +2017,63 @@ class SDLInputConnection extends BaseInputConnection {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateText() {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = content.toString();
|
||||
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||
int matchLength, offset;
|
||||
|
||||
/* Backspace over characters that are no longer in the string */
|
||||
for (matchLength = 0; matchLength < compareLength; ) {
|
||||
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||
if (codePoint != text.codePointAt(matchLength)) {
|
||||
break;
|
||||
}
|
||||
matchLength += Character.charCount(codePoint);
|
||||
}
|
||||
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||
for (offset = matchLength; offset < mCommittedText.length(); ) {
|
||||
int codePoint = mCommittedText.codePointAt(offset);
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
if (matchLength < text.length()) {
|
||||
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||
for (offset = 0; offset < pendingText.length(); ) {
|
||||
int codePoint = pendingText.codePointAt(offset);
|
||||
if (codePoint == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Higher code points don't generate simulated scancodes */
|
||||
if (codePoint < 128) {
|
||||
nativeGenerateScancodeForUnichar((char)codePoint);
|
||||
}
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||
}
|
||||
mCommittedText = text;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||
}
|
||||
|
||||
class SDLClipboardHandler implements
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecord;
|
||||
|
@ -8,16 +11,49 @@ import android.media.MediaRecorder;
|
|||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
public class SDLAudioManager
|
||||
{
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static AudioTrack mAudioTrack;
|
||||
protected static AudioRecord mAudioRecord;
|
||||
protected static Context mContext;
|
||||
|
||||
private static final int[] NO_DEVICES = {};
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
public static void initialize() {
|
||||
mAudioTrack = null;
|
||||
mAudioRecord = null;
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
mContext = context;
|
||||
if (context != null) {
|
||||
registerAudioDeviceCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public static void release(Context context) {
|
||||
unregisterAudioDeviceCallback(context);
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
@ -35,7 +71,7 @@ public class SDLAudioManager
|
|||
}
|
||||
}
|
||||
|
||||
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
int channelConfig;
|
||||
int sampleSize;
|
||||
int frameSize;
|
||||
|
@ -43,14 +79,14 @@ public class SDLAudioManager
|
|||
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
|
||||
|
||||
/* On older devices let's use known good settings */
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
if (desiredChannels > 2) {
|
||||
desiredChannels = 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
|
||||
if (Build.VERSION.SDK_INT < 22) {
|
||||
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
|
||||
if (sampleRate < 8000) {
|
||||
sampleRate = 8000;
|
||||
} else if (sampleRate > 48000) {
|
||||
|
@ -59,7 +95,7 @@ public class SDLAudioManager
|
|||
}
|
||||
|
||||
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
|
||||
int minSDKVersion = (isCapture ? 23 : 21);
|
||||
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
|
||||
if (Build.VERSION.SDK_INT < minSDKVersion) {
|
||||
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
@ -120,7 +156,7 @@ public class SDLAudioManager
|
|||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||
break;
|
||||
case 8:
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
} else {
|
||||
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
|
||||
|
@ -201,6 +237,10 @@ public class SDLAudioManager
|
|||
return null;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
|
||||
}
|
||||
|
||||
mAudioRecord.startRecording();
|
||||
}
|
||||
|
||||
|
@ -224,6 +264,10 @@ public class SDLAudioManager
|
|||
return null;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
|
||||
}
|
||||
|
||||
mAudioTrack.play();
|
||||
}
|
||||
|
||||
|
@ -238,11 +282,73 @@ public class SDLAudioManager
|
|||
return results;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
|
||||
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
|
||||
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void unregisterAudioDeviceCallback(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
|
||||
public static int[] getAudioOutputDevices() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||
} else {
|
||||
return NO_DEVICES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] getAudioInputDevices() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||
} else {
|
||||
return NO_DEVICES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,6 +360,11 @@ public class SDLAudioManager
|
|||
return;
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < buffer.length;) {
|
||||
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
|
||||
if (result > 0) {
|
||||
|
@ -326,18 +437,22 @@ public class SDLAudioManager
|
|||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
|
||||
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
|
||||
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return 0;
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
|
@ -346,7 +461,7 @@ public class SDLAudioManager
|
|||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||
} else {
|
||||
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||
|
@ -391,4 +506,9 @@ public class SDLAudioManager
|
|||
}
|
||||
|
||||
public static native int nativeSetupJNI();
|
||||
|
||||
public static native void removeAudioDevice(boolean isCapture, int deviceId);
|
||||
|
||||
public static native void addAudioDevice(boolean isCapture, int deviceId);
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ public class SDLControllerManager
|
|||
public static native int nativeAddJoystick(int device_id, String name, String desc,
|
||||
int vendor_id, int product_id,
|
||||
boolean is_accelerometer, int button_mask,
|
||||
int naxes, int nhats, int nballs);
|
||||
int naxes, int axis_mask, int nhats, int nballs);
|
||||
public static native int nativeRemoveJoystick(int device_id);
|
||||
public static native int nativeAddHaptic(int device_id, String name);
|
||||
public static native int nativeRemoveHaptic(int device_id);
|
||||
|
@ -42,7 +42,7 @@ public class SDLControllerManager
|
|||
|
||||
public static void initialize() {
|
||||
if (mJoystickHandler == null) {
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||
mJoystickHandler = new SDLJoystickHandler_API19();
|
||||
} else {
|
||||
mJoystickHandler = new SDLJoystickHandler_API16();
|
||||
|
@ -50,7 +50,7 @@ public class SDLControllerManager
|
|||
}
|
||||
|
||||
if (mHapticHandler == null) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
|
||||
mHapticHandler = new SDLHapticHandler_API26();
|
||||
} else {
|
||||
mHapticHandler = new SDLHapticHandler();
|
||||
|
@ -168,6 +168,32 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
|||
arg1Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
|
||||
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
|
||||
// This is because the usual pairing are:
|
||||
// - AXIS_X + AXIS_Y (left stick).
|
||||
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
|
||||
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
|
||||
// This sorts the axes in the above order, which tends to be correct
|
||||
// for Xbox-ish game pads that have the right stick on RX/RY and the
|
||||
// triggers on Z/RZ.
|
||||
//
|
||||
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
|
||||
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
|
||||
//
|
||||
// References:
|
||||
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
|
||||
if (arg0Axis == MotionEvent.AXIS_Z) {
|
||||
arg0Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg0Axis;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_Z) {
|
||||
arg1Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg1Axis;
|
||||
}
|
||||
|
||||
return arg0Axis - arg1Axis;
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +236,7 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
|||
mJoysticks.add(joystick);
|
||||
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||
getVendorId(joystickDevice), getProductId(joystickDevice), false,
|
||||
getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0);
|
||||
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,6 +317,9 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
|||
public int getVendorId(InputDevice joystickDevice) {
|
||||
return 0;
|
||||
}
|
||||
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||
return -1;
|
||||
}
|
||||
public int getButtonMask(InputDevice joystickDevice) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -308,6 +337,43 @@ class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
|
|||
return joystickDevice.getVendorId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||
// For compatibility, keep computing the axis mask like before,
|
||||
// only really distinguishing 2, 4 and 6 axes.
|
||||
int axis_mask = 0;
|
||||
if (ranges.size() >= 2) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
|
||||
axis_mask |= 0x0003;
|
||||
}
|
||||
if (ranges.size() >= 4) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
|
||||
axis_mask |= 0x000c;
|
||||
}
|
||||
if (ranges.size() >= 6) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
|
||||
axis_mask |= 0x0030;
|
||||
}
|
||||
// Also add an indicator bit for whether the sorting order has changed.
|
||||
// This serves to disable outdated gamecontrollerdb.txt mappings.
|
||||
boolean have_z = false;
|
||||
boolean have_past_z_before_rz = false;
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
int axis = range.getAxis();
|
||||
if (axis == MotionEvent.AXIS_Z) {
|
||||
have_z = true;
|
||||
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
|
||||
have_past_z_before_rz = true;
|
||||
}
|
||||
}
|
||||
if (have_z && have_past_z_before_rz) {
|
||||
// If both these exist, the compare() function changed sorting order.
|
||||
// Set a bit to indicate this fact.
|
||||
axis_mask |= 0x8000;
|
||||
}
|
||||
return axis_mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getButtonMask(InputDevice joystickDevice) {
|
||||
int button_mask = 0;
|
||||
|
@ -743,7 +809,7 @@ class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
|||
|
||||
@Override
|
||||
public boolean supportsRelativeMouse() {
|
||||
return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27));
|
||||
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -753,7 +819,7 @@ class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
|||
|
||||
@Override
|
||||
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) {
|
||||
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
|
||||
if (enabled) {
|
||||
SDLActivity.getContentView().requestPointerCapture();
|
||||
} else {
|
||||
|
|
405
android/app/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
|
@ -0,0 +1,405 @@
|
|||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
public boolean mIsSurfaceReady;
|
||||
|
||||
// Startup
|
||||
public SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
public void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
public void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
public Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
try
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip in MultiWindow.
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
||||
Log.v("SDL", "Don't skip in Multi-Window");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerFingerId;
|
||||
int i = -1;
|
||||
float x,y,p;
|
||||
|
||||
/*
|
||||
* Prevent id to be -1, since it's used in SDL internal for synthetic events
|
||||
* Appears when using Android emulator, eg:
|
||||
* adb shell input mouse tap 100 100
|
||||
* adb shell input touchscreen tap 100 100
|
||||
*/
|
||||
if (touchDevId < 0) {
|
||||
touchDevId -= 1;
|
||||
}
|
||||
|
||||
// 12290 = Samsung DeX mode desktop mouse
|
||||
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
||||
// 0x2 = SOURCE_CLASS_POINTER
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||
int mouseButton = 1;
|
||||
try {
|
||||
Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
||||
if (object != null) {
|
||||
mouseButton = (Integer) object;
|
||||
}
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event);
|
||||
y = motionListener.getEventY(event);
|
||||
|
||||
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
||||
} else {
|
||||
switch(action) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Primary pointer up/down, the index is always zero
|
||||
i = 0;
|
||||
/* fallthrough */
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
// Non primary pointer up/down
|
||||
if (i == -1) {
|
||||
i = event.getActionIndex();
|
||||
}
|
||||
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
for (i = 0; i < pointerCount; i++) {
|
||||
pointerFingerId = event.getPointerId(i);
|
||||
x = event.getX(i) / mWidth;
|
||||
y = event.getY(i) / mHeight;
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
public void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
mSensorManager.registerListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME, null);
|
||||
} else {
|
||||
mSensorManager.unregisterListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newOrientation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newOrientation != SDLActivity.mCurrentOrientation) {
|
||||
SDLActivity.mCurrentOrientation = newOrientation;
|
||||
SDLActivity.onNativeOrientationChanged(newOrientation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(0);
|
||||
y = event.getY(0);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
BIN
demos/arcade/Destiny_Islands_Irem_M92.fur
Normal file
BIN
demos/arcade/Some_Creatures_SegaPCM.fur
Normal file
BIN
demos/arcade/physics_exam.fur
Normal file
BIN
demos/ay8910/Flat Wave Society.fur
Normal file
BIN
demos/ay8930/AY8930Shuffle.fur
Normal file
BIN
demos/gameboy/finger.fur
Normal file
BIN
demos/genesis/Fancy_Promenard.fur
Normal file
BIN
demos/genesis/Shovel_Knight_Title.fur
Normal file
BIN
demos/genesis/inside_the_computer.fur
Normal file
BIN
demos/misc/Night_Market_TI994A.fur
Normal file
BIN
demos/misc/QSound_smile.fur
Normal file
BIN
demos/misc/mushroomhill_SM8521.fur
Normal file
BIN
demos/msx/OPLL_High_and_Rising.fur
Normal file
BIN
demos/msx/Striking_Towards_Opposition.fur
Normal file
BIN
demos/multichip/invicibility_mmc5_n163_fds.fur
Normal file
BIN
demos/opl/Fly_to_the_Leaden_Sky_OPL3.fur
Normal file
BIN
demos/opl/Sliding_on_a_Rainbow.fur
Normal file
BIN
demos/opl/attack_the_barbarian_opl.fur
Normal file
BIN
demos/pce/Warpdrive_Engage.fur
Normal file
BIN
demos/sms/FlowOfSN7.fur
Normal file
BIN
demos/snes/Cosmic_Warehouse.fur
Normal file
BIN
demos/snes/changeyourheart.fur
Normal file
BIN
demos/x16/Shades of Blue.fur
Normal file
BIN
demos/x16/keygen19.fur
Normal file
23
doc/1-intro/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# introduction
|
||||
|
||||
Furnace is a tool which allows you to create music using sound chips ("chiptune"), most from the 8/16-bit era.
|
||||
|
||||
it has a large selection of features and sound chips. from the NES, SNES and Genesis to ES5506, VIC-20 or even Arcade, Furnace has most likely covered your target with many presets to choose from.
|
||||
|
||||
every chip is emulated using many emulation cores, therefore the sound that Furnace produces is authentic to that of real hardware.
|
||||
|
||||
## hexadecimal
|
||||
|
||||
Furnace uses hexadecimal (abbreviated as "hex") numbers frequently. see [this guide](hex.md) for a crash course.
|
||||
|
||||
## interface
|
||||
|
||||
Furnace uses a music tracker interface. think of a table with music notes written on it. then that table scrolls up and plays the notes. even experienced tracker musicians might benefit from a quick review of [tracker concepts and terms](concepts.md) before using Furnace.
|
||||
|
||||
due to its nature of being feature-packed, it may be technical and somewhat difficult to get around. therefore we added a basic mode, which hides several advanced features.
|
||||
|
||||
it also has a flexible windowing system which you may move around and organize.
|
||||
|
||||
see [2-interface](../2-interface/README.md) and [3-pattern](../3-pattern/README.md) for more information.
|
||||
|
||||
once familiar with the tracker, look to [9-guides](../9-guides/README.md) for useful techniques.
|
36
doc/1-intro/concepts.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# concepts and terms
|
||||
|
||||
- A **module** is a file for a tracker that contains at least one **song**.
|
||||
- Each Furnace module involves at least one **[chip](../7-systems/README.md)**, an emulation of a specific audio processor.
|
||||
|
||||
## tracking
|
||||
|
||||
The **[pattern view](../3-pattern/README.md)** is like a spreadsheet that displays the following:
|
||||
- Each labeled column represents a **channel** of sound provided by the chips in use.
|
||||
- Each **note** starts a sound playing. Within a channel, only one note can play at a time.
|
||||
- Each note is assigned an **[instrument](../4-instrument/README.md)** which describes what it will sound like.
|
||||
- An **effect** is a command that changes some aspect of playback. It can alter note pitch, volume, timing, and more.
|
||||
- An instrument **macro** is an automated sequence of effects that applies to every note of that instrument.
|
||||
|
||||
## structure
|
||||
|
||||
The **order list** is a smaller spreadsheet showing the overall song structure.
|
||||
- A song is made up of a list of **orders**.
|
||||
- An **order** is a set of numbered **patterns** used for each channel.
|
||||
- Each channel has its own unique list of patterns.
|
||||
- Each pattern contains note and effect data for that channel only.
|
||||
- Patterns may be used multiple times in the order list. Changing a pattern's data in one order will affect the same pattern used in other orders.
|
||||
|
||||
## time
|
||||
|
||||
- Each pattern is made of the same number of **rows** as seen in the tracker view.
|
||||
- During playback, Each row lasts a number of **ticks** determined by its **speed** value.
|
||||
- A tick is the smallest measure of time to which all note, effect, and macro times are quantized.
|
||||
|
||||
## sound
|
||||
|
||||
Different chips have different capabilities. Even within the same chip, each channel may have its own ways of making sound.
|
||||
- Some channels use one or more waveform **generators** (sine, square, noise...) to build up a sound.
|
||||
- Of special note are **[FM (frequency modulation)](../4-instrument/fm.md)** channels, which use a number of generators called **operators** that can interact to make very complex sounds.
|
||||
- Some channels use **[samples](../6-sample/README.md)** - recordings of sounds, often with defined loop points to allow a note to sustain.
|
||||
- Some channels use **[wavetables](../5-wave/README.md)**, which are like very short samples of fixed length that automatically loop.
|
118
doc/1-intro/hex.md
Normal file
|
@ -0,0 +1,118 @@
|
|||
# hexadecimal
|
||||
|
||||
the hexadecimal numeral system differs from the decimal system by having 16 digits rather than 10:
|
||||
|
||||
```
|
||||
hex| decimal
|
||||
---|---------
|
||||
0 | 0
|
||||
1 | 1
|
||||
2 | 2
|
||||
3 | 3
|
||||
4 | 4
|
||||
5 | 5
|
||||
6 | 6
|
||||
7 | 7
|
||||
8 | 8
|
||||
9 | 9
|
||||
A | 10
|
||||
B | 11
|
||||
C | 12
|
||||
D | 13
|
||||
E | 14
|
||||
F | 15
|
||||
```
|
||||
|
||||
when there is more than one digit, these are multiplied by 16, 256, 4096 and so on rather than 10, 100, 1000:
|
||||
|
||||
```
|
||||
hex | decimal
|
||||
----|---------
|
||||
00 | 0
|
||||
04 | 4
|
||||
08 | 8
|
||||
0F | 15
|
||||
10 | 16
|
||||
11 | 17
|
||||
12 | 18
|
||||
13 | 19
|
||||
20 | 32
|
||||
30 | 48
|
||||
40 | 64
|
||||
```
|
||||
|
||||
# hex to decimal
|
||||
|
||||
for example, take hexadecimal number `AA`:
|
||||
|
||||
```
|
||||
2nd digit -\ /- 1st digit
|
||||
A A
|
||||
16^1*10 = 16*10 = 160 + 10 = 170
|
||||
```
|
||||
|
||||
now for hexadecimal number `4C5F`:
|
||||
|
||||
```
|
||||
|
||||
3rd digit -\ /- 2nd digit
|
||||
4th digit -\ | | /- 1st digit
|
||||
4 C 5 F
|
||||
| | | |
|
||||
| | | 15 = 15 = 15 +
|
||||
| | \16^1*5 = 16 * 5 = 80
|
||||
| \--- 16^2*12 = 256 * 12 = 3072
|
||||
\--------- 16^3*4 = 4096 * 4 = 16384
|
||||
-------
|
||||
= 19551
|
||||
```
|
||||
|
||||
# decimal to hex
|
||||
|
||||
if it's less than 16, just memorize the table at the top of this document.
|
||||
|
||||
otherwise find the power of 16 that is closest to the number you want to convert, but no larger than the number.
|
||||
then divide, and take the remainder.
|
||||
divide the remainder with the previous power of 16, until the divider is 1.
|
||||
|
||||
for example, for the decimal number `220`:
|
||||
|
||||
```
|
||||
220 ÷ 16 = 13 (r = 12) D
|
||||
12 ÷ 1 = 12 (stop here) C
|
||||
|
||||
= DC
|
||||
```
|
||||
|
||||
now for decimal number `69420`:
|
||||
|
||||
```
|
||||
69420 ÷ 65536 = 1 (r = 3884) 1
|
||||
3884 ÷ 4096 = 0 (r = 3884) 0
|
||||
3884 ÷ 256 = 15 (r = 44) F
|
||||
44 ÷ 16 = 2 (r = 12) 2
|
||||
12 ÷ 1 = 12 (stop here) C
|
||||
|
||||
= 10F2C
|
||||
```
|
||||
|
||||
# hex-decimal table
|
||||
|
||||
hex | `0` | `1` | `2` | `3` | `4` | `5` | `6` | `7` | `8` | `9` | `A` | `B` | `C` | `D` | `E` | `F`
|
||||
-----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:|----:
|
||||
`00` | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||
`10` | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31
|
||||
`20` | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47
|
||||
`30` | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63
|
||||
`40` | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79
|
||||
`50` | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95
|
||||
`60` | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111
|
||||
`70` | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127
|
||||
`80` | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143
|
||||
`90` | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159
|
||||
`A0` | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175
|
||||
`B0` | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191
|
||||
`C0` | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207
|
||||
`D0` | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223
|
||||
`E0` | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239
|
||||
`F0` | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255
|
43
doc/2-interface/README.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
# interface
|
||||
|
||||
the Furnace user interface is where the job gets done.
|
||||
|
||||
the default layout of Furnace is depicted below.
|
||||
|
||||
![interface](interface1.png)
|
||||
|
||||
primary topics:
|
||||
|
||||
- [menu bar](menu-bar.md)
|
||||
- [order list](order-list.md)
|
||||
- [play/edit controls](play-edit-controls.md)
|
||||
- [instrument/wavetable/sample list](asset-list.md)
|
||||
- [song information](song-info.md)
|
||||
- [pattern view](../3-pattern/README.md)
|
||||
- [instrument editor](../4-instrument/README.md)
|
||||
- [wavetable editor](../5-wave/README.md)
|
||||
- [sample editor](../6-sample/README.md)
|
||||
|
||||
advanced topics:
|
||||
|
||||
- [mixer](../8-advanced/mixer.md)
|
||||
- [grooves](../8-advanced/grooves.md)
|
||||
- [channels](../8-advanced/channels.md)
|
||||
- [pattern manager](../8-advanced/pat-manager.md)
|
||||
- [chip manager](../8-advanced/chip-manager.md)
|
||||
- [compatibility flags](../8-advanced/compat-flags.md)
|
||||
- [song comments](../8-advanced/comments.md)
|
||||
- [piano/input pad](../8-advanced/piano.md)
|
||||
- [oscilloscope](../8-advanced/osc.md)
|
||||
- [oscilloscope (per channel)](../8-advanced/chanosc.md)
|
||||
- [clock](../8-advanced/clock.md)
|
||||
- [register view](../8-advanced/regview.md)
|
||||
- [log viewer](../8-advanced/log-viewer.md)
|
||||
- [statistics](../8-advanced/stats.md)
|
||||
|
||||
other topics:
|
||||
|
||||
- [settings](../2-interface/settings.md)
|
||||
- [UI components](components.md)
|
||||
- [global keyboard shortcuts](keyboard.md)
|
||||
- [basic mode](basic-mode.md)
|
BIN
doc/2-interface/asset-add.png
Normal file
After Width: | Height: | Size: 423 B |
BIN
doc/2-interface/asset-delete.png
Normal file
After Width: | Height: | Size: 576 B |
BIN
doc/2-interface/asset-duplicate.png
Normal file
After Width: | Height: | Size: 648 B |
BIN
doc/2-interface/asset-folderview.png
Normal file
After Width: | Height: | Size: 436 B |
38
doc/2-interface/asset-list.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# instrument list
|
||||
|
||||
![instruments window](instruments.png)
|
||||
|
||||
Buttons from left to right:
|
||||
- **Add**: Creates a new, default instrument.
|
||||
- **Duplicate**: Duplicates the currently selected instrument.
|
||||
- **Open**: Brings up a file dialog to load a file as a new instrument at the end of the list.
|
||||
- **Save**: Brings up a file dialog to save the currently selected instrument.
|
||||
- **Toggle folders/standard view**: Enables (and disables) folder view, explained below.
|
||||
- **Move up**: Moves the currently selected instrument up in the list. Pattern data will automatically be adjusted to match.
|
||||
- **Move down**: Same, but downward.
|
||||
- **Delete**: Deletes the currently selected instrument. Pattern data will be adjusted to use the next available instrument in the list.
|
||||
|
||||
## folder view
|
||||
|
||||
![instruments window in folder view](instruments-folder.png)
|
||||
|
||||
In folder view, the "Move up" and "Move down buttons disappear and a new one appears:
|
||||
- **New folder**: Creates a new folder.
|
||||
|
||||
Instruments may be dragged from folder to folder and even rearranged within folders without changing their associated numbers.
|
||||
|
||||
Right-clicking on a folder allows one to rename or delete it. Deleting a folder does not remove the instruments in it.
|
||||
|
||||
# wavetable list
|
||||
|
||||
![wavetables window](wavetables.png)
|
||||
|
||||
Everything from the instrument list applies here also, with one major difference: Moving waves around with the buttons will change their associated numbers in the list but _not_ in pattern or instrument data. Be careful!
|
||||
|
||||
# sample list
|
||||
|
||||
![samples window](samples.png)
|
||||
|
||||
Everything from the wavetables list applies here also, with the addition of two buttons:
|
||||
- **Preview**: Plays the selected sample at its default note.
|
||||
- **Stop preview**: Stops the sample playback.
|
BIN
doc/2-interface/asset-move.png
Normal file
After Width: | Height: | Size: 725 B |
BIN
doc/2-interface/asset-newfolder.png
Normal file
After Width: | Height: | Size: 464 B |
BIN
doc/2-interface/asset-open.png
Normal file
After Width: | Height: | Size: 684 B |
BIN
doc/2-interface/asset-preview.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
doc/2-interface/asset-previewstop.png
Normal file
After Width: | Height: | Size: 406 B |
BIN
doc/2-interface/asset-save.png
Normal file
After Width: | Height: | Size: 620 B |
|
@ -1,6 +1,6 @@
|
|||
# basic mode
|
||||
|
||||
Furnace comes with a "basic mode" that can be toggled through the "settings" menu. it disables certain features in Furnace that may look intimidating or confusing for newcomers. if you find that a certain feature of furnace is missing, see if this setting is enabled or not.
|
||||
Furnace comes with a "basic mode" that can be toggled through the "settings" menu. it disables certain features in Furnace that may look intimidating or confusing for newcomers. if you find that a certain feature of Furnace is missing, see if this setting is enabled or not.
|
||||
|
||||
among the features that cannot be accessed in this mode are:
|
||||
* file menu:
|
|
@ -4,14 +4,14 @@ the user interface consists of several components. this paper describes some of
|
|||
|
||||
## windows
|
||||
|
||||
TODO: image
|
||||
![window](window.png)
|
||||
|
||||
windows may be moved, collapsed, closed or even docked around the workspace.
|
||||
|
||||
to move a window, press and hold the mouse button while on title bar or any empty space on it.
|
||||
then drag your mouse, and release it to stop moving.
|
||||
|
||||
to resize a window, drag any of the bottom corners (marked by triangular tabs).
|
||||
to resize a window, drag the bottom right corner (marked by a triangular tab) or the borders.
|
||||
|
||||
to collapse a window, click on the triangle in the title bar.
|
||||
clicking again expands it.
|
||||
|
@ -24,33 +24,40 @@ windows may be docked, which comes in handy.
|
|||
|
||||
to dock a window, drag it from its title bar to another location in the workspace or to the location of another window.
|
||||
|
||||
while dragging, an overlay with five options will appear, allowing you to select where and how to dock that window.
|
||||
while dragging, an overlay with some options will appear, allowing you to select where and how to dock that window.
|
||||
the options are:
|
||||
|
||||
```
|
||||
UP
|
||||
LEFT CENTER RIGHT
|
||||
DOWN
|
||||
```
|
||||
![docking options](docking.png)
|
||||
|
||||
drag your mouse cursor to any of the options to dock the window.
|
||||
|
||||
if you drag the window to `CENTER`, the window will be maximized to cover the workspace (if you do this on the workspace), or it will appear as another tab (if you do this on a window).
|
||||
if you drag to the sides (marked with blue text), the window will cover that side of the workspace.
|
||||
|
||||
if you drag it to a window or empty space (marked with yellow text), five docking positions will appear.
|
||||
|
||||
if you drag the window to the center of another window, it will appear as another tab.
|
||||
|
||||
if you drag the window to the center of empty space, the window will cover aforementioned empty space.
|
||||
|
||||
otherwise the window will be split in two, with the first half covered by the window you docked and the second half covered by the other window.
|
||||
|
||||
![tab1](tab1.png)
|
||||
|
||||
when a window is docked, its title bar turns into a tab bar, and the function provided by the "collapse" triangle at the top left changes.
|
||||
|
||||
if this triangle is clicked, a menu will appear with a single option: "Hide tab bar".
|
||||
![tab2](tab2.png)
|
||||
|
||||
if this triangle is clicked, a menu will appear with a list of tabs, or a single option if there's only one tab: "Hide tab bar".
|
||||
selecting this option will hide the tab bar of that window.
|
||||
|
||||
![tab3](tab3.png)
|
||||
|
||||
to bring it back, click on the top left corner.
|
||||
|
||||
to undock a window, drag its tab away from where it is docked. then it will be floating again.
|
||||
|
||||
## text fields
|
||||
|
||||
TODO: image
|
||||
|
||||
text fields are able to hold... text.
|
||||
|
||||
click on a text field to start editing, and click away to stop editing.
|
||||
|
@ -60,24 +67,18 @@ the following keyboard shortcuts work while on a text field:
|
|||
- `Ctrl-X`: cut
|
||||
- `Ctrl-C`: copy
|
||||
- `Ctrl-V`: paste
|
||||
- `Ctrl-Z`: undo
|
||||
- `Ctrl-Y`: redo
|
||||
- `Ctrl-A`: select all
|
||||
|
||||
(replace Ctrl with Command on macOS)
|
||||
|
||||
## number input fields
|
||||
|
||||
TODO: image
|
||||
|
||||
these work similar to text fields, but you may only input numbers.
|
||||
|
||||
they also usually have two buttons which allow you to increase/decrease the amount when clicked (and rapidly do so when click-holding).
|
||||
|
||||
## sliders
|
||||
|
||||
TODO: image
|
||||
|
||||
sliders are used for controlling values in a quick manner by being dragged.
|
||||
|
||||
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.
|
BIN
doc/2-interface/control-edit.png
Normal file
After Width: | Height: | Size: 458 B |
BIN
doc/2-interface/control-metronome.png
Normal file
After Width: | Height: | Size: 553 B |
BIN
doc/2-interface/control-play-pattern.png
Normal file
After Width: | Height: | Size: 898 B |
BIN
doc/2-interface/control-play-repeat.png
Normal file
After Width: | Height: | Size: 469 B |
BIN
doc/2-interface/control-play.png
Normal file
After Width: | Height: | Size: 525 B |
BIN
doc/2-interface/control-repeat.png
Normal file
After Width: | Height: | Size: 562 B |
BIN
doc/2-interface/control-step.png
Normal file
After Width: | Height: | Size: 617 B |
BIN
doc/2-interface/control-stop.png
Normal file
After Width: | Height: | Size: 310 B |
BIN
doc/2-interface/controls-classic.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
doc/2-interface/controls-compact.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
doc/2-interface/controls-split.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
doc/2-interface/controls-vertical.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
doc/2-interface/docking.png
Normal file
After Width: | Height: | Size: 362 KiB |
BIN
doc/2-interface/instruments-folder.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
doc/2-interface/instruments.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
doc/2-interface/interface1.png
Normal file
After Width: | Height: | Size: 368 KiB |
250
doc/2-interface/menu-bar.md
Normal file
|
@ -0,0 +1,250 @@
|
|||
# menu bar
|
||||
|
||||
the menu bar allows you to select from five menus: file, edit, settings, window and help.
|
||||
|
||||
# file
|
||||
|
||||
- **new...**: create a new song.
|
||||
- **open...**: opens the file picker, allowing you to select a song to open.
|
||||
- **open recent**: contains a list of the songs you've opened before.
|
||||
- **clear history**: this option erases the file history.
|
||||
|
||||
- **save**: saves the current song.
|
||||
- opens the file picker if this is a new song, or a backup.
|
||||
- **save as...**: opens the file picker, allowing you to save the song under a different name.
|
||||
- **save as .dmf (1.1.3+)...**: opens the file picker, allowing you to save your song as a .dmf which is compatible with DefleMask 1.1.3 onwards.
|
||||
- this will only work with the systems mentioned in the next option, plus:
|
||||
- Sega Master System (with FM expansion)
|
||||
- NES + Konami VRC7
|
||||
- Famicom Disk System
|
||||
- only use this option if you really need it. there are features which DefleMask does not support, like some effects and FM macros, so these will be lost.
|
||||
- **save as .dmf (1.0/legacy)...**: opens the file picker, allowing you to save your song as a .dmf which is compatible with DefleMask Legacy (0.12) or 1.0.
|
||||
- this will only work on the following systems:
|
||||
- Sega Genesis/Mega Drive (YM2612 + SN76489)
|
||||
- Sega Genesis/Mega Drive (YM2612 + SN76489, extended channel 3)
|
||||
- Sega Master System
|
||||
- Game Boy
|
||||
- PC Engine
|
||||
- NES
|
||||
- Commodore 64
|
||||
- Arcade (YM2151 + SegaPCM 5-channel compatibility)
|
||||
- Neo Geo CD (DefleMask 1.0+)
|
||||
- only use this option if you really need it. there are features which DefleMask does not support, like some effects and FM macros, so these will be lost.
|
||||
|
||||
- **export audio...**: export your song to a .wav file. see next section for more details.
|
||||
- **export VGM...**: export your song to a .vgm file. see next section for more details.
|
||||
- **export ZSM...**: export your song to a .zsm file. see next section for more details.
|
||||
- only available when there's a YM2151 and/or VERA.
|
||||
- **export command stream...**: export song data to a command stream file. see next section for more details.
|
||||
- this option is for developers.
|
||||
|
||||
- **add chip...**: add a chip to the current song.
|
||||
- **configure chip...**: set a chip's parameters.
|
||||
- for a list of parameters, see [7-systems](../7-systems/README.md).
|
||||
- **change chip...**: change a chip to another.
|
||||
- **Preserve channel positions**: enable this option to make sure Furnace does not auto-arrange/delete channels to compensate for differing channel counts. this can be useful for doing ports, e.g. from Genesis to PC-98.
|
||||
- **remove chip...**: remove a chip.
|
||||
- **Preserve channel positions**: same thing as above.
|
||||
|
||||
- **restore backup**: restore a previously saved backup.
|
||||
- Furnace keeps up to 5 backups of a song.
|
||||
- the backup directory is located in:
|
||||
- Windows: `%USERPROFILE%\AppData\Roaming\furnace\backups`
|
||||
- macOS: `~/Library/Application Support/Furnace/backups`
|
||||
- Linux/other: `~/.config/furnace/backups`
|
||||
- this directory grows in size as you use Furnace. remember to delete old backups periodically to save space.
|
||||
|
||||
- **exit**: I think you know what this does.
|
||||
|
||||
## export audio
|
||||
|
||||
this option allows you to export your song in .wav format. I know I know, no .mp3 or .ogg export yet, but you can use a converter.
|
||||
|
||||
there are two parameters:
|
||||
|
||||
- **Loops**: sets the number of times the song will loop.
|
||||
- does not have effect if the song ends with `FFxx` effect.
|
||||
- **Fade out (seconds)**: sets the fade out time when the song is over.
|
||||
- does not have effect if the song ends with `FFxx` effect.
|
||||
|
||||
and three export choices:
|
||||
|
||||
- **one file**: exports your song to one .wav file.
|
||||
- **multiple files (one per chip)**: exports the output of each chip to .wav files.
|
||||
- **multiple files (one per channel)**: exports the output of each channel to .wav files.
|
||||
- useful for usage with a channel visualizer such as corrscope.
|
||||
|
||||
## export VGM
|
||||
|
||||
this option allows exporting to a VGM (Video Game Music) file. these can be played back with VGMPlay (for example).
|
||||
|
||||
the following settings exist:
|
||||
|
||||
- **format version**: sets the VGM format version to use.
|
||||
- versions under 1.70 do not support per-chip volumes, and therefore will ignore the Mixer completely.
|
||||
- other versions may not support all chips.
|
||||
- use this option if you need to export for a quirky player or parser.
|
||||
- for example, RYMCast is picky with format versions. if you're going to use this player, select 1.60.
|
||||
- **loop**: writes loop. if disabled, the resulting file won't loop.
|
||||
- **loop trail**: this option allows you to set how much of the song is written after it loops.
|
||||
- the reason this exists is to work around a VGM format limitation in where post-loop state isn't recorded at all.
|
||||
- this may change the song length as it appears on a player.
|
||||
- **auto-detect**: detect how much to write automatically.
|
||||
- **add one loop**: add one more loop.
|
||||
- **custom**: allows you to specify how many ticks to add.
|
||||
- `0` is effectively none, disabling loop trail completely.
|
||||
- this option will not appear if the loop modality isn't set to None as there wouldn't be a need to.
|
||||
- **chips to export**: select which chips are going to be exported.
|
||||
- due to VGM format limitations, you can only select up to two of each chip type.
|
||||
- some chips will not be available, either because VGM doesn't support these yet, or because you selected an old format version.
|
||||
- **add pattern change hints**: this option adds a "hint" when a pattern change occurs. only useful if you're a developer.
|
||||
- the format of the "hint" data block that gets written is: `67 66 FE ll ll ll ll 01 oo rr pp pp pp ...`
|
||||
- ll: length, a 32-bit little-endian number
|
||||
- oo: order
|
||||
- rr: initial row (a 0Dxx effect is able to select a different row)
|
||||
- pp: pattern index (one per channel)
|
||||
- **direct stream mode**: this option allows DualPCM to work. don't use this for other chips.
|
||||
- may or may not play well with hardware VGM players.
|
||||
|
||||
click on **click to export** to begin exporting.
|
||||
|
||||
## export ZSM
|
||||
|
||||
ZSM (ZSound Music) is a format designed for the Commander X16 to allow hardware playback.
|
||||
it may contain data for either YM2151 or VERA chips.
|
||||
Calliope is one of the programs that supports playback of ZSM files.
|
||||
|
||||
the following settings are available:
|
||||
|
||||
- **Tick Rate (Hz)**: select the tick rate the song will run at.
|
||||
- I suggest you use the same rate as the song's.
|
||||
- apparently ZSM doesn't support changing the rate mid-song.
|
||||
- **loop**: enables loop. if disabled, the song won't loop.
|
||||
|
||||
click on **Begin Export** to... you know.
|
||||
|
||||
## export command stream
|
||||
|
||||
this option exports a text or binary file which contains a dump of the internal command stream produced when playing the song.
|
||||
|
||||
it's not really useful, unless you're a developer and want to use a command stream dump for some reason (e.g. writing a hardware sound driver).
|
||||
|
||||
- **export (binary)**: exports in Furnace's own command stream format (FCS). see `export-tech.md` in `papers/` for details.
|
||||
- **export (text)**: exports the command stream as a text file. only useful for analysis, really.
|
||||
|
||||
# edit
|
||||
|
||||
- **undo**: reverts the last action.
|
||||
- **redo**: repeats what you undid previously.
|
||||
|
||||
- **cut**: moves the current selection in the pattern view to clipboard.
|
||||
- **copy**: copies the current selection in the pattern view to clipboard.
|
||||
- **paste**: inserts the clipboard's contents in the cursor position.
|
||||
- **paste special...**: variants of the paste feature.
|
||||
- **paste mix**: inserts the clipboard's contents in the cursor position, but does not erase the occupied region.
|
||||
- **paste mix (background)**: does the same thing as paste mix, but doesn't alter content which is already there.
|
||||
- **paste with ins (foreground)**: same thing as paste mix, but changes the instrument.
|
||||
- **paste with ins (background)**: same thing as paste mix (background), but changes the instrument.
|
||||
- **paste flood**: inserts the clipboard's contents in the cursor position, and repeats until it hits the end of a pattern.
|
||||
- **paste overflow**: paste, but it will keep pasting even if it runs over another pattern.
|
||||
- **delete**: clears the contents in the selection.
|
||||
- **select all**: changes the selection so it covers a larger area.
|
||||
- if the selection is wide, it will select the rows in a column.
|
||||
- if the selection is tall, it will select the entire column.
|
||||
- if a column is already selected, it will select the entire channel.
|
||||
- if a channel is already selected, it will select the entire pattern.
|
||||
|
||||
- **operation mask**: toggles which columns will be affected by the listed operations. [more information here.](../8-advanced/opmask.md)
|
||||
- **input latch**: determines which data are placed along with a note. [more information here.](../8-advanced/inputlatch.md)
|
||||
|
||||
- **note/octave up/down**: transposes notes in the current selection.
|
||||
|
||||
- **values up/down**: changes values in the current selection by ±1 or ±16.
|
||||
|
||||
- **transpose**: transpose notes or change values by a specific amount.
|
||||
|
||||
- **interpolate**: fills in gaps in the selection by interpolation between values.
|
||||
- **change instrument**: changes the instrument number in a selection.
|
||||
- **gradient/fade**: replace the selection with a "gradient" that goes from the beginning of the selection to the end.
|
||||
- does not affect the note column.
|
||||
- **Nibble mode**: when enabled, the fade will be per-nibble (0 to F) rather than per-value (00 to FF).
|
||||
- use for effects like `04xy` (vibrato).
|
||||
- **scale**: scales values in the selection by a specific amount.
|
||||
- use to change volume in a selection for example.
|
||||
- **randomize**: replaces the selection with random values.
|
||||
- does not affect the note column.
|
||||
- **invert values**: `00` becomes `FF`, `01` becomes `FE`, `02` becomes `FD` and so on.
|
||||
|
||||
- **flip selection**: flips the selection so it is backwards.
|
||||
- **collapse/expand amount**: allows you to specify how much to collapse/expand in the next two menu items.
|
||||
- **collapse**: shrinks the selected contents.
|
||||
- **expand**: expands the selected contents.
|
||||
|
||||
- **collapse pattern**: same as collapse, but affects the entire pattern.
|
||||
- **expand pattern**: same as expand, but affects the entire pattern.
|
||||
|
||||
- **collapse song**: same as collapse, but affects the entire song.
|
||||
- it also changes speeds and pattern length to compensate.
|
||||
- **expand song**: same as expand, but affects the entire song.
|
||||
- it also changes speeds and pattern length to compensate.
|
||||
|
||||
- **find/replace**: shows [the Find/Replace window](../8-advanced/find-replace.md).
|
||||
|
||||
- **clear**: allows you to mass-delete things like songs, instruments and the like.
|
||||
|
||||
# settings
|
||||
|
||||
- **full screen**: expands the Furnace window so it covers your screen.
|
||||
- **lock layout**: prevents you from dragging/resizing docked windows, or docking more.
|
||||
- **basic mode**: toggles [Basic Mode](basic-mode.md).
|
||||
- **visualizer**: toggles pattern view particle effects when the song plays.
|
||||
- **reset layout**: resets the workspace to its defaults.
|
||||
- **settings...**: shows the Settings window. these are detailed in [settings.md].
|
||||
|
||||
# window
|
||||
|
||||
all these menu items show or hide their associated windows.
|
||||
|
||||
- [song information](song-info.md)
|
||||
- [subsongs](song-info.md)
|
||||
- [speed](song-info.md)
|
||||
- [instruments](../4-instrument/README.md)
|
||||
- [wavetables](../5-wave/README.md)
|
||||
- [samples](../6-sample/README.md)
|
||||
- [orders](order-list.md)
|
||||
- [pattern](../3-pattern/README.md)
|
||||
- [mixer](mixer.md)
|
||||
- [grooves](grooves.md)
|
||||
- [channels](channels.md)
|
||||
- [pattern manager](pat-manager.md)
|
||||
- [chip manager](chip-manager.md)
|
||||
- [compatibility flags](compat-flags.md)
|
||||
- [song comments](comments.md)
|
||||
|
||||
- [piano](piano.md)
|
||||
- [oscilloscope](osc.md)
|
||||
- [oscilloscopes (per-channel)](chanosc.md)
|
||||
- [clock](clock.md)
|
||||
- [register view](regview.md)
|
||||
- [log viewer](log-viewer.md)
|
||||
- [stats](stats.md)
|
||||
|
||||
# help
|
||||
|
||||
- **effect list**: displays the effect list.
|
||||
- **debug menu**: this menu contains various debug utilities.
|
||||
- unless you are working with the Furnace codebase, it's not useful.
|
||||
- **inspector**: this option shows the Dear ImGui Metrics/Debugger window.
|
||||
- unless you are working with the Furnace codebase, it's not useful.
|
||||
- **panic**: this resets all chips while the song is playing, effectively silencing everything.
|
||||
- **about...**: displays the About screen.
|
||||
|
||||
at the end of the menu bar, more information may be shown:
|
||||
- during editing, information about the data under the cursor will be shown here:
|
||||
- note or note modifier.
|
||||
- instrument number and name.
|
||||
- volume in decimal, hex, and percentage.
|
||||
- effect type and description.
|
||||
- during playback, the current values of the following will be listed:\
|
||||
speed/groove @ tick rate (BPM) | order | row | elapsed time.
|
||||
- if any changes or edits have been made but not yet saved, "modified" will appear.
|
29
doc/2-interface/order-list.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# order list
|
||||
|
||||
the order list is a playlist for patterns.
|
||||
|
||||
![order list](order-list.png)
|
||||
|
||||
along the top are the available channels. their abbreviations can be set in the [channels window](../8-advanced/channels.md). the highlighted channel follows the channel the pattern view cursor is in.
|
||||
|
||||
along the left are the order numbers. these are referenced with the `0Bxx` command. the highlighted row follows the order the pattern view cursor is in.
|
||||
|
||||
each entry in the table is the pattern that will play during that order. these can be changed according to the order edit mode.
|
||||
|
||||
hovering over a pattern number will pop up a tooltip showing the name of that pattern, if it has one.
|
||||
|
||||
The buttons are as follows:
|
||||
- **Add new order**.
|
||||
- **Remove order**.
|
||||
- **Duplicate order**: adds a new order with patterns matching the selected one directly below it. right-click to "deep clone"; this copies all patterns involved to new ones.
|
||||
- **Move order up**: swaps the selected order with the one above it.
|
||||
- **Move order down**: swaps the selected order with the one below it.
|
||||
- **Duplicate order at end of song**: same as "Duplicate order" except the new order is added at the bottom of the list.
|
||||
- **Order change mode**: selects how much of the order will change with an edit. only applies if "Order edit mode" is set to "Click to change".
|
||||
- **one**: only current channel's pattern will change.
|
||||
- **entire row**: all patterns in the order will change.
|
||||
- **Order edit mode**: selects the method of changing orders.
|
||||
- **Click to change**: a click will add one to the pattern number. a right-click will subtract one.
|
||||
- **Select and type (don't scroll)**: select a pattern and type.
|
||||
- **Select and type (scroll horizontally)**: as above, but after entering two digits, the cursor moves to the next channel.
|
||||
- **Select and type (scroll vertically)**: as above, but after entering two digits, the cursor moves to the next order.
|
BIN
doc/2-interface/order-list.png
Normal file
After Width: | Height: | Size: 72 KiB |
37
doc/2-interface/play-edit-controls.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# play/edit controls
|
||||
|
||||
The "Play/Edit Controls" are used to control playback and change parameters of the pattern view.
|
||||
|
||||
- ![](control-play.png) **Play**: Plays from cursor position.
|
||||
- ![](control-stop.png) **Stop**: Stops all playback.
|
||||
- ![](control-play-pattern.png) **Play from the beginning of this pattern**: Plays from the start of current pattern.
|
||||
- ![](control-play-repeat.png) **Repeat from the beginning of this pattern**: Repeats current pattern from its start.
|
||||
- ![](control-step.png) **Step one row**: Plays only the row at cursor position and moves down one.
|
||||
- ![](control-edit.png) **Edit**: Toggles edit mode. If off, nothing can be edited in the pattern window. (Great for playing around on the keyboard!)
|
||||
- ![](control-metronome.png) **Metronome**: Toggles the metronome, which only sounds during playback.
|
||||
- ![](control-repeat.png) **Repeat pattern**: Toggles pattern repeat. During playback while this is on, the current pattern will play over and over instead of following the order list.
|
||||
- **Poly**: Turns on polyphony for previewing notes. Toggles to **Mono** for monophony (one note at a time only).
|
||||
- **Octave**: Sets current input octave.
|
||||
- **Step**: Sets edit step. If this is 1, entering a note or effect will move to the next row. If this is a larger number, rows will be skipped. If this is 0, the cursor will stay in place.
|
||||
- **Follow orders**: If on, the selected order in the orders window will follow the song during playback.
|
||||
- **Follow pattern**: If on, the cursor will follow playback and the song will scroll by as it plays.
|
||||
|
||||
## layouts
|
||||
|
||||
The layout can be changed in Settings > Appearance to one of these:
|
||||
|
||||
### classic
|
||||
|
||||
![classic play/edit controls](controls-classic.png)
|
||||
|
||||
### compact
|
||||
|
||||
![compact play/edit controls](controls-compact.png)
|
||||
|
||||
### compact (vertical)
|
||||
|
||||
![compact vertical play/edit controls](controls-vertical.png)
|
||||
|
||||
### split
|
||||
|
||||
![split play and edit controls](controls-split.png)
|
BIN
doc/2-interface/samples.png
Normal file
After Width: | Height: | Size: 40 KiB |
387
doc/2-interface/settings.md
Normal file
|
@ -0,0 +1,387 @@
|
|||
# settings
|
||||
|
||||
settings are saved when clicking the **OK** button at the bottom of the dialog.
|
||||
|
||||
|
||||
|
||||
# General
|
||||
|
||||
- **Workspace layout**
|
||||
- **Import**: reads a .ini layout file.
|
||||
- **Export**: writes current layout to a .ini file.
|
||||
- **Reset**: resets layout to default.
|
||||
|
||||
- **Initial system**: the system of chips loaded on starting Furnace.
|
||||
- **Current system**: sets current chips as default.
|
||||
- **Randomize**: set default to a random system.
|
||||
- this will not choose a random system at each start.
|
||||
- **Reset to defaults**: sets default to "Sega Genesis/Mega Drive".
|
||||
- **Name**: name for the default system. may be set to any text.
|
||||
- system configuration: same as in the [chip manager](../8-advanced/chip-manager.md) and [mixer](../8-advanced/mixer.md).
|
||||
|
||||
- **Play intro on start-up:**
|
||||
- **No**: skips intro entirely.
|
||||
- **Short**: shows silent title screen briefly.
|
||||
- **Full (short when loading song)**: shows animated musical intro unless started with a song (command line, double-clicking a .fur file, etc.)
|
||||
- **Full (always)**: always shows animated musical intro.
|
||||
- **When creating new song**:
|
||||
- **Display system preset selector**
|
||||
- **Start with initial system**
|
||||
|
||||
- **Double-click time (seconds)**: maximum time between mouse clicks to recognize them as a double-click.
|
||||
- **Toggle channel solo on:** select which interactions with a channel header will toggle solo for that channel.
|
||||
- **Push value when overwriting instead of clearing it**: in the order list and pattern editors, typing into an already-filled value will shift digits instead of starting fresh.
|
||||
- if off: moving the cursor onto the value `A5` and typing a "B" results in `0B`.
|
||||
- if on: with the cursor on the value `A5` and typing a "B" results in `5B`.
|
||||
- **Move cursor up on backspace-delete**
|
||||
- **Move cursor by edit step on delete**
|
||||
- **Change current instrument when changing instrument column (absorb)**
|
||||
- **Delete effect value when deleting effect**
|
||||
- **Change order when scrolling outside of pattern bounds**:
|
||||
- if off, the pattern edit cursor will stay locked within the current order.
|
||||
- if on, moving the cursor past the edge of the previous or next order will move to that order.
|
||||
- **Move cursor by edit step on insert (push)**
|
||||
- **Move cursor to end of clipboard content when pasting**
|
||||
- **Don't scroll when moving cursor**
|
||||
- **Double click selects entire column**
|
||||
- **Allow docking editors**
|
||||
- **Don't raise pattern editor on click**
|
||||
- **Focus pattern editor when selecting instrument**
|
||||
- **Restart song when changing chip properties**
|
||||
- **Use system file picker**: use native OS file dialog instead of Furnace's.
|
||||
- **Only allow window movement when clicking on title bar**
|
||||
- **Enable event delay**
|
||||
- may cause issues with high-polling-rate mice when previewing notes.
|
||||
- **Power-saving mode**
|
||||
- saves power by lowering the frame rate to 2fps when idle.
|
||||
- may cause issues under Mesa drivers!
|
||||
- **Disable threaded input (restart after changing!)**
|
||||
- threaded input processes key presses for note preview on a separate thread (on supported platforms), which reduces latency.
|
||||
- however, crashes have been reported when threaded input is on. enable this option if that is the case.
|
||||
- **Remember window position**
|
||||
- remembers the window's last position on start-up.
|
||||
- **New instruments are blank**
|
||||
- **Save unused patterns**
|
||||
- **Compress when saving**
|
||||
- use zlib to compress saved songs.
|
||||
- **Cursor follows current order when moving it**
|
||||
- applies when playback is stopped.
|
||||
- **Audio export loop/fade out time:**
|
||||
- **Set to these values on start-up:**
|
||||
- **Loops**: number of additional times to play through `0Bxx` song loop.
|
||||
- **Fade out (seconds)**: length of fade out after final loop.
|
||||
- **Remember last values**
|
||||
- **Note preview behavior:**
|
||||
- **Never**
|
||||
- **When cursor is in Note column**
|
||||
- **When cursor is in Note column or not in edit mode**
|
||||
- **Always**
|
||||
- **Wrap pattern cursor horizontally:**
|
||||
- **No**
|
||||
- **Yes**
|
||||
- **Yes, and move to next/prev row**
|
||||
- **Wrap pattern cursor vertically:**
|
||||
- **No**
|
||||
- **Yes**
|
||||
- **Yes, and move to next/prev pattern**
|
||||
- **Cursor movement keys behavior:**
|
||||
- **Move by one**
|
||||
- **Move by Edit Step**
|
||||
- **Effect input cursor behavior:**
|
||||
- **Move down**
|
||||
- **Move to effect value (otherwise move down)**
|
||||
- **Move to effect value/next effect and wrap around**
|
||||
- **Allow dragging selection:**
|
||||
- **No**
|
||||
- **Yes**
|
||||
- **Yes (while holding Ctrl only)**
|
||||
|
||||
|
||||
|
||||
# Audio/MIDI
|
||||
|
||||
- **Backend**: select SDL or JACK for audio output.
|
||||
- only appears on Linux, or MacOS compiled with JACK support
|
||||
- **Device**: audio device for playback.
|
||||
- **Sample rate**
|
||||
- **Outputs**: select number of audio outputs created, up to 16.
|
||||
- only appears when Backend is JACK.
|
||||
- **Channels**: number of output channels to use.
|
||||
- **Buffer size**: size of buffer in both samples and milliseconds.
|
||||
- **Quality**: selects quality of resampling. low quality reduces CPU load.
|
||||
- **Metronome volume**
|
||||
- **Low-latency mode (experimental!)**: reduces latency by running the engine faster than the tick rate. useful for live playback/jam mode.
|
||||
- _warning:_ experimental! may produce glitches. only enable if your buffer size is small (10ms or less).
|
||||
- **Force mono audio**
|
||||
- **Software clipping**: clips output to nominal range (-1.0 to 1.0) before passing it to the audio device.
|
||||
- this avoids activating Windows' built-in limiter.
|
||||
- **want:** displays requested audio configuration.
|
||||
- **got:** displays actual audio configuration returned by audio backend.
|
||||
|
||||
- **MIDI input**
|
||||
- **MIDI output**
|
||||
- **MIDI input settings**
|
||||
- **Note input**
|
||||
- **Velocity input**
|
||||
- **Map MIDI channels to direct channels**
|
||||
- **Map Yamaha FM voice data to instruments**
|
||||
- **Program change is instrument selection**
|
||||
- **Value input style**:
|
||||
- **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)**
|
||||
- **CC of upper nibble**
|
||||
- **CC of lower nibble**
|
||||
- **Use 14-bit control change**
|
||||
- **MSB CC**
|
||||
- **LSB CC**
|
||||
- **Use single control change**
|
||||
- **Control**
|
||||
- **Per-column control change**
|
||||
- **Instrument**\
|
||||
**Volume**\
|
||||
**Effect `x` type**\
|
||||
**Effect `x` value**
|
||||
- **Disabled/custom**
|
||||
- **Use dual control change (one for each nibble)**
|
||||
- **CC of upper nibble**
|
||||
- **CC of lower nibble**
|
||||
- **Use 14-bit control change**
|
||||
- **MSB CC**
|
||||
- **LSB CC**
|
||||
- **Use single control change (imprecise)**
|
||||
- **Control**
|
||||
- **Volume curve**
|
||||
- **Actions:**
|
||||
- **`+`** button: adds a new action.
|
||||
- window-with-arrow button: new action with learning! press a button or move a slider/knob/something on your device.
|
||||
- each action has the following:
|
||||
- **Type**
|
||||
- **Channel**
|
||||
- **Note/Control**
|
||||
- **Velocity/Value**
|
||||
- **Action**
|
||||
- **Learn**
|
||||
- **Remove**
|
||||
|
||||
- **MIDI output settings**
|
||||
- **Output mode:**
|
||||
- **Off (use for TX81Z)**
|
||||
- **Melodic**
|
||||
- **Send Program Change**
|
||||
- **Send MIDI clock**
|
||||
- **Send MIDI timecode**
|
||||
- **Timecode frame rate:**
|
||||
- **Closest to Tick Rate**
|
||||
- **Film (24fps)**
|
||||
- **PAL (25fps)**
|
||||
- **NTSC drop (29.97fps)**
|
||||
- **NTSC non-drop (30fps)**
|
||||
|
||||
# Emulation
|
||||
- **Arcade/YM2151 core**
|
||||
- **ymfm**
|
||||
- **Nuked-OPM**
|
||||
- **Genesis/YM2612 core**
|
||||
- **Nuked-OPN2**
|
||||
- **ymfm**
|
||||
- **SN76489 core**
|
||||
- **MAME**
|
||||
- **Nuked-PSG Mod**
|
||||
- **NES core**
|
||||
- **puNES**
|
||||
- **NSFplay**
|
||||
- **FDS core**
|
||||
- **puNES**
|
||||
- **NSFplay**
|
||||
- **SID core**
|
||||
- **reSID**
|
||||
- **reSIDfp**
|
||||
- **POKEY core**
|
||||
- **Atari800 (mzpokeysnd)**
|
||||
- **ASAP (C++ port)**
|
||||
- **OPN/OPNA/OPNB cores**
|
||||
- **ymfm only**
|
||||
- **Nuked-OPN2 (FM) + ymfm (SSG/ADPCM)**
|
||||
|
||||
- **PC Speaker strategy:**
|
||||
- **evdev SND_TONE**
|
||||
- **KIOCSOUND on /dev/tty1**
|
||||
- **/dev/port**
|
||||
- **KIOCSOUND on standard output**
|
||||
- **outb()**
|
||||
|
||||
- **Sample ROMs:**
|
||||
- **OPL4 YRW801 path**
|
||||
- **MultiPCM TG100 path**
|
||||
- **MultiPCM MU5 path**
|
||||
|
||||
|
||||
|
||||
# Appearance
|
||||
|
||||
- **Render driver**
|
||||
- **Automatic UI scaling factor**: automatically match the OS's UI scaling.
|
||||
- **UI scaling factor**: only if "Automatic UI scaling factor" is off.
|
||||
- **Main font**: if "Custom...", a file path selector will appear beneath.
|
||||
- **Size**
|
||||
- **Pattern font**: if "Custom...", a file path selector will appear beneath.
|
||||
- **Size**
|
||||
- **Icon size**
|
||||
- **Display Japanese characters**\
|
||||
**Display Chinese (Simplified) characters**\
|
||||
**Display Chinese (Traditional) characters**\
|
||||
**Display Korean characters**
|
||||
- only toggle these options if you have enough graphics memory.
|
||||
- these are a temporary solution until dynamic font atlas is implemented in Dear ImGui.
|
||||
|
||||
- **Number of recent files**
|
||||
|
||||
- **Pattern view labels:**
|
||||
- **Note off (3-char)**: default is `OFF`
|
||||
- **Note release (3-char)**: default is `===`.
|
||||
- **Macro release (3-char)**: default is `REL`.
|
||||
- **Empty field (3-char)**: default is `...`.
|
||||
- **Empty field (2-char)**: default is `..`.
|
||||
|
||||
- **Orders row number format:**
|
||||
- **Decimal**
|
||||
- **Hexadecimal**
|
||||
- **Pattern row number format:**
|
||||
- **Decimal**
|
||||
- **Hexadecimal**
|
||||
- **FM parameter names:**
|
||||
- **Friendly**
|
||||
- **Technical**
|
||||
- **Technical (alternate)**
|
||||
|
||||
- **Title bar:**
|
||||
- **Furnace**
|
||||
- **Song Name - Furnace**
|
||||
- **file_name.fur - Furnace**
|
||||
- **/path/to/file.fur - Furnace**
|
||||
- **Display system name on title bar**
|
||||
- **Display chip names instead of "multi-system" in title bar**
|
||||
- **Status bar:**
|
||||
- **Cursor details**
|
||||
- **File path**
|
||||
- **Cursor details or file path**
|
||||
- **Nothing**
|
||||
- **Play/edit controls layout:**
|
||||
- **Classic**
|
||||
- **Compact**
|
||||
- **Compact (vertical)**
|
||||
- **Split**
|
||||
- **Position of buttons in Orders:**
|
||||
- **Top**
|
||||
- **Left**
|
||||
- **Right**
|
||||
- **FM parameter editor layout:**
|
||||
- **Modern**
|
||||
- **Compact (2x2, classic)**
|
||||
- **Compact (1x4)**
|
||||
- **Compact (4x1)**
|
||||
- **Alternate (2x2)**
|
||||
- **Alternate (1x4)**
|
||||
- **Alternate (4x1)**
|
||||
- **Position of Sustain in FM editor:**
|
||||
- **Between Decay and Sustain Rate**
|
||||
- **After Release Rate**
|
||||
- **Macro editor layout:**
|
||||
- **Unified**
|
||||
- **Mobile**
|
||||
- **Grid**
|
||||
- **Single (with list)**
|
||||
- **Single (combo box)**
|
||||
|
||||
- **Namco 163 chip name**
|
||||
|
||||
- **Channel colors:**
|
||||
- **Single**
|
||||
- **Channel type**
|
||||
- **Instrument type**
|
||||
- **Channel name colors:**
|
||||
- **Single**
|
||||
- **Channel type**
|
||||
- **Instrument type**
|
||||
- **Channel style:**
|
||||
- **Classic**
|
||||
- **Line**
|
||||
- **Round**
|
||||
- **Split button**
|
||||
- **Square border**
|
||||
- **Round border**
|
||||
- **Channel volume bar:**
|
||||
- **None**
|
||||
- **Simple**
|
||||
- **Stereo**
|
||||
- **Real**
|
||||
- **Real (stereo)**
|
||||
- **Channel feedback style:**
|
||||
- **Off**
|
||||
- **Note**
|
||||
- **Volume**
|
||||
- **Active**
|
||||
- **Channel font:**
|
||||
- **Regular**
|
||||
- **Monospace**
|
||||
- **Center channel name**
|
||||
|
||||
- **Colorize instrument editor using instrument type**
|
||||
- **Use separate colors for carriers/modulators in FM editor**
|
||||
- **Unified instrument/wavetable/sample list**
|
||||
- **Horizontal instrument list**
|
||||
- **Use standard OPL waveform names**
|
||||
- **Overflow pattern highlights**
|
||||
- **Display previous/next pattern**
|
||||
- **Use German notation**: display `B` notes as `H`, and `A#` notes as `B`.
|
||||
- **Single-digit effects for 00-0F**
|
||||
- **Center pattern view**: centers pattern horizontally in view.
|
||||
- **Unsigned FM detune values**
|
||||
- **Highlight channel at cursor in Orders**
|
||||
- **About screen party time**
|
||||
- _warning:_ may cause epileptic seizures.
|
||||
|
||||
- **Use compact wave editor**
|
||||
- **Use classic macro editor vertical slider**
|
||||
- **Rounded window corners**
|
||||
- **Rounded buttons**
|
||||
- **Rounded menu corners**
|
||||
- **Borders around widgets**
|
||||
- **Disable fade-in during start-up**
|
||||
|
||||
- **Oscilloscope settings:**
|
||||
- **Rounded corners**
|
||||
- **Fill entire window**
|
||||
- **Waveform goes out of bounds**
|
||||
- **Border**
|
||||
|
||||
- **Pattern view spacing after:**
|
||||
- **Note**
|
||||
- **Instrument**
|
||||
- **Volume**
|
||||
- **Effect**
|
||||
- **Effect value**
|
||||
- **Color scheme**
|
||||
- **Import**
|
||||
- **Export**
|
||||
- **Reset defaults**
|
||||
- **General**
|
||||
- **Color scheme type:**
|
||||
- **Dark**
|
||||
- **Light**
|
||||
- **Frame shading**
|
||||
- several more categories...
|
||||
|
||||
|
||||
|
||||
# Keyboard
|
||||
|
||||
- **Import**
|
||||
- **Export**
|
||||
- **Reset defaults**
|
||||
- several categories of keybinds...
|
||||
- click on a keybind then enter a key or key combination to change it
|
||||
- right-click to clear the keybind
|
49
doc/2-interface/song-info.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
# song info
|
||||
|
||||
- **Name**: The track's title.
|
||||
- **Author**: List of contributors to a song. If the song is a cover of someone else's track, it's customary to list their name first, followed by `[cv. YourName]`.
|
||||
- **Album**: The associated album name, the name of the game the song is from, or whatever.
|
||||
- **System**: The game console or computer the track is designed for. This is automatically set when creating a new tune, but it can be changed to anything one wants. The **Auto** button will provide a guess based on the chips in use.
|
||||
|
||||
All of this metadata will be included in a VGM export. This isn't the case for a WAV export, however.
|
||||
|
||||
**Tuning (A-4)**: Set tuning based on the note A-4, which should be 440 in most cases. Opening an Amiga MOD will set it to 436 for hardware compatibility.
|
||||
|
||||
# subsongs
|
||||
|
||||
This window allows one to create **subsongs** - multiple individual songs within a single file. Each song has its own order list and patterns, but all songs within a file share the same chips, samples, and so forth.
|
||||
|
||||
- The drop-down box selects the current subsong.
|
||||
- The **`+`** button adds a new subsong.
|
||||
- The **`−`** button permanently deletes the current subsong (unless it's the only one).
|
||||
- **Name**: Title of the current subsong.
|
||||
- The box at the bottom can store any arbitrary text, like a separate "Comments" box for the current subsong.
|
||||
|
||||
# speed
|
||||
|
||||
There are multiple ways to set the tempo of a song.
|
||||
|
||||
**Tick Rate**: The frequency of ticks per second, thus the rate at which notes and effects are processed.
|
||||
- All values are allowed for all chips, though most chips have hardware limitations that mean they should stay at either 60 (approximately NTSC) or 50 (exactly PAL).
|
||||
- Clicking the Tick Rate button switches to a more traditional **Base Tempo** BPM setting.
|
||||
|
||||
**Speed**: The number of ticks per row.
|
||||
- Clicking the "Speed" button changes to more complex modes covered in the [grooves] page.
|
||||
|
||||
**Virtual Tempo**: Simulates any arbitrary tempo without altering the tick rate. It does this by adding or skipping ticks to approximate the tempo. The two numbers represent a ratio applied to the actual tick rate. Example:
|
||||
- Set tick rate to 150 BPM (60 Hz) and speed to 6.
|
||||
- Set the first virtual tempo number (numerator) to 200.
|
||||
- Set the second virtual tempo number (denominator) to 150.
|
||||
- The track will play at 200 BPM.
|
||||
- The ratio doesn't have to match BPM numbers. Set the numerator to 4 and the denominator to 5, and the virtual BPM becomes 150 × 4/5 = 120.
|
||||
|
||||
**Divider**: Changes the effective tick rate. A tick rate of 60Hz and a divisor of 6 will result in ticks lasting a tenth of a second each!
|
||||
|
||||
**Highlight**: Sets the pattern row highlights:
|
||||
- The first value represents the number of rows per beat.
|
||||
- The second value represents the number of rows per measure.
|
||||
- These don't have to line up with the music's actual beats and measures. Set them as preferred for tracking. _Note:_ These values are used for the metronome and calculating BPM.
|
||||
|
||||
**Pattern Length**: The length of each pattern in rows. This affects all patterns in the song, and every pattern must be the same length. (Individual patterns can be cut short by `0Bxx`, `0Dxx`, and `FFxx` commands.)
|
||||
|
||||
**Song Length**: How many orders are in the order list. Decreasing it will hide the orders at the bottom. Increasing it will restore those orders; increasing it further will add new orders of all `00` patterns.
|
BIN
doc/2-interface/tab1.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
doc/2-interface/tab2.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
doc/2-interface/tab3.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
doc/2-interface/wavetables.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
doc/2-interface/window.png
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
doc/3-pattern/channelbar.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
doc/3-pattern/channels.png
Normal file
After Width: | Height: | Size: 65 KiB |
164
doc/3-pattern/effects.md
Normal file
|
@ -0,0 +1,164 @@
|
|||
# effect list
|
||||
|
||||
most of the effect numbers are from ProTracker / FastTracker 2.
|
||||
|
||||
however, effects are continuous, which means you only need to type it once and then stop it with an effect value of `00` or no effect value at all.
|
||||
|
||||
## volume
|
||||
|
||||
- `0Axy`: **Volume slide.**
|
||||
- If `x` is 0 then this slides volume down by `y` each tick.
|
||||
- If `y` is 0 then this slides volume up by `x` each tick.
|
||||
- `FAxy`: **Fast volume slide.** same as `0Axy` above but 4× faster.
|
||||
- `F3xx`: **Fine volume slide up.** same as `0Ax0` but 64× slower.
|
||||
- `F4xx`: **Fine volume slide down.** same as `0A0x` but 64× slower.
|
||||
- `F8xx`: **Single tick volume slide up.** adds `x` to volume on first tick only.
|
||||
- `F9xx`: **Single tick volume slide down.** subtracts `x` from volume on first tick only.
|
||||
|
||||
- `07xy`: **Tremolo.** changes volume to be "wavy" with a sine LFO. `x` is the speed. `y` is the depth.
|
||||
- Tremolo is downward only.
|
||||
- Maximum tremolo depth is -60 volume steps.
|
||||
|
||||
## pitch
|
||||
|
||||
- `E5xx`: **Set pitch.** `00` is -1 semitone, `80` is base pitch, `FF` is nearly +1 semitone.
|
||||
- `01xx`: **Pitch slide up.**
|
||||
- `02xx`: **Pitch slide down.**
|
||||
- `F1xx`: **Single tick pitch slide up.**
|
||||
- `F2xx`: **Single tick pitch slide down.**
|
||||
|
||||
- `03xx`: **Portamento.** slides the current note's pitch to the specified note. `x` is the slide speed.
|
||||
- A note _must_ be present for this effect to work.
|
||||
- `E1xy`: **Note slide up.** `x` is the speed, while `y` is how many semitones to slide up.
|
||||
- `E2xy`: **Note slide down.** `x` is the speed, while `y` is how many semitones to slide down.
|
||||
|
||||
- `EAxx`: **Toggle legato.** while on, notes instantly change the pitch of the currrently playing sound instead of starting it over.
|
||||
- `00xy`: **Arpeggio.** after using this effect the channel will rapidly switch between semitone values of `note`, `note + x` and `note + y`.
|
||||
- `E0xx`: **Set arpeggio speed.** this sets the number of ticks between arpeggio values. default is 1.
|
||||
|
||||
- `04xy`: **Vibrato.** changes pitch to be "wavy" with a sine LFO. `x` is the speed, while `y` is the depth.
|
||||
- Maximum vibrato depth is ±1 semitone.
|
||||
- `E3xx`: **Set vibrato direction.** `xx` may be one of the following:
|
||||
- `00`: Up and down. default.
|
||||
- `01`: Up only.
|
||||
- `02`: Down only.
|
||||
- `E4xx`: **Set vibrato range** in 1/16th of a semitone.
|
||||
|
||||
## panning
|
||||
|
||||
not all chips support these effects.
|
||||
|
||||
- `08xy`: **Set panning.** changes stereo volumes independently. `x` is the left channel and `y` is the right one.
|
||||
- `88xy`: **Set rear panning.** changes rear channel volumes independently. `x` is the rear left channel and `y` is the rear right one.
|
||||
- `81xx`: **Set volume of left channel** (from `00` to `FF`).
|
||||
- `82xx`: **Set volume of right channel** (from `00` to `FF`).
|
||||
- `89xx`: **Set volume of rear left channel** (from `00` to `FF`).
|
||||
- `8Axx`: **Set volume of rear right channel** (from `00` to `FF`).
|
||||
|
||||
- `80xx`: **Set panning (linear).** this effect behaves more like other trackers:
|
||||
- `00` is left.
|
||||
- `80` is center.
|
||||
- `FF` is right.
|
||||
|
||||
## time
|
||||
|
||||
- `09xx`: **Set speed/groove.** if no grooves are defined, this sets speed. If alternating speeds are active, this sets the first speed.
|
||||
- `0Fxx`: **Set speed 2.** during alternating speeds or a groove, this sets the second speed.
|
||||
|
||||
- `Cxxx`: **Set tick rate.** changes tick rate to `xxx` Hz (ticks per second).
|
||||
- `xxx` may be from `000` to `3FF`.
|
||||
- `F0xx`: **Set BPM.** changes tick rate according to beats per minute. range is `01` to `FF`.
|
||||
|
||||
- `0Bxx`: **Jump to order.** `x` is the order to play after the current row.
|
||||
- this marks the end of a loop with order `x` as the loop start.
|
||||
- `0Dxx`: **Jump to next pattern.** skips the current row and remainder of current order.
|
||||
- this can be used to shorten the current order.
|
||||
- `FFxx`: **Stop song.** stops playback and ends the song. `x` is ignored.
|
||||
|
||||
## note
|
||||
|
||||
- `0Cxx`: **Retrigger.** repeats current note every `xx` ticks.
|
||||
- This effect is not continuous; it must be entered on every row.
|
||||
- `ECxx`: **Note cut.** ends current note after `xx` ticks. For FM instruments, it's equivalent to a "key off".
|
||||
- `EDxx`: **Note delay.** delays note by `x` ticks.
|
||||
|
||||
## other
|
||||
|
||||
- `9xxx`: **Set sample position.** jumps current sample to position `xxx * 0x100`.
|
||||
- Not all chips support this effect.
|
||||
- `EBxx`: **Set sample bank.**
|
||||
- Does not apply on Amiga.
|
||||
- `EExx`: **Send external command.**
|
||||
- This effect is currently incomplete.
|
||||
- `F5xx`: **Disable macro.** see macro table at the end of this document for possible values.
|
||||
- `F6xx`: **Enable macro.**
|
||||
|
||||
additionally, [each chip has its own effects](../7-systems/README.md).
|
||||
|
||||
## macro table
|
||||
|
||||
ID | macro
|
||||
-----|-----------------------------
|
||||
`00` | volume
|
||||
`01` | arpeggio
|
||||
`02` | duty/noise
|
||||
`03` | waveform
|
||||
`04` | pitch
|
||||
`05` | extra 1
|
||||
`06` | extra 2
|
||||
`07` | extra 3
|
||||
`08` | extra A (ALG)
|
||||
`09` | extra B (FM)
|
||||
`0A` | extra C (FMS)
|
||||
`0B` | extra D (AMS)
|
||||
`0C` | panning left
|
||||
`0D` | panning right
|
||||
`0E` | phase reset
|
||||
`0F` | extra 4
|
||||
`10` | extra 5
|
||||
`11` | extra 6
|
||||
`12` | extra 7
|
||||
`13` | extra 8
|
||||
| | **operator 1 macros**
|
||||
`20` | AM
|
||||
`21` | AR
|
||||
`22` | DR
|
||||
`23` | MULT
|
||||
`24` | RR
|
||||
`25` | SL
|
||||
`26` | TL
|
||||
`27` | DT2
|
||||
`28` | RS
|
||||
`29` | DT
|
||||
`2A` | D2R
|
||||
`2B` | SSG-EG
|
||||
`2C` | DAM
|
||||
`2D` | DVB
|
||||
`2E` | EGT
|
||||
`2F` | KSL
|
||||
`30` | SUS
|
||||
`31` | VIB
|
||||
`32` | WS
|
||||
`33` | KSR
|
||||
`40` | **operator 2 macros**
|
||||
`60` | **operator 3 macros**
|
||||
`80` | **operator 4 macros**
|
||||
|
||||
the interpretation of duty, wave and extra macros depends on chip/instrument type:
|
||||
|
||||
ex | FM | OPM | OPZ | OPLL | AY-3-8910 | AY8930 | Lynx | C64 | SAA1099 | X1-010 | Namco 163 | FDS | Sound Unit | ES5506 | MSM6258 | QSound | SNES | MSM5232 |
|
||||
---|--------|-----------|-----------|-------|------------|------------|----------|------------|----------|------------|------------|-----------|------------|-----------|----------|--------------|-----------|-----------|
|
||||
D | NoiseF | NoiseFreq | | | NoiseFreq | NoiseFreq | Duty/Int | Duty | | | Wave Pos | | Duty | Filt Mode | FreqDiv | Echo Level | NoiseFreq | GroupCtrl |
|
||||
W | | LFO Shape | LFO Shape | Patch | Waveform | Waveform | | Waveform | Waveform | Waveform | Waveform | Waveform | Waveform | | | | Waveform | |
|
||||
1 | | AMD | AMD | | | Duty | | FilterMode | Envelope | EnvMode | WaveLen | Mod Depth | Cutoff | Filter K1 | ClockDiv | EchoFeedback | Special | GroupAtk |
|
||||
2 | | PMD | PMD | | Envelope | Envelope | | Resonance | | Envelope | WaveUpdate | Mod Speed | Resonance | Filter K2 | | Echo Length | Gain | GroupDec |
|
||||
3 | LFOSpd | LFO Speed | LFO Speed | | AutoEnvNum | AutoEnvNum | | Special | | AutoEnvNum | WaveLoad W | | Control | Env Count | | | | Noise |
|
||||
A | ALG | ALG | ALG | | AutoEnvDen | AutoEnvDen | | | | AutoEnvDen | WaveLoad P | | | Control | | | | |
|
||||
B | FB | FB | FB | | | Noise AND | | | | | WaveLoad L | | | | | | | |
|
||||
C | FMS | FMS | FMS | | | Noise OR | | | | | WaveLoad T | | | | | | | |
|
||||
D | AMS | AMS | AMS | | | | | | | | | | | | | | | |
|
||||
4 | OpMask | OpMask | | | | | | Test/Gate | | | | | PResetTime | EnvRampL | | | | |
|
||||
5 | | | AMD2 | | | | | | | | | | | EnvRampR | | | | |
|
||||
6 | | | PMD2 | | | | | | | | | | | EnvRampK1 | | | | |
|
||||
7 | | | LFO2Speed | | | | | | | | | | | EnvRampK2 | | | | |
|
||||
8 | | | LFO2Shape | | | | | | | | | | | Env Mode | | | | |
|