Use 'atexit' registration

This commit is contained in:
vittorioromeo 2024-05-19 19:18:55 +02:00
commit 7229a6de15
88 changed files with 2618 additions and 1033 deletions

View File

@ -9,6 +9,7 @@ concurrency:
env: env:
DISPLAY: ":99" # Display number to use for the X server DISPLAY: ":99" # Display number to use for the X server
GALLIUM_DRIVER: llvmpipe # Use Mesa 3D software OpenGL renderer GALLIUM_DRIVER: llvmpipe # Use Mesa 3D software OpenGL renderer
ANDROID_NDK_VERSION: "26.1.10909125" # Android NDK version to use
defaults: defaults:
run: run:
@ -35,13 +36,13 @@ jobs:
- { name: Windows VS2022 Unity, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_UNITY_BUILD=ON -GNinja } - { name: Windows VS2022 Unity, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_UNITY_BUILD=ON -GNinja }
- { name: Windows LLVM/Clang, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=clang++ -GNinja } - { name: Windows LLVM/Clang, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=clang++ -GNinja }
- { name: Windows MinGW, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -GNinja } - { name: Windows MinGW, os: windows-2022, flags: -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -GNinja }
- { name: Linux GCC, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja } - { name: Linux GCC, os: ubuntu-22.04, flags: -GNinja }
- { name: Linux Clang, os: ubuntu-22.04, flags: -DCMAKE_CXX_COMPILER=clang++ -DSFML_RUN_DISPLAY_TESTS=ON -GNinja , gcovr_options: '--gcov-executable="llvm-cov-$CLANG_VERSION gcov"' } - { name: Linux Clang, os: ubuntu-22.04, flags: -DCMAKE_CXX_COMPILER=clang++ -GNinja , gcovr_options: '--gcov-executable="llvm-cov-$CLANG_VERSION gcov"' }
- { name: Linux GCC DRM, os: ubuntu-22.04, flags: -DSFML_USE_DRM=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja } - { name: Linux GCC DRM, os: ubuntu-22.04, flags: -DSFML_USE_DRM=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja }
- { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_OPENGL_ES=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja } - { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_OPENGL_ES=ON -DSFML_RUN_DISPLAY_TESTS=OFF -GNinja }
- { name: macOS x64, os: macos-12, flags: -GNinja } - { name: macOS x64, os: macos-12, flags: -GNinja }
- { name: macOS x64 Xcode, os: macos-12, flags: -GXcode } - { name: macOS x64 Xcode, os: macos-12, flags: -GXcode }
- { name: macOS arm64, os: macos-14, flags: -GNinja } - { name: macOS arm64, os: macos-14, flags: -GNinja -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF }
- { name: iOS, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 } - { name: iOS, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 }
- { name: iOS Xcode, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -GXcode -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO } - { name: iOS Xcode, os: macos-12, flags: -DCMAKE_SYSTEM_NAME=iOS -GXcode -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO }
config: config:
@ -55,29 +56,56 @@ jobs:
- platform: { name: Windows VS2022 x64, os: windows-2022 } - platform: { name: Windows VS2022 x64, os: windows-2022 }
config: { name: Static with PCH (MSVC), flags: -DSFML_USE_MESA3D=TRUE -GNinja -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } config: { name: Static with PCH (MSVC), flags: -DSFML_USE_MESA3D=TRUE -GNinja -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 }
- platform: { name: Linux GCC, os: ubuntu-22.04 } - platform: { name: Linux GCC, os: ubuntu-22.04 }
config: { name: Static with PCH (GCC), flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } config: { name: Static with PCH (GCC), flags: -GNinja -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 }
- platform: { name: Linux Clang, os: ubuntu-22.04 } - platform: { name: Linux Clang, os: ubuntu-22.04 }
config: { name: Static with PCH (Clang), flags: -DSFML_RUN_DISPLAY_TESTS=ON -GNinja -DCMAKE_CXX_COMPILER=clang++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } config: { name: Static with PCH (Clang), flags: -GNinja -DCMAKE_CXX_COMPILER=clang++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 }
- platform: { name: Windows MinGW, os: windows-2022 } - platform: { name: Windows MinGW, os: windows-2022 }
config: { name: Static Standard Libraries, flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DSFML_USE_STATIC_STD_LIBS=TRUE } config: { name: Static Standard Libraries, flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DSFML_USE_STATIC_STD_LIBS=TRUE }
- platform: { name: Windows MinGW, os: windows-2022 } - platform: { name: Windows MinGW, os: windows-2022 }
config: { name: Static with PCH (GCC), flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 } config: { name: Static with PCH (GCC), flags: -GNinja -DSFML_USE_MESA3D=TRUE -DCMAKE_CXX_COMPILER=g++ -DBUILD_SHARED_LIBS=FALSE -DSFML_ENABLE_PCH=1 }
- platform: { name: macOS, os: macos-12 } - platform: { name: macOS, os: macos-12 }
config: { name: Frameworks, flags: -GNinja -DSFML_BUILD_FRAMEWORKS=TRUE -DBUILD_SHARED_LIBS=TRUE } config: { name: Frameworks, flags: -GNinja -DSFML_BUILD_FRAMEWORKS=TRUE -DBUILD_SHARED_LIBS=TRUE }
- platform: { name: Android, os: ubuntu-22.04 }
config: { name: x86 (API 21), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: x86, api: 21 }
type: { name: Release }
- platform: { name: Android, os: ubuntu-22.04 }
config: { name: x86_64 (API 24), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86_64 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=24 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: x86_64, api: 24 }
type: { name: Release }
- platform: { name: Android, os: ubuntu-22.04 }
config: { name: armeabi-v7a (API 29), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=29 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: armeabi-v7a, api: 29 }
type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug }
- platform: { name: Android, os: ubuntu-22.04 }
config: { name: arm64-v8a (API 33), flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=33 -DCMAKE_ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared, arch: arm64-v8a, api: 33 }
type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug }
- platform: { name: macOS , os: macos-12 } - platform: { name: macOS , os: macos-12 }
config: { name: System Deps, flags: -GNinja -DBUILD_SHARED_LIBS=TRUE -DSFML_USE_SYSTEM_DEPS=TRUE } config: { name: System Deps, flags: -GNinja -DBUILD_SHARED_LIBS=TRUE -DSFML_USE_SYSTEM_DEPS=TRUE }
- platform: { name: Android, os: ubuntu-latest }
config:
name: x86 (API 21)
flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=21 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF
arch: x86
api: 21
libcxx: i686-linux-android/libc++_shared.so
emuarch: x86
# emuapi: 29 # Removing this causes the tests to not run. This works around an issue that causes the test step to hang indefinitely.
type: { name: Release }
- platform: { name: Android, os: ubuntu-latest }
config:
name: x86_64 (API 24)
flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=x86_64 -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=24 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF
arch: x86_64
api: 24
libcxx: x86_64-linux-android/libc++_shared.so
emuarch: x86_64
# emuapi: 34 # Removing this causes the tests to not run. This works around an issue that causes the Network module tests to fail.
type: { name: Release }
- platform: { name: Android, os: ubuntu-latest }
config:
name: armeabi-v7a (API 29)
flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=29 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared
arch: armeabi-v7a
api: 29
# There are no emulators available for armeabi-v7a so we skip running the tests (we still build them) by not specifying emuapi
type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug }
- platform: { name: Android, os: ubuntu-latest }
config:
name: arm64-v8a (API 33)
flags: -GNinja -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a -DCMAKE_SYSTEM_NAME=Android -DCMAKE_SYSTEM_VERSION=33 -DCMAKE_ANDROID_NDK=$ANDROID_NDK_ROOT -DBUILD_SHARED_LIBS=TRUE -DCMAKE_ANDROID_STL_TYPE=c++_shared -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_RUN_AUDIO_DEVICE_TESTS=OFF
arch: arm64-v8a
api: 33
libcxx: aarch64-linux-android/libc++_shared.so
emuarch: arm64-v8a
emuapi: 27
emuflags: -qemu -machine virt
type: { name: Debug, flags: -DCMAKE_BUILD_TYPE=Debug }
steps: steps:
@ -107,11 +135,21 @@ jobs:
echo "CLANG_VERSION=$CLANG_VERSION" >> $GITHUB_ENV echo "CLANG_VERSION=$CLANG_VERSION" >> $GITHUB_ENV
sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox ccache gcovr ${{ matrix.platform.name == 'Linux Clang' && 'llvm-$CLANG_VERSION' || '' }} sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox ccache gcovr ${{ matrix.platform.name == 'Linux Clang' && 'llvm-$CLANG_VERSION' || '' }}
- name: Remove ALSA Library
if: runner.os == 'Linux' && matrix.platform.name != 'Android'
run: sudo apt-get remove -y libasound2
# LIBCXX_SHARED_SO is the path used by CMake to copy the necessary runtime library to the AVD
# We find it by searching ANDROID_NDK_ROOT for file paths ending with matrix.config.libcxx
- name: Install Android Components - name: Install Android Components
if: matrix.platform.name == 'Android' if: matrix.platform.name == 'Android'
run: | run: |
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;33.0.2" echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;33.0.2"
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.1.10909125" echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;26.1.10909125"
ANDROID_NDK_ROOT=$(echo $ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION)
echo "ANDROID_NDK_ROOT=$ANDROID_NDK_ROOT" >> $GITHUB_ENV
LIBCXX_SHARED_SO=$(find $ANDROID_NDK_ROOT -path \*/${{ matrix.config.libcxx }})
echo "LIBCXX_SHARED_SO=$LIBCXX_SHARED_SO" >> $GITHUB_ENV
- name: Install macOS Tools - name: Install macOS Tools
if: runner.os == 'macOS' if: runner.os == 'macOS'
@ -197,11 +235,11 @@ jobs:
# Make use of a test to print OpenGL vendor/renderer/version info to the console # Make use of a test to print OpenGL vendor/renderer/version info to the console
find build/bin -name test-sfml-window -or -name test-sfml-window.exe -exec sh -c "{} *sf::Context* --section=\"Version String\" --success | grep OpenGL" \; find build/bin -name test-sfml-window -or -name test-sfml-window.exe -exec sh -c "{} *sf::Context* --section=\"Version String\" --success | grep OpenGL" \;
- name: Test - name: Test (Windows)
if: runner.os == 'Windows' && !contains(matrix.platform.name, 'MinGW') if: runner.os == 'Windows' && !contains(matrix.platform.name, 'MinGW')
run: cmake --build build --target runtests --config ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} run: cmake --build build --target runtests --config ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }}
- name: Test - name: Test (Linux/macOS/MinGW)
if: (runner.os != 'Windows' || contains(matrix.platform.name, 'MinGW')) && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android') if: (runner.os != 'Windows' || contains(matrix.platform.name, 'MinGW')) && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android')
run: | run: |
ctest --test-dir build --output-on-failure -C ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --repeat until-pass:3 ctest --test-dir build --output-on-failure -C ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --repeat until-pass:3
@ -210,6 +248,47 @@ jobs:
gcovr -r $GITHUB_WORKSPACE -x build/coverage.out -s -f 'src/SFML/.*' -f 'include/SFML/.*' ${{ matrix.platform.gcovr_options }} $GITHUB_WORKSPACE gcovr -r $GITHUB_WORKSPACE -x build/coverage.out -s -f 'src/SFML/.*' -f 'include/SFML/.*' ${{ matrix.platform.gcovr_options }} $GITHUB_WORKSPACE
fi fi
- name: Enable KVM
if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Cache AVD
if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.config.emuarch }}-${{ matrix.config.emuapi }}
- name: Create AVD and Generate Snapshot for Caching
if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi && steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.config.emuapi }}
arch: ${{ matrix.config.emuarch }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none ${{ matrix.config.emuflags }}
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Test (Android)
if: contains(matrix.platform.name, 'Android') && matrix.config.emuapi
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.config.emuapi }}
arch: ${{ matrix.config.emuarch }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none ${{ matrix.config.emuflags }}
disable-animations: true
script: |
cmake --build build --config ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --target prepare-android-files
ctest --test-dir build --output-on-failure -C ${{ matrix.type.name == 'Debug' && 'Debug' || 'Release' }} --repeat until-pass:3
- name: Upload Coverage Report to Coveralls - name: Upload Coverage Report to Coveralls
if: matrix.type.name == 'Debug' && github.repository == 'SFML/SFML' && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android') # Disable upload in forks if: matrix.type.name == 'Debug' && github.repository == 'SFML/SFML' && !contains(matrix.platform.name, 'iOS') && !contains(matrix.platform.name, 'Android') # Disable upload in forks
uses: coverallsapp/github-action@v2 uses: coverallsapp/github-action@v2
@ -311,7 +390,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
platform: platform:
- { name: Linux, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=ON } - { name: Linux, os: ubuntu-22.04, flags: }
- { name: Linux DRM, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_USE_DRM=ON } - { name: Linux DRM, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_USE_DRM=ON }
- { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_OPENGL_ES=ON } - { name: Linux GCC OpenGL ES, os: ubuntu-22.04, flags: -DSFML_RUN_DISPLAY_TESTS=OFF -DSFML_OPENGL_ES=ON }
@ -327,7 +406,7 @@ jobs:
- name: Install Linux Dependencies - name: Install Linux Dependencies
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox run: sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox && sudo apt-get remove -y libasound2
- name: Configure - name: Configure
run: cmake --preset dev -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DSFML_BUILD_EXAMPLES=OFF -DSFML_ENABLE_SANITIZERS=ON ${{matrix.platform.flags}} run: cmake --preset dev -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DSFML_BUILD_EXAMPLES=OFF -DSFML_ENABLE_SANITIZERS=ON ${{matrix.platform.flags}}

View File

@ -224,9 +224,12 @@ endif()
sfml_set_option(SFML_INSTALL_PKGCONFIG_FILES ${SFML_INSTALL_PKGCONFIG_DEFAULT} BOOL "TRUE to automatically install pkg-config files so other projects can find SFML") sfml_set_option(SFML_INSTALL_PKGCONFIG_FILES ${SFML_INSTALL_PKGCONFIG_DEFAULT} BOOL "TRUE to automatically install pkg-config files so other projects can find SFML")
if(SFML_INSTALL_PKGCONFIG_FILES) if(SFML_INSTALL_PKGCONFIG_FILES)
# account for CMAKE_INSTALL_LIBDIR potentially being an absolute path
file(RELATIVE_PATH SFML_RELATIVE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX} ${CMAKE_INSTALL_FULL_LIBDIR})
# set pkgconfig install directory # set pkgconfig install directory
# this could be e.g. macports on mac or msys2 on windows etc. # this could be e.g. macports on mac or msys2 on windows etc.
set(SFML_PKGCONFIG_DIR "/${CMAKE_INSTALL_LIBDIR}/pkgconfig") set(SFML_PKGCONFIG_DIR "/${SFML_RELATIVE_INSTALL_LIBDIR}/pkgconfig")
if(SFML_OS_FREEBSD OR SFML_OS_OPENBSD OR SFML_OS_NETBSD) if(SFML_OS_FREEBSD OR SFML_OS_OPENBSD OR SFML_OS_NETBSD)
set(SFML_PKGCONFIG_DIR "/libdata/pkgconfig") set(SFML_PKGCONFIG_DIR "/libdata/pkgconfig")
@ -465,6 +468,13 @@ endif()
sfml_export_targets() sfml_export_targets()
# configure extras by default when building SFML directly, otherwise hide them
sfml_set_option(SFML_CONFIGURE_EXTRAS ${PROJECT_IS_TOP_LEVEL} BOOL "TRUE to configure extras, FALSE to ignore them")
if(NOT SFML_CONFIGURE_EXTRAS)
return()
endif()
set(CPACK_PACKAGE_NAME_SUMMARY "Simple and Fast Multimedia Library") set(CPACK_PACKAGE_NAME_SUMMARY "Simple and Fast Multimedia Library")
set(CPACK_PACKAGE_VENDOR "SFML Team") set(CPACK_PACKAGE_VENDOR "SFML Team")
set(CPACK_PACKAGE_FILE_NAME "SFML-${PROJECT_VERSION}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}") set(CPACK_PACKAGE_FILE_NAME "SFML-${PROJECT_VERSION}-${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}-${CMAKE_BUILD_TYPE}")
@ -485,13 +495,6 @@ set(CPACK_NSIS_INSTALLER_MUI_ICON_CODE "!define MUI_WELCOMEFINISHPAGE_BITMAP \\\
include(CPack) include(CPack)
# configure extras by default when building SFML directly, otherwise hide them
sfml_set_option(SFML_CONFIGURE_EXTRAS ${PROJECT_IS_TOP_LEVEL} BOOL "TRUE to configure extras, FALSE to ignore them")
if(NOT SFML_CONFIGURE_EXTRAS)
return()
endif()
# add an option for building the API documentation # add an option for building the API documentation
sfml_set_option(SFML_BUILD_DOC FALSE BOOL "TRUE to generate the API documentation, FALSE to ignore it") sfml_set_option(SFML_BUILD_DOC FALSE BOOL "TRUE to generate the API documentation, FALSE to ignore it")
if(SFML_BUILD_DOC) if(SFML_BUILD_DOC)

View File

@ -6,6 +6,8 @@
- Ensure GNUInstallDirs cache vars are included before first used (#2778, #2779) - Ensure GNUInstallDirs cache vars are included before first used (#2778, #2779)
- [macOS] Fix incorrect variable expansion (#2780) - [macOS] Fix incorrect variable expansion (#2780)
- Issue warning when trying to use UCRT MinGW with precompiled MSVCRT depenencies (#2821)
- Fix Nix pkg-config support
### Audio ### Audio

View File

@ -368,6 +368,11 @@ function(sfml_add_test target SOURCES DEPENDS)
# Delay test registration when cross compiling to avoid running crosscompiled app on host OS # Delay test registration when cross compiling to avoid running crosscompiled app on host OS
if(CMAKE_CROSSCOMPILING) if(CMAKE_CROSSCOMPILING)
set(CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) set(CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
# When running tests on Android, use a custom shell script to invoke commands using adb shell
if(SFML_OS_ANDROID)
set_target_properties(${target} PROPERTIES CROSSCOMPILING_EMULATOR "${PROJECT_BINARY_DIR}/run-in-adb-shell.sh")
endif()
endif() endif()
# Add the test # Add the test

View File

@ -26,7 +26,12 @@ if(SFML_OS_WINDOWS)
PATHS "C:/Program Files/HTML Help Workshop" "C:/Program Files (x86)/HTML Help Workshop" PATHS "C:/Program Files/HTML Help Workshop" "C:/Program Files (x86)/HTML Help Workshop"
DOC "HTML Help Compiler program") DOC "HTML Help Compiler program")
if(DOXYGEN_HHC_PROGRAM) if(DOXYGEN_HHC_PROGRAM)
set(DOXYGEN_GENERATE_HTMLHELP YES) if(DOXYGEN_VERSION VERSION_LESS "1.10.0")
set(DOXYGEN_GENERATE_HTMLHELP YES)
else()
message("Due to conflicts with the HTML output settings in Doxygen ${DOXYGEN_VERSION}, the HTML Help generation will be disabled")
set(DOXYGEN_GENERATE_HTMLHELP NO)
endif()
else() else()
set(DOXYGEN_GENERATE_HTMLHELP NO) set(DOXYGEN_GENERATE_HTMLHELP NO)
endif() endif()

View File

@ -1,4 +1,4 @@
# Doxyfile 1.9.6 # Doxyfile 1.10.0
# This file describes the settings to be used by the documentation system # This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project. # doxygen (www.doxygen.org) for a project.
@ -63,6 +63,12 @@ PROJECT_BRIEF =
PROJECT_LOGO = PROJECT_LOGO =
# With the PROJECT_ICON tag one can specify an icon that is included in the tabs
# when the HTML document is shown. Doxygen will copy the logo to the output
# directory.
PROJECT_ICON =
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
# into which the generated documentation will be written. If a relative path is # into which the generated documentation will be written. If a relative path is
# entered, it will be relative to the location where doxygen was started. If # entered, it will be relative to the location where doxygen was started. If
@ -380,6 +386,17 @@ MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 0 TOC_INCLUDE_HEADINGS = 0
# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
# generate identifiers for the Markdown headings. Note: Every identifier is
# unique.
# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
# sequence number starting at 0 and GITHUB use the lower case version of title
# with any whitespace replaced by '-' and punctuation characters removed.
# The default value is: DOXYGEN.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
MARKDOWN_ID_STYLE = DOXYGEN
# When enabled doxygen tries to link words that correspond to documented # When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can # classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or # be prevented in individual cases by putting a % sign in front of the word or
@ -504,6 +521,14 @@ LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1 NUM_PROC_THREADS = 1
# If the TIMESTAMP tag is set different from NO then each generated page will
# contain the date or date and time when the page was generated. Setting this to
# NO can help when comparing the output of multiple runs.
# Possible values are: YES, NO, DATETIME and DATE.
# The default value is: NO.
TIMESTAMP = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Build related configuration options # Build related configuration options
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@ -516,7 +541,7 @@ NUM_PROC_THREADS = 1
# normally produced when WARNINGS is set to YES. # normally produced when WARNINGS is set to YES.
# The default value is: NO. # The default value is: NO.
EXTRACT_ALL = NO EXTRACT_ALL = YES
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation. # be included in the documentation.
@ -889,7 +914,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status. # at the end of the doxygen process doxygen will return with a non-zero status.
# Possible values are: NO, YES and FAIL_ON_WARNINGS. # If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
# write the warning messages in between other messages but write them at the end
# of a run, in case a WARN_LOGFILE is defined the warning messages will be
# besides being in the defined file also be shown at the end of a run, unless
# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
# the behavior will remain as with the setting FAIL_ON_WARNINGS.
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO. # The default value is: NO.
WARN_AS_ERROR = NO WARN_AS_ERROR = NO
@ -968,12 +1000,12 @@ INPUT_FILE_ENCODING =
# Note the list of default checked file patterns might differ from the list of # Note the list of default checked file patterns might differ from the list of
# default file extension mappings. # default file extension mappings.
# #
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, # *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl,
# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, # *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d,
# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C # *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to
# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, # be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
# *.vhdl, *.ucf, *.qsf and *.ice. # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.hpp FILE_PATTERNS = *.hpp
@ -1020,9 +1052,6 @@ EXCLUDE_PATTERNS = .git \
# output. The symbol name can be a fully qualified name, a word, or if the # output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass, # wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test # ANamespace::AClass, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS = priv EXCLUDE_SYMBOLS = priv
@ -1136,7 +1165,8 @@ FORTRAN_COMMENT_AFTER = 72
SOURCE_BROWSER = YES SOURCE_BROWSER = YES
# Setting the INLINE_SOURCES tag to YES will include the body of functions, # Setting the INLINE_SOURCES tag to YES will include the body of functions,
# classes and enums directly into the documentation. # multi-line macros, enums or list initialized variables directly into the
# documentation.
# The default value is: NO. # The default value is: NO.
INLINE_SOURCES = NO INLINE_SOURCES = NO
@ -1384,7 +1414,7 @@ HTML_COLORSTYLE = LIGHT
# Minimum value: 0, maximum value: 359, default value: 220. # Minimum value: 0, maximum value: 359, default value: 220.
# This tag requires that the tag GENERATE_HTML is set to YES. # This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_HUE = 85
# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
# in the HTML output. For a value of 0 the output will use gray-scales only. A # in the HTML output. For a value of 0 the output will use gray-scales only. A
@ -1405,15 +1435,6 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80 HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to YES can help to show when doxygen was last run and thus if the
# documentation is up to date.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_TIMESTAMP = NO
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that # documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will # are dynamically created via JavaScript. If disabled, the navigation index will
@ -1433,6 +1454,33 @@ HTML_DYNAMIC_MENUS = NO
HTML_DYNAMIC_SECTIONS = NO HTML_DYNAMIC_SECTIONS = NO
# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
# dynamically folded and expanded in the generated HTML source code.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_CODE_FOLDING = YES
# If the HTML_COPY_CLIPBOARD tag is set to YES then doxygen will show an icon in
# the top right corner of code and text fragments that allows the user to copy
# its content to the clipboard. Note this only works if supported by the browser
# and the web page is served via a secure context (see:
# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file:
# protocol.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COPY_CLIPBOARD = YES
# Doxygen stores a couple of settings persistently in the browser (via e.g.
# cookies). By default these settings apply to all HTML pages generated by
# doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store
# the settings under a project specific key, such that the user preferences will
# be stored separately.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_PROJECT_COOKIE =
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand # shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to # and collapse entries dynamically later on. Doxygen will expand the tree to
@ -1563,6 +1611,16 @@ BINARY_TOC = NO
TOC_EXPAND = NO TOC_EXPAND = NO
# The SITEMAP_URL tag is used to specify the full URL of the place where the
# generated documentation will be placed on the server by the user during the
# deployment of the documentation. The generated sitemap is called sitemap.xml
# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
# is specified no sitemap is generated. For information about the sitemap
# protocol see https://www.sitemaps.org
# This tag requires that the tag GENERATE_HTML is set to YES.
SITEMAP_URL =
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@ -2051,9 +2109,16 @@ PDF_HYPERLINKS = YES
USE_PDFLATEX = YES USE_PDFLATEX = YES
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode # The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
# command to the generated LaTeX files. This will instruct LaTeX to keep running # Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
# if errors occur, instead of asking the user for help. # mode nothing is printed on the terminal, errors are scrolled as if <return> is
# hit at every error; missing files that TeX tries to input or request from
# keyboard input (\read on a not open input stream) cause the job to abort,
# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
# but there is no possibility of user interaction just like in batch mode,
# SCROLL In scroll mode, TeX will stop only for missing files to input or if
# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
# each error, asking for user intervention.
# The default value is: NO. # The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES. # This tag requires that the tag GENERATE_LATEX is set to YES.
@ -2074,14 +2139,6 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain LATEX_BIB_STYLE = plain
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
LATEX_TIMESTAMP = NO
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered, # path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the # it will be relative to the LATEX_OUTPUT directory. If left blank the
@ -2247,7 +2304,7 @@ DOCBOOK_OUTPUT = docbook
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures # AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature # the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment. # is still experimental and incomplete at the moment.
# The default value is: NO. # The default value is: NO.
@ -2258,6 +2315,28 @@ GENERATE_AUTOGEN_DEF = NO
# Configuration options related to Sqlite3 output # Configuration options related to Sqlite3 output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
# database with symbols found by doxygen stored in tables.
# The default value is: NO.
GENERATE_SQLITE3 = NO
# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
# in front of it.
# The default directory is: sqlite3.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
SQLITE3_OUTPUT = sqlite3
# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db
# database file will be recreated with each doxygen run. If set to NO, doxygen
# will warn if a database file is already found and not modify it.
# The default value is: YES.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
SQLITE3_RECREATE_DB = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the Perl module output # Configuration options related to the Perl module output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@ -2405,15 +2484,15 @@ TAGFILES =
GENERATE_TAGFILE = "@DOXYGEN_OUTPUT_DIR@/SFML.tag" GENERATE_TAGFILE = "@DOXYGEN_OUTPUT_DIR@/SFML.tag"
# If the ALLEXTERNALS tag is set to YES, all external class will be listed in # If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
# the class index. If set to NO, only the inherited external classes will be # will be listed in the class and namespace index. If set to NO, only the
# listed. # inherited external classes will be listed.
# The default value is: NO. # The default value is: NO.
ALLEXTERNALS = NO ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the modules index. If set to NO, only the current project's groups will be # in the topic index. If set to NO, only the current project's groups will be
# listed. # listed.
# The default value is: YES. # The default value is: YES.
@ -2427,16 +2506,9 @@ EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES EXTERNAL_PAGES = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the dot tool # Configuration options related to diagram generator tools
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
DIA_PATH =
# If set to YES the inheritance and collaboration graphs will hide inheritance # If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class. # and usage relations if the target is undocumented or is not a class.
# The default value is: YES. # The default value is: YES.
@ -2445,7 +2517,7 @@ HIDE_UNDOC_RELATIONS = YES
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see: # available from the path. This tool is part of Graphviz (see:
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is # Bell Labs. The other options in this section have no effect if this option is
# set to NO # set to NO
# The default value is: NO. # The default value is: NO.
@ -2498,13 +2570,19 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
DOT_FONTPATH = DOT_FONTPATH =
# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a # If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
# graph for each documented class showing the direct and indirect inheritance # generate a graph for each documented class showing the direct and indirect
# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, # inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set # HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
# to TEXT the direct and indirect inheritance relations will be shown as texts / # the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
# links. # CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
# Possible values are: NO, YES, TEXT and GRAPH. # If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
# relations will be shown as texts / links. Explicit enabling an inheritance
# graph or choosing a different representation for an inheritance graph of a
# specific class, can be accomplished by means of the command \inheritancegraph.
# Disabling an inheritance graph can be accomplished by means of the command
# \hideinheritancegraph.
# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
# The default value is: YES. # The default value is: YES.
CLASS_GRAPH = YES CLASS_GRAPH = YES
@ -2512,15 +2590,21 @@ CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation # graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the # dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes. # class with other documented classes. Explicit enabling a collaboration graph,
# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
# command \collaborationgraph. Disabling a collaboration graph can be
# accomplished by means of the command \hidecollaborationgraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies. See also the chapter Grouping # groups, showing the direct groups dependencies. Explicit enabling a group
# in the manual. # dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
# of the command \groupgraph. Disabling a directory graph can be accomplished by
# means of the command \hidegroupgraph. See also the chapter Grouping in the
# manual.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@ -2562,8 +2646,8 @@ DOT_UML_DETAILS = NO
# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters # The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
# to display on a single line. If the actual line length exceeds this threshold # to display on a single line. If the actual line length exceeds this threshold
# significantly it will wrapped across multiple lines. Some heuristics are apply # significantly it will be wrapped across multiple lines. Some heuristics are
# to avoid ugly line breaks. # applied to avoid ugly line breaks.
# Minimum value: 0, maximum value: 1000, default value: 17. # Minimum value: 0, maximum value: 1000, default value: 17.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@ -2580,7 +2664,9 @@ TEMPLATE_RELATIONS = NO
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the # YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented # direct and indirect include dependencies of the file with other documented
# files. # files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
# can be accomplished by means of the command \includegraph. Disabling an
# include graph can be accomplished by means of the command \hideincludegraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@ -2589,7 +2675,10 @@ INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing # set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented # the direct and indirect include dependencies of the file with other documented
# files. # files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
# to NO, can be accomplished by means of the command \includedbygraph. Disabling
# an included by graph can be accomplished by means of the command
# \hideincludedbygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@ -2629,7 +2718,10 @@ GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The # dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the # dependency relations are determined by the #include relations between the
# files in the directories. # files in the directories. Explicit enabling a directory graph, when
# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
# \directorygraph. Disabling a directory graph can be accomplished by means of
# the command \hidedirectorygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@ -2645,7 +2737,7 @@ DIR_GRAPH_MAX_DEPTH = 1
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section # generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see: # output formats in the documentation of the dot tool (Graphviz (see:
# http://www.graphviz.org/)). # https://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this # to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement). # requirement).
@ -2682,11 +2774,12 @@ DOT_PATH =
DOTFILE_DIRS = DOTFILE_DIRS =
# The MSCFILE_DIRS tag can be used to specify one or more directories that # You can include diagrams made with dia in doxygen documentation. Doxygen will
# contain msc files that are included in the documentation (see the \mscfile # then run dia to produce the diagram and insert it in the documentation. The
# command). # DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
MSCFILE_DIRS = DIA_PATH =
# The DIAFILE_DIRS tag can be used to specify one or more directories that # The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile # contain dia files that are included in the documentation (see the \diafile
@ -2763,3 +2856,19 @@ GENERATE_LEGEND = YES
# The default value is: YES. # The default value is: YES.
DOT_CLEANUP = YES DOT_CLEANUP = YES
# You can define message sequence charts within doxygen comments using the \msc
# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
# use a built-in version of mscgen tool to produce the charts. Alternatively,
# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
# specifying prog as the value, doxygen will call the tool as prog -T
# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
# output file formats "png", "eps", "svg", and "ismap".
MSCGEN_TOOL =
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
MSCFILE_DIRS =

View File

@ -147,12 +147,53 @@ div.fragment {
font-family: Consolas, "Liberation Mono", Courier, monospace; font-family: Consolas, "Liberation Mono", Courier, monospace;
font-size: 10pt; font-size: 10pt;
position: relative;
padding: 0.5em 1em; padding: 0.5em 1em;
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #bbb; border: 1px solid #bbb;
border-radius(5px); border-radius(5px);
} }
.clipboard {
width: 24px;
height: 24px;
right: 5px;
top: 5px;
opacity: 0;
position: absolute;
display: inline;
overflow: auto;
fill: black;
justify-content: center;
align-items: center;
cursor: pointer;
}
.clipboard.success {
border: 1px solid black;
border-radius: 4px;
}
.fragment:hover .clipboard, .clipboard.success {
opacity: .28;
}
.clipboard:hover, .clipboard.success {
opacity: 1 !important;
}
.clipboard:active:not([class~=success]) svg {
transform: scale(.91);
}
.clipboard.success svg {
fill: #2EC82E;
}
.clipboard.success {
border-color: #2EC82E;
}
div.line { div.line {
min-height: 13px; min-height: 13px;
text-wrap: unrestricted; text-wrap: unrestricted;
@ -1254,7 +1295,7 @@ div.contents ul li {
width: 24px; width: 24px;
height: 18px; height: 18px;
margin-bottom: 4px; margin-bottom: 4px;
background-image:url('folderopen.png'); background-image:url('folderopen.svg');
background-position: 0px -4px; background-position: 0px -4px;
background-repeat: repeat-y; background-repeat: repeat-y;
vertical-align:top; vertical-align:top;
@ -1265,7 +1306,7 @@ div.contents ul li {
width: 24px; width: 24px;
height: 18px; height: 18px;
margin-bottom: 4px; margin-bottom: 4px;
background-image:url('folderclosed.png'); background-image:url('folderclosed.svg');
background-position: 0px -4px; background-position: 0px -4px;
background-repeat: repeat-y; background-repeat: repeat-y;
vertical-align:top; vertical-align:top;
@ -1276,7 +1317,7 @@ div.contents ul li {
width: 24px; width: 24px;
height: 18px; height: 18px;
margin-bottom: 4px; margin-bottom: 4px;
background-image:url('doc.png'); background-image:url('doc.svg');
background-position: 0px -4px; background-position: 0px -4px;
background-repeat: repeat-y; background-repeat: repeat-y;
vertical-align:top; vertical-align:top;

View File

@ -1,7 +1,7 @@
</div> </div>
<div id="footer-container"> <div id="footer-container">
<div id="footer"> <div id="footer">
SFML is licensed under the terms and conditions of the <a href="https://www.sfml-dev.org/license.php">zlib/png license</a>.<br> SFML is licensed under the terms and conditions of the <a href="https://www.sfml-dev.org/license.php">zlib/png license</a>.<br />
Copyright &copy; Laurent Gomila &nbsp;::&nbsp; Copyright &copy; Laurent Gomila &nbsp;::&nbsp;
Documentation generated by <a href="http://www.doxygen.org/" title="doxygen website">doxygen</a> &nbsp;::&nbsp; Documentation generated by <a href="http://www.doxygen.org/" title="doxygen website">doxygen</a> &nbsp;::&nbsp;
</div> </div>

View File

@ -2,11 +2,12 @@
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>SFML - Simple and Fast Multimedia Library</title> <title>SFML - Simple and Fast Multimedia Library</title>
<meta http-equiv="Content-Type" content="text/html;"/> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="doxygen.css" title="default" media="screen,print" /> <link rel="stylesheet" type="text/css" href="doxygen.css" title="default" media="screen,print" />
<script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="dynsections.js"></script> <script type="text/javascript" src="dynsections.js"></script>
<script type="text/javascript" src="clipboard.js"></script>
<script type="text/javascript" src="cookie.js"></script>
<link rel="stylesheet" type="text/css" href="search/search.css" /> <link rel="stylesheet" type="text/css" href="search/search.css" />
<link rel="stylesheet" type="text/css" href="searchOverrides.css" /> <link rel="stylesheet" type="text/css" href="searchOverrides.css" />
<script type="text/javascript" src="search/searchdata.js"></script> <script type="text/javascript" src="search/searchdata.js"></script>
@ -24,3 +25,4 @@
</div> </div>
</div> </div>
<div id="content"> <div id="content">
<div>

View File

@ -6,6 +6,7 @@
#include <SFML/Audio.hpp> #include <SFML/Audio.hpp>
#include <algorithm> #include <algorithm>
#include <iostream>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -18,6 +19,7 @@ namespace
constexpr auto windowWidth = 800u; constexpr auto windowWidth = 800u;
constexpr auto windowHeight = 600u; constexpr auto windowHeight = 600u;
constexpr auto pi = 3.14159265359f; constexpr auto pi = 3.14159265359f;
constexpr auto sqrt2 = 2.0f * 0.707106781186547524401f;
std::filesystem::path resourcesDir() std::filesystem::path resourcesDir()
{ {
@ -84,10 +86,10 @@ protected:
private: private:
// Virtual functions to be implemented in derived effects // Virtual functions to be implemented in derived effects
virtual void onUpdate(float time, float x, float y) = 0; virtual void onUpdate(float time, float x, float y) = 0;
virtual void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const = 0; virtual void onDraw(sf::RenderTarget& target, sf::RenderStates states) const = 0;
virtual void onStart() = 0; virtual void onStart() = 0;
virtual void onStop() = 0; virtual void onStop() = 0;
virtual void onKey(sf::Keyboard::Key) virtual void onKey(sf::Keyboard::Key)
{ {
@ -113,7 +115,7 @@ public:
// Load the music file // Load the music file
if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg"))
sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl;
// Set the music to loop // Set the music to loop
m_music.setLoop(true); m_music.setLoop(true);
@ -128,7 +130,7 @@ public:
m_music.setPosition({m_position.x, m_position.y, 0.f}); m_music.setPosition({m_position.x, m_position.y, 0.f});
} }
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{ {
auto statesCopy(states); auto statesCopy(states);
statesCopy.transform = sf::Transform::Identity; statesCopy.transform = sf::Transform::Identity;
@ -172,7 +174,7 @@ public:
{ {
// Load the music file // Load the music file
if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg"))
sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl;
// Set the music to loop // Set the music to loop
m_music.setLoop(true); m_music.setLoop(true);
@ -202,7 +204,7 @@ public:
m_volumeText.setString("Volume: " + std::to_string(m_volume)); m_volumeText.setString("Volume: " + std::to_string(m_volume));
} }
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{ {
target.draw(m_pitchText, states); target.draw(m_pitchText, states);
target.draw(m_volumeText, states); target.draw(m_volumeText, states);
@ -275,7 +277,7 @@ public:
// Load the music file // Load the music file
if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg")) if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg"))
sf::err() << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl; std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl;
// Set the music to loop // Set the music to loop
m_music.setLoop(true); m_music.setLoop(true);
@ -305,7 +307,7 @@ public:
m_music.setPosition({m_position.x, m_position.y, 0.f}); m_music.setPosition({m_position.x, m_position.y, 0.f});
} }
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{ {
auto statesCopy(states); auto statesCopy(states);
@ -375,7 +377,7 @@ public:
m_currentFrequency.setString("Frequency: " + std::to_string(m_frequency) + " Hz"); m_currentFrequency.setString("Frequency: " + std::to_string(m_frequency) + " Hz");
} }
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{ {
target.draw(m_instruction, states); target.draw(m_instruction, states);
target.draw(m_currentType, states); target.draw(m_currentType, states);
@ -547,7 +549,7 @@ public:
setDopplerFactor(m_factor); setDopplerFactor(m_factor);
} }
void onDraw(sf::RenderTarget& target, const sf::RenderStates& states) const override void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{ {
auto statesCopy(states); auto statesCopy(states);
statesCopy.transform = sf::Transform::Identity; statesCopy.transform = sf::Transform::Identity;
@ -615,6 +617,425 @@ private:
}; };
////////////////////////////////////////////////////////////
// Processing base class
////////////////////////////////////////////////////////////
class Processing : public Effect
{
public:
void onUpdate([[maybe_unused]] float time, float x, float y) override
{
m_position = {windowWidth * x - 10.f, windowHeight * y - 10.f};
m_music.setPosition({m_position.x, m_position.y, 0.f});
}
void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override
{
auto statesCopy(states);
statesCopy.transform = sf::Transform::Identity;
statesCopy.transform.translate(m_position);
target.draw(m_listener, states);
target.draw(m_soundShape, statesCopy);
target.draw(m_enabledText);
target.draw(m_instructions);
}
void onStart() override
{
// Synchronize listener audio position with graphical position
sf::Listener::setPosition({m_listener.getPosition().x, m_listener.getPosition().y, 0.f});
m_music.play();
}
void onStop() override
{
m_music.stop();
}
protected:
Processing(std::string name) :
Effect(std::move(name)),
m_enabledText(getFont(), "Processing: Enabled"),
m_instructions(getFont(), "Press Space to enable/disable processing")
{
m_listener.setPosition({(windowWidth - 20.f) / 2.f, (windowHeight - 20.f) / 2.f});
m_listener.setFillColor(sf::Color::Red);
m_enabledText.setPosition({windowWidth / 2.f - 120.f, windowHeight * 3.f / 4.f - 50.f});
m_instructions.setPosition({windowWidth / 2.f - 250.f, windowHeight * 3.f / 4.f});
// Load the music file
if (!m_music.openFromFile(resourcesDir() / "doodle_pop.ogg"))
std::cerr << "Failed to load " << (resourcesDir() / "doodle_pop.ogg").string() << std::endl;
// Set the music to loop
m_music.setLoop(true);
// Set attenuation to a nice value
m_music.setAttenuation(0.0f);
}
sf::Music& getMusic()
{
return m_music;
}
const std::shared_ptr<bool>& getEnabled() const
{
return m_enabled;
}
private:
void onKey(sf::Keyboard::Key key) override
{
if (key == sf::Keyboard::Key::Space)
*m_enabled = !*m_enabled;
m_enabledText.setString(*m_enabled ? "Processing: Enabled" : "Processing: Disabled");
}
sf::CircleShape m_listener{20.f};
sf::CircleShape m_soundShape{20.f};
sf::Vector2f m_position;
sf::Music m_music;
std::shared_ptr<bool> m_enabled{std::make_shared<bool>(true)};
sf::Text m_enabledText;
sf::Text m_instructions;
};
////////////////////////////////////////////////////////////
// Biquad Filter (https://github.com/dimtass/DSP-Cpp-filters)
////////////////////////////////////////////////////////////
class BiquadFilter : public Processing
{
protected:
struct Coefficients
{
float a0{};
float a1{};
float a2{};
float b1{};
float b2{};
float c0{};
float d0{};
};
using Processing::Processing;
void setCoefficients(const Coefficients& coefficients)
{
auto& music = getMusic();
struct State
{
float xnz1{};
float xnz2{};
float ynz1{};
float ynz2{};
};
// We use a mutable lambda to tie the lifetime of the state and coefficients to the lambda itself
// This is necessary since the Echo object will be destroyed before the Music object
// While the Music object exists, it is possible that the audio engine will try to call
// this lambda hence we need to always have usable coefficients and state until the Music and the
// associated lambda are destroyed
music.setEffectProcessor(
[coefficients,
enabled = getEnabled(),
state = std::vector<State>(music.getChannelCount())](const float* inputFrames,
unsigned int& inputFrameCount,
float* outputFrames,
unsigned int& outputFrameCount,
unsigned int frameChannelCount) mutable
{
for (auto frame = 0u; frame < outputFrameCount; ++frame)
{
for (auto channel = 0u; channel < frameChannelCount; ++channel)
{
auto& channelState = state[channel];
const auto xn = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available
const auto yn = coefficients.a0 * xn + coefficients.a1 * channelState.xnz1 +
coefficients.a2 * channelState.xnz2 - coefficients.b1 * channelState.ynz1 -
coefficients.b2 * channelState.ynz2;
channelState.xnz2 = channelState.xnz1;
channelState.xnz1 = xn;
channelState.ynz2 = channelState.ynz1;
channelState.ynz1 = yn;
outputFrames[channel] = *enabled ? yn : xn;
}
inputFrames += (inputFrames ? frameChannelCount : 0u);
outputFrames += frameChannelCount;
}
// We processed data 1:1
inputFrameCount = outputFrameCount;
});
}
};
////////////////////////////////////////////////////////////
// High-pass Filter (https://github.com/dimtass/DSP-Cpp-filters)
////////////////////////////////////////////////////////////
struct HighPassFilter : BiquadFilter
{
HighPassFilter() : BiquadFilter("High-pass Filter")
{
static constexpr auto cutoffFrequency = 2000.f;
const auto c = std::tan(pi * cutoffFrequency / static_cast<float>(getMusic().getSampleRate()));
Coefficients coefficients;
coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f));
coefficients.a1 = -2.f * coefficients.a0;
coefficients.a2 = coefficients.a0;
coefficients.b1 = 2.f * coefficients.a0 * (std::pow(c, 2.f) - 1.f);
coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f));
setCoefficients(coefficients);
}
};
////////////////////////////////////////////////////////////
// Low-pass Filter (https://github.com/dimtass/DSP-Cpp-filters)
////////////////////////////////////////////////////////////
struct LowPassFilter : BiquadFilter
{
LowPassFilter() : BiquadFilter("Low-pass Filter")
{
static constexpr auto cutoffFrequency = 500.f;
const auto c = 1.f / std::tan(pi * cutoffFrequency / static_cast<float>(getMusic().getSampleRate()));
Coefficients coefficients;
coefficients.a0 = 1.f / (1.f + sqrt2 * c + std::pow(c, 2.f));
coefficients.a1 = 2.f * coefficients.a0;
coefficients.a2 = coefficients.a0;
coefficients.b1 = 2.f * coefficients.a0 * (1.f - std::pow(c, 2.f));
coefficients.b2 = coefficients.a0 * (1.f - sqrt2 * c + std::pow(c, 2.f));
setCoefficients(coefficients);
}
};
////////////////////////////////////////////////////////////
// Echo (miniaudio implementation)
////////////////////////////////////////////////////////////
struct Echo : Processing
{
Echo() : Processing("Echo")
{
auto& music = getMusic();
static constexpr auto delay = 0.2f;
static constexpr auto decay = 0.75f;
static constexpr auto wet = 0.8f;
static constexpr auto dry = 1.f;
const auto channelCount = music.getChannelCount();
const auto sampleRate = music.getSampleRate();
const auto delayInFrames = static_cast<unsigned int>(static_cast<float>(sampleRate) * delay);
// We use a mutable lambda to tie the lifetime of the state to the lambda itself
// This is necessary since the Echo object will be destroyed before the Music object
// While the Music object exists, it is possible that the audio engine will try to call
// this lambda hence we need to always have a usable state until the Music and the
// associated lambda are destroyed
music.setEffectProcessor(
[delayInFrames,
enabled = getEnabled(),
buffer = std::vector<float>(delayInFrames * channelCount, 0.f),
cursor = 0u](const float* inputFrames,
unsigned int& inputFrameCount,
float* outputFrames,
unsigned int& outputFrameCount,
unsigned int frameChannelCount) mutable
{
for (auto frame = 0u; frame < outputFrameCount; ++frame)
{
for (auto channel = 0u; channel < frameChannelCount; ++channel)
{
const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available
const auto bufferIndex = (cursor * frameChannelCount) + channel;
buffer[bufferIndex] = (buffer[bufferIndex] * decay) + (input * dry);
outputFrames[channel] = *enabled ? buffer[bufferIndex] * wet : input;
}
cursor = (cursor + 1) % delayInFrames;
inputFrames += (inputFrames ? frameChannelCount : 0u);
outputFrames += frameChannelCount;
}
// We processed data 1:1
inputFrameCount = outputFrameCount;
});
}
};
////////////////////////////////////////////////////////////
// Reverb (https://github.com/sellicott/DSP-FFMpeg-Reverb)
////////////////////////////////////////////////////////////
class Reverb : public Processing
{
public:
Reverb() : Processing("Reverb")
{
auto& music = getMusic();
static constexpr auto sustain = 0.7f; // [0.f; 1.f]
const auto channelCount = music.getChannelCount();
const auto sampleRate = music.getSampleRate();
std::vector<ReverbFilter<float>> filters;
filters.reserve(channelCount);
for (auto i = 0u; i < channelCount; ++i)
filters.emplace_back(sampleRate, sustain);
// We use a mutable lambda to tie the lifetime of the state to the lambda itself
// This is necessary since the Echo object will be destroyed before the Music object
// While the Music object exists, it is possible that the audio engine will try to call
// this lambda hence we need to always have a usable state until the Music and the
// associated lambda are destroyed
music.setEffectProcessor(
[filters, enabled = getEnabled()](const float* inputFrames,
unsigned int& inputFrameCount,
float* outputFrames,
unsigned int& outputFrameCount,
unsigned int frameChannelCount) mutable
{
for (auto frame = 0u; frame < outputFrameCount; ++frame)
{
for (auto channel = 0u; channel < frameChannelCount; ++channel)
{
const auto input = inputFrames ? inputFrames[channel] : 0.f; // Read silence if no input data available
outputFrames[channel] = *enabled ? filters[channel](input) : input;
}
inputFrames += (inputFrames ? frameChannelCount : 0u);
outputFrames += frameChannelCount;
}
// We processed data 1:1
inputFrameCount = outputFrameCount;
});
}
private:
template <typename T>
class AllPassFilter
{
public:
AllPassFilter(std::size_t delay, float theGain) : m_buffer(delay, {}), m_gain(theGain)
{
}
T operator()(T input)
{
const auto output = m_buffer[m_cursor];
input = static_cast<T>(input + m_gain * output);
m_buffer[m_cursor] = input;
m_cursor = (m_cursor + 1) % m_buffer.size();
return static_cast<T>(-m_gain * input + output);
}
private:
std::vector<T> m_buffer;
std::size_t m_cursor{};
const float m_gain{};
};
template <typename T>
class FIRFilter
{
public:
FIRFilter(std::vector<float> taps) : m_taps(std::move(taps))
{
}
T operator()(T input)
{
m_buffer[m_cursor] = input;
m_cursor = (m_cursor + 1) % m_buffer.size();
T output{};
for (auto i = 0u; i < m_taps.size(); ++i)
output += static_cast<T>(m_taps[i] * m_buffer[(m_cursor + i) % m_buffer.size()]);
return output;
}
private:
const std::vector<float> m_taps;
std::vector<T> m_buffer = std::vector<T>(m_taps.size(), {});
std::size_t m_cursor{};
};
template <typename T>
class ReverbFilter
{
public:
ReverbFilter(unsigned int sampleRate, float feedbackGain) :
m_allPass{{sampleRate / 10, 0.6f}, {sampleRate / 30, -0.6f}, {sampleRate / 90, 0.6f}, {sampleRate / 270, -0.6f}},
m_fir({0.003369f, 0.002810f, 0.001758f, 0.000340f, -0.001255f, -0.002793f, -0.004014f, -0.004659f,
-0.004516f, -0.003464f, -0.001514f, 0.001148f, 0.004157f, 0.006986f, 0.009003f, 0.009571f,
0.008173f, 0.004560f, -0.001120f, -0.008222f, -0.015581f, -0.021579f, -0.024323f, -0.021933f,
-0.012904f, 0.003500f, 0.026890f, 0.055537f, 0.086377f, 0.115331f, 0.137960f, 0.150407f,
0.150407f, 0.137960f, 0.115331f, 0.086377f, 0.055537f, 0.026890f, 0.003500f, -0.012904f,
-0.021933f, -0.024323f, -0.021579f, -0.015581f, -0.008222f, -0.001120f, 0.004560f, 0.008173f,
0.009571f, 0.009003f, 0.006986f, 0.004157f, 0.001148f, -0.001514f, -0.003464f, -0.004516f,
-0.004659f, -0.004014f, -0.002793f, -0.001255f, 0.000340f, 0.001758f, 0.002810f, 0.003369f}),
m_buffer(sampleRate / 5, {}), // sample rate / 5 = 200ms buffer size
m_feedbackGain(feedbackGain)
{
}
T operator()(T input)
{
auto output = static_cast<T>(0.7f * input + m_feedbackGain * m_buffer[m_cursor]);
for (auto& f : m_allPass)
output = f(output);
output = m_fir(output);
m_buffer[m_cursor] = output;
m_cursor = (m_cursor + 1) % m_buffer.size();
output += 0.5f * m_buffer[(m_cursor + 1 * m_interval - 1) % m_buffer.size()];
output += 0.25f * m_buffer[(m_cursor + 2 * m_interval - 1) % m_buffer.size()];
output += 0.125f * m_buffer[(m_cursor + 3 * m_interval - 1) % m_buffer.size()];
return 0.6f * output + input;
}
private:
AllPassFilter<T> m_allPass[4];
FIRFilter<T> m_fir;
std::vector<T> m_buffer;
std::size_t m_cursor{};
const std::size_t m_interval{m_buffer.size() / 3};
const float m_feedbackGain{};
};
};
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// Entry point of application /// Entry point of application
/// ///
@ -636,19 +1057,25 @@ int main()
Effect::setFont(font); Effect::setFont(font);
// Create the effects // Create the effects
Surround surroundEffect; Surround surroundEffect;
PitchVolume pitchVolumeEffect; PitchVolume pitchVolumeEffect;
Attenuation attenuationEffect; Attenuation attenuationEffect;
Tone toneEffect; Tone toneEffect;
Doppler dopplerEffect; Doppler dopplerEffect;
HighPassFilter highPassFilterEffect;
LowPassFilter lowPassFilterEffect;
Echo echoEffect;
Reverb reverbEffect;
const std::array<Effect*, 5> effects{ const std::array<Effect*, 9> effects{&surroundEffect,
&surroundEffect, &pitchVolumeEffect,
&pitchVolumeEffect, &attenuationEffect,
&attenuationEffect, &toneEffect,
&toneEffect, &dopplerEffect,
&dopplerEffect, &highPassFilterEffect,
}; &lowPassFilterEffect,
&echoEffect,
&reverbEffect};
std::size_t current = 0; std::size_t current = 0;

View File

@ -1,4 +1,4 @@
/* stb_image - v2.28 - public domain image loader - http://nothings.org/stb /* stb_image - v2.29 - public domain image loader - http://nothings.org/stb
no warranty implied; use at your own risk no warranty implied; use at your own risk
Do this: Do this:
@ -48,6 +48,7 @@ LICENSE
RECENT REVISION HISTORY: RECENT REVISION HISTORY:
2.29 (2023-05-xx) optimizations
2.28 (2023-01-29) many error fixes, security errors, just tons of stuff 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff
2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes
2.26 (2020-07-13) many minor fixes 2.26 (2020-07-13) many minor fixes
@ -1072,8 +1073,8 @@ static int stbi__addints_valid(int a, int b)
return a <= INT_MAX - b; return a <= INT_MAX - b;
} }
// returns 1 if the product of two signed shorts is valid, 0 on overflow. // returns 1 if the product of two ints fits in a signed short, 0 on overflow.
static int stbi__mul2shorts_valid(short a, short b) static int stbi__mul2shorts_valid(int a, int b)
{ {
if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow
if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid
@ -3384,13 +3385,13 @@ static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan)
return 1; return 1;
} }
static int stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j)
{ {
// some JPEGs have junk at end, skip over it but if we find what looks // some JPEGs have junk at end, skip over it but if we find what looks
// like a valid marker, resume there // like a valid marker, resume there
while (!stbi__at_eof(j->s)) { while (!stbi__at_eof(j->s)) {
int x = stbi__get8(j->s); stbi_uc x = stbi__get8(j->s);
while (x == 255) { // might be a marker while (x == 0xff) { // might be a marker
if (stbi__at_eof(j->s)) return STBI__MARKER_none; if (stbi__at_eof(j->s)) return STBI__MARKER_none;
x = stbi__get8(j->s); x = stbi__get8(j->s);
if (x != 0x00 && x != 0xff) { if (x != 0x00 && x != 0xff) {
@ -4176,6 +4177,7 @@ typedef struct
{ {
stbi_uc *zbuffer, *zbuffer_end; stbi_uc *zbuffer, *zbuffer_end;
int num_bits; int num_bits;
int hit_zeof_once;
stbi__uint32 code_buffer; stbi__uint32 code_buffer;
char *zout; char *zout;
@ -4242,9 +4244,20 @@ stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)
int b,s; int b,s;
if (a->num_bits < 16) { if (a->num_bits < 16) {
if (stbi__zeof(a)) { if (stbi__zeof(a)) {
return -1; /* report error for unexpected end of data. */ if (!a->hit_zeof_once) {
// This is the first time we hit eof, insert 16 extra padding btis
// to allow us to keep going; if we actually consume any of them
// though, that is invalid data. This is caught later.
a->hit_zeof_once = 1;
a->num_bits += 16; // add 16 implicit zero bits
} else {
// We already inserted our extra 16 padding bits and are again
// out, this stream is actually prematurely terminated.
return -1;
}
} else {
stbi__fill_bits(a);
} }
stbi__fill_bits(a);
} }
b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; b = z->fast[a->code_buffer & STBI__ZFAST_MASK];
if (b) { if (b) {
@ -4309,6 +4322,13 @@ static int stbi__parse_huffman_block(stbi__zbuf *a)
int len,dist; int len,dist;
if (z == 256) { if (z == 256) {
a->zout = zout; a->zout = zout;
if (a->hit_zeof_once && a->num_bits < 16) {
// The first time we hit zeof, we inserted 16 extra zero bits into our bit
// buffer so the decoder can just do its speculative decoding. But if we
// actually consumed any of those bits (which is the case when num_bits < 16),
// the stream actually read past the end so it is malformed.
return stbi__err("unexpected end","Corrupt PNG");
}
return 1; return 1;
} }
if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data
@ -4320,7 +4340,7 @@ static int stbi__parse_huffman_block(stbi__zbuf *a)
dist = stbi__zdist_base[z]; dist = stbi__zdist_base[z];
if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]);
if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG");
if (zout + len > a->zout_end) { if (len > a->zout_end - zout) {
if (!stbi__zexpand(a, zout, len)) return 0; if (!stbi__zexpand(a, zout, len)) return 0;
zout = a->zout; zout = a->zout;
} }
@ -4464,6 +4484,7 @@ static int stbi__parse_zlib(stbi__zbuf *a, int parse_header)
if (!stbi__parse_zlib_header(a)) return 0; if (!stbi__parse_zlib_header(a)) return 0;
a->num_bits = 0; a->num_bits = 0;
a->code_buffer = 0; a->code_buffer = 0;
a->hit_zeof_once = 0;
do { do {
final = stbi__zreceive(a,1); final = stbi__zreceive(a,1);
type = stbi__zreceive(a,2); type = stbi__zreceive(a,2);
@ -4619,9 +4640,8 @@ enum {
STBI__F_up=2, STBI__F_up=2,
STBI__F_avg=3, STBI__F_avg=3,
STBI__F_paeth=4, STBI__F_paeth=4,
// synthetic filters used for first scanline to avoid needing a dummy row of 0s // synthetic filter used for first scanline to avoid needing a dummy row of 0s
STBI__F_avg_first, STBI__F_avg_first
STBI__F_paeth_first
}; };
static stbi_uc first_row_filter[5] = static stbi_uc first_row_filter[5] =
@ -4630,29 +4650,56 @@ static stbi_uc first_row_filter[5] =
STBI__F_sub, STBI__F_sub,
STBI__F_none, STBI__F_none,
STBI__F_avg_first, STBI__F_avg_first,
STBI__F_paeth_first STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub
}; };
static int stbi__paeth(int a, int b, int c) static int stbi__paeth(int a, int b, int c)
{ {
int p = a + b - c; // This formulation looks very different from the reference in the PNG spec, but is
int pa = abs(p-a); // actually equivalent and has favorable data dependencies and admits straightforward
int pb = abs(p-b); // generation of branch-free code, which helps performance significantly.
int pc = abs(p-c); int thresh = c*3 - (a + b);
if (pa <= pb && pa <= pc) return a; int lo = a < b ? a : b;
if (pb <= pc) return b; int hi = a < b ? b : a;
return c; int t0 = (hi <= thresh) ? lo : c;
int t1 = (thresh <= lo) ? hi : t0;
return t1;
} }
static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 };
// adds an extra all-255 alpha channel
// dest == src is legal
// img_n must be 1 or 3
static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n)
{
int i;
// must process data backwards since we allow dest==src
if (img_n == 1) {
for (i=x-1; i >= 0; --i) {
dest[i*2+1] = 255;
dest[i*2+0] = src[i];
}
} else {
STBI_ASSERT(img_n == 3);
for (i=x-1; i >= 0; --i) {
dest[i*4+3] = 255;
dest[i*4+2] = src[i*3+2];
dest[i*4+1] = src[i*3+1];
dest[i*4+0] = src[i*3+0];
}
}
}
// create the png data from post-deflated data // create the png data from post-deflated data
static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color)
{ {
int bytes = (depth == 16? 2 : 1); int bytes = (depth == 16 ? 2 : 1);
stbi__context *s = a->s; stbi__context *s = a->s;
stbi__uint32 i,j,stride = x*out_n*bytes; stbi__uint32 i,j,stride = x*out_n*bytes;
stbi__uint32 img_len, img_width_bytes; stbi__uint32 img_len, img_width_bytes;
stbi_uc *filter_buf;
int all_ok = 1;
int k; int k;
int img_n = s->img_n; // copy it into a local for later int img_n = s->img_n; // copy it into a local for later
@ -4664,8 +4711,11 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r
a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into
if (!a->out) return stbi__err("outofmem", "Out of memory"); if (!a->out) return stbi__err("outofmem", "Out of memory");
// note: error exits here don't need to clean up a->out individually,
// stbi__do_png always does on error.
if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG");
img_width_bytes = (((img_n * x * depth) + 7) >> 3); img_width_bytes = (((img_n * x * depth) + 7) >> 3);
if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG");
img_len = (img_width_bytes + 1) * y; img_len = (img_width_bytes + 1) * y;
// we used to check for exact match between raw_len and img_len on non-interlaced PNGs, // we used to check for exact match between raw_len and img_len on non-interlaced PNGs,
@ -4673,189 +4723,137 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r
// so just check for raw_len < img_len always. // so just check for raw_len < img_len always.
if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG");
// Allocate two scan lines worth of filter workspace buffer.
filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0);
if (!filter_buf) return stbi__err("outofmem", "Out of memory");
// Filtering for low-bit-depth images
if (depth < 8) {
filter_bytes = 1;
width = img_width_bytes;
}
for (j=0; j < y; ++j) { for (j=0; j < y; ++j) {
stbi_uc *cur = a->out + stride*j; // cur/prior filter buffers alternate
stbi_uc *prior; stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes;
stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes;
stbi_uc *dest = a->out + stride*j;
int nk = width * filter_bytes;
int filter = *raw++; int filter = *raw++;
if (filter > 4) // check filter type
return stbi__err("invalid filter","Corrupt PNG"); if (filter > 4) {
all_ok = stbi__err("invalid filter","Corrupt PNG");
if (depth < 8) { break;
if (img_width_bytes > x) return stbi__err("invalid width","Corrupt PNG");
cur += x*out_n - img_width_bytes; // store output to the rightmost img_len bytes, so we can decode in place
filter_bytes = 1;
width = img_width_bytes;
} }
prior = cur - stride; // bugfix: need to compute this after 'cur +=' computation above
// if first row, use special filter that doesn't sample previous row // if first row, use special filter that doesn't sample previous row
if (j == 0) filter = first_row_filter[filter]; if (j == 0) filter = first_row_filter[filter];
// handle first byte explicitly // perform actual filtering
for (k=0; k < filter_bytes; ++k) { switch (filter) {
switch (filter) { case STBI__F_none:
case STBI__F_none : cur[k] = raw[k]; break; memcpy(cur, raw, nk);
case STBI__F_sub : cur[k] = raw[k]; break; break;
case STBI__F_up : cur[k] = STBI__BYTECAST(raw[k] + prior[k]); break; case STBI__F_sub:
case STBI__F_avg : cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); break; memcpy(cur, raw, filter_bytes);
case STBI__F_paeth : cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(0,prior[k],0)); break; for (k = filter_bytes; k < nk; ++k)
case STBI__F_avg_first : cur[k] = raw[k]; break; cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]);
case STBI__F_paeth_first: cur[k] = raw[k]; break; break;
} case STBI__F_up:
for (k = 0; k < nk; ++k)
cur[k] = STBI__BYTECAST(raw[k] + prior[k]);
break;
case STBI__F_avg:
for (k = 0; k < filter_bytes; ++k)
cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1));
for (k = filter_bytes; k < nk; ++k)
cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1));
break;
case STBI__F_paeth:
for (k = 0; k < filter_bytes; ++k)
cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0)
for (k = filter_bytes; k < nk; ++k)
cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes]));
break;
case STBI__F_avg_first:
memcpy(cur, raw, filter_bytes);
for (k = filter_bytes; k < nk; ++k)
cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1));
break;
} }
if (depth == 8) { raw += nk;
if (img_n != out_n)
cur[img_n] = 255; // first pixel
raw += img_n;
cur += out_n;
prior += out_n;
} else if (depth == 16) {
if (img_n != out_n) {
cur[filter_bytes] = 255; // first pixel top byte
cur[filter_bytes+1] = 255; // first pixel bottom byte
}
raw += filter_bytes;
cur += output_bytes;
prior += output_bytes;
} else {
raw += 1;
cur += 1;
prior += 1;
}
// this is a little gross, so that we don't switch per-pixel or per-component // expand decoded bits in cur to dest, also adding an extra alpha channel if desired
if (depth < 8 || img_n == out_n) { if (depth < 8) {
int nk = (width - 1)*filter_bytes;
#define STBI__CASE(f) \
case f: \
for (k=0; k < nk; ++k)
switch (filter) {
// "none" filter turns into a memcpy here; make that explicit.
case STBI__F_none: memcpy(cur, raw, nk); break;
STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); } break;
STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break;
STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); } break;
STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],prior[k],prior[k-filter_bytes])); } break;
STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); } break;
STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes],0,0)); } break;
}
#undef STBI__CASE
raw += nk;
} else {
STBI_ASSERT(img_n+1 == out_n);
#define STBI__CASE(f) \
case f: \
for (i=x-1; i >= 1; --i, cur[filter_bytes]=255,raw+=filter_bytes,cur+=output_bytes,prior+=output_bytes) \
for (k=0; k < filter_bytes; ++k)
switch (filter) {
STBI__CASE(STBI__F_none) { cur[k] = raw[k]; } break;
STBI__CASE(STBI__F_sub) { cur[k] = STBI__BYTECAST(raw[k] + cur[k- output_bytes]); } break;
STBI__CASE(STBI__F_up) { cur[k] = STBI__BYTECAST(raw[k] + prior[k]); } break;
STBI__CASE(STBI__F_avg) { cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k- output_bytes])>>1)); } break;
STBI__CASE(STBI__F_paeth) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],prior[k],prior[k- output_bytes])); } break;
STBI__CASE(STBI__F_avg_first) { cur[k] = STBI__BYTECAST(raw[k] + (cur[k- output_bytes] >> 1)); } break;
STBI__CASE(STBI__F_paeth_first) { cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k- output_bytes],0,0)); } break;
}
#undef STBI__CASE
// the loop above sets the high byte of the pixels' alpha, but for
// 16 bit png files we also need the low byte set. we'll do that here.
if (depth == 16) {
cur = a->out + stride*j; // start at the beginning of the row again
for (i=0; i < x; ++i,cur+=output_bytes) {
cur[filter_bytes+1] = 255;
}
}
}
}
// we make a separate pass to expand bits to pixels; for performance,
// this could run two scanlines behind the above code, so it won't
// intefere with filtering but will still be in the cache.
if (depth < 8) {
for (j=0; j < y; ++j) {
stbi_uc *cur = a->out + stride*j;
stbi_uc *in = a->out + stride*j + x*out_n - img_width_bytes;
// unpack 1/2/4-bit into a 8-bit buffer. allows us to keep the common 8-bit path optimal at minimal cost for 1/2/4-bit
// png guarante byte alignment, if width is not multiple of 8/4/2 we'll decode dummy trailing data that will be skipped in the later loop
stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range
stbi_uc *in = cur;
stbi_uc *out = dest;
stbi_uc inb = 0;
stbi__uint32 nsmp = x*img_n;
// note that the final byte might overshoot and write more data than desired. // expand bits to bytes first
// we can allocate enough data that this never writes out of memory, but it
// could also overwrite the next scanline. can it overwrite non-empty data
// on the next scanline? yes, consider 1-pixel-wide scanlines with 1-bit-per-pixel.
// so we need to explicitly clamp the final ones
if (depth == 4) { if (depth == 4) {
for (k=x*img_n; k >= 2; k-=2, ++in) { for (i=0; i < nsmp; ++i) {
*cur++ = scale * ((*in >> 4) ); if ((i & 1) == 0) inb = *in++;
*cur++ = scale * ((*in ) & 0x0f); *out++ = scale * (inb >> 4);
inb <<= 4;
} }
if (k > 0) *cur++ = scale * ((*in >> 4) );
} else if (depth == 2) { } else if (depth == 2) {
for (k=x*img_n; k >= 4; k-=4, ++in) { for (i=0; i < nsmp; ++i) {
*cur++ = scale * ((*in >> 6) ); if ((i & 3) == 0) inb = *in++;
*cur++ = scale * ((*in >> 4) & 0x03); *out++ = scale * (inb >> 6);
*cur++ = scale * ((*in >> 2) & 0x03); inb <<= 2;
*cur++ = scale * ((*in ) & 0x03);
} }
if (k > 0) *cur++ = scale * ((*in >> 6) ); } else {
if (k > 1) *cur++ = scale * ((*in >> 4) & 0x03); STBI_ASSERT(depth == 1);
if (k > 2) *cur++ = scale * ((*in >> 2) & 0x03); for (i=0; i < nsmp; ++i) {
} else if (depth == 1) { if ((i & 7) == 0) inb = *in++;
for (k=x*img_n; k >= 8; k-=8, ++in) { *out++ = scale * (inb >> 7);
*cur++ = scale * ((*in >> 7) ); inb <<= 1;
*cur++ = scale * ((*in >> 6) & 0x01);
*cur++ = scale * ((*in >> 5) & 0x01);
*cur++ = scale * ((*in >> 4) & 0x01);
*cur++ = scale * ((*in >> 3) & 0x01);
*cur++ = scale * ((*in >> 2) & 0x01);
*cur++ = scale * ((*in >> 1) & 0x01);
*cur++ = scale * ((*in ) & 0x01);
} }
if (k > 0) *cur++ = scale * ((*in >> 7) );
if (k > 1) *cur++ = scale * ((*in >> 6) & 0x01);
if (k > 2) *cur++ = scale * ((*in >> 5) & 0x01);
if (k > 3) *cur++ = scale * ((*in >> 4) & 0x01);
if (k > 4) *cur++ = scale * ((*in >> 3) & 0x01);
if (k > 5) *cur++ = scale * ((*in >> 2) & 0x01);
if (k > 6) *cur++ = scale * ((*in >> 1) & 0x01);
} }
if (img_n != out_n) {
int q; // insert alpha=255 values if desired
// insert alpha = 255 if (img_n != out_n)
cur = a->out + stride*j; stbi__create_png_alpha_expand8(dest, dest, x, img_n);
} else if (depth == 8) {
if (img_n == out_n)
memcpy(dest, cur, x*img_n);
else
stbi__create_png_alpha_expand8(dest, cur, x, img_n);
} else if (depth == 16) {
// convert the image data from big-endian to platform-native
stbi__uint16 *dest16 = (stbi__uint16*)dest;
stbi__uint32 nsmp = x*img_n;
if (img_n == out_n) {
for (i = 0; i < nsmp; ++i, ++dest16, cur += 2)
*dest16 = (cur[0] << 8) | cur[1];
} else {
STBI_ASSERT(img_n+1 == out_n);
if (img_n == 1) { if (img_n == 1) {
for (q=x-1; q >= 0; --q) { for (i = 0; i < x; ++i, dest16 += 2, cur += 2) {
cur[q*2+1] = 255; dest16[0] = (cur[0] << 8) | cur[1];
cur[q*2+0] = cur[q]; dest16[1] = 0xffff;
} }
} else { } else {
STBI_ASSERT(img_n == 3); STBI_ASSERT(img_n == 3);
for (q=x-1; q >= 0; --q) { for (i = 0; i < x; ++i, dest16 += 4, cur += 6) {
cur[q*4+3] = 255; dest16[0] = (cur[0] << 8) | cur[1];
cur[q*4+2] = cur[q*3+2]; dest16[1] = (cur[2] << 8) | cur[3];
cur[q*4+1] = cur[q*3+1]; dest16[2] = (cur[4] << 8) | cur[5];
cur[q*4+0] = cur[q*3+0]; dest16[3] = 0xffff;
} }
} }
} }
} }
} else if (depth == 16) {
// force the image data from big-endian to platform-native.
// this is done in a separate pass due to the decoding relying
// on the data being untouched, but could probably be done
// per-line during decode if care is taken.
stbi_uc *cur = a->out;
stbi__uint16 *cur16 = (stbi__uint16*)cur;
for(i=0; i < x*y*out_n; ++i,cur16++,cur+=2) {
*cur16 = (cur[0] << 8) | cur[1];
}
} }
STBI_FREE(filter_buf);
if (!all_ok) return 0;
return 1; return 1;
} }

View File

@ -162,6 +162,17 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
void setPlayingOffset(Time timeOffset); void setPlayingOffset(Time timeOffset);
////////////////////////////////////////////////////////////
/// \brief Set the effect processor to be applied to the sound
///
/// The effect processor is a callable that will be called
/// with sound data to be processed.
///
/// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing
///
////////////////////////////////////////////////////////////
void setEffectProcessor(EffectProcessor effectProcessor) override;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Get the audio buffer attached to the sound /// \brief Get the audio buffer attached to the sound
/// ///

View File

@ -63,7 +63,7 @@ public:
/// This function uses its own thread so that it doesn't block /// This function uses its own thread so that it doesn't block
/// the rest of the program while the capture runs. /// the rest of the program while the capture runs.
/// Please note that only one capture can happen at the same time. /// Please note that only one capture can happen at the same time.
/// You can select which capture device will be used, by passing /// You can select which capture device will be used by passing
/// the name to the setDevice() method. If none was selected /// the name to the setDevice() method. If none was selected
/// before, the default capture device will be used. You can get a /// before, the default capture device will be used. You can get a
/// list of the names of all available capture devices by calling /// list of the names of all available capture devices by calling

View File

@ -32,6 +32,8 @@
#include <SFML/System/Angle.hpp> #include <SFML/System/Angle.hpp>
#include <SFML/System/Vector3.hpp> #include <SFML/System/Vector3.hpp>
#include <functional>
namespace sf namespace sf
{ {
@ -72,6 +74,76 @@ public:
float outerGain{}; //!< Outer gain float outerGain{}; //!< Outer gain
}; };
////////////////////////////////////////////////////////////
/// \brief Callable that is provided with sound data for processing
///
/// When the audio engine sources sound data from sound
/// sources it will pass the data through an effects
/// processor if one is set. The sound data will already be
/// converted to the internal floating point format.
///
/// Sound data that is processed this way is provided in
/// frames. Each frame contains 1 floating point sample per
/// channel. If e.g. the data source provides stereo data,
/// each frame will contain 2 floats.
///
/// The effects processor function takes 4 parameters:
/// - The input data frames, channels interleaved
/// - The number of input data frames available
/// - The buffer to write output data frames to, channels interleaved
/// - The number of output data frames that the output buffer can hold
/// - The channel count
///
/// The input and output frame counts are in/out parameters.
///
/// When this function is called, the input count will
/// contain the number of frames available in the input
/// buffer. The output count will contain the size of the
/// output buffer i.e. the maximum number of frames that
/// can be written to the output buffer.
///
/// Attempting to read more frames than the input frame
/// count or write more frames than the output frame count
/// will result in undefined behaviour.
///
/// When done processing the frames, the input and output
/// frame counts must be updated to reflect the actual
/// number of frames that were read from the input and
/// written to the output.
///
/// The processing function should always try to process as
/// much sound data as possible i.e. always try to fill the
/// output buffer to the maximum. In certain situations for
/// specific effects it can be possible that the input frame
/// count and output frame count aren't equal. As long as
/// the frame counts are updated accordingly this is
/// perfectly valid.
///
/// If the audio engine determines that no audio data is
/// available from the data source, the input data frames
/// pointer is set to nullptr and the input frame count is
/// set to 0. In this case it is up to the function to
/// decide how to handle the situation. For specific effects
/// e.g. Echo/Delay buffered data might still be able to be
/// written to the output buffer even if there is no longer
/// any input data.
///
/// An important thing to remember is that this function is
/// directly called by the audio engine. Because the audio
/// engine runs on an internal thread of its own, make sure
/// access to shared data is synchronized appropriately.
///
/// Because this function is stored by the SoundSource
/// object it will be able to be called as long as the
/// SoundSource object hasn't yet been destroyed. Make sure
/// that any data this function references outlives the
/// SoundSource object otherwise use-after-free errors will
/// occur.
///
////////////////////////////////////////////////////////////
using EffectProcessor = std::function<
void(const float* inputFrames, unsigned int& inputFrameCount, float* outputFrames, unsigned int& outputFrameCount, unsigned int frameChannelCount)>;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Copy constructor /// \brief Copy constructor
/// ///
@ -329,6 +401,17 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
void setAttenuation(float attenuation); void setAttenuation(float attenuation);
////////////////////////////////////////////////////////////
/// \brief Set the effect processor to be applied to the sound
///
/// The effect processor is a callable that will be called
/// with sound data to be processed.
///
/// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing
///
////////////////////////////////////////////////////////////
virtual void setEffectProcessor(EffectProcessor effectProcessor);
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Get the pitch of the sound /// \brief Get the pitch of the sound
/// ///

View File

@ -194,6 +194,17 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
bool getLoop() const; bool getLoop() const;
////////////////////////////////////////////////////////////
/// \brief Set the effect processor to be applied to the sound
///
/// The effect processor is a callable that will be called
/// with sound data to be processed.
///
/// \param effectProcessor The effect processor to attach to this sound, attach an empty processor to disable processing
///
////////////////////////////////////////////////////////////
void setEffectProcessor(EffectProcessor effectProcessor) override;
protected: protected:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Default constructor /// \brief Default constructor

View File

@ -100,11 +100,15 @@ public:
/// Before calling this function, the render-texture is in /// Before calling this function, the render-texture is in
/// an invalid state, thus it is mandatory to call it before /// an invalid state, thus it is mandatory to call it before
/// doing anything with the render-texture. /// doing anything with the render-texture.
///
/// The last parameter, \a settings, is useful if you want to enable /// The last parameter, \a settings, is useful if you want to enable
/// multi-sampling or use the render-texture for OpenGL rendering that /// multi-sampling or use the render-texture for OpenGL rendering that
/// requires a depth or stencil buffer. Otherwise it is unnecessary, and /// requires a depth or stencil buffer. Otherwise it is unnecessary, and
/// you should leave this parameter at its default value. /// you should leave this parameter at its default value.
/// ///
/// After creation, the contents of the render-texture are undefined.
/// Call `RenderTexture::clear` first to ensure a single color fill.
///
/// \param size Width and height of the render-texture /// \param size Width and height of the render-texture
/// \param settings Additional settings for the underlying OpenGL texture and context /// \param settings Additional settings for the underlying OpenGL texture and context
/// ///

View File

@ -261,7 +261,7 @@ public:
/// the \a area rectangle, and to contain 32-bits RGBA pixels. /// the \a area rectangle, and to contain 32-bits RGBA pixels.
/// ///
/// No additional check is performed on the size of the pixel /// No additional check is performed on the size of the pixel
/// array, passing invalid arguments will lead to an undefined /// array. Passing invalid arguments will lead to an undefined
/// behavior. /// behavior.
/// ///
/// This function does nothing if \a pixels is null or if the /// This function does nothing if \a pixels is null or if the
@ -279,7 +279,7 @@ public:
/// \a height arguments, and it must contain 32-bits RGBA pixels. /// \a height arguments, and it must contain 32-bits RGBA pixels.
/// ///
/// No additional check is performed on the size of the pixel /// No additional check is performed on the size of the pixel
/// array or the bounds of the area to update, passing invalid /// array or the bounds of the area to update. Passing invalid
/// arguments will lead to an undefined behavior. /// arguments will lead to an undefined behavior.
/// ///
/// This function does nothing if \a pixels is null or if the /// This function does nothing if \a pixels is null or if the
@ -297,11 +297,12 @@ public:
/// ///
/// Although the source texture can be smaller than this texture, /// Although the source texture can be smaller than this texture,
/// this function is usually used for updating the whole texture. /// this function is usually used for updating the whole texture.
/// The other overload, which has (x, y) additional arguments, /// The other overload, which has an additional destination
/// is more convenient for updating a sub-area of this texture. /// argument, is more convenient for updating a sub-area of this
/// texture.
/// ///
/// No additional check is performed on the size of the passed /// No additional check is performed on the size of the passed
/// texture, passing a texture bigger than this texture /// texture. Passing a texture bigger than this texture
/// will lead to an undefined behavior. /// will lead to an undefined behavior.
/// ///
/// This function does nothing if either texture was not /// This function does nothing if either texture was not
@ -315,8 +316,8 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Update a part of this texture from another texture /// \brief Update a part of this texture from another texture
/// ///
/// No additional check is performed on the size of the texture, /// No additional check is performed on the size of the texture.
/// passing an invalid combination of texture size and destination /// Passing an invalid combination of texture size and destination
/// will lead to an undefined behavior. /// will lead to an undefined behavior.
/// ///
/// This function does nothing if either texture was not /// This function does nothing if either texture was not
@ -333,11 +334,12 @@ public:
/// ///
/// Although the source image can be smaller than the texture, /// Although the source image can be smaller than the texture,
/// this function is usually used for updating the whole texture. /// this function is usually used for updating the whole texture.
/// The other overload, which has (x, y) additional arguments, /// The other overload, which has an additional destination
/// is more convenient for updating a sub-area of the texture. /// argument, is more convenient for updating a sub-area of the
/// texture.
/// ///
/// No additional check is performed on the size of the image, /// No additional check is performed on the size of the image.
/// passing an image bigger than the texture will lead to an /// Passing an image bigger than the texture will lead to an
/// undefined behavior. /// undefined behavior.
/// ///
/// This function does nothing if the texture was not /// This function does nothing if the texture was not
@ -351,8 +353,8 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Update a part of the texture from an image /// \brief Update a part of the texture from an image
/// ///
/// No additional check is performed on the size of the image, /// No additional check is performed on the size of the image.
/// passing an invalid combination of image size and destination /// Passing an invalid combination of image size and destination
/// will lead to an undefined behavior. /// will lead to an undefined behavior.
/// ///
/// This function does nothing if the texture was not /// This function does nothing if the texture was not
@ -369,11 +371,12 @@ public:
/// ///
/// Although the source window can be smaller than the texture, /// Although the source window can be smaller than the texture,
/// this function is usually used for updating the whole texture. /// this function is usually used for updating the whole texture.
/// The other overload, which has (x, y) additional arguments, /// The other overload, which has an additional destination
/// is more convenient for updating a sub-area of the texture. /// argument, is more convenient for updating a sub-area of the
/// texture.
/// ///
/// No additional check is performed on the size of the window, /// No additional check is performed on the size of the window.
/// passing a window bigger than the texture will lead to an /// Passing a window bigger than the texture will lead to an
/// undefined behavior. /// undefined behavior.
/// ///
/// This function does nothing if either the texture or the window /// This function does nothing if either the texture or the window
@ -387,8 +390,8 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Update a part of the texture from the contents of a window /// \brief Update a part of the texture from the contents of a window
/// ///
/// No additional check is performed on the size of the window, /// No additional check is performed on the size of the window.
/// passing an invalid combination of window size and destination /// Passing an invalid combination of window size and destination
/// will lead to an undefined behavior. /// will lead to an undefined behavior.
/// ///
/// This function does nothing if either the texture or the window /// This function does nothing if either the texture or the window

View File

@ -33,6 +33,8 @@
#include <SFML/System/Vector2.hpp> #include <SFML/System/Vector2.hpp>
#include <array>
namespace sf namespace sf
{ {
@ -267,10 +269,10 @@ private:
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// clang-format off // clang-format off
float m_matrix[16]{1.f, 0.f, 0.f, 0.f, std::array<float, 16> m_matrix{1.f, 0.f, 0.f, 0.f,
0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f,
0.f, 0.f, 0.f, 1.f}; //!< 4x4 matrix defining the transformation 0.f, 0.f, 0.f, 1.f}; //!< 4x4 matrix defining the transformation
// clang-format off // clang-format off
}; };

View File

@ -55,7 +55,7 @@ constexpr Transform::Transform(float a00, float a01, float a02,
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr const float* Transform::getMatrix() const constexpr const float* Transform::getMatrix() const
{ {
return m_matrix; return m_matrix.data();
} }
@ -133,8 +133,8 @@ constexpr FloatRect Transform::transformRect(const FloatRect& rectangle) const
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Transform& Transform::combine(const Transform& transform) constexpr Transform& Transform::combine(const Transform& transform)
{ {
const float* a = m_matrix; const auto& a = m_matrix;
const float* b = transform.m_matrix; const auto& b = transform.m_matrix;
// clang-format off // clang-format off
*this = Transform(a[0] * b[0] + a[4] * b[1] + a[12] * b[3], *this = Transform(a[0] * b[0] + a[4] * b[1] + a[12] * b[3],

View File

@ -154,7 +154,7 @@ public:
/// the \a created buffer. /// the \a created buffer.
/// ///
/// No additional check is performed on the size of the vertex /// No additional check is performed on the size of the vertex
/// array, passing invalid arguments will lead to undefined /// array. Passing invalid arguments will lead to undefined
/// behavior. /// behavior.
/// ///
/// This function does nothing if \a vertices is null or if the /// This function does nothing if \a vertices is null or if the
@ -188,7 +188,7 @@ public:
/// than the size of the currently created buffer, the update fails. /// than the size of the currently created buffer, the update fails.
/// ///
/// No additional check is performed on the size of the vertex /// No additional check is performed on the size of the vertex
/// array, passing invalid arguments will lead to undefined /// array. Passing invalid arguments will lead to undefined
/// behavior. /// behavior.
/// ///
/// \param vertices Array of vertices to copy to the buffer /// \param vertices Array of vertices to copy to the buffer
@ -386,7 +386,7 @@ SFML_GRAPHICS_API void swap(VertexBuffer& left, VertexBuffer& right) noexcept;
/// Simultaneous updates to the vertex buffer are not guaranteed to be /// Simultaneous updates to the vertex buffer are not guaranteed to be
/// carried out by the driver in any specific order. Updating the same /// carried out by the driver in any specific order. Updating the same
/// region of the buffer from multiple threads will not cause undefined /// region of the buffer from multiple threads will not cause undefined
/// behaviour, however the final state of the buffer will be unpredictable. /// behavior, however the final state of the buffer will be unpredictable.
/// ///
/// Simultaneous updates of distinct non-overlapping regions of the buffer /// Simultaneous updates of distinct non-overlapping regions of the buffer
/// are also not guaranteed to complete in a specific order. However, in /// are also not guaranteed to complete in a specific order. However, in

View File

@ -144,20 +144,20 @@ private:
friend constexpr Angle radians(float angle); friend constexpr Angle radians(float angle);
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Construct from a number of degrees /// \brief Construct from a number of radians
/// ///
/// This function is internal. To construct angle values, /// This function is internal. To construct angle values,
/// use sf::radians or sf::degrees instead. /// use sf::radians or sf::degrees instead.
/// ///
/// \param degrees Angle in degrees /// \param radians Angle in radians
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr explicit Angle(float degrees); constexpr explicit Angle(float radians);
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
float m_degrees{}; //!< Angle value stored as degrees float m_radians{}; //!< Angle value stored as radians
}; };
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@ -187,6 +187,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of == operator to compare two angle values /// \brief Overload of == operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)
@ -199,6 +200,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of != operator to compare two angle values /// \brief Overload of != operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)
@ -211,6 +213,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of < operator to compare two angle values /// \brief Overload of < operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)
@ -223,6 +226,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of > operator to compare two angle values /// \brief Overload of > operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)
@ -235,6 +239,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of <= operator to compare two angle values /// \brief Overload of <= operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)
@ -247,6 +252,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \relates Angle /// \relates Angle
/// \brief Overload of >= operator to compare two angle values /// \brief Overload of >= operator to compare two angle values
/// \note Does not automatically wrap the angle value
/// ///
/// \param left Left operand (an angle) /// \param left Left operand (an angle)
/// \param right Right operand (an angle) /// \param right Right operand (an angle)

View File

@ -34,16 +34,14 @@ namespace sf
{ {
namespace priv namespace priv
{ {
constexpr float pi = 3.141592654f; constexpr float pi = 3.141592654f;
constexpr float tau = pi * 2.f;
constexpr float positiveRemainder(float a, float b) constexpr float positiveRemainder(float a, float b)
{ {
assert(b > 0.0f && "Cannot calculate remainder with non-positive divisor"); assert(b > 0.f && "Cannot calculate remainder with non-positive divisor");
const float val = a - static_cast<float>(static_cast<int>(a / b)) * b; const float val = a - static_cast<float>(static_cast<int>(a / b)) * b;
if (val >= 0.f) return val >= 0.f ? val : val + b;
return val;
else
return val + b;
} }
} // namespace priv } // namespace priv
@ -55,33 +53,33 @@ constexpr Angle::Angle() = default;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr float Angle::asDegrees() const constexpr float Angle::asDegrees() const
{ {
return m_degrees; return m_radians * (180.f / priv::pi);
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr float Angle::asRadians() const constexpr float Angle::asRadians() const
{ {
return m_degrees * (priv::pi / 180); return m_radians;
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle Angle::wrapSigned() const constexpr Angle Angle::wrapSigned() const
{ {
return degrees(priv::positiveRemainder(m_degrees + 180, 360) - 180); return radians(priv::positiveRemainder(m_radians + priv::pi, priv::tau) - priv::pi);
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle Angle::wrapUnsigned() const constexpr Angle Angle::wrapUnsigned() const
{ {
return degrees(priv::positiveRemainder(m_degrees, 360)); return radians(priv::positiveRemainder(m_radians, priv::tau));
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle::Angle(float degrees) : m_degrees(degrees) constexpr Angle::Angle(float radians) : m_radians(radians)
{ {
} }
@ -89,70 +87,70 @@ constexpr Angle::Angle(float degrees) : m_degrees(degrees)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle degrees(float angle) constexpr Angle degrees(float angle)
{ {
return Angle(angle); return Angle(angle * (priv::pi / 180.f));
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle radians(float angle) constexpr Angle radians(float angle)
{ {
return Angle(angle * (180 / priv::pi)); return Angle(angle);
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator==(Angle left, Angle right) constexpr bool operator==(Angle left, Angle right)
{ {
return left.asDegrees() == right.asDegrees(); return left.asRadians() == right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator!=(Angle left, Angle right) constexpr bool operator!=(Angle left, Angle right)
{ {
return left.asDegrees() != right.asDegrees(); return left.asRadians() != right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator<(Angle left, Angle right) constexpr bool operator<(Angle left, Angle right)
{ {
return left.asDegrees() < right.asDegrees(); return left.asRadians() < right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator>(Angle left, Angle right) constexpr bool operator>(Angle left, Angle right)
{ {
return left.asDegrees() > right.asDegrees(); return left.asRadians() > right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator<=(Angle left, Angle right) constexpr bool operator<=(Angle left, Angle right)
{ {
return left.asDegrees() <= right.asDegrees(); return left.asRadians() <= right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr bool operator>=(Angle left, Angle right) constexpr bool operator>=(Angle left, Angle right)
{ {
return left.asDegrees() >= right.asDegrees(); return left.asRadians() >= right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator-(Angle right) constexpr Angle operator-(Angle right)
{ {
return degrees(-right.asDegrees()); return radians(-right.asRadians());
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator+(Angle left, Angle right) constexpr Angle operator+(Angle left, Angle right)
{ {
return degrees(left.asDegrees() + right.asDegrees()); return radians(left.asRadians() + right.asRadians());
} }
@ -166,7 +164,7 @@ constexpr Angle& operator+=(Angle& left, Angle right)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator-(Angle left, Angle right) constexpr Angle operator-(Angle left, Angle right)
{ {
return degrees(left.asDegrees() - right.asDegrees()); return radians(left.asRadians() - right.asRadians());
} }
@ -180,7 +178,7 @@ constexpr Angle& operator-=(Angle& left, Angle right)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator*(Angle left, float right) constexpr Angle operator*(Angle left, float right)
{ {
return degrees(left.asDegrees() * right); return radians(left.asRadians() * right);
} }
@ -201,15 +199,15 @@ constexpr Angle& operator*=(Angle& left, float right)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator/(Angle left, float right) constexpr Angle operator/(Angle left, float right)
{ {
assert(right != 0 && "Angle::operator/ cannot divide by 0"); assert(right != 0.f && "Angle::operator/ cannot divide by 0");
return degrees(left.asDegrees() / right); return radians(left.asRadians() / right);
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle& operator/=(Angle& left, float right) constexpr Angle& operator/=(Angle& left, float right)
{ {
assert(right != 0 && "Angle::operator/= cannot divide by 0"); assert(right != 0.f && "Angle::operator/= cannot divide by 0");
return left = left / right; return left = left / right;
} }
@ -217,23 +215,23 @@ constexpr Angle& operator/=(Angle& left, float right)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr float operator/(Angle left, Angle right) constexpr float operator/(Angle left, Angle right)
{ {
assert(right.asDegrees() != 0 && "Angle::operator/ cannot divide by 0"); assert(right.asRadians() != 0.f && "Angle::operator/ cannot divide by 0");
return left.asDegrees() / right.asDegrees(); return left.asRadians() / right.asRadians();
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle operator%(Angle left, Angle right) constexpr Angle operator%(Angle left, Angle right)
{ {
assert(right.asDegrees() != 0 && "Angle::operator% cannot modulus by 0"); assert(right.asRadians() != 0.f && "Angle::operator% cannot modulus by 0");
return degrees(priv::positiveRemainder(left.asDegrees(), right.asDegrees())); return radians(priv::positiveRemainder(left.asRadians(), right.asRadians()));
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
constexpr Angle& operator%=(Angle& left, Angle right) constexpr Angle& operator%=(Angle& left, Angle right)
{ {
assert(right.asDegrees() != 0 && "Angle::operator%= cannot modulus by 0"); assert(right.asRadians() != 0.f && "Angle::operator%= cannot modulus by 0");
return left = left % right; return left = left % right;
} }

View File

@ -147,8 +147,8 @@ private:
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#ifdef SFML_SYSTEM_ANDROID #ifdef SFML_SYSTEM_ANDROID
std::unique_ptr<priv::ResourceStream> m_file; std::unique_ptr<priv::ResourceStream> m_androidFile;
#else #endif
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Deleter for stdio file stream that closes the file stream /// \brief Deleter for stdio file stream that closes the file stream
/// ///
@ -159,7 +159,6 @@ private:
}; };
std::unique_ptr<std::FILE, FileCloser> m_file; //!< stdio file stream std::unique_ptr<std::FILE, FileCloser> m_file; //!< stdio file stream
#endif
}; };
} // namespace sf } // namespace sf

View File

@ -29,6 +29,7 @@
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#include <SFML/Config.hpp> #include <SFML/Config.hpp>
#include <array>
#include <locale> #include <locale>
#include <cstdint> #include <cstdint>

View File

@ -56,7 +56,7 @@ In Utf<8>::decode(In begin, In end, std::uint32_t& output, std::uint32_t replace
{ {
// clang-format off // clang-format off
// Some useful precomputed data // Some useful precomputed data
static constexpr int trailing[256] = static constexpr std::array<std::uint8_t, 256> trailing =
{ {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@ -68,14 +68,14 @@ In Utf<8>::decode(In begin, In end, std::uint32_t& output, std::uint32_t replace
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5
}; };
static constexpr std::uint32_t offsets[6] = static constexpr std::array<std::uint32_t, 6> offsets =
{ {
0x00000000, 0x00003080, 0x000E2080, 0x03C82080, 0xFA082080, 0x82082080 0x00000000, 0x00003080, 0x000E2080, 0x03C82080, 0xFA082080, 0x82082080
}; };
// clang-format on // clang-format on
// decode the character // decode the character
const int trailingBytes = trailing[static_cast<std::uint8_t>(*begin)]; const auto trailingBytes = trailing[static_cast<std::uint8_t>(*begin)];
if (trailingBytes < std::distance(begin, end)) if (trailingBytes < std::distance(begin, end))
{ {
output = 0; output = 0;
@ -110,7 +110,7 @@ template <typename Out>
Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement) Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement)
{ {
// Some useful precomputed data // Some useful precomputed data
static constexpr std::uint8_t firstBytes[7] = {0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC}; static constexpr std::array<std::uint8_t, 7> firstBytes = {0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC};
// encode the character // encode the character
if ((input > 0x0010FFFF) || ((input >= 0xD800) && (input <= 0xDBFF))) if ((input > 0x0010FFFF) || ((input >= 0xD800) && (input <= 0xDBFF)))
@ -134,7 +134,7 @@ Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement)
// clang-format on // clang-format on
// Extract the bytes to write // Extract the bytes to write
std::byte bytes[4]; std::array<std::byte, 4> bytes{};
// clang-format off // clang-format off
switch (bytestoWrite) switch (bytestoWrite)
@ -147,7 +147,7 @@ Out Utf<8>::encode(std::uint32_t input, Out output, std::uint8_t replacement)
// clang-format on // clang-format on
// Add them to the output // Add them to the output
output = priv::copy(bytes, bytes + bytestoWrite, output); output = priv::copy(bytes.data(), bytes.data() + bytestoWrite, output);
} }
return output; return output;

View File

@ -240,7 +240,7 @@ private:
/// with either loadFromPixels() or loadFromSystem(), the /// with either loadFromPixels() or loadFromSystem(), the
/// cursor can be changed with sf::WindowBase::setMouseCursor(). /// cursor can be changed with sf::WindowBase::setMouseCursor().
/// ///
/// The behaviour is undefined if the cursor is destroyed while /// The behavior is undefined if the cursor is destroyed while
/// in use by the window. /// in use by the window.
/// ///
/// Usage example: /// Usage example:

View File

@ -329,10 +329,10 @@ private:
static constexpr bool isEventType = isInParameterPack<T>(decltype(m_data)()); static constexpr bool isEventType = isInParameterPack<T>(decltype(m_data)());
}; };
#include <SFML/Window/Event.inl>
} // namespace sf } // namespace sf
#include <SFML/Window/Event.inl>
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \class sf::Event /// \class sf::Event

View File

@ -30,6 +30,14 @@
// to compile the code within the compiletime conditional when // to compile the code within the compiletime conditional when
// an incorrect template parameter is provided. // an incorrect template parameter is provided.
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Window/Event.hpp> // NOLINT(misc-header-include-cycle)
namespace sf
{
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
template <typename T> template <typename T>
Event::Event(const T& t) Event::Event(const T& t)
@ -58,3 +66,5 @@ const T* Event::getIf() const
if constexpr (isEventType<T>) if constexpr (isEventType<T>)
return std::get_if<T>(&m_data); return std::get_if<T>(&m_data);
} }
} // namespace sf

View File

@ -44,11 +44,11 @@ namespace sf::Sensor
enum class Type enum class Type
{ {
Accelerometer, //!< Measures the raw acceleration (m/s^2) Accelerometer, //!< Measures the raw acceleration (m/s^2)
Gyroscope, //!< Measures the raw rotation rates (degrees/s) Gyroscope, //!< Measures the raw rotation rates (radians/s)
Magnetometer, //!< Measures the ambient magnetic field (micro-teslas) Magnetometer, //!< Measures the ambient magnetic field (micro-teslas)
Gravity, //!< Measures the direction and intensity of gravity, independent of device acceleration (m/s^2) Gravity, //!< Measures the direction and intensity of gravity, independent of device acceleration (m/s^2)
UserAcceleration, //!< Measures the direction and intensity of device acceleration, independent of the gravity (m/s^2) UserAcceleration, //!< Measures the direction and intensity of device acceleration, independent of the gravity (m/s^2)
Orientation //!< Measures the absolute 3D orientation (degrees) Orientation //!< Measures the absolute 3D orientation (radians)
}; };
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)

View File

@ -30,8 +30,12 @@
#include <SFML/System/Err.hpp> #include <SFML/System/Err.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <ostream> #include <ostream>
#include <cassert>
#include <cstdlib>
namespace sf::priv namespace sf::priv
{ {
@ -64,33 +68,52 @@ AudioDevice::AudioDevice()
// Create the context // Create the context
m_context.emplace(); m_context.emplace();
auto contextConfig = ma_context_config_init(); auto contextConfig = ma_context_config_init();
contextConfig.pLog = &*m_log; contextConfig.pLog = &*m_log;
ma_uint32 deviceCount = 0;
const auto nullBackend = ma_backend_null;
const std::array<const ma_backend*, 2> backendLists{nullptr, &nullBackend};
if (const auto result = ma_context_init(nullptr, 0, &contextConfig, &*m_context); result != MA_SUCCESS) for (const auto* backendList : backendLists)
{ {
m_context.reset(); // We can set backendCount to 1 since it is ignored when backends is set to nullptr
err() << "Failed to initialize the audio context: " << ma_result_description(result) << std::endl; if (const auto result = ma_context_init(backendList, 1, &contextConfig, &*m_context); result != MA_SUCCESS)
return; {
m_context.reset();
err() << "Failed to initialize the audio playback context: " << ma_result_description(result) << std::endl;
return;
}
// Count the playback devices
if (const auto result = ma_context_get_devices(&*m_context, nullptr, &deviceCount, nullptr, nullptr);
result != MA_SUCCESS)
{
err() << "Failed to get audio playback devices: " << ma_result_description(result) << std::endl;
return;
}
// Check if there are audio playback devices available on the system
if (deviceCount > 0)
break;
// Warn if no devices were found using the default backend list
if (backendList == nullptr)
err() << "No audio playback devices available on the system" << std::endl;
// Clean up the context if we didn't find any devices
ma_context_uninit(&*m_context);
} }
// Count the playback devices // If the NULL audio backend also doesn't provide a device we give up
ma_uint32 deviceCount = 0;
if (const auto result = ma_context_get_devices(&*m_context, nullptr, &deviceCount, nullptr, nullptr);
result != MA_SUCCESS)
{
err() << "Failed to get audio playback devices: " << ma_result_description(result) << std::endl;
return;
}
// Check if there are audio playback devices available on the system
if (deviceCount == 0) if (deviceCount == 0)
{ {
err() << "No audio playback devices available on the system" << std::endl; m_context.reset();
return; return;
} }
if (m_context->backend == ma_backend_null)
err() << "Using NULL audio backend for playback" << std::endl;
// Create the playback device // Create the playback device
m_playbackDevice.emplace(); m_playbackDevice.emplace();
@ -159,11 +182,16 @@ AudioDevice::AudioDevice()
m_listenerProperties.upVector.x, m_listenerProperties.upVector.x,
m_listenerProperties.upVector.y, m_listenerProperties.upVector.y,
m_listenerProperties.upVector.z); m_listenerProperties.upVector.z);
// Setup cleanup function with `atexit` to work around destruction order issue with
// miniaudio internal audio processing threads on Windows
const int rc = std::atexit([] { AudioDevice::get().cleanup(); });
assert(rc == 0);
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
AudioDevice::~AudioDevice() void AudioDevice::cleanup()
{ {
// Destroy the engine // Destroy the engine
if (m_engine) if (m_engine)
@ -286,8 +314,8 @@ void AudioDevice::setCone(const Listener::Cone& cone)
ma_engine_listener_set_cone(&*m_engine, ma_engine_listener_set_cone(&*m_engine,
0, 0,
std::clamp(cone.innerAngle.asRadians(), 0.f, sf::degrees(360).asRadians()), std::clamp(cone.innerAngle, Angle::Zero, degrees(360.f)).asRadians(),
std::clamp(cone.outerAngle.asRadians(), 0.f, sf::degrees(360).asRadians()), std::clamp(cone.outerAngle, Angle::Zero, degrees(360.f)).asRadians(),
cone.outerGain); cone.outerGain);
} }

View File

@ -54,10 +54,10 @@ private:
AudioDevice(); AudioDevice();
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Destructor /// \brief Cleanup function, called via `atexit`
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
~AudioDevice(); void cleanup();
public: public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@ -227,11 +227,11 @@ private:
struct ListenerProperties struct ListenerProperties
{ {
float volume{100.f}; float volume{100.f};
sf::Vector3f position{0, 0, 0}; Vector3f position{0, 0, 0};
sf::Vector3f direction{0, 0, -1}; Vector3f direction{0, 0, -1};
sf::Vector3f velocity{0, 0, 0}; Vector3f velocity{0, 0, 0};
Listener::Cone cone{sf::degrees(360), sf::degrees(360), 1}; Listener::Cone cone{degrees(360.f), degrees(360.f), 1};
sf::Vector3f upVector{0, 1, 0}; Vector3f upVector{0, 1, 0};
}; };
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////

View File

@ -82,7 +82,7 @@ sfml_add_library(Audio
target_compile_definitions(sfml-audio PRIVATE OV_EXCLUDE_STATIC_CALLBACKS FLAC__NO_DLL) target_compile_definitions(sfml-audio PRIVATE OV_EXCLUDE_STATIC_CALLBACKS FLAC__NO_DLL)
# disable miniaudio features we do not use # disable miniaudio features we do not use
target_compile_definitions(sfml-audio PRIVATE MA_NO_DECODING MA_NO_ENCODING MA_NO_RESOURCE_MANAGER MA_NO_GENERATION) target_compile_definitions(sfml-audio PRIVATE MA_NO_MP3 MA_NO_FLAC MA_NO_ENCODING MA_NO_RESOURCE_MANAGER MA_NO_GENERATION)
# setup dependencies # setup dependencies
target_link_libraries(sfml-audio target_link_libraries(sfml-audio

View File

@ -63,8 +63,8 @@ struct SavedSettings
float minGain{0.f}; float minGain{0.f};
float maxGain{1.f}; float maxGain{1.f};
float rollOff{1.f}; float rollOff{1.f};
float innerAngle{sf::degrees(360).asRadians()}; float innerAngle{degrees(360.f).asRadians()};
float outerAngle{sf::degrees(360).asRadians()}; float outerAngle{degrees(360.f).asRadians()};
float outerGain{0.f}; float outerGain{0.f};
}; };
@ -135,7 +135,7 @@ void initializeDataSource(ma_data_source_base& dataSourceBase, const ma_data_sou
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(sf::SoundChannel soundChannel) ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(SoundChannel soundChannel)
{ {
switch (soundChannel) switch (soundChannel)
{ {
@ -184,6 +184,56 @@ ma_channel MiniaudioUtils::soundChannelToMiniaudioChannel(sf::SoundChannel sound
} }
////////////////////////////////////////////////////////////
SoundChannel MiniaudioUtils::miniaudioChannelToSoundChannel(ma_channel soundChannel)
{
switch (soundChannel)
{
case MA_CHANNEL_NONE:
return SoundChannel::Unspecified;
case MA_CHANNEL_MONO:
return SoundChannel::Mono;
case MA_CHANNEL_FRONT_LEFT:
return SoundChannel::FrontLeft;
case MA_CHANNEL_FRONT_RIGHT:
return SoundChannel::FrontRight;
case MA_CHANNEL_FRONT_CENTER:
return SoundChannel::FrontCenter;
case MA_CHANNEL_FRONT_LEFT_CENTER:
return SoundChannel::FrontLeftOfCenter;
case MA_CHANNEL_FRONT_RIGHT_CENTER:
return SoundChannel::FrontRightOfCenter;
case MA_CHANNEL_LFE:
return SoundChannel::LowFrequencyEffects;
case MA_CHANNEL_BACK_LEFT:
return SoundChannel::BackLeft;
case MA_CHANNEL_BACK_RIGHT:
return SoundChannel::BackRight;
case MA_CHANNEL_BACK_CENTER:
return SoundChannel::BackCenter;
case MA_CHANNEL_SIDE_LEFT:
return SoundChannel::SideLeft;
case MA_CHANNEL_SIDE_RIGHT:
return SoundChannel::SideRight;
case MA_CHANNEL_TOP_CENTER:
return SoundChannel::TopCenter;
case MA_CHANNEL_TOP_FRONT_LEFT:
return SoundChannel::TopFrontLeft;
case MA_CHANNEL_TOP_FRONT_RIGHT:
return SoundChannel::TopFrontRight;
case MA_CHANNEL_TOP_FRONT_CENTER:
return SoundChannel::TopFrontCenter;
case MA_CHANNEL_TOP_BACK_LEFT:
return SoundChannel::TopBackLeft;
case MA_CHANNEL_TOP_BACK_RIGHT:
return SoundChannel::TopBackRight;
default:
assert(soundChannel == MA_CHANNEL_TOP_BACK_CENTER);
return SoundChannel::TopBackCenter;
}
}
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Time MiniaudioUtils::getPlayingOffset(ma_sound& sound) Time MiniaudioUtils::getPlayingOffset(ma_sound& sound)
{ {

View File

@ -41,17 +41,18 @@
namespace sf namespace sf
{ {
class Time; class Time;
}
namespace sf::priv::MiniaudioUtils namespace priv::MiniaudioUtils
{ {
[[nodiscard]] ma_channel soundChannelToMiniaudioChannel(sf::SoundChannel soundChannel); [[nodiscard]] ma_channel soundChannelToMiniaudioChannel(SoundChannel soundChannel);
[[nodiscard]] Time getPlayingOffset(ma_sound& sound); [[nodiscard]] SoundChannel miniaudioChannelToSoundChannel(ma_channel soundChannel);
[[nodiscard]] ma_uint64 getFrameIndex(ma_sound& sound, Time timeOffset); [[nodiscard]] Time getPlayingOffset(ma_sound& sound);
[[nodiscard]] ma_uint64 getFrameIndex(ma_sound& sound, Time timeOffset);
void reinitializeSound(ma_sound& sound, const std::function<void()>& initializeFn); void reinitializeSound(ma_sound& sound, const std::function<void()>& initializeFn);
void initializeSound(const ma_data_source_vtable& vtable, void initializeSound(const ma_data_source_vtable& vtable,
ma_data_source_base& dataSourceBase, ma_data_source_base& dataSourceBase,
ma_sound& sound, ma_sound& sound,
const std::function<void()>& initializeFn); const std::function<void()>& initializeFn);
} // namespace sf::priv::MiniaudioUtils } // namespace priv::MiniaudioUtils
} // namespace sf

View File

@ -55,6 +55,7 @@ struct Sound::Impl
~Impl() ~Impl()
{ {
ma_sound_uninit(&sound); ma_sound_uninit(&sound);
ma_node_uninit(&effectNode, nullptr);
ma_data_source_uninit(&dataSourceBase); ma_data_source_uninit(&dataSourceBase);
} }
@ -90,6 +91,34 @@ struct Sound::Impl
return; return;
} }
// Initialize the custom effect node
effectNodeVTable.onProcess =
[](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut)
{ static_cast<EffectNode*>(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); };
effectNodeVTable.onGetRequiredInputFrameCount = nullptr;
effectNodeVTable.inputBusCount = 1;
effectNodeVTable.outputBusCount = 1;
effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT;
const auto nodeChannelCount = ma_engine_get_channels(engine);
ma_node_config nodeConfig = ma_node_config_init();
nodeConfig.vtable = &effectNodeVTable;
nodeConfig.pInputChannels = &nodeChannelCount;
nodeConfig.pOutputChannels = &nodeChannelCount;
if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode);
result != MA_SUCCESS)
{
err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl;
return;
}
effectNode.impl = this;
effectNode.channelCount = nodeChannelCount;
// Route the sound through the effect node depending on whether an effect processor is set
connectEffect(bool{effectProcessor});
// Because we are providing a custom data source, we have to provide the channel map ourselves // Because we are providing a custom data source, we have to provide the channel map ourselves
if (buffer && !buffer->getChannelMap().empty()) if (buffer && !buffer->getChannelMap().empty())
{ {
@ -110,7 +139,80 @@ struct Sound::Impl
void reinitialize() void reinitialize()
{ {
priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); priv::MiniaudioUtils::reinitializeSound(sound,
[this]
{
ma_node_uninit(&effectNode, nullptr);
initialize();
});
}
void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const
{
// If a processor is set, call it
if (effectProcessor)
{
if (!framesIn)
frameCountIn = 0;
effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount);
return;
}
// Otherwise just pass the data through 1:1
if (framesIn == nullptr)
{
frameCountIn = 0;
frameCountOut = 0;
return;
}
const auto toProcess = std::min(frameCountIn, frameCountOut);
std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float));
frameCountIn = toProcess;
frameCountOut = toProcess;
}
void connectEffect(bool connect)
{
auto* engine = priv::AudioDevice::get().getEngine();
if (engine == nullptr)
{
err() << "Failed to connect effect: No engine available" << std::endl;
return;
}
if (connect)
{
// Attach the custom effect node output to our engine endpoint
if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0);
result != MA_SUCCESS)
{
err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl;
return;
}
}
else
{
// Detach the custom effect node output from our engine endpoint
if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS)
{
err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result)
<< std::endl;
return;
}
}
// Attach the sound output to the custom effect node or the engine endpoint
if (const ma_result
result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0);
result != MA_SUCCESS)
{
err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl;
return;
}
} }
static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead)
@ -208,13 +310,23 @@ struct Sound::Impl
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
struct EffectNode
{
ma_node_base base{};
Impl* impl{};
ma_uint32 channelCount{};
};
ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member)
ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node
EffectNode effectNode; //!< The engine node that performs effect processing
std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
ma_sound sound{}; //!< The sound ma_sound sound{}; //!< The sound
std::size_t cursor{}; //!< The current playing position std::size_t cursor{}; //!< The current playing position
bool looping{}; //!< True if we are looping the sound bool looping{}; //!< True if we are looping the sound
const SoundBuffer* buffer{}; //!< Sound buffer bound to the source const SoundBuffer* buffer{}; //!< Sound buffer bound to the source
Status status{Status::Stopped}; //!< The status Status status{Status::Stopped}; //!< The status
EffectProcessor effectProcessor; //!< The effect processor
}; };
@ -334,6 +446,14 @@ void Sound::setPlayingOffset(Time timeOffset)
} }
////////////////////////////////////////////////////////////
void Sound::setEffectProcessor(EffectProcessor effectProcessor)
{
m_impl->effectProcessor = std::move(effectProcessor);
m_impl->connectEffect(bool{m_impl->effectProcessor});
}
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
const SoundBuffer& Sound::getBuffer() const const SoundBuffer& Sound::getBuffer() const
{ {

View File

@ -69,7 +69,7 @@ public:
/// \return Properties of the loaded sound if the file was successfully opened /// \return Properties of the loaded sound if the file was successfully opened
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
[[nodiscard]] std::optional<Info> open(sf::InputStream& stream) override; [[nodiscard]] std::optional<Info> open(InputStream& stream) override;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Change the current read position to the given sample offset /// \brief Change the current read position to the given sample offset

View File

@ -142,16 +142,16 @@ std::optional<SoundFileReader::Info> SoundFileReaderMp3::open(InputStream& strea
switch (info.channelCount) switch (info.channelCount)
{ {
case 0: case 0:
sf::err() << "No channels in MP3 file" << std::endl; err() << "No channels in MP3 file" << std::endl;
break; break;
case 1: case 1:
info.channelMap = {sf::SoundChannel::Mono}; info.channelMap = {SoundChannel::Mono};
break; break;
case 2: case 2:
info.channelMap = {sf::SoundChannel::SideLeft, sf::SoundChannel::SideRight}; info.channelMap = {SoundChannel::SideLeft, SoundChannel::SideRight};
break; break;
default: default:
sf::err() << "MP3 files with more than 2 channels not supported" << std::endl; err() << "MP3 files with more than 2 channels not supported" << std::endl;
assert(false); assert(false);
break; break;
} }

View File

@ -25,82 +25,64 @@
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Headers // Headers
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#include <SFML/Audio/MiniaudioUtils.hpp>
#include <SFML/Audio/SoundFileReaderWav.hpp> #include <SFML/Audio/SoundFileReaderWav.hpp>
#include <SFML/System/Err.hpp> #include <SFML/System/Err.hpp>
#include <SFML/System/InputStream.hpp> #include <SFML/System/InputStream.hpp>
#include <SFML/System/Utils.hpp>
#include <array>
#include <ostream> #include <ostream>
#include <vector>
#include <cassert> #include <cassert>
#include <cstddef> #include <cstddef>
#include <cstring>
namespace namespace
{ {
// The following functions read integers as little endian and ma_result onRead(ma_decoder* decoder, void* buffer, size_t bytesToRead, size_t* bytesRead)
// return them in the host byte order
bool decode(sf::InputStream& stream, std::uint8_t& value)
{ {
return static_cast<std::size_t>(stream.read(&value, sizeof(value))) == sizeof(value); auto* stream = static_cast<sf::InputStream*>(decoder->pUserData);
const auto count = stream->read(buffer, static_cast<std::int64_t>(bytesToRead));
if (count < 0)
return MA_ERROR;
*bytesRead = static_cast<size_t>(count);
return MA_SUCCESS;
} }
bool decode(sf::InputStream& stream, std::int16_t& value) ma_result onSeek(ma_decoder* decoder, ma_int64 byteOffset, ma_seek_origin origin)
{ {
std::byte bytes[sizeof(value)]; auto* stream = static_cast<sf::InputStream*>(decoder->pUserData);
if (static_cast<std::size_t>(stream.read(bytes, static_cast<std::int64_t>(sizeof(bytes)))) != sizeof(bytes))
return false;
value = sf::toInteger<std::int16_t>(bytes[0], bytes[1]); switch (origin)
{
case ma_seek_origin_start:
{
if (stream->seek(byteOffset) < 0)
return MA_ERROR;
return true; return MA_SUCCESS;
}
case ma_seek_origin_current:
{
const auto currentPosition = stream->tell();
if (currentPosition < 0)
return MA_ERROR;
if (stream->seek(stream->tell() + byteOffset) < 0)
return MA_ERROR;
return MA_SUCCESS;
}
// According to miniaudio comments, ma_seek_origin_end is not used by decoders
default:
return MA_ERROR;
}
} }
bool decode(sf::InputStream& stream, std::uint16_t& value)
{
std::byte bytes[sizeof(value)];
if (static_cast<std::size_t>(stream.read(bytes, static_cast<std::int64_t>(sizeof(bytes)))) != sizeof(bytes))
return false;
value = sf::toInteger<std::uint16_t>(bytes[0], bytes[1]);
return true;
}
bool decode24bit(sf::InputStream& stream, std::uint32_t& value)
{
std::byte bytes[3];
if (static_cast<std::size_t>(stream.read(bytes, static_cast<std::int64_t>(sizeof(bytes)))) != sizeof(bytes))
return false;
value = sf::toInteger<std::uint32_t>(bytes[0], bytes[1], bytes[2]);
return true;
}
bool decode(sf::InputStream& stream, std::uint32_t& value)
{
std::byte bytes[sizeof(value)];
if (static_cast<std::size_t>(stream.read(bytes, static_cast<std::int64_t>(sizeof(bytes)))) != sizeof(bytes))
return false;
value = sf::toInteger<std::uint32_t>(bytes[0], bytes[1], bytes[2], bytes[3]);
return true;
}
const std::uint64_t mainChunkSize = 12;
const std::uint16_t waveFormatPcm = 1;
const std::uint16_t waveFormatExtensible = 65534;
const char* waveSubformatPcm =
"\x01\x00\x00\x00\x00\x00\x10\x00"
"\x80\x00\x00\xAA\x00\x38\x9B\x71";
} // namespace } // namespace
namespace sf::priv namespace sf::priv
@ -108,331 +90,114 @@ namespace sf::priv
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
bool SoundFileReaderWav::check(InputStream& stream) bool SoundFileReaderWav::check(InputStream& stream)
{ {
char header[mainChunkSize]; auto config = ma_decoder_config_init_default();
if (stream.read(header, sizeof(header)) < static_cast<std::int64_t>(sizeof(header))) config.encodingFormat = ma_encoding_format_wav;
return false; config.format = ma_format_s16;
ma_decoder decoder{};
return (header[0] == 'R') && (header[1] == 'I') && (header[2] == 'F') && (header[3] == 'F') && (header[8] == 'W') && if (ma_decoder_init(&onRead, &onSeek, &stream, &config, &decoder) == MA_SUCCESS)
(header[9] == 'A') && (header[10] == 'V') && (header[11] == 'E'); {
ma_decoder_uninit(&decoder);
return true;
}
return false;
}
////////////////////////////////////////////////////////////
SoundFileReaderWav::~SoundFileReaderWav()
{
if (m_decoder)
{
if (const ma_result result = ma_decoder_uninit(&*m_decoder); result != MA_SUCCESS)
err() << "Failed to uninitialize wav decoder: " << ma_result_description(result) << std::endl;
}
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::optional<SoundFileReader::Info> SoundFileReaderWav::open(InputStream& stream) std::optional<SoundFileReader::Info> SoundFileReaderWav::open(InputStream& stream)
{ {
m_stream = &stream; if (m_decoder)
{
if (const ma_result result = ma_decoder_uninit(&*m_decoder); result != MA_SUCCESS)
{
err() << "Failed to uninitialize wav decoder: " << ma_result_description(result) << std::endl;
return std::nullopt;
}
}
else
{
m_decoder.emplace();
}
auto info = parseHeader(); auto config = ma_decoder_config_init_default();
if (!info) config.encodingFormat = ma_encoding_format_wav;
err() << "Failed to open WAV sound file (invalid or unsupported file)" << std::endl; config.format = ma_format_s16;
return info; if (const ma_result result = ma_decoder_init(&onRead, &onSeek, &stream, &config, &*m_decoder); result != MA_SUCCESS)
{
err() << "Failed to initialize wav decoder: " << ma_result_description(result) << std::endl;
m_decoder = std::nullopt;
return std::nullopt;
}
ma_uint64 frameCount{};
if (const ma_result result = ma_decoder_get_available_frames(&*m_decoder, &frameCount); result != MA_SUCCESS)
{
err() << "Failed to get available frames from wav decoder: " << ma_result_description(result) << std::endl;
return std::nullopt;
}
auto format = ma_format_unknown;
ma_uint32 sampleRate{};
std::array<ma_channel, 20> channelMap{};
if (const ma_result result = ma_decoder_get_data_format(&*m_decoder,
&format,
&m_channelCount,
&sampleRate,
channelMap.data(),
channelMap.size());
result != MA_SUCCESS)
{
err() << "Failed to get data format from wav decoder: " << ma_result_description(result) << std::endl;
return std::nullopt;
}
std::vector<SoundChannel> soundChannels;
soundChannels.reserve(m_channelCount);
for (auto i = 0u; i < m_channelCount; ++i)
soundChannels.emplace_back(priv::MiniaudioUtils::miniaudioChannelToSoundChannel(channelMap[i]));
return Info{frameCount * m_channelCount, m_channelCount, sampleRate, std::move(soundChannels)};
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
void SoundFileReaderWav::seek(std::uint64_t sampleOffset) void SoundFileReaderWav::seek(std::uint64_t sampleOffset)
{ {
assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it."); assert(m_decoder && "wav decoder not initialized. Call SoundFileReaderWav::open() to initialize it.");
if (m_stream->seek(static_cast<std::int64_t>(m_dataStart + sampleOffset * m_bytesPerSample) == -1)) if (const ma_result result = ma_decoder_seek_to_pcm_frame(&*m_decoder, sampleOffset / m_channelCount);
err() << "Failed to seek WAV sound stream" << std::endl; result != MA_SUCCESS)
err() << "Failed to seek wav sound stream: " << ma_result_description(result) << std::endl;
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::uint64_t SoundFileReaderWav::read(std::int16_t* samples, std::uint64_t maxCount) std::uint64_t SoundFileReaderWav::read(std::int16_t* samples, std::uint64_t maxCount)
{ {
assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it."); assert(m_decoder && "wav decoder not initialized. Call SoundFileReaderWav::open() to initialize it.");
std::uint64_t count = 0; ma_uint64 framesRead{};
const auto startPos = static_cast<std::uint64_t>(m_stream->tell());
// Tracking of m_dataEnd is important to prevent sf::Music from reading if (const ma_result result = ma_decoder_read_pcm_frames(&*m_decoder, samples, maxCount / m_channelCount, &framesRead);
// data until EOF, as WAV files may have metadata at the end. result != MA_SUCCESS)
while ((count < maxCount) && (startPos + count * m_bytesPerSample < m_dataEnd)) err() << "Failed to read from wav sound stream: " << ma_result_description(result) << std::endl;
{
switch (m_bytesPerSample)
{
case 1:
{
std::uint8_t sample = 0;
if (decode(*m_stream, sample))
*samples++ = static_cast<std::int16_t>((static_cast<std::int16_t>(sample) - 128) << 8);
else
return count;
break;
}
case 2: return framesRead * m_channelCount;
{
std::int16_t sample = 0;
if (decode(*m_stream, sample))
*samples++ = sample;
else
return count;
break;
}
case 3:
{
std::uint32_t sample = 0;
if (decode24bit(*m_stream, sample))
*samples++ = static_cast<std::int16_t>(sample >> 8);
else
return count;
break;
}
case 4:
{
std::uint32_t sample = 0;
if (decode(*m_stream, sample))
*samples++ = static_cast<std::int16_t>(sample >> 16);
else
return count;
break;
}
default:
{
assert(false && "Invalid bytes per sample. Must be 1, 2, 3, or 4.");
return 0;
}
}
++count;
}
return count;
}
////////////////////////////////////////////////////////////
std::optional<SoundFileReader::Info> SoundFileReaderWav::parseHeader()
{
assert(m_stream && "Input stream cannot be null. Call SoundFileReaderWav::open() to initialize it.");
// If we are here, it means that the first part of the header
// (the format) has already been checked
char mainChunk[mainChunkSize];
if (static_cast<std::size_t>(m_stream->read(mainChunk, static_cast<std::int64_t>(sizeof(mainChunk)))) !=
sizeof(mainChunk))
return std::nullopt;
// Parse all the sub-chunks
Info info;
bool dataChunkFound = false;
while (!dataChunkFound)
{
// Parse the sub-chunk id and size
char subChunkId[4];
if (static_cast<std::size_t>(m_stream->read(subChunkId, static_cast<std::int64_t>(sizeof(subChunkId)))) !=
sizeof(subChunkId))
return std::nullopt;
std::uint32_t subChunkSize = 0;
if (!decode(*m_stream, subChunkSize))
return std::nullopt;
const std::int64_t subChunkStart = m_stream->tell();
if (subChunkStart == -1)
return std::nullopt;
// Check which chunk it is
if ((subChunkId[0] == 'f') && (subChunkId[1] == 'm') && (subChunkId[2] == 't') && (subChunkId[3] == ' '))
{
// "fmt" chunk
// Audio format
std::uint16_t format = 0;
if (!decode(*m_stream, format))
return std::nullopt;
if ((format != waveFormatPcm) && (format != waveFormatExtensible))
return std::nullopt;
// Channel count
std::uint16_t channelCount = 0;
if (!decode(*m_stream, channelCount))
return std::nullopt;
info.channelCount = channelCount;
// Sample rate
std::uint32_t sampleRate = 0;
if (!decode(*m_stream, sampleRate))
return std::nullopt;
info.sampleRate = sampleRate;
// Byte rate
std::uint32_t byteRate = 0;
if (!decode(*m_stream, byteRate))
return std::nullopt;
// Block align
std::uint16_t blockAlign = 0;
if (!decode(*m_stream, blockAlign))
return std::nullopt;
// Bits per sample
std::uint16_t bitsPerSample = 0;
if (!decode(*m_stream, bitsPerSample))
return std::nullopt;
if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 24 && bitsPerSample != 32)
{
err() << "Unsupported sample size: " << bitsPerSample
<< " bit (Supported sample sizes are 8/16/24/32 bit)" << std::endl;
return std::nullopt;
}
m_bytesPerSample = bitsPerSample / 8;
if (format == waveFormatExtensible)
{
// Extension size
std::uint16_t extensionSize = 0;
if (!decode(*m_stream, extensionSize))
return std::nullopt;
// Valid bits per sample
std::uint16_t validBitsPerSample = 0;
if (!decode(*m_stream, validBitsPerSample))
return std::nullopt;
// Channel mask
std::uint32_t channelMask = 0;
if (!decode(*m_stream, channelMask))
return std::nullopt;
// NOLINTBEGIN(readability-identifier-naming)
// For WAVE channel mapping refer to: https://learn.microsoft.com/en-us/previous-versions/windows/hardware/design/dn653308(v=vs.85)#default-channel-ordering
static constexpr auto SPEAKER_FRONT_LEFT = 0x1u;
static constexpr auto SPEAKER_FRONT_RIGHT = 0x2u;
static constexpr auto SPEAKER_FRONT_CENTER = 0x4u;
static constexpr auto SPEAKER_LOW_FREQUENCY = 0x8u;
static constexpr auto SPEAKER_BACK_LEFT = 0x10u;
static constexpr auto SPEAKER_BACK_RIGHT = 0x20u;
static constexpr auto SPEAKER_FRONT_LEFT_OF_CENTER = 0x40u;
static constexpr auto SPEAKER_FRONT_RIGHT_OF_CENTER = 0x80u;
static constexpr auto SPEAKER_BACK_CENTER = 0x100u;
static constexpr auto SPEAKER_SIDE_LEFT = 0x200u;
static constexpr auto SPEAKER_SIDE_RIGHT = 0x400u;
static constexpr auto SPEAKER_TOP_CENTER = 0x800u;
static constexpr auto SPEAKER_TOP_FRONT_LEFT = 0x1000u;
static constexpr auto SPEAKER_TOP_FRONT_CENTER = 0x2000u;
static constexpr auto SPEAKER_TOP_FRONT_RIGHT = 0x4000u;
static constexpr auto SPEAKER_TOP_BACK_LEFT = 0x8000u;
static constexpr auto SPEAKER_TOP_BACK_CENTER = 0x10000u;
static constexpr auto SPEAKER_TOP_BACK_RIGHT = 0x20000u;
// NOLINTEND(readability-identifier-naming)
info.channelMap.clear();
const auto checkChannel = [channelMask, &info](auto bit, auto soundChannel)
{
if ((channelMask & bit) != 0)
info.channelMap.push_back(soundChannel);
};
checkChannel(SPEAKER_FRONT_LEFT, SoundChannel::FrontLeft);
checkChannel(SPEAKER_FRONT_RIGHT, SoundChannel::FrontRight);
checkChannel(SPEAKER_FRONT_CENTER, SoundChannel::FrontCenter);
checkChannel(SPEAKER_LOW_FREQUENCY, SoundChannel::LowFrequencyEffects);
checkChannel(SPEAKER_BACK_LEFT, SoundChannel::BackLeft);
checkChannel(SPEAKER_BACK_RIGHT, SoundChannel::BackRight);
checkChannel(SPEAKER_FRONT_LEFT_OF_CENTER, SoundChannel::FrontLeftOfCenter);
checkChannel(SPEAKER_FRONT_RIGHT_OF_CENTER, SoundChannel::FrontRightOfCenter);
checkChannel(SPEAKER_BACK_CENTER, SoundChannel::BackCenter);
checkChannel(SPEAKER_SIDE_LEFT, SoundChannel::SideLeft);
checkChannel(SPEAKER_SIDE_RIGHT, SoundChannel::SideRight);
checkChannel(SPEAKER_TOP_CENTER, SoundChannel::TopCenter);
checkChannel(SPEAKER_TOP_FRONT_LEFT, SoundChannel::TopFrontLeft);
checkChannel(SPEAKER_TOP_FRONT_CENTER, SoundChannel::TopFrontCenter);
checkChannel(SPEAKER_TOP_FRONT_RIGHT, SoundChannel::TopFrontRight);
checkChannel(SPEAKER_TOP_BACK_LEFT, SoundChannel::TopBackLeft);
checkChannel(SPEAKER_TOP_BACK_CENTER, SoundChannel::TopBackCenter);
checkChannel(SPEAKER_TOP_BACK_RIGHT, SoundChannel::TopBackRight);
assert(info.channelCount == info.channelMap.size());
if (info.channelCount != info.channelMap.size())
{
err() << "WAV sound file channel count does not match number of set bits in channel mask" << std::endl;
return std::nullopt;
}
// Subformat
char subformat[16];
if (static_cast<std::size_t>(m_stream->read(subformat, static_cast<std::int64_t>(sizeof(subformat)))) !=
sizeof(subformat))
return std::nullopt;
if (std::memcmp(subformat, waveSubformatPcm, sizeof(subformat)) != 0)
{
err() << "Unsupported format: extensible format with non-PCM subformat" << std::endl;
return std::nullopt;
}
if (validBitsPerSample != bitsPerSample)
{
err() << "Unsupported format: sample size (" << validBitsPerSample
<< " bits) and "
"sample container size ("
<< bitsPerSample << " bits) differ" << std::endl;
return std::nullopt;
}
}
else
{
// If we don't have a waveFormatExtensible header, fill the channel map based on a best guess for mono/stereo
info.channelMap.clear();
if (info.channelCount == 0)
{
err() << "WAV sound file channel count 0" << std::endl;
return std::nullopt;
}
else if (info.channelCount == 1)
{
info.channelMap.push_back(SoundChannel::Mono);
}
else if (info.channelCount == 2)
{
info.channelMap.push_back(SoundChannel::FrontLeft);
info.channelMap.push_back(SoundChannel::FrontRight);
}
else
{
info.channelMap.push_back(SoundChannel::FrontLeft);
info.channelMap.push_back(SoundChannel::FrontRight);
for (auto i = 2u; i < info.channelCount; ++i)
info.channelMap.push_back(SoundChannel::Unspecified);
}
}
// Skip potential extra information
if (m_stream->seek(subChunkStart + subChunkSize) == -1)
return std::nullopt;
}
else if ((subChunkId[0] == 'd') && (subChunkId[1] == 'a') && (subChunkId[2] == 't') && (subChunkId[3] == 'a'))
{
// "data" chunk
// Compute the total number of samples
info.sampleCount = subChunkSize / m_bytesPerSample;
// Store the start and end position of samples in the file
m_dataStart = static_cast<std::uint64_t>(subChunkStart);
m_dataEnd = m_dataStart + info.sampleCount * m_bytesPerSample;
dataChunkFound = true;
}
else
{
// unknown chunk, skip it
if (m_stream->seek(m_stream->tell() + subChunkSize) == -1)
return std::nullopt;
}
}
return info;
} }
} // namespace sf::priv } // namespace sf::priv

View File

@ -29,6 +29,8 @@
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#include <SFML/Audio/SoundFileReader.hpp> #include <SFML/Audio/SoundFileReader.hpp>
#include <miniaudio.h>
#include <optional> #include <optional>
#include <cstdint> #include <cstdint>
@ -58,6 +60,12 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
[[nodiscard]] static bool check(InputStream& stream); [[nodiscard]] static bool check(InputStream& stream);
////////////////////////////////////////////////////////////
/// \brief Destructor
///
////////////////////////////////////////////////////////////
~SoundFileReaderWav() override;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Open a sound file for reading /// \brief Open a sound file for reading
/// ///
@ -66,7 +74,7 @@ public:
/// \return Properties of the loaded sound if the file was successfully opened /// \return Properties of the loaded sound if the file was successfully opened
/// ///
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
[[nodiscard]] std::optional<Info> open(sf::InputStream& stream) override; [[nodiscard]] std::optional<Info> open(InputStream& stream) override;
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Change the current read position to the given sample offset /// \brief Change the current read position to the given sample offset
@ -95,23 +103,11 @@ public:
[[nodiscard]] std::uint64_t read(std::int16_t* samples, std::uint64_t maxCount) override; [[nodiscard]] std::uint64_t read(std::int16_t* samples, std::uint64_t maxCount) override;
private: private:
////////////////////////////////////////////////////////////
/// \brief Read the header of the open file
///
/// \param info Attributes of the sound file
///
/// \return True on success, false on error
///
////////////////////////////////////////////////////////////
std::optional<Info> parseHeader();
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
InputStream* m_stream{}; //!< Source stream to read from std::optional<ma_decoder> m_decoder; //!< wav decoder
unsigned int m_bytesPerSample{}; //!< Size of a sample, in bytes ma_uint32 m_channelCount{}; //!< Number of channels
std::uint64_t m_dataStart{}; //!< Starting position of the audio data in the open file
std::uint64_t m_dataEnd{}; //!< Position one byte past the end of the audio data in the open file
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -31,6 +31,7 @@
#include <vorbis/vorbisenc.h> #include <vorbis/vorbisenc.h>
#include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@ -103,12 +104,12 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
unsigned int m_channelCount{}; //!< Channel count of the sound being written unsigned int m_channelCount{}; //!< Channel count of the sound being written
std::size_t m_remapTable[8]{}; //!< Table we use to remap source to target channel order std::array<std::size_t, 8> m_remapTable{}; //!< Table we use to remap source to target channel order
std::ofstream m_file; //!< Output file std::ofstream m_file; //!< Output file
ogg_stream_state m_ogg{}; //!< OGG stream ogg_stream_state m_ogg{}; //!< OGG stream
vorbis_info m_vorbis{}; //!< Vorbis handle vorbis_info m_vorbis{}; //!< Vorbis handle
vorbis_dsp_state m_state{}; //!< Current encoding state vorbis_dsp_state m_state{}; //!< Current encoding state
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -31,6 +31,7 @@
#include <SFML/System/Utils.hpp> #include <SFML/System/Utils.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <ostream> #include <ostream>
#include <cassert> #include <cassert>
@ -45,25 +46,25 @@ namespace
void encode(std::ostream& stream, std::int16_t value) void encode(std::ostream& stream, std::int16_t value)
{ {
const std::byte bytes[] = {static_cast<std::byte>(value & 0xFF), static_cast<std::byte>(value >> 8)}; const std::array bytes = {static_cast<char>(value & 0xFF), static_cast<char>(value >> 8)};
stream.write(reinterpret_cast<const char*>(bytes), sizeof(bytes)); stream.write(bytes.data(), bytes.size());
} }
void encode(std::ostream& stream, std::uint16_t value) void encode(std::ostream& stream, std::uint16_t value)
{ {
const std::byte bytes[] = {static_cast<std::byte>(value & 0xFF), static_cast<std::byte>(value >> 8)}; const std::array bytes = {static_cast<char>(value & 0xFF), static_cast<char>(value >> 8)};
stream.write(reinterpret_cast<const char*>(bytes), sizeof(bytes)); stream.write(bytes.data(), bytes.size());
} }
void encode(std::ostream& stream, std::uint32_t value) void encode(std::ostream& stream, std::uint32_t value)
{ {
const std::byte bytes[] = { const std::array bytes = {
static_cast<std::byte>(value & 0x000000FF), static_cast<char>(value & 0x000000FF),
static_cast<std::byte>((value & 0x0000FF00) >> 8), static_cast<char>((value & 0x0000FF00) >> 8),
static_cast<std::byte>((value & 0x00FF0000) >> 16), static_cast<char>((value & 0x00FF0000) >> 16),
static_cast<std::byte>((value & 0xFF000000) >> 24), static_cast<char>((value & 0xFF000000) >> 24),
}; };
stream.write(reinterpret_cast<const char*>(bytes), sizeof(bytes)); stream.write(bytes.data(), bytes.size());
} }
} // namespace } // namespace
@ -247,17 +248,17 @@ void SoundFileWriterWav::writeHeader(unsigned int sampleRate, unsigned int chann
assert(m_file.good() && "Most recent I/O operation failed"); assert(m_file.good() && "Most recent I/O operation failed");
// Write the main chunk ID // Write the main chunk ID
char mainChunkId[4] = {'R', 'I', 'F', 'F'}; std::array mainChunkId = {'R', 'I', 'F', 'F'};
m_file.write(mainChunkId, sizeof(mainChunkId)); m_file.write(mainChunkId.data(), mainChunkId.size());
// Write the main chunk header // Write the main chunk header
encode(m_file, static_cast<std::uint32_t>(0)); // 0 is a placeholder, will be written later encode(m_file, static_cast<std::uint32_t>(0)); // 0 is a placeholder, will be written later
char mainChunkFormat[4] = {'W', 'A', 'V', 'E'}; std::array mainChunkFormat = {'W', 'A', 'V', 'E'};
m_file.write(mainChunkFormat, sizeof(mainChunkFormat)); m_file.write(mainChunkFormat.data(), mainChunkFormat.size());
// Write the sub-chunk 1 ("format") id and size // Write the sub-chunk 1 ("format") id and size
char fmtChunkId[4] = {'f', 'm', 't', ' '}; std::array fmtChunkId = {'f', 'm', 't', ' '};
m_file.write(fmtChunkId, sizeof(fmtChunkId)); m_file.write(fmtChunkId.data(), fmtChunkId.size());
if (channelCount > 2) if (channelCount > 2)
{ {
@ -295,14 +296,14 @@ void SoundFileWriterWav::writeHeader(unsigned int sampleRate, unsigned int chann
encode(m_file, bitsPerSample); encode(m_file, bitsPerSample);
encode(m_file, channelMask); encode(m_file, channelMask);
// Write the subformat (PCM) // Write the subformat (PCM)
char subformat[16] = std::array subformat =
{'\x01', '\x00', '\x00', '\x00', '\x00', '\x00', '\x10', '\x00', '\x80', '\x00', '\x00', '\xAA', '\x00', '\x38', '\x9B', '\x71'}; {'\x01', '\x00', '\x00', '\x00', '\x00', '\x00', '\x10', '\x00', '\x80', '\x00', '\x00', '\xAA', '\x00', '\x38', '\x9B', '\x71'};
m_file.write(subformat, sizeof(subformat)); m_file.write(subformat.data(), subformat.size());
} }
// Write the sub-chunk 2 ("data") id and size // Write the sub-chunk 2 ("data") id and size
char dataChunkId[4] = {'d', 'a', 't', 'a'}; std::array dataChunkId = {'d', 'a', 't', 'a'};
m_file.write(dataChunkId, sizeof(dataChunkId)); m_file.write(dataChunkId.data(), dataChunkId.size());
const std::uint32_t dataChunkSize = 0; // placeholder, will be written later const std::uint32_t dataChunkSize = 0; // placeholder, will be written later
encode(m_file, dataChunkSize); encode(m_file, dataChunkSize);
} }

View File

@ -29,6 +29,7 @@
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#include <SFML/Audio/SoundFileWriter.hpp> #include <SFML/Audio/SoundFileWriter.hpp>
#include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@ -105,9 +106,9 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::ofstream m_file; //!< File stream to write to std::ofstream m_file; //!< File stream to write to
unsigned int m_channelCount{}; //!< Channel count of the sound being written unsigned int m_channelCount{}; //!< Channel count of the sound being written
std::size_t m_remapTable[18]{}; //!< Table we use to remap source to target channel order std::array<std::size_t, 18> m_remapTable{}; //!< Table we use to remap source to target channel order
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -33,6 +33,7 @@
#include <miniaudio.h> #include <miniaudio.h>
#include <algorithm> #include <algorithm>
#include <array>
#include <optional> #include <optional>
#include <ostream> #include <ostream>
@ -187,16 +188,52 @@ SoundRecorder::SoundRecorder() : m_impl(std::make_unique<Impl>(this))
// Create the context // Create the context
m_impl->context.emplace(); m_impl->context.emplace();
auto contextConfig = ma_context_config_init(); auto contextConfig = ma_context_config_init();
contextConfig.pLog = &*m_impl->log; contextConfig.pLog = &*m_impl->log;
ma_uint32 deviceCount = 0;
const auto nullBackend = ma_backend_null;
const std::array<const ma_backend*, 2> backendLists{nullptr, &nullBackend};
if (const auto result = ma_context_init(nullptr, 0, &contextConfig, &*m_impl->context); result != MA_SUCCESS) for (const auto* backendList : backendLists)
{
// We can set backendCount to 1 since it is ignored when backends is set to nullptr
if (const auto result = ma_context_init(backendList, 1, &contextConfig, &*m_impl->context); result != MA_SUCCESS)
{
m_impl->context.reset();
err() << "Failed to initialize the audio capture context: " << ma_result_description(result) << std::endl;
return;
}
// Count the capture devices
if (const auto result = ma_context_get_devices(&*m_impl->context, nullptr, nullptr, nullptr, &deviceCount);
result != MA_SUCCESS)
{
err() << "Failed to get audio capture devices: " << ma_result_description(result) << std::endl;
return;
}
// Check if there are audio capture devices available on the system
if (deviceCount > 0)
break;
// Warn if no devices were found using the default backend list
if (backendList == nullptr)
err() << "No audio capture devices available on the system" << std::endl;
// Clean up the context if we didn't find any devices
ma_context_uninit(&*m_impl->context);
}
// If the NULL audio backend also doesn't provide a device we give up
if (deviceCount == 0)
{ {
m_impl->context.reset(); m_impl->context.reset();
err() << "Failed to initialize the audio context: " << ma_result_description(result) << std::endl;
return; return;
} }
if (m_impl->context->backend == ma_backend_null)
err() << "Using NULL audio backend for capture" << std::endl;
// Create the capture device // Create the capture device
m_impl->initialize(); m_impl->initialize();
} }

View File

@ -88,8 +88,8 @@ void SoundSource::setCone(const Cone& cone)
{ {
if (auto* sound = static_cast<ma_sound*>(getSound())) if (auto* sound = static_cast<ma_sound*>(getSound()))
ma_sound_set_cone(sound, ma_sound_set_cone(sound,
std::clamp(cone.innerAngle, sf::degrees(0), sf::degrees(360)).asRadians(), std::clamp(cone.innerAngle, Angle::Zero, degrees(360.f)).asRadians(),
std::clamp(cone.outerAngle, sf::degrees(0), sf::degrees(360)).asRadians(), std::clamp(cone.outerAngle, Angle::Zero, degrees(360.f)).asRadians(),
cone.outerGain); cone.outerGain);
} }
@ -166,6 +166,13 @@ void SoundSource::setAttenuation(float attenuation)
} }
////////////////////////////////////////////////////////////
// NOLINTNEXTLINE(performance-unnecessary-value-param)
void SoundSource::setEffectProcessor(EffectProcessor)
{
}
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
float SoundSource::getPitch() const float SoundSource::getPitch() const
{ {
@ -241,12 +248,12 @@ SoundSource::Cone SoundSource::getCone() const
float outerAngle = 0.f; float outerAngle = 0.f;
Cone cone; Cone cone;
ma_sound_get_cone(sound, &innerAngle, &outerAngle, &cone.outerGain); ma_sound_get_cone(sound, &innerAngle, &outerAngle, &cone.outerGain);
cone.innerAngle = sf::radians(innerAngle); cone.innerAngle = radians(innerAngle);
cone.outerAngle = sf::radians(outerAngle); cone.outerAngle = radians(outerAngle);
return cone; return cone;
} }
return Cone{sf::radians(0), sf::radians(0), 0.f}; return Cone{radians(0), radians(0), 0.f};
} }

View File

@ -55,6 +55,7 @@ struct SoundStream::Impl
~Impl() ~Impl()
{ {
ma_sound_uninit(&sound); ma_sound_uninit(&sound);
ma_node_uninit(&effectNode, nullptr);
ma_data_source_uninit(&dataSourceBase); ma_data_source_uninit(&dataSourceBase);
} }
@ -91,6 +92,34 @@ struct SoundStream::Impl
return; return;
} }
// Initialize the custom effect node
effectNodeVTable.onProcess =
[](ma_node* node, const float** framesIn, ma_uint32* frameCountIn, float** framesOut, ma_uint32* frameCountOut)
{ static_cast<EffectNode*>(node)->impl->processEffect(framesIn, *frameCountIn, framesOut, *frameCountOut); };
effectNodeVTable.onGetRequiredInputFrameCount = nullptr;
effectNodeVTable.inputBusCount = 1;
effectNodeVTable.outputBusCount = 1;
effectNodeVTable.flags = MA_NODE_FLAG_CONTINUOUS_PROCESSING | MA_NODE_FLAG_ALLOW_NULL_INPUT;
const auto nodeChannelCount = ma_engine_get_channels(engine);
ma_node_config nodeConfig = ma_node_config_init();
nodeConfig.vtable = &effectNodeVTable;
nodeConfig.pInputChannels = &nodeChannelCount;
nodeConfig.pOutputChannels = &nodeChannelCount;
if (const ma_result result = ma_node_init(ma_engine_get_node_graph(engine), &nodeConfig, nullptr, &effectNode);
result != MA_SUCCESS)
{
err() << "Failed to initialize effect node: " << ma_result_description(result) << std::endl;
return;
}
effectNode.impl = this;
effectNode.channelCount = nodeChannelCount;
// Route the sound through the effect node depending on whether an effect processor is set
connectEffect(bool{effectProcessor});
// Because we are providing a custom data source, we have to provide the channel map ourselves // Because we are providing a custom data source, we have to provide the channel map ourselves
if (!channelMap.empty()) if (!channelMap.empty())
{ {
@ -111,7 +140,79 @@ struct SoundStream::Impl
void reinitialize() void reinitialize()
{ {
priv::MiniaudioUtils::reinitializeSound(sound, [this] { initialize(); }); priv::MiniaudioUtils::reinitializeSound(sound,
[this]
{
ma_node_uninit(&effectNode, nullptr);
initialize();
});
}
void processEffect(const float** framesIn, ma_uint32& frameCountIn, float** framesOut, ma_uint32& frameCountOut) const
{
// If a processor is set, call it
if (effectProcessor)
{
if (!framesIn)
frameCountIn = 0;
effectProcessor(framesIn ? framesIn[0] : nullptr, frameCountIn, framesOut[0], frameCountOut, effectNode.channelCount);
return;
}
// Otherwise just pass the data through 1:1
if (framesIn == nullptr)
{
frameCountIn = 0;
frameCountOut = 0;
return;
}
const auto toProcess = std::min(frameCountIn, frameCountOut);
std::memcpy(framesOut[0], framesIn[0], toProcess * effectNode.channelCount * sizeof(float));
frameCountIn = toProcess;
frameCountOut = toProcess;
}
void connectEffect(bool connect)
{
auto* engine = priv::AudioDevice::get().getEngine();
if (engine == nullptr)
{
err() << "Failed to connect effect: No engine available" << std::endl;
return;
}
if (connect)
{
// Attach the custom effect node output to our engine endpoint
if (const ma_result result = ma_node_attach_output_bus(&effectNode, 0, ma_engine_get_endpoint(engine), 0);
result != MA_SUCCESS)
{
err() << "Failed to attach effect node output to endpoint: " << ma_result_description(result) << std::endl;
return;
}
}
else
{
// Detach the custom effect node output from our engine endpoint
if (const ma_result result = ma_node_detach_output_bus(&effectNode, 0); result != MA_SUCCESS)
{
err() << "Failed to detach effect node output from endpoint: " << ma_result_description(result)
<< std::endl;
return;
}
}
// Attach the sound output to the custom effect node or the engine endpoint
if (const ma_result
result = ma_node_attach_output_bus(&sound, 0, connect ? &effectNode : ma_engine_get_endpoint(engine), 0);
result != MA_SUCCESS)
{
err() << "Failed to attach sound node output to effect node: " << ma_result_description(result) << std::endl;
return;
}
} }
static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead) static ma_result read(ma_data_source* dataSource, void* framesOut, ma_uint64 frameCount, ma_uint64* framesRead)
@ -238,8 +339,17 @@ struct SoundStream::Impl
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
struct EffectNode
{
ma_node_base base{};
Impl* impl{};
ma_uint32 channelCount{};
};
ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member) ma_data_source_base dataSourceBase{}; //!< The struct that makes this object a miniaudio data source (must be first member)
SoundStream* const owner; //!< Owning SoundStream object SoundStream* const owner; //!< Owning SoundStream object
ma_node_vtable effectNodeVTable{}; //!< Vtable of the effect node
EffectNode effectNode; //!< The engine node that performs effect processing
std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels) std::vector<ma_channel> soundChannelMap; //!< The map of position in sample frame to sound channel (miniaudio channels)
ma_sound sound{}; //!< The sound ma_sound sound{}; //!< The sound
std::vector<std::int16_t> sampleBuffer; //!< Our temporary sample buffer std::vector<std::int16_t> sampleBuffer; //!< Our temporary sample buffer
@ -251,6 +361,7 @@ struct SoundStream::Impl
bool loop{}; //!< Loop flag (true to loop, false to play once) bool loop{}; //!< Loop flag (true to loop, false to play once)
bool streaming{true}; //!< True if we are still streaming samples from the source bool streaming{true}; //!< True if we are still streaming samples from the source
Status status{Status::Stopped}; //!< The status Status status{Status::Stopped}; //!< The status
EffectProcessor effectProcessor; //!< The effect processor
}; };
@ -395,6 +506,14 @@ bool SoundStream::getLoop() const
} }
////////////////////////////////////////////////////////////
void SoundStream::setEffectProcessor(EffectProcessor effectProcessor)
{
m_impl->effectProcessor = std::move(effectProcessor);
m_impl->connectEffect(bool{m_impl->effectProcessor});
}
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::optional<std::uint64_t> SoundStream::onLoop() std::optional<std::uint64_t> SoundStream::onLoop()
{ {

View File

@ -71,7 +71,7 @@ std::size_t CircleShape::getPointCount() const
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Vector2f CircleShape::getPoint(std::size_t index) const Vector2f CircleShape::getPoint(std::size_t index) const
{ {
const Angle angle = static_cast<float>(index) / static_cast<float>(m_pointCount) * sf::degrees(360) - sf::degrees(90); const Angle angle = static_cast<float>(index) / static_cast<float>(m_pointCount) * degrees(360.f) - degrees(90.f);
return Vector2f(m_radius, m_radius) + Vector2f(m_radius, angle); return Vector2f(m_radius, m_radius) + Vector2f(m_radius, angle);
} }

View File

@ -784,7 +784,7 @@ bool Font::setCurrentSize(unsigned int characterSize) const
Font::Page::Page(bool smooth) Font::Page::Page(bool smooth)
{ {
// Make sure that the texture is initialized by default // Make sure that the texture is initialized by default
sf::Image image; Image image;
image.create({128, 128}, Color::Transparent); image.create({128, 128}, Color::Transparent);
// Reserve a 2x2 white square for texturing underlines // Reserve a 2x2 white square for texturing underlines

View File

@ -61,9 +61,9 @@ void ensureExtensionsInit()
initialized = true; initialized = true;
#ifdef SFML_OPENGL_ES #ifdef SFML_OPENGL_ES
gladLoadGLES1(sf::Context::getFunction); gladLoadGLES1(Context::getFunction);
#else #else
gladLoadGL(sf::Context::getFunction); gladLoadGL(Context::getFunction);
#endif #endif
// Retrieve the context version number // Retrieve the context version number

View File

@ -31,6 +31,7 @@
#include <SFML/System/InputStream.hpp> #include <SFML/System/InputStream.hpp>
#include <SFML/System/Utils.hpp> #include <SFML/System/Utils.hpp>
#ifdef SFML_SYSTEM_ANDROID #ifdef SFML_SYSTEM_ANDROID
#include <SFML/System/Android/Activity.hpp>
#include <SFML/System/Android/ResourceStream.hpp> #include <SFML/System/Android/ResourceStream.hpp>
#endif #endif
@ -157,7 +158,15 @@ void Image::create(const Vector2u& size, const std::uint8_t* pixels)
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
bool Image::loadFromFile(const std::filesystem::path& filename) bool Image::loadFromFile(const std::filesystem::path& filename)
{ {
#ifndef SFML_SYSTEM_ANDROID #ifdef SFML_SYSTEM_ANDROID
if (priv::getActivityStatesPtr() != nullptr)
{
priv::ResourceStream stream(filename);
return loadFromStream(stream);
}
#endif
// Clear the array (just in case) // Clear the array (just in case)
m_pixels.clear(); m_pixels.clear();
@ -186,13 +195,6 @@ bool Image::loadFromFile(const std::filesystem::path& filename)
return false; return false;
} }
#else
priv::ResourceStream stream(filename);
return loadFromStream(stream);
#endif
} }

View File

@ -740,7 +740,7 @@ bool Shader::isAvailable()
const TransientContextLock contextLock; const TransientContextLock contextLock;
// Make sure that extensions are initialized // Make sure that extensions are initialized
sf::priv::ensureExtensionsInit(); priv::ensureExtensionsInit();
return GLEXT_multitexture && GLEXT_shading_language_100 && GLEXT_shader_objects && GLEXT_vertex_shader && return GLEXT_multitexture && GLEXT_shading_language_100 && GLEXT_shader_objects && GLEXT_vertex_shader &&
GLEXT_fragment_shader; GLEXT_fragment_shader;
@ -758,7 +758,7 @@ bool Shader::isGeometryAvailable()
const TransientContextLock contextLock; const TransientContextLock contextLock;
// Make sure that extensions are initialized // Make sure that extensions are initialized
sf::priv::ensureExtensionsInit(); priv::ensureExtensionsInit();
return isAvailable() && (GLEXT_geometry_shader4 || GLEXT_GL_VERSION_3_2); return isAvailable() && (GLEXT_geometry_shader4 || GLEXT_GL_VERSION_3_2);
}(); }();

View File

@ -381,7 +381,7 @@ void Text::ensureGeometryUpdate() const
const bool isBold = m_style & Bold; const bool isBold = m_style & Bold;
const bool isUnderlined = m_style & Underlined; const bool isUnderlined = m_style & Underlined;
const bool isStrikeThrough = m_style & StrikeThrough; const bool isStrikeThrough = m_style & StrikeThrough;
const float italicShear = (m_style & Italic) ? sf::degrees(12).asRadians() : 0.f; const float italicShear = (m_style & Italic) ? degrees(12).asRadians() : 0.f;
const float underlineOffset = m_font->getUnderlinePosition(m_characterSize); const float underlineOffset = m_font->getUnderlinePosition(m_characterSize);
const float underlineThickness = m_font->getUnderlineThickness(m_characterSize); const float underlineThickness = m_font->getUnderlineThickness(m_characterSize);

View File

@ -37,6 +37,7 @@
#include <SFML/System/Err.hpp> #include <SFML/System/Err.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <atomic> #include <atomic>
#include <ostream> #include <ostream>
#include <utility> #include <utility>
@ -861,10 +862,10 @@ void Texture::bind(const Texture* texture, CoordinateType coordinateType)
if ((coordinateType == CoordinateType::Pixels) || texture->m_pixelsFlipped) if ((coordinateType == CoordinateType::Pixels) || texture->m_pixelsFlipped)
{ {
// clang-format off // clang-format off
GLfloat matrix[16] = {1.f, 0.f, 0.f, 0.f, std::array matrix = {1.f, 0.f, 0.f, 0.f,
0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f,
0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f,
0.f, 0.f, 0.f, 1.f}; 0.f, 0.f, 0.f, 1.f};
// clang-format on // clang-format on
// If non-normalized coordinates (= pixels) are requested, we need to // If non-normalized coordinates (= pixels) are requested, we need to
@ -884,7 +885,7 @@ void Texture::bind(const Texture* texture, CoordinateType coordinateType)
// Load the matrix // Load the matrix
glCheck(glMatrixMode(GL_TEXTURE)); glCheck(glMatrixMode(GL_TEXTURE));
glCheck(glLoadMatrixf(matrix)); glCheck(glLoadMatrixf(matrix.data()));
} }
else else
{ {
@ -921,7 +922,7 @@ unsigned int Texture::getMaximumSize()
GLint value = 0; GLint value = 0;
// Make sure that extensions are initialized // Make sure that extensions are initialized
sf::priv::ensureExtensionsInit(); priv::ensureExtensionsInit();
glCheck(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &value)); glCheck(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &value));

View File

@ -31,6 +31,8 @@
#include <SFML/System/String.hpp> #include <SFML/System/String.hpp>
#include <SFML/System/Utils.hpp> #include <SFML/System/Utils.hpp>
#include <array>
#include <cstring> #include <cstring>
#include <cwchar> #include <cwchar>
@ -212,8 +214,8 @@ Packet& Packet::operator>>(std::uint64_t& data)
{ {
// Since ntohll is not available everywhere, we have to convert // Since ntohll is not available everywhere, we have to convert
// to network byte order (big endian) manually // to network byte order (big endian) manually
std::byte bytes[sizeof(data)]; std::array<std::byte, sizeof(data)> bytes{};
std::memcpy(bytes, &m_data[m_readPos], sizeof(data)); std::memcpy(bytes.data(), &m_data[m_readPos], sizeof(data));
data = toInteger<std::uint64_t>(bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]); data = toInteger<std::uint64_t>(bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]);
@ -427,14 +429,14 @@ Packet& Packet::operator<<(std::int64_t data)
// Since htonll is not available everywhere, we have to convert // Since htonll is not available everywhere, we have to convert
// to network byte order (big endian) manually // to network byte order (big endian) manually
std::uint8_t toWrite[] = {static_cast<std::uint8_t>((data >> 56) & 0xFF), std::array toWrite = {static_cast<std::uint8_t>((data >> 56) & 0xFF),
static_cast<std::uint8_t>((data >> 48) & 0xFF), static_cast<std::uint8_t>((data >> 48) & 0xFF),
static_cast<std::uint8_t>((data >> 40) & 0xFF), static_cast<std::uint8_t>((data >> 40) & 0xFF),
static_cast<std::uint8_t>((data >> 32) & 0xFF), static_cast<std::uint8_t>((data >> 32) & 0xFF),
static_cast<std::uint8_t>((data >> 24) & 0xFF), static_cast<std::uint8_t>((data >> 24) & 0xFF),
static_cast<std::uint8_t>((data >> 16) & 0xFF), static_cast<std::uint8_t>((data >> 16) & 0xFF),
static_cast<std::uint8_t>((data >> 8) & 0xFF), static_cast<std::uint8_t>((data >> 8) & 0xFF),
static_cast<std::uint8_t>((data)&0xFF)}; static_cast<std::uint8_t>((data)&0xFF)};
append(&toWrite, sizeof(toWrite)); append(&toWrite, sizeof(toWrite));
return *this; return *this;
@ -447,14 +449,14 @@ Packet& Packet::operator<<(std::uint64_t data)
// Since htonll is not available everywhere, we have to convert // Since htonll is not available everywhere, we have to convert
// to network byte order (big endian) manually // to network byte order (big endian) manually
std::uint8_t toWrite[] = {static_cast<std::uint8_t>((data >> 56) & 0xFF), std::array toWrite = {static_cast<std::uint8_t>((data >> 56) & 0xFF),
static_cast<std::uint8_t>((data >> 48) & 0xFF), static_cast<std::uint8_t>((data >> 48) & 0xFF),
static_cast<std::uint8_t>((data >> 40) & 0xFF), static_cast<std::uint8_t>((data >> 40) & 0xFF),
static_cast<std::uint8_t>((data >> 32) & 0xFF), static_cast<std::uint8_t>((data >> 32) & 0xFF),
static_cast<std::uint8_t>((data >> 24) & 0xFF), static_cast<std::uint8_t>((data >> 24) & 0xFF),
static_cast<std::uint8_t>((data >> 16) & 0xFF), static_cast<std::uint8_t>((data >> 16) & 0xFF),
static_cast<std::uint8_t>((data >> 8) & 0xFF), static_cast<std::uint8_t>((data >> 8) & 0xFF),
static_cast<std::uint8_t>((data)&0xFF)}; static_cast<std::uint8_t>((data)&0xFF)};
append(&toWrite, sizeof(toWrite)); append(&toWrite, sizeof(toWrite));
return *this; return *this;

View File

@ -33,6 +33,7 @@
#include <SFML/System/Err.hpp> #include <SFML/System/Err.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <ostream> #include <ostream>
#include <cstring> #include <cstring>
@ -401,12 +402,12 @@ Socket::Status TcpSocket::receive(Packet& packet)
} }
// Loop until we receive all the packet data // Loop until we receive all the packet data
char buffer[1024]; std::array<char, 1024> buffer{};
while (m_pendingPacket.data.size() < packetSize) while (m_pendingPacket.data.size() < packetSize)
{ {
// Receive a chunk of data // Receive a chunk of data
const std::size_t sizeToGet = std::min(packetSize - m_pendingPacket.data.size(), sizeof(buffer)); const std::size_t sizeToGet = std::min(packetSize - m_pendingPacket.data.size(), sizeof(buffer));
const Status status = receive(buffer, sizeToGet, received); const Status status = receive(buffer.data(), sizeToGet, received);
if (status != Status::Done) if (status != Status::Done)
return status; return status;
@ -415,7 +416,7 @@ Socket::Status TcpSocket::receive(Packet& packet)
{ {
m_pendingPacket.data.resize(m_pendingPacket.data.size() + received); m_pendingPacket.data.resize(m_pendingPacket.data.size() + received);
std::byte* begin = m_pendingPacket.data.data() + m_pendingPacket.data.size() - received; std::byte* begin = m_pendingPacket.data.data() + m_pendingPacket.data.size() - received;
std::memcpy(begin, buffer, received); std::memcpy(begin, buffer.data(), received);
} }
} }

View File

@ -45,6 +45,7 @@
#include <SFML/System/Vector2.hpp> #include <SFML/System/Vector2.hpp>
#include <algorithm> #include <algorithm>
#include <array>
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>

View File

@ -27,6 +27,7 @@
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#include <SFML/System/FileInputStream.hpp> #include <SFML/System/FileInputStream.hpp>
#ifdef SFML_SYSTEM_ANDROID #ifdef SFML_SYSTEM_ANDROID
#include <SFML/System/Android/Activity.hpp>
#include <SFML/System/Android/ResourceStream.hpp> #include <SFML/System/Android/ResourceStream.hpp>
#endif #endif
#include <memory> #include <memory>
@ -36,12 +37,10 @@
namespace sf namespace sf
{ {
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
#ifndef SFML_SYSTEM_ANDROID
void FileInputStream::FileCloser::operator()(std::FILE* file) void FileInputStream::FileCloser::operator()(std::FILE* file)
{ {
std::fclose(file); std::fclose(file);
} }
#endif
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@ -64,69 +63,88 @@ FileInputStream& FileInputStream::operator=(FileInputStream&&) noexcept = defaul
bool FileInputStream::open(const std::filesystem::path& filename) bool FileInputStream::open(const std::filesystem::path& filename)
{ {
#ifdef SFML_SYSTEM_ANDROID #ifdef SFML_SYSTEM_ANDROID
m_file = std::make_unique<priv::ResourceStream>(filename); if (priv::getActivityStatesPtr() != nullptr)
return m_file->tell() != -1; {
#else m_androidFile = std::make_unique<priv::ResourceStream>(filename);
return m_androidFile->tell() != -1;
}
#endif
#ifdef SFML_SYSTEM_WINDOWS #ifdef SFML_SYSTEM_WINDOWS
m_file.reset(_wfopen(filename.c_str(), L"rb")); m_file.reset(_wfopen(filename.c_str(), L"rb"));
#else #else
m_file.reset(std::fopen(filename.c_str(), "rb")); m_file.reset(std::fopen(filename.c_str(), "rb"));
#endif #endif
return m_file != nullptr; return m_file != nullptr;
#endif
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::int64_t FileInputStream::read(void* data, std::int64_t size) std::int64_t FileInputStream::read(void* data, std::int64_t size)
{ {
#ifdef SFML_SYSTEM_ANDROID
if (priv::getActivityStatesPtr() != nullptr)
{
if (!m_androidFile)
return -1;
return m_androidFile->read(data, size);
}
#endif
if (!m_file) if (!m_file)
return -1; return -1;
#ifdef SFML_SYSTEM_ANDROID
return m_file->read(data, size);
#else
return static_cast<std::int64_t>(std::fread(data, 1, static_cast<std::size_t>(size), m_file.get())); return static_cast<std::int64_t>(std::fread(data, 1, static_cast<std::size_t>(size), m_file.get()));
#endif
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::int64_t FileInputStream::seek(std::int64_t position) std::int64_t FileInputStream::seek(std::int64_t position)
{ {
#ifdef SFML_SYSTEM_ANDROID
if (priv::getActivityStatesPtr() != nullptr)
{
if (!m_androidFile)
return -1;
return m_androidFile->seek(position);
}
#endif
if (!m_file) if (!m_file)
return -1; return -1;
#ifdef SFML_SYSTEM_ANDROID
return m_file->seek(position);
#else
if (std::fseek(m_file.get(), static_cast<long>(position), SEEK_SET)) if (std::fseek(m_file.get(), static_cast<long>(position), SEEK_SET))
return -1; return -1;
return tell(); return tell();
#endif
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::int64_t FileInputStream::tell() std::int64_t FileInputStream::tell()
{ {
#ifdef SFML_SYSTEM_ANDROID
if (priv::getActivityStatesPtr() != nullptr)
{
if (!m_androidFile)
return -1;
return m_androidFile->tell();
}
#endif
if (!m_file) if (!m_file)
return -1; return -1;
#ifdef SFML_SYSTEM_ANDROID
return m_file->tell();
#else
return std::ftell(m_file.get()); return std::ftell(m_file.get());
#endif
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
std::int64_t FileInputStream::getSize() std::int64_t FileInputStream::getSize()
{ {
#ifdef SFML_SYSTEM_ANDROID
if (priv::getActivityStatesPtr() != nullptr)
{
if (!m_androidFile)
return -1;
return m_androidFile->getSize();
}
#endif
if (!m_file) if (!m_file)
return -1; return -1;
#ifdef SFML_SYSTEM_ANDROID
return m_file->getSize();
#else
const std::int64_t position = tell(); const std::int64_t position = tell();
std::fseek(m_file.get(), 0, SEEK_END); std::fseek(m_file.get(), 0, SEEK_END);
const std::int64_t size = tell(); const std::int64_t size = tell();
@ -135,7 +153,6 @@ std::int64_t FileInputStream::getSize()
return -1; return -1;
return size; return size;
#endif
} }
} // namespace sf } // namespace sf

View File

@ -264,10 +264,10 @@ std::wstring String::toWideString() const
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
sf::U8String String::toUtf8() const U8String String::toUtf8() const
{ {
// Prepare the output string // Prepare the output string
sf::U8String output; U8String output;
output.reserve(m_string.length()); output.reserve(m_string.length());
// Convert // Convert

View File

@ -38,7 +38,7 @@ namespace sf::priv
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
String ClipboardImpl::getString() String ClipboardImpl::getString()
{ {
sf::err() << "Clipboard API not implemented for Android.\n"; err() << "Clipboard API not implemented for Android.\n";
return {}; return {};
} }
@ -46,7 +46,7 @@ String ClipboardImpl::getString()
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
void ClipboardImpl::setString(const String& /* text */) void ClipboardImpl::setString(const String& /* text */)
{ {
sf::err() << "Clipboard API not implemented for Android.\n"; err() << "Clipboard API not implemented for Android.\n";
} }
} // namespace sf::priv } // namespace sf::priv

View File

@ -55,7 +55,7 @@ struct JoystickState
{ {
bool connected{}; //!< Is the joystick currently connected? bool connected{}; //!< Is the joystick currently connected?
EnumArray<Joystick::Axis, float, Joystick::AxisCount> axes{}; //!< Position of each axis, in range [-100, 100] EnumArray<Joystick::Axis, float, Joystick::AxisCount> axes{}; //!< Position of each axis, in range [-100, 100]
bool buttons[Joystick::ButtonCount]{}; //!< Status of each button (true = pressed) std::array<bool, Joystick::ButtonCount> buttons{}; //!< Status of each button (true = pressed)
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -30,6 +30,8 @@
#include <SFML/Window/Joystick.hpp> #include <SFML/Window/Joystick.hpp>
#include <SFML/Window/JoystickImpl.hpp> #include <SFML/Window/JoystickImpl.hpp>
#include <array>
namespace sf::priv namespace sf::priv
{ {
@ -124,7 +126,7 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Item m_joysticks[Joystick::Count]; //!< Joysticks information and state std::array<Item, Joystick::Count> m_joysticks; //!< Joysticks information and state
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -33,6 +33,7 @@
#include <SFML/System/Err.hpp> #include <SFML/System/Err.hpp>
#include <array>
#include <mutex> #include <mutex>
#include <ostream> #include <ostream>
#include <vector> #include <vector>
@ -513,10 +514,10 @@ void GlxContext::createSurface(GlxContext* shared, const Vector2u& size, unsigne
if (config) if (config)
{ {
int attributes[] = std::array attributes =
{GLX_PBUFFER_WIDTH, static_cast<int>(size.x), GLX_PBUFFER_HEIGHT, static_cast<int>(size.y), 0, 0}; {GLX_PBUFFER_WIDTH, static_cast<int>(size.x), GLX_PBUFFER_HEIGHT, static_cast<int>(size.y), 0, 0};
m_pbuffer = glXCreatePbuffer(m_display.get(), *config, attributes); m_pbuffer = glXCreatePbuffer(m_display.get(), *config, attributes.data());
updateSettingsFromVisualInfo(&visualInfo); updateSettingsFromVisualInfo(&visualInfo);
@ -578,11 +579,11 @@ void GlxContext::createContext(GlxContext* shared)
glXQueryDrawable(m_display.get(), m_pbuffer, GLX_FBCONFIG_ID, &fbConfigId); glXQueryDrawable(m_display.get(), m_pbuffer, GLX_FBCONFIG_ID, &fbConfigId);
int attributes[] = {GLX_FBCONFIG_ID, static_cast<int>(fbConfigId), 0, 0}; std::array attributes = {GLX_FBCONFIG_ID, static_cast<int>(fbConfigId), 0, 0};
int count = 0; int count = 0;
const auto fbconfig = X11Ptr<GLXFBConfig>( const auto fbconfig = X11Ptr<GLXFBConfig>(
glXChooseFBConfig(m_display.get(), DefaultScreen(m_display.get()), attributes, &count)); glXChooseFBConfig(m_display.get(), DefaultScreen(m_display.get()), attributes.data(), &count));
if (count == 1) if (count == 1)
visualInfo = X11Ptr<XVisualInfo>(glXGetVisualFromFBConfig(m_display.get(), *fbconfig)); visualInfo = X11Ptr<XVisualInfo>(glXGetVisualFromFBConfig(m_display.get(), *fbconfig));

View File

@ -400,14 +400,13 @@ std::string getJoystickName(unsigned int index)
if (fd >= 0) if (fd >= 0)
{ {
// Get the name // Get the name
char name[128] = {}; std::array<char, 128> name{};
const int result = ioctl(fd, JSIOCGNAME(name.size()), name.data());
const int result = ioctl(fd, JSIOCGNAME(sizeof(name)), name);
::close(fd); ::close(fd);
if (result >= 0) if (result >= 0)
return name; return name.data();
} }
// Fall back to manual USB chain walk via udev // Fall back to manual USB chain walk via udev
@ -600,7 +599,7 @@ JoystickCaps JoystickImpl::getCapabilities() const
ioctl(m_file, JSIOCGAXES, &axesCount); ioctl(m_file, JSIOCGAXES, &axesCount);
for (int i = 0; i < axesCount; ++i) for (int i = 0; i < axesCount; ++i)
{ {
switch (m_mapping[i]) switch (m_mapping[static_cast<std::size_t>(i)])
{ {
// clang-format off // clang-format off
case ABS_X: caps.axes[Joystick::Axis::X] = true; break; case ABS_X: caps.axes[Joystick::Axis::X] = true; break;
@ -650,7 +649,7 @@ JoystickState JoystickImpl::JoystickImpl::update()
{ {
const float value = joyState.value * 100.f / 32767.f; const float value = joyState.value * 100.f / 32767.f;
if (joyState.number < ABS_MAX + 1) if (joyState.number < m_mapping.size())
{ {
switch (m_mapping[joyState.number]) switch (m_mapping[joyState.number])
{ {

View File

@ -105,10 +105,10 @@ private:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
int m_file{-1}; ///< File descriptor of the joystick int m_file{-1}; ///< File descriptor of the joystick
char m_mapping[ABS_MAX + 1]{0}; ///< Axes mapping (index to axis id) std::array<char, ABS_CNT> m_mapping{}; ///< Axes mapping (index to axis id)
JoystickState m_state; ///< Current state of the joystick JoystickState m_state; ///< Current state of the joystick
sf::Joystick::Identification m_identification; ///< Identification of the joystick Joystick::Identification m_identification; ///< Identification of the joystick
}; };
} // namespace sf::priv } // namespace sf::priv

View File

@ -138,7 +138,7 @@ bool WindowBase::isOpen() const
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Event WindowBase::pollEvent() Event WindowBase::pollEvent()
{ {
sf::Event event; Event event;
if (m_impl && (event = m_impl->popEvent(false))) if (m_impl && (event = m_impl->popEvent(false)))
filterEvent(event); filterEvent(event);
return event; return event;
@ -148,7 +148,7 @@ Event WindowBase::pollEvent()
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Event WindowBase::waitEvent() Event WindowBase::waitEvent()
{ {
sf::Event event; Event event;
if (m_impl && (event = m_impl->popEvent(true))) if (m_impl && (event = m_impl->popEvent(true)))
filterEvent(event); filterEvent(event);
return event; return event;

View File

@ -34,6 +34,7 @@
#include <SFML/System/Sleep.hpp> #include <SFML/System/Sleep.hpp>
#include <SFML/System/Time.hpp> #include <SFML/System/Time.hpp>
#include <array>
#include <memory> #include <memory>
#include <cmath> #include <cmath>
@ -96,7 +97,7 @@ namespace sf::priv
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
struct WindowImpl::JoystickStatesImpl struct WindowImpl::JoystickStatesImpl
{ {
JoystickState states[Joystick::Count]{}; //!< Previous state of the joysticks std::array<JoystickState, Joystick::Count> states{}; //!< Previous state of the joysticks
}; };
@ -131,7 +132,7 @@ WindowImpl::WindowImpl() : m_joystickStatesImpl(std::make_unique<JoystickStatesI
} }
// Get the initial sensor states // Get the initial sensor states
for (sf::Vector3f& vec : m_sensorValue) for (Vector3f& vec : m_sensorValue)
vec = Vector3f(0, 0, 0); vec = Vector3f(0, 0, 0);
} }
@ -202,7 +203,7 @@ Event WindowImpl::popEvent(bool block)
} }
} }
sf::Event event; Event event;
// Pop the first event of the queue, if it is not empty // Pop the first event of the queue, if it is not empty
if (!m_events.empty()) if (!m_events.empty())

View File

@ -34,11 +34,6 @@
namespace namespace
{ {
unsigned int deviceMotionEnabledCount = 0; unsigned int deviceMotionEnabledCount = 0;
float toDegrees(float radians)
{
return sf::radians(radians).asDegrees();
}
} }
@ -145,10 +140,10 @@ Vector3f SensorImpl::update()
break; break;
case Sensor::Type::Gyroscope: case Sensor::Type::Gyroscope:
// Rotation rates are given in rad/s, convert to deg/s // Rotation rates are given in rad/s
value.x = toDegrees(static_cast<float>(manager.gyroData.rotationRate.x)); value.x = static_cast<float>(manager.gyroData.rotationRate.x);
value.y = toDegrees(static_cast<float>(manager.gyroData.rotationRate.y)); value.y = static_cast<float>(manager.gyroData.rotationRate.y);
value.z = toDegrees(static_cast<float>(manager.gyroData.rotationRate.z)); value.z = static_cast<float>(manager.gyroData.rotationRate.z);
break; break;
case Sensor::Type::Magnetometer: case Sensor::Type::Magnetometer:
@ -166,10 +161,10 @@ Vector3f SensorImpl::update()
break; break;
case Sensor::Type::Orientation: case Sensor::Type::Orientation:
// Absolute rotation (Euler) angles are given in radians, convert to degrees // Absolute rotation (Euler) angles are given in radians
value.x = toDegrees(static_cast<float>(manager.deviceMotion.attitude.yaw)); value.x = static_cast<float>(manager.deviceMotion.attitude.yaw);
value.y = toDegrees(static_cast<float>(manager.deviceMotion.attitude.pitch)); value.y = static_cast<float>(manager.deviceMotion.attitude.pitch);
value.z = toDegrees(static_cast<float>(manager.deviceMotion.attitude.roll)); value.z = static_cast<float>(manager.deviceMotion.attitude.roll);
break; break;
default: default:

View File

@ -29,6 +29,8 @@
#include <SFML/Window/macOS/HIDInputManager.hpp> #include <SFML/Window/macOS/HIDInputManager.hpp>
#include <SFML/Window/macOS/HIDJoystickManager.hpp> #include <SFML/Window/macOS/HIDJoystickManager.hpp>
#include <array>
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
// Private data // Private data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@ -75,11 +77,8 @@ HIDJoystickManager::HIDJoystickManager()
CFDictionaryRef mask1 = HIDInputManager::copyDevicesMask(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad); CFDictionaryRef mask1 = HIDInputManager::copyDevicesMask(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad);
CFDictionaryRef maskArray[2]; std::array maskArray = {mask0, mask1};
maskArray[0] = mask0; CFArrayRef mask = CFArrayCreate(nullptr, reinterpret_cast<const void**>(maskArray.data()), maskArray.size(), nullptr);
maskArray[1] = mask1;
CFArrayRef mask = CFArrayCreate(nullptr, reinterpret_cast<const void**>(maskArray), 2, nullptr);
IOHIDManagerSetDeviceMatchingMultiple(m_manager, mask); IOHIDManagerSetDeviceMatchingMultiple(m_manager, mask);
CFRelease(mask); CFRelease(mask);

View File

@ -111,7 +111,7 @@ private:
// Member data // Member data
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
using Location = long; using Location = long;
using AxisMap = std::unordered_map<sf::Joystick::Axis, IOHIDElementRef>; using AxisMap = std::unordered_map<Joystick::Axis, IOHIDElementRef>;
using ButtonsVector = std::vector<IOHIDElementRef>; using ButtonsVector = std::vector<IOHIDElementRef>;
AxisMap m_axis; ///< Axes (but not POV/Hat) of the joystick AxisMap m_axis; ///< Axes (but not POV/Hat) of the joystick
@ -121,7 +121,7 @@ private:
Joystick::Identification m_identification; ///< Joystick identification Joystick::Identification m_identification; ///< Joystick identification
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
static inline Location m_locationIDs[sf::Joystick::Count]{}; ///< Global Joystick register static inline std::array<Location, Joystick::Count> m_locationIDs{}; ///< Global Joystick register
/// For a corresponding SFML index, m_locationIDs is either some USB /// For a corresponding SFML index, m_locationIDs is either some USB
/// location or 0 if there isn't currently a connected joystick device /// location or 0 if there isn't currently a connected joystick device
}; };

View File

@ -229,7 +229,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons
{ {
if (!(m_settings.attributeFlags & ContextSettings::Core)) if (!(m_settings.attributeFlags & ContextSettings::Core))
{ {
sf::err() << "Warning. Compatibility profile not supported on this platform." << std::endl; err() << "Warning. Compatibility profile not supported on this platform." << std::endl;
m_settings.attributeFlags |= ContextSettings::Core; m_settings.attributeFlags |= ContextSettings::Core;
} }
m_settings.majorVersion = 3; m_settings.majorVersion = 3;
@ -240,7 +240,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons
if (m_settings.attributeFlags & ContextSettings::Debug) if (m_settings.attributeFlags & ContextSettings::Debug)
{ {
sf::err() << "Warning. OpenGL debugging not supported on this platform." << std::endl; err() << "Warning. OpenGL debugging not supported on this platform." << std::endl;
m_settings.attributeFlags &= ~static_cast<unsigned int>(ContextSettings::Debug); m_settings.attributeFlags &= ~static_cast<unsigned int>(ContextSettings::Debug);
} }
@ -254,7 +254,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons
if (pixFmt == nil) if (pixFmt == nil)
{ {
sf::err() << "Error. Unable to find a suitable pixel format." << std::endl; err() << "Error. Unable to find a suitable pixel format." << std::endl;
return; return;
} }
@ -267,7 +267,7 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons
if (sharedContext == [NSOpenGLContext currentContext]) if (sharedContext == [NSOpenGLContext currentContext])
{ {
sf::err() << "Failed to deactivate shared context before sharing" << std::endl; err() << "Failed to deactivate shared context before sharing" << std::endl;
return; return;
} }
} }
@ -277,13 +277,13 @@ void SFContext::createContext(SFContext* shared, unsigned int bitsPerPixel, cons
if (m_context == nil) if (m_context == nil)
{ {
sf::err() << "Error. Unable to create the context. Retrying without shared context." << std::endl; err() << "Error. Unable to create the context. Retrying without shared context." << std::endl;
m_context = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:nil]; m_context = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:nil];
if (m_context == nil) if (m_context == nil)
sf::err() << "Error. Unable to create the context." << std::endl; err() << "Error. Unable to create the context." << std::endl;
else else
sf::err() << "Warning. New context created without shared context." << std::endl; err() << "Warning. New context created without shared context." << std::endl;
} }
// Free up. // Free up.

View File

@ -1,8 +1,209 @@
#include <SFML/Audio/Music.hpp> #include <SFML/Audio/Music.hpp>
// Other 1st party headers
#include <SFML/System/FileInputStream.hpp>
#include <catch2/catch_test_macros.hpp>
#include <AudioUtil.hpp>
#include <SystemUtil.hpp>
#include <array>
#include <fstream>
#include <thread>
#include <type_traits> #include <type_traits>
static_assert(!std::is_copy_constructible_v<sf::Music>); TEST_CASE("[Audio] sf::Music", runAudioDeviceTests())
static_assert(!std::is_copy_assignable_v<sf::Music>); {
static_assert(!std::is_nothrow_move_constructible_v<sf::Music>); SECTION("Type traits")
static_assert(!std::is_nothrow_move_assignable_v<sf::Music>); {
STATIC_CHECK(!std::is_copy_constructible_v<sf::Music>);
STATIC_CHECK(!std::is_copy_assignable_v<sf::Music>);
STATIC_CHECK(!std::is_nothrow_move_constructible_v<sf::Music>);
STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::Music>);
STATIC_CHECK(std::has_virtual_destructor_v<sf::Music>);
}
SECTION("Span")
{
const sf::Music::Span<float> span;
CHECK(span.offset == 0);
CHECK(span.length == 0);
const sf::Music::TimeSpan timeSpan;
CHECK(timeSpan.offset == sf::Time::Zero);
CHECK(timeSpan.length == sf::Time::Zero);
}
SECTION("Construction")
{
const sf::Music music;
CHECK(music.getDuration() == sf::Time::Zero);
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::Time::Zero);
CHECK(music.getChannelCount() == 0);
CHECK(music.getSampleRate() == 0);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
SECTION("openFromFile()")
{
sf::Music music;
SECTION("Invalid file")
{
REQUIRE(!music.openFromFile("does/not/exist.wav"));
CHECK(music.getDuration() == sf::Time::Zero);
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::Time::Zero);
CHECK(music.getChannelCount() == 0);
CHECK(music.getSampleRate() == 0);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
SECTION("Valid file")
{
REQUIRE(music.openFromFile("Audio/ding.mp3"));
CHECK(music.getDuration() == sf::microseconds(1990884));
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::microseconds(1990884));
CHECK(music.getChannelCount() == 1);
CHECK(music.getSampleRate() == 44100);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
}
SECTION("openFromMemory()")
{
std::vector<std::byte> memory;
sf::Music music;
SECTION("Invalid buffer")
{
REQUIRE(!music.openFromMemory(memory.data(), memory.size()));
CHECK(music.getDuration() == sf::Time::Zero);
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::Time::Zero);
CHECK(music.getChannelCount() == 0);
CHECK(music.getSampleRate() == 0);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
SECTION("Valid buffer")
{
memory = loadIntoMemory("Audio/ding.flac");
REQUIRE(music.openFromMemory(memory.data(), memory.size()));
CHECK(music.getDuration() == sf::microseconds(1990884));
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::microseconds(1990884));
CHECK(music.getChannelCount() == 1);
CHECK(music.getSampleRate() == 44100);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
}
SECTION("openFromStream()")
{
sf::FileInputStream stream;
sf::Music music;
SECTION("Invalid stream")
{
CHECK(!music.openFromStream(stream));
CHECK(music.getDuration() == sf::Time::Zero);
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::Time::Zero);
CHECK(music.getChannelCount() == 0);
CHECK(music.getSampleRate() == 0);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
SECTION("Valid stream")
{
REQUIRE(stream.open("Audio/doodle_pop.ogg"));
REQUIRE(music.openFromStream(stream));
CHECK(music.getDuration() == sf::microseconds(24002176));
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::microseconds(24002176));
CHECK(music.getChannelCount() == 2);
CHECK(music.getSampleRate() == 44100);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
}
SECTION("play/pause/stop")
{
sf::Music music;
REQUIRE(music.openFromFile("Audio/ding.mp3"));
// Wait for background thread to start
music.play();
while (music.getStatus() == sf::Music::Status::Stopped)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(music.getStatus() == sf::Music::Status::Playing);
// Wait for background thread to pause
music.pause();
while (music.getStatus() == sf::Music::Status::Playing)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(music.getStatus() == sf::Music::Status::Paused);
// Wait for background thread to stop
music.stop();
while (music.getStatus() == sf::Music::Status::Paused)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(music.getStatus() == sf::Music::Status::Stopped);
}
SECTION("setLoopPoints()")
{
sf::Music music;
SECTION("No file")
{
music.setLoopPoints({sf::Time::Zero, sf::Time::Zero});
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::Time::Zero);
CHECK(length == sf::Time::Zero);
CHECK(music.getChannelCount() == 0);
CHECK(music.getSampleRate() == 0);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
SECTION("Loaded file")
{
REQUIRE(music.openFromFile("Audio/killdeer.wav"));
music.setLoopPoints({sf::seconds(1), sf::seconds(2)});
const auto [offset, length] = music.getLoopPoints();
CHECK(offset == sf::seconds(1));
CHECK(length == sf::seconds(2));
CHECK(music.getChannelCount() == 1);
CHECK(music.getSampleRate() == 22050);
CHECK(music.getStatus() == sf::Music::Status::Stopped);
CHECK(music.getPlayingOffset() == sf::Time::Zero);
CHECK(!music.getLoop());
}
}
}

View File

@ -1,11 +1,86 @@
#include <SFML/Audio/Sound.hpp> #include <SFML/Audio/Sound.hpp>
// Other 1st party headers
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/System/Time.hpp>
#include <catch2/catch_test_macros.hpp>
#include <AudioUtil.hpp>
#include <SystemUtil.hpp>
#include <type_traits> #include <type_traits>
static_assert(!std::is_constructible_v<sf::Sound, sf::SoundBuffer&&>); TEST_CASE("[Audio] sf::Sound", runAudioDeviceTests())
static_assert(std::is_copy_constructible_v<sf::Sound>); {
static_assert(std::is_copy_assignable_v<sf::Sound>); SECTION("Type traits")
static_assert(std::is_move_constructible_v<sf::Sound>); {
static_assert(!std::is_nothrow_move_constructible_v<sf::Sound>); STATIC_CHECK(!std::is_constructible_v<sf::Sound, sf::SoundBuffer&&>);
static_assert(std::is_move_assignable_v<sf::Sound>); STATIC_CHECK(std::is_copy_constructible_v<sf::Sound>);
static_assert(!std::is_nothrow_move_assignable_v<sf::Sound>); STATIC_CHECK(std::is_copy_assignable_v<sf::Sound>);
STATIC_CHECK(std::is_move_constructible_v<sf::Sound>);
STATIC_CHECK(!std::is_nothrow_move_constructible_v<sf::Sound>);
STATIC_CHECK(std::is_move_assignable_v<sf::Sound>);
STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::Sound>);
STATIC_CHECK(std::has_virtual_destructor_v<sf::Sound>);
}
sf::SoundBuffer soundBuffer;
REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac"));
SECTION("Construction")
{
const sf::Sound sound(soundBuffer);
CHECK(&sound.getBuffer() == &soundBuffer);
CHECK(!sound.getLoop());
CHECK(sound.getPlayingOffset() == sf::Time::Zero);
CHECK(sound.getStatus() == sf::Sound::Status::Stopped);
}
SECTION("Copy semantics")
{
const sf::Sound sound(soundBuffer);
SECTION("Construction")
{
const sf::Sound soundCopy(sound); // NOLINT(performance-unnecessary-copy-initialization)
CHECK(&soundCopy.getBuffer() == &soundBuffer);
CHECK(!soundCopy.getLoop());
CHECK(soundCopy.getPlayingOffset() == sf::Time::Zero);
CHECK(soundCopy.getStatus() == sf::Sound::Status::Stopped);
}
SECTION("Assignment")
{
const sf::SoundBuffer emptySoundBuffer;
sf::Sound soundCopy(emptySoundBuffer);
soundCopy = sound;
CHECK(&soundCopy.getBuffer() == &soundBuffer);
CHECK(!soundCopy.getLoop());
CHECK(soundCopy.getPlayingOffset() == sf::Time::Zero);
CHECK(soundCopy.getStatus() == sf::Sound::Status::Stopped);
}
}
SECTION("Set/get buffer")
{
const sf::SoundBuffer otherSoundBuffer;
sf::Sound sound(soundBuffer);
sound.setBuffer(otherSoundBuffer);
CHECK(&sound.getBuffer() == &otherSoundBuffer);
}
SECTION("Set/get loop")
{
sf::Sound sound(soundBuffer);
sound.setLoop(true);
CHECK(sound.getLoop());
}
SECTION("Set/get playing offset")
{
sf::Sound sound(soundBuffer);
sound.setPlayingOffset(sf::seconds(10));
CHECK(sound.getPlayingOffset() == sf::seconds(10));
}
}

View File

@ -1,10 +1,147 @@
#include <SFML/Audio/SoundBuffer.hpp> #include <SFML/Audio/SoundBuffer.hpp>
// Other 1st party headers
#include <SFML/System/FileInputStream.hpp>
#include <catch2/catch_test_macros.hpp>
#include <AudioUtil.hpp>
#include <SystemUtil.hpp>
#include <array>
#include <type_traits> #include <type_traits>
static_assert(std::is_copy_constructible_v<sf::SoundBuffer>); TEST_CASE("[Audio] sf::SoundBuffer", runAudioDeviceTests())
static_assert(std::is_copy_assignable_v<sf::SoundBuffer>); {
static_assert(std::is_move_constructible_v<sf::SoundBuffer>); SECTION("Type traits")
static_assert(!std::is_nothrow_move_constructible_v<sf::SoundBuffer>); {
static_assert(std::is_move_assignable_v<sf::SoundBuffer>); STATIC_CHECK(std::is_copy_constructible_v<sf::SoundBuffer>);
static_assert(!std::is_nothrow_move_assignable_v<sf::SoundBuffer>); STATIC_CHECK(std::is_copy_assignable_v<sf::SoundBuffer>);
STATIC_CHECK(std::is_move_constructible_v<sf::SoundBuffer>);
STATIC_CHECK(!std::is_nothrow_move_constructible_v<sf::SoundBuffer>);
STATIC_CHECK(std::is_move_assignable_v<sf::SoundBuffer>);
STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::SoundBuffer>);
}
SECTION("Construction")
{
const sf::SoundBuffer soundBuffer;
CHECK(soundBuffer.getSamples() == nullptr);
CHECK(soundBuffer.getSampleCount() == 0);
CHECK(soundBuffer.getSampleRate() == 44100);
CHECK(soundBuffer.getChannelCount() == 1);
CHECK(soundBuffer.getDuration() == sf::Time::Zero);
}
SECTION("Copy semantics")
{
sf::SoundBuffer soundBuffer;
REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac"));
SECTION("Construction")
{
const sf::SoundBuffer soundBufferCopy(soundBuffer); // NOLINT(performance-unnecessary-copy-initialization)
CHECK(soundBufferCopy.getSamples() != nullptr);
CHECK(soundBufferCopy.getSampleCount() == 87798);
CHECK(soundBufferCopy.getSampleRate() == 44100);
CHECK(soundBufferCopy.getChannelCount() == 1);
CHECK(soundBufferCopy.getDuration() == sf::microseconds(1990884));
}
SECTION("Assignment")
{
sf::SoundBuffer soundBufferCopy;
soundBufferCopy = soundBuffer;
CHECK(soundBufferCopy.getSamples() != nullptr);
CHECK(soundBufferCopy.getSampleCount() == 87798);
CHECK(soundBufferCopy.getSampleRate() == 44100);
CHECK(soundBufferCopy.getChannelCount() == 1);
CHECK(soundBufferCopy.getDuration() == sf::microseconds(1990884));
}
}
SECTION("loadFromFile()")
{
sf::SoundBuffer soundBuffer;
SECTION("Invalid filename")
{
CHECK(!soundBuffer.loadFromFile("does/not/exist.wav"));
}
SECTION("Valid file")
{
REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac"));
CHECK(soundBuffer.getSamples() != nullptr);
CHECK(soundBuffer.getSampleCount() == 87798);
CHECK(soundBuffer.getSampleRate() == 44100);
CHECK(soundBuffer.getChannelCount() == 1);
CHECK(soundBuffer.getDuration() == sf::microseconds(1990884));
}
}
SECTION("loadFromMemory()")
{
sf::SoundBuffer soundBuffer;
SECTION("Invalid memory")
{
CHECK(!soundBuffer.loadFromMemory(nullptr, 0));
constexpr std::array<std::byte, 5> memory{};
CHECK(!soundBuffer.loadFromMemory(memory.data(), memory.size()));
}
SECTION("Valid memory")
{
const auto memory = loadIntoMemory("Audio/ding.flac");
REQUIRE(soundBuffer.loadFromMemory(memory.data(), memory.size()));
CHECK(soundBuffer.getSamples() != nullptr);
CHECK(soundBuffer.getSampleCount() == 87798);
CHECK(soundBuffer.getSampleRate() == 44100);
CHECK(soundBuffer.getChannelCount() == 1);
CHECK(soundBuffer.getDuration() == sf::microseconds(1990884));
}
}
SECTION("loadFromStream()")
{
sf::FileInputStream stream;
sf::SoundBuffer soundBuffer;
SECTION("Invalid stream")
{
CHECK(!soundBuffer.loadFromStream(stream));
}
SECTION("Valid stream")
{
REQUIRE(stream.open("Audio/ding.flac"));
REQUIRE(soundBuffer.loadFromStream(stream));
CHECK(soundBuffer.getSamples() != nullptr);
CHECK(soundBuffer.getSampleCount() == 87798);
CHECK(soundBuffer.getSampleRate() == 44100);
CHECK(soundBuffer.getChannelCount() == 1);
CHECK(soundBuffer.getDuration() == sf::microseconds(1990884));
}
}
SECTION("saveToFile()")
{
const auto filename = std::filesystem::temp_directory_path() / "ding.flac";
{
sf::SoundBuffer soundBuffer;
REQUIRE(soundBuffer.loadFromFile("Audio/ding.flac"));
REQUIRE(soundBuffer.saveToFile(filename));
}
sf::SoundBuffer soundBuffer;
REQUIRE(soundBuffer.loadFromFile(filename));
CHECK(soundBuffer.getSamples() != nullptr);
CHECK(soundBuffer.getSampleCount() == 87798);
CHECK(soundBuffer.getSampleRate() == 44100);
CHECK(soundBuffer.getChannelCount() == 1);
CHECK(soundBuffer.getDuration() == sf::microseconds(1990884));
CHECK(std::filesystem::remove(filename));
}
}

View File

@ -1,10 +1,134 @@
#include <SFML/Audio/SoundSource.hpp> #include <SFML/Audio/SoundSource.hpp>
#include <catch2/catch_test_macros.hpp>
#include <AudioUtil.hpp>
#include <SystemUtil.hpp>
#include <type_traits> #include <type_traits>
static_assert(!std::is_constructible_v<sf::SoundSource>); namespace
static_assert(!std::is_copy_constructible_v<sf::SoundSource>); {
static_assert(std::is_copy_assignable_v<sf::SoundSource>); class SoundSource : public sf::SoundSource
static_assert(!std::is_move_constructible_v<sf::SoundSource>); {
static_assert(std::is_move_assignable_v<sf::SoundSource>); void play() override
static_assert(!std::is_nothrow_move_assignable_v<sf::SoundSource>); {
}
void pause() override
{
}
void stop() override
{
}
void* getSound() const override
{
return {};
}
public:
Status getStatus() const override
{
return {};
}
};
} // namespace
TEST_CASE("[Audio] sf::SoundSource", runAudioDeviceTests())
{
SECTION("Type traits")
{
STATIC_CHECK(!std::is_constructible_v<sf::SoundSource>);
STATIC_CHECK(!std::is_copy_constructible_v<sf::SoundSource>);
STATIC_CHECK(std::is_copy_assignable_v<sf::SoundSource>);
STATIC_CHECK(!std::is_move_constructible_v<sf::SoundSource>);
STATIC_CHECK(std::is_move_assignable_v<sf::SoundSource>);
STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::SoundSource>);
STATIC_CHECK(std::has_virtual_destructor_v<sf::SoundSource>);
}
SECTION("Construction")
{
const SoundSource soundSource;
CHECK(soundSource.getPitch() == 0);
CHECK(soundSource.getVolume() == 0);
CHECK(soundSource.getPosition() == sf::Vector3f());
CHECK(!soundSource.isRelativeToListener());
CHECK(soundSource.getMinDistance() == 0);
CHECK(soundSource.getAttenuation() == 0);
CHECK(soundSource.getStatus() == sf::SoundSource::Status::Stopped);
}
SECTION("Copy semantics")
{
const SoundSource soundSource;
SECTION("Construction")
{
const SoundSource soundSourceCopy(soundSource); // NOLINT(performance-unnecessary-copy-initialization)
CHECK(soundSourceCopy.getPitch() == 0);
CHECK(soundSourceCopy.getVolume() == 0);
CHECK(soundSourceCopy.getPosition() == sf::Vector3f());
CHECK(!soundSourceCopy.isRelativeToListener());
CHECK(soundSourceCopy.getMinDistance() == 0);
CHECK(soundSourceCopy.getAttenuation() == 0);
CHECK(soundSourceCopy.getStatus() == sf::SoundSource::Status::Stopped);
}
SECTION("Assignment")
{
SoundSource soundSourceCopy;
soundSourceCopy = soundSource;
CHECK(soundSourceCopy.getPitch() == 0);
CHECK(soundSourceCopy.getVolume() == 0);
CHECK(soundSourceCopy.getPosition() == sf::Vector3f());
CHECK(!soundSourceCopy.isRelativeToListener());
CHECK(soundSourceCopy.getMinDistance() == 0);
CHECK(soundSourceCopy.getAttenuation() == 0);
CHECK(soundSourceCopy.getStatus() == sf::SoundSource::Status::Stopped);
}
}
SECTION("Set/get pitch")
{
SoundSource soundSource;
soundSource.setPitch(42);
CHECK(soundSource.getPitch() == 0);
}
SECTION("Set/get volume")
{
SoundSource soundSource;
soundSource.setVolume(0.5f);
CHECK(soundSource.getVolume() == 0);
}
SECTION("Set/get position")
{
SoundSource soundSource;
soundSource.setPosition({1, 2, 3});
CHECK(soundSource.getPosition() == sf::Vector3f());
}
SECTION("Set/get relative to listener")
{
SoundSource soundSource;
soundSource.setRelativeToListener(true);
CHECK(!soundSource.isRelativeToListener());
}
SECTION("Set/get min distance")
{
SoundSource soundSource;
soundSource.setMinDistance(12.34f);
CHECK(soundSource.getMinDistance() == 0);
}
SECTION("Set/get attenuation")
{
SoundSource soundSource;
soundSource.setAttenuation(10);
CHECK(soundSource.getAttenuation() == 0);
}
}

View File

@ -2,9 +2,26 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <AudioUtil.hpp>
#include <SystemUtil.hpp>
#include <type_traits> #include <type_traits>
TEST_CASE("[Audio] sf::SoundStream") namespace
{
class SoundStream : public sf::SoundStream
{
[[nodiscard]] bool onGetData(Chunk& /* data */) override
{
return true;
}
void onSeek(sf::Time /* timeOffset */) override
{
}
};
} // namespace
TEST_CASE("[Audio] sf::SoundStream", runAudioDeviceTests())
{ {
SECTION("Type traits") SECTION("Type traits")
{ {
@ -13,6 +30,7 @@ TEST_CASE("[Audio] sf::SoundStream")
STATIC_CHECK(!std::is_copy_assignable_v<sf::SoundStream>); STATIC_CHECK(!std::is_copy_assignable_v<sf::SoundStream>);
STATIC_CHECK(!std::is_nothrow_move_constructible_v<sf::SoundStream>); STATIC_CHECK(!std::is_nothrow_move_constructible_v<sf::SoundStream>);
STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::SoundStream>); STATIC_CHECK(!std::is_nothrow_move_assignable_v<sf::SoundStream>);
STATIC_CHECK(std::has_virtual_destructor_v<sf::SoundStream>);
} }
SECTION("Chunk") SECTION("Chunk")
@ -21,4 +39,28 @@ TEST_CASE("[Audio] sf::SoundStream")
CHECK(chunk.samples == nullptr); CHECK(chunk.samples == nullptr);
CHECK(chunk.sampleCount == 0); CHECK(chunk.sampleCount == 0);
} }
SECTION("Construction")
{
const SoundStream soundStream;
CHECK(soundStream.getChannelCount() == 0);
CHECK(soundStream.getSampleRate() == 0);
CHECK(soundStream.getStatus() == sf::SoundStream::Status::Stopped);
CHECK(soundStream.getPlayingOffset() == sf::Time::Zero);
CHECK(!soundStream.getLoop());
}
SECTION("Set/get playing offset")
{
SoundStream soundStream;
soundStream.setPlayingOffset(sf::milliseconds(100));
CHECK(soundStream.getPlayingOffset() == sf::milliseconds(0));
}
SECTION("Set/get loop")
{
SoundStream soundStream;
soundStream.setLoop(true);
CHECK(soundStream.getLoop());
}
} }

View File

@ -7,7 +7,7 @@ set(CATCH_CONFIG_FAST_COMPILE ON CACHE BOOL "")
set(CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ON CACHE BOOL "") set(CATCH_CONFIG_NO_EXPERIMENTAL_STATIC_ANALYSIS_SUPPORT ON CACHE BOOL "")
FetchContent_Declare(Catch2 FetchContent_Declare(Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.4 GIT_TAG v3.6.0
GIT_SHALLOW ON) GIT_SHALLOW ON)
FetchContent_MakeAvailable(Catch2) FetchContent_MakeAvailable(Catch2)
include(Catch) include(Catch)
@ -28,6 +28,8 @@ add_library(sfml-test-main STATIC
TestUtilities/WindowUtil.cpp TestUtilities/WindowUtil.cpp
TestUtilities/GraphicsUtil.hpp TestUtilities/GraphicsUtil.hpp
TestUtilities/GraphicsUtil.cpp TestUtilities/GraphicsUtil.cpp
TestUtilities/AudioUtil.hpp
TestUtilities/AudioUtil.cpp
) )
target_include_directories(sfml-test-main PUBLIC TestUtilities) target_include_directories(sfml-test-main PUBLIC TestUtilities)
target_link_libraries(sfml-test-main PUBLIC SFML::System Catch2::Catch2WithMain) target_link_libraries(sfml-test-main PUBLIC SFML::System Catch2::Catch2WithMain)
@ -43,6 +45,11 @@ if(SFML_RUN_DISPLAY_TESTS)
target_compile_definitions(sfml-test-main PRIVATE SFML_RUN_DISPLAY_TESTS) target_compile_definitions(sfml-test-main PRIVATE SFML_RUN_DISPLAY_TESTS)
endif() endif()
sfml_set_option(SFML_RUN_AUDIO_DEVICE_TESTS ON BOOL "TRUE to run tests that require an audio device, FALSE to ignore it")
if(SFML_RUN_AUDIO_DEVICE_TESTS)
target_compile_definitions(sfml-test-main PRIVATE SFML_RUN_AUDIO_DEVICE_TESTS)
endif()
set(SYSTEM_SRC set(SYSTEM_SRC
System/Angle.test.cpp System/Angle.test.cpp
System/Clock.test.cpp System/Clock.test.cpp
@ -145,6 +152,41 @@ set(AUDIO_SRC
) )
sfml_add_test(test-sfml-audio "${AUDIO_SRC}" SFML::Audio) sfml_add_test(test-sfml-audio "${AUDIO_SRC}" SFML::Audio)
if(SFML_OS_ANDROID AND DEFINED ENV{LIBCXX_SHARED_SO})
# Because we can only write to the tmp directory on the Android virtual device we will need to build our directory tree under it
set(TARGET_DIR "/data/local/tmp/$<TARGET_FILE_DIR:test-sfml-system>")
# Generate script that copies necessary files over to the Android virtual device
file(GENERATE OUTPUT "${PROJECT_BINARY_DIR}/prepare-android-files.sh" CONTENT
"#!/bin/bash\n\
adb shell \"mkdir -p ${TARGET_DIR}\"\n\
adb push $<TARGET_FILE:test-sfml-audio> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:test-sfml-graphics> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:test-sfml-network> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:test-sfml-system> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:test-sfml-window> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:SFML::Audio> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:SFML::Graphics> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:SFML::Network> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:SFML::System> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:SFML::Window> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:Catch2> ${TARGET_DIR}\n\
adb push $<TARGET_FILE:Catch2WithMain> ${TARGET_DIR}\n\
adb push $ENV{LIBCXX_SHARED_SO} ${TARGET_DIR}\n\
adb push ${CMAKE_CURRENT_LIST_DIR} ${TARGET_DIR}\n\
adb shell \"chmod -R 775 ${TARGET_DIR} && ls -la ${TARGET_DIR}\"\n"
FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
# Add the target to invoke the file copy script
add_custom_target(prepare-android-files COMMAND "${PROJECT_BINARY_DIR}/prepare-android-files.sh")
# Generate proxy script that translates CTest commands into adb shell commands
file(GENERATE OUTPUT "${PROJECT_BINARY_DIR}/run-in-adb-shell.sh" CONTENT
"#!/bin/bash\n\
adb shell \"cd ${TARGET_DIR}/test; LD_LIBRARY_PATH=${TARGET_DIR} /data/local/tmp/$1 \\\"$2\\\" \\\"$3\\\" \\\"$4\\\" \\\"$5\\\" \\\"$6\\\" \\\"$7\\\" \\\"$8\\\" \\\"$9\\\"\"\n"
FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
endif()
if(SFML_ENABLE_COVERAGE AND SFML_OS_WINDOWS AND NOT SFML_COMPILER_GCC) if(SFML_ENABLE_COVERAGE AND SFML_OS_WINDOWS AND NOT SFML_COMPILER_GCC)
# Try to find and use OpenCppCoverage for coverage reporting when building with MSVC # Try to find and use OpenCppCoverage for coverage reporting when building with MSVC
find_program(OpenCppCoverage_BINARY "OpenCppCoverage.exe") find_program(OpenCppCoverage_BINARY "OpenCppCoverage.exe")

View File

@ -34,11 +34,11 @@ TEST_CASE("[Graphics] sf::Transformable")
CHECK(transformable.getPosition() == sf::Vector2f(3, 4)); CHECK(transformable.getPosition() == sf::Vector2f(3, 4));
transformable.setRotation(sf::degrees(3.14f)); transformable.setRotation(sf::degrees(3.14f));
CHECK(transformable.getRotation() == sf::degrees(3.14f)); CHECK(transformable.getRotation() == Approx(sf::degrees(3.14f)));
transformable.setRotation(sf::degrees(540)); transformable.setRotation(sf::degrees(540));
CHECK(transformable.getRotation() == sf::degrees(180)); CHECK(transformable.getRotation() == Approx(sf::degrees(180)));
transformable.setRotation(sf::degrees(-72)); transformable.setRotation(sf::degrees(-72));
CHECK(transformable.getRotation() == sf::degrees(288)); CHECK(transformable.getRotation() == Approx(sf::degrees(288)));
transformable.setScale({5, 6}); transformable.setScale({5, 6});
CHECK(transformable.getScale() == sf::Vector2f(5, 6)); CHECK(transformable.getScale() == sf::Vector2f(5, 6));
@ -102,15 +102,15 @@ TEST_CASE("[Graphics] sf::Transformable")
sf::Transformable transformable; sf::Transformable transformable;
CHECK(transformable.getRotation() == sf::Angle::Zero); CHECK(transformable.getRotation() == sf::Angle::Zero);
transformable.rotate(sf::degrees(15)); transformable.rotate(sf::degrees(15));
CHECK(transformable.getRotation() == sf::degrees(15)); CHECK(transformable.getRotation() == Approx(sf::degrees(15)));
transformable.rotate(sf::degrees(360)); transformable.rotate(sf::degrees(360));
CHECK(transformable.getRotation() == sf::degrees(15)); CHECK(transformable.getRotation() == Approx(sf::degrees(15)));
transformable.rotate(sf::degrees(-25)); transformable.rotate(sf::degrees(-25));
CHECK(transformable.getRotation() == sf::degrees(350)); CHECK(transformable.getRotation() == Approx(sf::degrees(350)));
transformable.rotate(sf::degrees(-720)); transformable.rotate(sf::degrees(-720));
CHECK(transformable.getRotation() == sf::degrees(350)); CHECK(transformable.getRotation() == Approx(sf::degrees(350)));
transformable.rotate(sf::degrees(-370)); transformable.rotate(sf::degrees(-370));
CHECK(transformable.getRotation() == sf::degrees(340)); CHECK(transformable.getRotation() == Approx(sf::degrees(340)));
} }
SECTION("scale()") SECTION("scale()")

View File

@ -76,13 +76,13 @@ TEST_CASE("[Graphics] sf::View")
{ {
sf::View view; sf::View view;
view.setRotation(sf::degrees(-345)); view.setRotation(sf::degrees(-345));
CHECK(view.getRotation() == sf::degrees(15)); CHECK(view.getRotation() == Approx(sf::degrees(15)));
CHECK(view.getTransform() == CHECK(view.getTransform() ==
Approx(sf::Transform(0.00193185f, 0.000517638f, -1.22474f, 0.000517638f, -0.00193185f, 0.707107f, 0, 0, 1))); Approx(sf::Transform(0.00193185f, 0.000517638f, -1.22474f, 0.000517638f, -0.00193185f, 0.707107f, 0, 0, 1)));
CHECK(view.getInverseTransform() == CHECK(view.getInverseTransform() ==
Approx(sf::Transform(482.963f, 129.41f, 500, 129.41f, -482.963f, 500, 0, 0, 1))); Approx(sf::Transform(482.963f, 129.41f, 500, 129.41f, -482.963f, 500, 0, 0, 1)));
view.setRotation(sf::degrees(400)); view.setRotation(sf::degrees(400));
CHECK(view.getRotation() == sf::degrees(40)); CHECK(view.getRotation() == Approx(sf::degrees(40)));
CHECK(view.getTransform() == CHECK(view.getTransform() ==
Approx(sf::Transform(0.00153209f, 0.00128558f, -1.40883f, 0.00128558f, -0.00153209f, 0.123257f, 0, 0, 1))); Approx(sf::Transform(0.00153209f, 0.00128558f, -1.40883f, 0.00128558f, -0.00153209f, 0.123257f, 0, 0, 1)));
CHECK(view.getInverseTransform() == CHECK(view.getInverseTransform() ==
@ -140,7 +140,7 @@ TEST_CASE("[Graphics] sf::View")
sf::View view; sf::View view;
view.setRotation(sf::degrees(45)); view.setRotation(sf::degrees(45));
view.rotate(sf::degrees(-15)); view.rotate(sf::degrees(-15));
CHECK(view.getRotation() == sf::degrees(30)); CHECK(view.getRotation() == Approx(sf::degrees(30)));
CHECK(view.getTransform() == CHECK(view.getTransform() ==
Approx(sf::Transform(0.00173205f, 0.001f, -1.36603f, 0.001f, -0.00173205f, 0.366025f, 0, 0, 1))); Approx(sf::Transform(0.00173205f, 0.001f, -1.36603f, 0.001f, -0.00173205f, 0.366025f, 0, 0, 1)));
CHECK(view.getInverseTransform() == Approx(sf::Transform(433.013f, 250, 500, 250, -433.013f, 500, 0, 0, 1))); CHECK(view.getInverseTransform() == Approx(sf::Transform(433.013f, 250, 500, 250, -433.013f, 500, 0, 0, 1)));

View File

@ -25,33 +25,33 @@ TEST_CASE("[System] sf::Angle")
SECTION("wrapSigned()") SECTION("wrapSigned()")
{ {
STATIC_CHECK(sf::Angle::Zero.wrapSigned() == sf::Angle::Zero); STATIC_CHECK(sf::Angle::Zero.wrapSigned() == sf::Angle::Zero);
STATIC_CHECK(sf::degrees(0).wrapSigned() == sf::degrees(0)); CHECK(sf::degrees(0).wrapSigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(1).wrapSigned() == sf::degrees(1)); CHECK(sf::degrees(1).wrapSigned() == Approx(sf::degrees(1)));
STATIC_CHECK(sf::degrees(-1).wrapSigned() == sf::degrees(-1)); CHECK(sf::degrees(-1).wrapSigned() == Approx(sf::degrees(-1)));
STATIC_CHECK(sf::degrees(90).wrapSigned() == sf::degrees(90)); CHECK(sf::degrees(90).wrapSigned() == Approx(sf::degrees(90)));
STATIC_CHECK(sf::degrees(-90).wrapSigned() == sf::degrees(-90)); CHECK(sf::degrees(-90).wrapSigned() == Approx(sf::degrees(-90)));
STATIC_CHECK(sf::degrees(180).wrapSigned() == sf::degrees(-180)); CHECK(sf::degrees(180).wrapSigned() == Approx(sf::degrees(-180)));
STATIC_CHECK(sf::degrees(-180).wrapSigned() == sf::degrees(-180)); CHECK(sf::degrees(-180).wrapSigned() == Approx(sf::degrees(-180)));
STATIC_CHECK(sf::degrees(360).wrapSigned() == sf::degrees(0)); CHECK(sf::degrees(360).wrapSigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(-360).wrapSigned() == sf::degrees(0)); CHECK(sf::degrees(-360).wrapSigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(720).wrapSigned() == sf::degrees(0)); CHECK(sf::degrees(720).wrapSigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(-720).wrapSigned() == sf::degrees(0)); CHECK(sf::degrees(-720).wrapSigned() == Approx(sf::degrees(0)));
} }
SECTION("wrapUnsigned()") SECTION("wrapUnsigned()")
{ {
STATIC_CHECK(sf::Angle::Zero.wrapUnsigned() == sf::Angle::Zero); STATIC_CHECK(sf::Angle::Zero.wrapUnsigned() == sf::Angle::Zero);
STATIC_CHECK(sf::degrees(0).wrapUnsigned() == sf::degrees(0)); CHECK(sf::degrees(0).wrapUnsigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(1).wrapUnsigned() == sf::degrees(1)); CHECK(sf::degrees(1).wrapUnsigned() == Approx(sf::degrees(1)));
STATIC_CHECK(sf::degrees(-1).wrapUnsigned() == sf::degrees(359)); CHECK(sf::degrees(-1).wrapUnsigned() == Approx(sf::degrees(359)));
STATIC_CHECK(sf::degrees(90).wrapUnsigned() == sf::degrees(90)); CHECK(sf::degrees(90).wrapUnsigned() == Approx(sf::degrees(90)));
STATIC_CHECK(sf::degrees(-90).wrapUnsigned() == sf::degrees(270)); CHECK(sf::degrees(-90).wrapUnsigned() == Approx(sf::degrees(270)));
STATIC_CHECK(sf::degrees(180).wrapUnsigned() == sf::degrees(180)); CHECK(sf::degrees(180).wrapUnsigned() == Approx(sf::degrees(180)));
STATIC_CHECK(sf::degrees(-180).wrapUnsigned() == sf::degrees(180)); CHECK(sf::degrees(-180).wrapUnsigned() == Approx(sf::degrees(180)));
STATIC_CHECK(sf::degrees(360).wrapUnsigned() == sf::degrees(0)); CHECK(sf::degrees(360).wrapUnsigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(-360).wrapUnsigned() == sf::degrees(0)); CHECK(sf::degrees(-360).wrapUnsigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(720).wrapUnsigned() == sf::degrees(0)); CHECK(sf::degrees(720).wrapUnsigned() == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(-720).wrapUnsigned() == sf::degrees(0)); CHECK(sf::degrees(-720).wrapUnsigned() == Approx(sf::degrees(0)));
} }
SECTION("degrees()") SECTION("degrees()")
@ -185,7 +185,7 @@ TEST_CASE("[System] sf::Angle")
{ {
sf::Angle angle = sf::degrees(-15); sf::Angle angle = sf::degrees(-15);
angle += sf::degrees(15); angle += sf::degrees(15);
CHECK(angle == sf::degrees(0)); CHECK(angle == Approx(sf::degrees(0)));
angle += sf::radians(10); angle += sf::radians(10);
CHECK(angle == sf::radians(10)); CHECK(angle == sf::radians(10));
} }
@ -202,7 +202,7 @@ TEST_CASE("[System] sf::Angle")
{ {
sf::Angle angle = sf::degrees(15); sf::Angle angle = sf::degrees(15);
angle -= sf::degrees(15); angle -= sf::degrees(15);
CHECK(angle == sf::degrees(0)); CHECK(angle == Approx(sf::degrees(0)));
angle -= sf::radians(10); angle -= sf::radians(10);
CHECK(angle == sf::radians(-10)); CHECK(angle == sf::radians(-10));
} }
@ -210,19 +210,19 @@ TEST_CASE("[System] sf::Angle")
SECTION("operator*") SECTION("operator*")
{ {
STATIC_CHECK(sf::radians(0) * 10 == sf::Angle::Zero); STATIC_CHECK(sf::radians(0) * 10 == sf::Angle::Zero);
STATIC_CHECK(sf::degrees(10) * 2.5f == sf::degrees(25)); CHECK(sf::degrees(10) * 2.5f == Approx(sf::degrees(25)));
STATIC_CHECK(sf::degrees(100) * 10.0f == sf::degrees(1000)); CHECK(sf::degrees(100) * 10.0f == Approx(sf::degrees(1000)));
STATIC_CHECK(10 * sf::radians(0) == sf::Angle::Zero); STATIC_CHECK(10 * sf::radians(0) == sf::Angle::Zero);
STATIC_CHECK(2.5f * sf::degrees(10) == sf::degrees(25)); CHECK(2.5f * sf::degrees(10) == Approx(sf::degrees(25)));
STATIC_CHECK(10.0f * sf::degrees(100) == sf::degrees(1000)); CHECK(10.0f * sf::degrees(100) == Approx(sf::degrees(1000)));
} }
SECTION("operator*=") SECTION("operator*=")
{ {
sf::Angle angle = sf::degrees(1); sf::Angle angle = sf::degrees(1);
angle *= 10; angle *= 10;
CHECK(angle == sf::degrees(10)); CHECK(angle == Approx(sf::degrees(10)));
} }
SECTION("operator/") SECTION("operator/")
@ -240,24 +240,24 @@ TEST_CASE("[System] sf::Angle")
{ {
sf::Angle angle = sf::degrees(60); sf::Angle angle = sf::degrees(60);
angle /= 5; angle /= 5;
CHECK(angle == sf::degrees(12)); CHECK(angle == Approx(sf::degrees(12)));
} }
SECTION("operator%") SECTION("operator%")
{ {
STATIC_CHECK(sf::Angle::Zero % sf::radians(0.5f) == sf::Angle::Zero); STATIC_CHECK(sf::Angle::Zero % sf::radians(0.5f) == sf::Angle::Zero);
STATIC_CHECK(sf::radians(10) % sf::radians(1) == sf::radians(0)); STATIC_CHECK(sf::radians(10) % sf::radians(1) == sf::radians(0));
STATIC_CHECK(sf::degrees(90) % sf::degrees(30) == sf::degrees(0)); CHECK(sf::degrees(90) % sf::degrees(30) == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(90) % sf::degrees(40) == sf::degrees(10)); CHECK(sf::degrees(90) % sf::degrees(40) == Approx(sf::degrees(10)));
STATIC_CHECK(sf::degrees(-90) % sf::degrees(30) == sf::degrees(0)); CHECK(sf::degrees(-90) % sf::degrees(30) == Approx(sf::degrees(0)));
STATIC_CHECK(sf::degrees(-90) % sf::degrees(40) == sf::degrees(30)); CHECK(sf::degrees(-90) % sf::degrees(40) == Approx(sf::degrees(30)));
} }
SECTION("operator%=") SECTION("operator%=")
{ {
sf::Angle angle = sf::degrees(59); sf::Angle angle = sf::degrees(59);
angle %= sf::degrees(10); angle %= sf::degrees(10);
CHECK(angle == sf::degrees(9)); CHECK(angle == Approx(sf::degrees(9)));
} }
SECTION("operator _deg") SECTION("operator _deg")

View File

@ -2,6 +2,7 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <array>
#include <ostream> #include <ostream>
#include <string_view> #include <string_view>
@ -32,9 +33,9 @@ TEST_CASE("[System] sf::MemoryInputStream")
sf::MemoryInputStream mis; sf::MemoryInputStream mis;
mis.open(memoryContents.data(), sizeof(char) * memoryContents.size()); mis.open(memoryContents.data(), sizeof(char) * memoryContents.size());
char buffer[32]; std::array<char, 32> buffer{};
CHECK(mis.read(buffer, 5) == 5); CHECK(mis.read(buffer.data(), 5) == 5);
CHECK(std::string_view(buffer, 5) == std::string_view(memoryContents.data(), 5)); CHECK(std::string_view(buffer.data(), 5) == std::string_view(memoryContents.data(), 5));
CHECK(mis.seek(10) == 10); CHECK(mis.seek(10) == 10);
CHECK(mis.tell() == 10); CHECK(mis.tell() == 10);
CHECK(mis.getSize() == 11); CHECK(mis.getSize() == 11);

View File

@ -0,0 +1,12 @@
#include <AudioUtil.hpp>
std::string runAudioDeviceTests()
{
#ifdef SFML_RUN_AUDIO_DEVICE_TESTS
return "";
#else
// https://github.com/catchorg/Catch2/blob/devel/docs/test-cases-and-sections.md#special-tags
// This tag tells Catch2 to not run a given TEST_CASE
return "[.audio_device]";
#endif
}

View File

@ -0,0 +1,5 @@
#pragma once
#include <string>
[[nodiscard]] std::string runAudioDeviceTests();

View File

@ -77,7 +77,7 @@ bool operator==(const sf::Vector3f& lhs, const Approx<sf::Vector3f>& rhs)
bool operator==(const sf::Angle& lhs, const Approx<sf::Angle>& rhs) bool operator==(const sf::Angle& lhs, const Approx<sf::Angle>& rhs)
{ {
return lhs.asDegrees() == Approx(rhs.value.asDegrees()); return lhs.asRadians() == Approx(rhs.value.asRadians());
} }
std::vector<std::byte> loadIntoMemory(const std::filesystem::path& path) std::vector<std::byte> loadIntoMemory(const std::filesystem::path& path)

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-all Name: SFML-all

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-audio Name: SFML-audio

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-graphics Name: SFML-graphics

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-network Name: SFML-network

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-system Name: SFML-system

View File

@ -1,6 +1,6 @@
prefix=@CMAKE_INSTALL_PREFIX@ prefix=@CMAKE_INSTALL_PREFIX@
exec_prefix=${prefix} exec_prefix=${prefix}
libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ libdir=${exec_prefix}/@SFML_RELATIVE_INSTALL_LIBDIR@
includedir=${prefix}/include includedir=${prefix}/include
Name: SFML-window Name: SFML-window