diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index ae56b1f97..5f05d0936 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libgl1-mesa-dev libfreetype6-dev libfontconfig1-dev libxxf86vm-dev libpng-dev libvorbis-dev libopenal-dev g++ libwebp-dev git libsdl2-dev ninja-build + sudo apt install libgl1-mesa-dev libfreetype6-dev libfontconfig1-dev libxxf86vm-dev libpng-dev libvorbis-dev g++ libwebp-dev git libsdl2-dev ninja-build - name: Configure with CMake and GCC run: cmake -Bbuild -G"Ninja Multi-Config" - name: Build Debug diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b7c056f1c..331ae1d81 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -24,7 +24,7 @@ jobs: image: registry.fedoraproject.org/fedora-minimal:37 steps: - name: Install dependencies - run: microdnf install -y cmake ninja-build mingw64-pkg-config mingw64-libvorbis mingw64-SDL2 mingw64-fontconfig mingw64-libwebp mingw64-dlfcn mingw64-openal-soft mingw32-nsis mingw64-gcc-c++ tar gzip + run: microdnf install -y cmake ninja-build mingw64-pkg-config mingw64-libvorbis mingw64-SDL2 mingw64-fontconfig mingw64-libwebp mingw64-dlfcn mingw32-nsis mingw64-gcc-c++ tar gzip - uses: actions/checkout@v3 - name: Build with CMake and GCC run: | diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 551add9fd..590f919fb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,7 +11,7 @@ image: registry.fedoraproject.org/fedora-minimal:38 linux: stage: build script: - - microdnf install -y cmake ninja-build gcc-c++ fontconfig-devel freetype-devel libvorbis-devel libwebp-devel SDL2-devel openal-soft-devel libtheora-devel clang util-linux >/dev/null + - microdnf install -y cmake ninja-build gcc-c++ fontconfig-devel freetype-devel libvorbis-devel libwebp-devel SDL2-devel libtheora-devel clang util-linux >/dev/null - CXXFLAGS=-fdiagnostics-color cmake -Bbuild -GNinja - cd build - ninja @@ -29,7 +29,7 @@ linux: mingw: stage: build script: - - microdnf install -y gcc-c++ cmake ninja-build mingw64-pkg-config mingw64-libvorbis mingw64-SDL2 mingw64-fontconfig mingw64-libwebp mingw64-dlfcn mingw64-openal-soft mingw64-gcc-c++ > /dev/null + - microdnf install -y cmake ninja-build mingw64-pkg-config mingw64-libvorbis mingw64-SDL2 mingw64-fontconfig mingw64-libwebp mingw64-dlfcn mingw64-gcc-c++ > /dev/null - mingw64-cmake -GNinja -Bbuild-debug -DCMAKE_BUILD_TYPE=Debug - ninja -C build-debug - mingw64-cmake -GNinja -Bbuild-release -DCMAKE_BUILD_TYPE=Release diff --git a/CMakeLists.txt b/CMakeLists.txt index 3820917fc..f667066e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,10 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -file(GLOB SRC src/*.cpp src/jngl/*.cpp) +file(GLOB SRC src/*.cpp src/jngl/*.cpp + src/audio/*.cpp + src/audio/effect/*.cpp +) add_library(jngl ${SRC}) file(GLOB HEADERS src/*.hpp src/jngl/*.hpp) target_sources(jngl PRIVATE ${HEADERS}) @@ -67,15 +70,13 @@ if(MSVC OR IOS OR ANDROID OR EMSCRIPTEN) endif() option(JNGL_BUILD_WEBP_FROM_SOURCE "Build libwebp from source instead of using the system version" ${JNGL_BUILD_WEBP_FROM_SOURCE_DEFAULT}) -set(ALSOFT_TESTS OFF CACHE BOOL "Build and install test programs" FORCE) -set(ALSOFT_EXAMPLES OFF CACHE BOOL "Build and install example programs" FORCE) -set(ALSOFT_UTILS OFF CACHE BOOL "Build and install utility programs" FORCE) - if (ANDROID) + find_package(oboe REQUIRED CONFIG) + target_link_libraries(jngl PRIVATE oboe::oboe) file(GLOB ANDROID_SOURCES src/android/*.cpp) - target_sources(jngl PRIVATE ${ANDROID_SOURCES}) + target_sources(jngl PRIVATE ${ANDROID_SOURCES} src/audio/android/engine.cpp) elseif(IOS) - file(GLOB SRC src/ios/*.cpp src/ios/*.mm src/ios/*.h) + file(GLOB SRC src/ios/*.cpp src/ios/*.mm src/audio/ios/Engine.mm src/ios/*.h) target_sources(jngl PRIVATE ${SRC} src/mac/message.cpp) elseif (UNIX) file(GLOB SRC src/sdl/*.cpp src/sdl/controller/SdlController.cpp) @@ -93,6 +94,7 @@ elseif (UNIX) else() file(GLOB SRC src/linux/*.cpp src/linux/*.c) target_sources(jngl PRIVATE ${SRC}) + target_link_libraries(jngl PRIVATE atomic) endif() target_link_libraries(jngl PRIVATE $<$,$,9.0>>:stdc++fs>) @@ -189,22 +191,12 @@ endif() if(ANDROID) target_compile_definitions(jngl PRIVATE NOPNG) - CPMAddPackage(NAME openal - URL https://github.com/kcat/openal-soft/archive/refs/tags/1.21.1.zip - URL_HASH SHA1=1442363faf47a5a7cd6287513068ecd6c70e747e - OPTIONS - "LIBTYPE STATIC" - "ALSOFT_INSTALL_CONFIG OFF" - "ALSOFT_INSTALL_HRTF_DATA OFF" - "ALSOFT_INSTALL_AMBDEC_PRESETS OFF" - "ALSOFT_BACKEND_WAVE OFF" - "ALSOFT_CPUEXT_NEON OFF" - ) - set(OPENAL_LIBRARY OpenAL) - target_include_directories(jngl PUBLIC ${ANDROID_NDK}/sources/android/native_app_glue) target_include_directories(jngl PRIVATE ${OGG_INCLUDE_DIRS} ${VORBIS_SRC_DIR}/include) else() + if(NOT IOS) + target_sources(jngl PRIVATE src/audio/sdl/engine.cpp) + endif() if(UNIX AND NOT IOS AND NOT EMSCRIPTEN) find_package(PkgConfig REQUIRED) pkg_check_modules(SDL2 REQUIRED sdl2) @@ -232,46 +224,51 @@ else() if(MSVC) target_compile_definitions(jngl PUBLIC _USE_MATH_DEFINES) if(JNGL_SDL2) - set(SDL_SENSOR OFF CACHE BOOL "" FORCE) # doesn't work with UWP - FetchContent_Declare(sdl2 + CPMAddPackage(NAME sdl2 + URL https://www.libsdl.org/release/SDL2-2.28.4.zip + URL_HASH SHA1=0d9e0da0e935c4f0524cfd16d47d38b6045b9573 + OPTIONS + "SDL_SENSOR OFF" # doesn't work with UWP + ) + else() + CPMAddPackage(NAME sdl2 URL https://www.libsdl.org/release/SDL2-2.28.4.zip URL_HASH SHA1=0d9e0da0e935c4f0524cfd16d47d38b6045b9573 + OPTIONS + "SDL_ATOMIC OFF" + "SDL_CPUINFO OFF" + "SDL_DLOPEN OFF" + "SDL_FILE OFF" + "SDL_FILESYSTEM OFF" + "SDL_HAPTIC OFF" + "SDL_LOADSO ON" + "SDL_LOCALE OFF" + "SDL_POWER OFF" + "SDL_RENDER OFF" + "SDL_SENSOR OFF" + "SDL_SHARED OFF" + "SDL_TIMERS OFF" ) - FetchContent_MakeAvailable(sdl2) - target_link_libraries(jngl PRIVATE SDL2-static) endif() + target_link_libraries(jngl PRIVATE SDL2-static) if(WINDOWS_STORE) target_compile_definitions(jngl PUBLIC JNGL_UWP) target_include_directories(jngl PUBLIC ${sdl2_SOURCE_DIR}/include ${sdl2_BINARY_DIR}/include) # TODO: This should be PRIVATE - FetchContent_Declare(openal - GIT_REPOSITORY https://github.com/jhasse/openal-soft-winphone - GIT_TAG e4f3681316b1e8d8d68dfc31b1fc952d9bba085d) target_link_options(jngl PUBLIC $,/defaultlib:vccorlibd.lib /defaultlib:msvcrtd.lib,/defaultlib:vccorlib.lib /defaultlib:msvcrt.lib>) - else() - FetchContent_Declare(openal - URL https://github.com/kcat/openal-soft/archive/refs/tags/1.21.1.zip - URL_HASH SHA1=1442363faf47a5a7cd6287513068ecd6c70e747e) - set(ALSOFT_NO_CONFIG_UTIL ON CACHE BOOL "Disable building the alsoft-config utility" FORCE) - set(LIBTYPE "STATIC" CACHE STRING "" FORCE) - target_compile_definitions(jngl PRIVATE AL_LIBTYPE_STATIC) endif() - FetchContent_MakeAvailable(openal) - set(OPENAL_LIBRARY OpenAL) - elseif(IOS) - target_compile_options(jngl PRIVATE -Wno-deprecated-declarations) # OpenAL target_compile_definitions(jngl PRIVATE IOS) target_compile_definitions(jngl PUBLIC GLES_SILENCE_DEPRECATION) target_include_directories(jngl PUBLIC include/ios) find_library(IOS_OPENGLES OpenGLES) - find_library(IOS_OPENAL OpenAL) find_library(IOS_QUARTZCORE QuartzCore) find_library(IOS_UIKIT UIKit) find_library(IOS_AVFOUNDATION AVFoundation) find_library(IOS_GAMECONTROLLER GameController) + find_library(IOS_AudioToolbox AudioToolbox) target_link_libraries(jngl PRIVATE - ${IOS_OPENGLES} ${IOS_OPENAL} ${IOS_QUARTZCORE} ${IOS_UIKIT} ${IOS_AVFOUNDATION} - ${IOS_GAMECONTROLLER} + ${IOS_OPENGLES} ${IOS_QUARTZCORE} ${IOS_UIKIT} ${IOS_AVFOUNDATION} + ${IOS_GAMECONTROLLER} ${IOS_AudioToolbox} ) if(NOT hasParent) @@ -287,8 +284,6 @@ else() set_target_properties(jngl-test PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/data/ios/jngl-test.in.plist") endif() else() - find_package(OpenAL REQUIRED) - if(NOT JNGL_BUILD_WEBP_FROM_SOURCE) find_package(WebP) if(WebP_FOUND) @@ -300,11 +295,9 @@ else() find_package(PkgConfig) if(PkgConfig_FOUND) - if(JNGL_SDL2) - pkg_check_modules(SDL2 REQUIRED sdl2) - target_include_directories(jngl PRIVATE ${SDL2_INCLUDE_DIRS}) - target_link_libraries(jngl PRIVATE ${SDL2_LINK_LIBRARIES}) - endif() + pkg_check_modules(SDL2 REQUIRED sdl2) + target_include_directories(jngl PRIVATE ${SDL2_INCLUDE_DIRS}) + target_link_libraries(jngl PRIVATE ${SDL2_LINK_LIBRARIES}) if(JNGL_VIDEO) if(APPLE) # On macOS we'll build theora from source, since Homebrew's version crashes due @@ -435,7 +428,7 @@ endif() target_include_directories(jngl PRIVATE ${FREETYPE_INCLUDE_DIRS}) target_link_directories(jngl PUBLIC ${FREETYPE_LIBRARY_DIRS}) -target_link_libraries(jngl PUBLIC ${FREETYPE_LIBRARIES} ${OPENAL_LIBRARY}) +target_link_libraries(jngl PUBLIC ${FREETYPE_LIBRARIES}) if(NOT hasParent AND NOT WINDOWS_STORE) include(CTest) diff --git a/README.md b/README.md index fb3fbe928..c95e0d141 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ cmake --build build ``` sudo apt-get install libgl1-mesa-dev libfreetype6-dev libfontconfig1-dev libxxf86vm-dev \ -libpng-dev libvorbis-dev libopenal-dev cmake g++ \ +libpng-dev libvorbis-dev cmake g++ \ libwebp-dev git libsdl2-dev ``` @@ -34,13 +34,13 @@ libwebp-dev git libsdl2-dev ``` sudo dnf install fontconfig-devel freetype-devel libvorbis-devel libwebp-devel \ -cmake SDL2-devel openal-soft-devel gcc-c++ +cmake SDL2-devel gcc-c++ ``` ### Arch Linux ``` -pacman -Syu --needed cmake gcc sdl2 pkg-config fontconfig libwebp openal libvorbis +pacman -Syu --needed cmake gcc sdl2 pkg-config fontconfig libwebp libvorbis ``` ## Windows @@ -50,7 +50,7 @@ pacman -Syu --needed cmake gcc sdl2 pkg-config fontconfig libwebp openal libvorb Set up [MSYS2](https://www.msys2.org/) and install the following in a MinGW-w64 Win64 Shell: ``` -pacman -Syu --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-openal \ +pacman -Syu --needed mingw-w64-x86_64-gcc \ mingw-w64-x86_64-freetype mingw-w64-x86_64-libvorbis mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-dlfcn mingw-w64-x86_64-cmake make mingw-w64-x86_64-gdb \ mingw-w64-x86_64-libtheora diff --git a/android/test/app/build.gradle b/android/test/app/build.gradle index 8660641c7..38b3af400 100644 --- a/android/test/app/build.gradle +++ b/android/test/app/build.gradle @@ -10,6 +10,14 @@ android { applicationId = 'com.bixense.jngl_test' minSdkVersion 21 targetSdkVersion 31 + externalNativeBuild { + cmake { + arguments "-DANDROID_STL=c++_shared" + } + } + } + buildFeatures { + prefab true } buildTypes { release { @@ -31,4 +39,5 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.google.oboe:oboe:1.7.0' } diff --git a/android/test/app/src/main/cpp/CMakeLists.txt b/android/test/app/src/main/cpp/CMakeLists.txt index 63ffdaf64..aea1f5f30 100644 --- a/android/test/app/src/main/cpp/CMakeLists.txt +++ b/android/test/app/src/main/cpp/CMakeLists.txt @@ -26,6 +26,7 @@ add_subdirectory(${PROJECT_SOURCE_DIR}/../../../../../.. ${CMAKE_CURRENT_BINARY_ # add lib dependencies target_link_libraries(jngl-test android + log native_app_glue EGL GLESv3 diff --git a/doc/index.md b/doc/index.md index 51c5d549d..f87b95dc7 100644 --- a/doc/index.md +++ b/doc/index.md @@ -13,7 +13,7 @@ Switch1 and the Web. static linking * OpenGL (ES) 3.0 accelerated * PNG, BMP and [WebP](https://developers.google.com/speed/webp/) image support -* Ogg Vorbis audio support using OpenAL +* Ogg Vorbis audio support with support for effects like pitch shifting * Theora video playback support * UTF-8 text output using [FreeType](http://www.freetype.org/) diff --git a/examples/videoplayer.cpp b/examples/videoplayer.cpp index 59667c1be..55110a02b 100644 --- a/examples/videoplayer.cpp +++ b/examples/videoplayer.cpp @@ -13,6 +13,9 @@ JNGL_MAIN_BEGIN { while (jngl::running()) { jngl::updateInput(); video.draw(); + if (video.finished()) { + jngl::setTitle(filename + " [FINISHED]"); + } jngl::swapBuffers(); } } JNGL_MAIN_END diff --git a/include/public/atomic_queue/atomic_queue.h b/include/public/atomic_queue/atomic_queue.h new file mode 100644 index 000000000..067c011ba --- /dev/null +++ b/include/public/atomic_queue/atomic_queue.h @@ -0,0 +1,626 @@ +/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ +#ifndef ATOMIC_QUEUE_ATOMIC_QUEUE_H_INCLUDED +#define ATOMIC_QUEUE_ATOMIC_QUEUE_H_INCLUDED + +// Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file LICENSE. + +#include "defs.h" + +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace atomic_queue { + +using std::uint32_t; +using std::uint64_t; +using std::uint8_t; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace details { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template struct GetCacheLineIndexBits { static int constexpr value = 0; }; +template<> struct GetCacheLineIndexBits<256> { static int constexpr value = 8; }; +template<> struct GetCacheLineIndexBits<128> { static int constexpr value = 7; }; +template<> struct GetCacheLineIndexBits< 64> { static int constexpr value = 6; }; +template<> struct GetCacheLineIndexBits< 32> { static int constexpr value = 5; }; +template<> struct GetCacheLineIndexBits< 16> { static int constexpr value = 4; }; +template<> struct GetCacheLineIndexBits< 8> { static int constexpr value = 3; }; +template<> struct GetCacheLineIndexBits< 4> { static int constexpr value = 2; }; +template<> struct GetCacheLineIndexBits< 2> { static int constexpr value = 1; }; + +template +struct GetIndexShuffleBits { + static int constexpr bits = GetCacheLineIndexBits::value; + static unsigned constexpr min_size = 1u << (bits * 2); + static int constexpr value = array_size < min_size ? 0 : bits; +}; + +template +struct GetIndexShuffleBits { + static int constexpr value = 0; +}; + +// Multiple writers/readers contend on the same cache line when storing/loading elements at +// subsequent indexes, aka false sharing. For power of 2 ring buffer size it is possible to re-map +// the index in such a way that each subsequent element resides on another cache line, which +// minimizes contention. This is done by swapping the lowest order N bits (which are the index of +// the element within the cache line) with the next N bits (which are the index of the cache line) +// of the element index. +template +constexpr unsigned remap_index(unsigned index) noexcept { + unsigned constexpr mix_mask{(1u << BITS) - 1}; + unsigned const mix{(index ^ (index >> BITS)) & mix_mask}; + return index ^ mix ^ (mix << BITS); +} + +template<> +constexpr unsigned remap_index<0>(unsigned index) noexcept { + return index; +} + +template +constexpr T& map(T* elements, unsigned index) noexcept { + return elements[remap_index(index)]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Implement a "bit-twiddling hack" for finding the next power of 2 in either 32 bits or 64 bits +// in C++11 compatible constexpr functions. The library no longer maintains C++11 compatibility. + +// "Runtime" version for 32 bits +// --a; +// a |= a >> 1; +// a |= a >> 2; +// a |= a >> 4; +// a |= a >> 8; +// a |= a >> 16; +// ++a; + +template +constexpr T decrement(T x) noexcept { + return x - 1; +} + +template +constexpr T increment(T x) noexcept { + return x + 1; +} + +template +constexpr T or_equal(T x, unsigned u) noexcept { + return x | x >> u; +} + +template +constexpr T or_equal(T x, unsigned u, Args... rest) noexcept { + return or_equal(or_equal(x, u), rest...); +} + +constexpr uint32_t round_up_to_power_of_2(uint32_t a) noexcept { + return increment(or_equal(decrement(a), 1, 2, 4, 8, 16)); +} + +constexpr uint64_t round_up_to_power_of_2(uint64_t a) noexcept { + return increment(or_equal(decrement(a), 1, 2, 4, 8, 16, 32)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +constexpr T nil() noexcept { +#if __cpp_lib_atomic_is_always_lock_free // Better compile-time error message requires C++17. + static_assert(std::atomic::is_always_lock_free, "Queue element type T is not atomic. Use AtomicQueue2/AtomicQueueB2 for such element types."); +#endif + return {}; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace details + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class AtomicQueueCommon { +protected: + // Put these on different cache lines to avoid false sharing between readers and writers. + alignas(CACHE_LINE_SIZE) std::atomic head_ = {}; + alignas(CACHE_LINE_SIZE) std::atomic tail_ = {}; + + // The special member functions are not thread-safe. + + AtomicQueueCommon() noexcept = default; + + AtomicQueueCommon(AtomicQueueCommon const& b) noexcept + : head_(b.head_.load(X)) + , tail_(b.tail_.load(X)) {} + + AtomicQueueCommon& operator=(AtomicQueueCommon const& b) noexcept { + head_.store(b.head_.load(X), X); + tail_.store(b.tail_.load(X), X); + return *this; + } + + void swap(AtomicQueueCommon& b) noexcept { + unsigned h = head_.load(X); + unsigned t = tail_.load(X); + head_.store(b.head_.load(X), X); + tail_.store(b.tail_.load(X), X); + b.head_.store(h, X); + b.tail_.store(t, X); + } + + template + static T do_pop_atomic(std::atomic& q_element) noexcept { + if(Derived::spsc_) { + for(;;) { + T element = q_element.load(A); + if(ATOMIC_QUEUE_LIKELY(element != NIL)) { + q_element.store(NIL, X); + return element; + } + if(Derived::maximize_throughput_) + spin_loop_pause(); + } + } + else { + for(;;) { + T element = q_element.exchange(NIL, A); // (2) The store to wait for. + if(ATOMIC_QUEUE_LIKELY(element != NIL)) + return element; + // Do speculative loads while busy-waiting to avoid broadcasting RFO messages. + do + spin_loop_pause(); + while(Derived::maximize_throughput_ && q_element.load(X) == NIL); + } + } + } + + template + static void do_push_atomic(T element, std::atomic& q_element) noexcept { + assert(element != NIL); + if(Derived::spsc_) { + while(ATOMIC_QUEUE_UNLIKELY(q_element.load(X) != NIL)) + if(Derived::maximize_throughput_) + spin_loop_pause(); + q_element.store(element, R); + } + else { + for(T expected = NIL; ATOMIC_QUEUE_UNLIKELY(!q_element.compare_exchange_strong(expected, element, R, X)); expected = NIL) { + do + spin_loop_pause(); // (1) Wait for store (2) to complete. + while(Derived::maximize_throughput_ && q_element.load(X) != NIL); + } + } + } + + enum State : unsigned char { EMPTY, STORING, STORED, LOADING }; + + template + static T do_pop_any(std::atomic& state, T& q_element) noexcept { + if(Derived::spsc_) { + while(ATOMIC_QUEUE_UNLIKELY(state.load(A) != STORED)) + if(Derived::maximize_throughput_) + spin_loop_pause(); + T element{std::move(q_element)}; + state.store(EMPTY, R); + return element; + } + else { + for(;;) { + unsigned char expected = STORED; + if(ATOMIC_QUEUE_LIKELY(state.compare_exchange_strong(expected, LOADING, A, X))) { + T element{std::move(q_element)}; + state.store(EMPTY, R); + return element; + } + // Do speculative loads while busy-waiting to avoid broadcasting RFO messages. + do + spin_loop_pause(); + while(Derived::maximize_throughput_ && state.load(X) != STORED); + } + } + } + + template + static void do_push_any(U&& element, std::atomic& state, T& q_element) noexcept { + if(Derived::spsc_) { + while(ATOMIC_QUEUE_UNLIKELY(state.load(A) != EMPTY)) + if(Derived::maximize_throughput_) + spin_loop_pause(); + q_element = std::forward(element); + state.store(STORED, R); + } + else { + for(;;) { + unsigned char expected = EMPTY; + if(ATOMIC_QUEUE_LIKELY(state.compare_exchange_strong(expected, STORING, A, X))) { + q_element = std::forward(element); + state.store(STORED, R); + return; + } + // Do speculative loads while busy-waiting to avoid broadcasting RFO messages. + do + spin_loop_pause(); + while(Derived::maximize_throughput_ && state.load(X) != EMPTY); + } + } + } + +public: + template + bool try_push(T&& element) noexcept { + auto head = head_.load(X); + if(Derived::spsc_) { + if(static_cast(head - tail_.load(X)) >= static_cast(static_cast(*this).size_)) + return false; + head_.store(head + 1, X); + } + else { + do { + if(static_cast(head - tail_.load(X)) >= static_cast(static_cast(*this).size_)) + return false; + } while(ATOMIC_QUEUE_UNLIKELY(!head_.compare_exchange_strong(head, head + 1, X, X))); // This loop is not FIFO. + } + + static_cast(*this).do_push(std::forward(element), head); + return true; + } + + template + bool try_pop(T& element) noexcept { + auto tail = tail_.load(X); + if(Derived::spsc_) { + if(static_cast(head_.load(X) - tail) <= 0) + return false; + tail_.store(tail + 1, X); + } + else { + do { + if(static_cast(head_.load(X) - tail) <= 0) + return false; + } while(ATOMIC_QUEUE_UNLIKELY(!tail_.compare_exchange_strong(tail, tail + 1, X, X))); // This loop is not FIFO. + } + + element = static_cast(*this).do_pop(tail); + return true; + } + + template + void push(T&& element) noexcept { + unsigned head; + if(Derived::spsc_) { + head = head_.load(X); + head_.store(head + 1, X); + } + else { + constexpr auto memory_order = Derived::total_order_ ? std::memory_order_seq_cst : std::memory_order_relaxed; + head = head_.fetch_add(1, memory_order); // FIFO and total order on Intel regardless, as of 2019. + } + static_cast(*this).do_push(std::forward(element), head); + } + + auto pop() noexcept { + unsigned tail; + if(Derived::spsc_) { + tail = tail_.load(X); + tail_.store(tail + 1, X); + } + else { + constexpr auto memory_order = Derived::total_order_ ? std::memory_order_seq_cst : std::memory_order_relaxed; + tail = tail_.fetch_add(1, memory_order); // FIFO and total order on Intel regardless, as of 2019. + } + return static_cast(*this).do_pop(tail); + } + + bool was_empty() const noexcept { + return !was_size(); + } + + bool was_full() const noexcept { + return was_size() >= static_cast(static_cast(*this).size_); + } + + unsigned was_size() const noexcept { + // tail_ can be greater than head_ because of consumers doing pop, rather that try_pop, when the queue is empty. + return std::max(static_cast(head_.load(X) - tail_.load(X)), 0); + } + + unsigned capacity() const noexcept { + return static_cast(*this).size_; + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template(), bool MINIMIZE_CONTENTION = true, bool MAXIMIZE_THROUGHPUT = true, bool TOTAL_ORDER = false, bool SPSC = false> +class AtomicQueue : public AtomicQueueCommon> { + using Base = AtomicQueueCommon>; + friend Base; + + static constexpr unsigned size_ = MINIMIZE_CONTENTION ? details::round_up_to_power_of_2(SIZE) : SIZE; + static constexpr int SHUFFLE_BITS = details::GetIndexShuffleBits)>::value; + static constexpr bool total_order_ = TOTAL_ORDER; + static constexpr bool spsc_ = SPSC; + static constexpr bool maximize_throughput_ = MAXIMIZE_THROUGHPUT; + + alignas(CACHE_LINE_SIZE) std::atomic elements_[size_] = {}; // Empty elements are NIL. + + T do_pop(unsigned tail) noexcept { + std::atomic& q_element = details::map(elements_, tail % size_); + return Base::template do_pop_atomic(q_element); + } + + void do_push(T element, unsigned head) noexcept { + std::atomic& q_element = details::map(elements_, head % size_); + Base::template do_push_atomic(element, q_element); + } + +public: + using value_type = T; + + AtomicQueue() noexcept { + assert(std::atomic{NIL}.is_lock_free()); // Queue element type T is not atomic. Use AtomicQueue2/AtomicQueueB2 for such element types. + if(details::nil() != NIL) + for(auto& element : elements_) + element.store(NIL, X); + } + + AtomicQueue(AtomicQueue const&) = delete; + AtomicQueue& operator=(AtomicQueue const&) = delete; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class AtomicQueue2 : public AtomicQueueCommon> { + using Base = AtomicQueueCommon>; + using State = typename Base::State; + friend Base; + + static constexpr unsigned size_ = MINIMIZE_CONTENTION ? details::round_up_to_power_of_2(SIZE) : SIZE; + static constexpr int SHUFFLE_BITS = details::GetIndexShuffleBits::value; + static constexpr bool total_order_ = TOTAL_ORDER; + static constexpr bool spsc_ = SPSC; + static constexpr bool maximize_throughput_ = MAXIMIZE_THROUGHPUT; + + alignas(CACHE_LINE_SIZE) std::atomic states_[size_] = {}; + alignas(CACHE_LINE_SIZE) T elements_[size_] = {}; + + T do_pop(unsigned tail) noexcept { + unsigned index = details::remap_index(tail % size_); + return Base::template do_pop_any(states_[index], elements_[index]); + } + + template + void do_push(U&& element, unsigned head) noexcept { + unsigned index = details::remap_index(head % size_); + Base::template do_push_any(std::forward(element), states_[index], elements_[index]); + } + +public: + using value_type = T; + + AtomicQueue2() noexcept = default; + AtomicQueue2(AtomicQueue2 const&) = delete; + AtomicQueue2& operator=(AtomicQueue2 const&) = delete; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template, T NIL = details::nil(), bool MAXIMIZE_THROUGHPUT = true, bool TOTAL_ORDER = false, bool SPSC = false> +class AtomicQueueB : public AtomicQueueCommon>, + private std::allocator_traits::template rebind_alloc> { + using Base = AtomicQueueCommon>; + friend Base; + + static constexpr bool total_order_ = TOTAL_ORDER; + static constexpr bool spsc_ = SPSC; + static constexpr bool maximize_throughput_ = MAXIMIZE_THROUGHPUT; + + using AllocatorElements = typename std::allocator_traits::template rebind_alloc>; + + static constexpr auto ELEMENTS_PER_CACHE_LINE = CACHE_LINE_SIZE / sizeof(std::atomic); + static_assert(ELEMENTS_PER_CACHE_LINE, "Unexpected ELEMENTS_PER_CACHE_LINE."); + + static constexpr auto SHUFFLE_BITS = details::GetCacheLineIndexBits::value; + static_assert(SHUFFLE_BITS, "Unexpected SHUFFLE_BITS."); + + // AtomicQueueCommon members are stored into by readers and writers. + // Allocate these immutable members on another cache line which never gets invalidated by stores. + alignas(CACHE_LINE_SIZE) unsigned size_; + std::atomic* elements_; + + T do_pop(unsigned tail) noexcept { + std::atomic& q_element = details::map(elements_, tail & (size_ - 1)); + return Base::template do_pop_atomic(q_element); + } + + void do_push(T element, unsigned head) noexcept { + std::atomic& q_element = details::map(elements_, head & (size_ - 1)); + Base::template do_push_atomic(element, q_element); + } + +public: + using value_type = T; + + // The special member functions are not thread-safe. + + AtomicQueueB(unsigned size) + : size_(std::max(details::round_up_to_power_of_2(size), 1u << (SHUFFLE_BITS * 2))) + , elements_(AllocatorElements::allocate(size_)) { + assert(std::atomic{NIL}.is_lock_free()); // Queue element type T is not atomic. Use AtomicQueue2/AtomicQueueB2 for such element types. + for(auto p = elements_, q = elements_ + size_; p < q; ++p) + p->store(NIL, X); + } + + AtomicQueueB(AtomicQueueB&& b) noexcept + : Base(static_cast(b)) + , AllocatorElements(static_cast(b)) // TODO: This must be noexcept, static_assert that. + , size_(b.size_) + , elements_(b.elements_) { + b.size_ = 0; + b.elements_ = 0; + } + + AtomicQueueB& operator=(AtomicQueueB&& b) noexcept { + b.swap(*this); + return *this; + } + + ~AtomicQueueB() noexcept { + if(elements_) + AllocatorElements::deallocate(elements_, size_); // TODO: This must be noexcept, static_assert that. + } + + void swap(AtomicQueueB& b) noexcept { + using std::swap; + this->Base::swap(b); + swap(static_cast(*this), static_cast(b)); + swap(size_, b.size_); + swap(elements_, b.elements_); + } + + friend void swap(AtomicQueueB& a, AtomicQueueB& b) { + a.swap(b); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template, bool MAXIMIZE_THROUGHPUT = true, bool TOTAL_ORDER = false, bool SPSC = false> +class AtomicQueueB2 : public AtomicQueueCommon>, + private A, + private std::allocator_traits::template rebind_alloc> { + using Base = AtomicQueueCommon>; + using State = typename Base::State; + friend Base; + + static constexpr bool total_order_ = TOTAL_ORDER; + static constexpr bool spsc_ = SPSC; + static constexpr bool maximize_throughput_ = MAXIMIZE_THROUGHPUT; + + using AllocatorElements = A; + using AllocatorStates = typename std::allocator_traits::template rebind_alloc>; + + // AtomicQueueCommon members are stored into by readers and writers. + // Allocate these immutable members on another cache line which never gets invalidated by stores. + alignas(CACHE_LINE_SIZE) unsigned size_; + std::atomic* states_; + T* elements_; + + static constexpr auto STATES_PER_CACHE_LINE = CACHE_LINE_SIZE / sizeof(State); + static_assert(STATES_PER_CACHE_LINE, "Unexpected STATES_PER_CACHE_LINE."); + + static constexpr auto SHUFFLE_BITS = details::GetCacheLineIndexBits::value; + static_assert(SHUFFLE_BITS, "Unexpected SHUFFLE_BITS."); + + T do_pop(unsigned tail) noexcept { + unsigned index = details::remap_index(tail & (size_ - 1)); + return Base::template do_pop_any(states_[index], elements_[index]); + } + + template + void do_push(U&& element, unsigned head) noexcept { + unsigned index = details::remap_index(head & (size_ - 1)); + Base::template do_push_any(std::forward(element), states_[index], elements_[index]); + } + +public: + using value_type = T; + + // The special member functions are not thread-safe. + + AtomicQueueB2(unsigned size) + : size_(std::max(details::round_up_to_power_of_2(size), 1u << (SHUFFLE_BITS * 2))) + , states_(AllocatorStates::allocate(size_)) + , elements_(AllocatorElements::allocate(size_)) { + for(auto p = states_, q = states_ + size_; p < q; ++p) + p->store(Base::EMPTY, X); + + AllocatorElements& ae = *this; + for(auto p = elements_, q = elements_ + size_; p < q; ++p) + std::allocator_traits::construct(ae, p); + } + + AtomicQueueB2(AtomicQueueB2&& b) noexcept + : Base(static_cast(b)) + , AllocatorElements(static_cast(b)) // TODO: This must be noexcept, static_assert that. + , AllocatorStates(static_cast(b)) // TODO: This must be noexcept, static_assert that. + , size_(b.size_) + , states_(b.states_) + , elements_(b.elements_) { + b.size_ = 0; + b.states_ = 0; + b.elements_ = 0; + } + + AtomicQueueB2& operator=(AtomicQueueB2&& b) noexcept { + b.swap(*this); + return *this; + } + + ~AtomicQueueB2() noexcept { + if(elements_) { + AllocatorElements& ae = *this; + for(auto p = elements_, q = elements_ + size_; p < q; ++p) + std::allocator_traits::destroy(ae, p); + AllocatorElements::deallocate(elements_, size_); // TODO: This must be noexcept, static_assert that. + AllocatorStates::deallocate(states_, size_); // TODO: This must be noexcept, static_assert that. + } + } + + void swap(AtomicQueueB2& b) noexcept { + using std::swap; + this->Base::swap(b); + swap(static_cast(*this), static_cast(b)); + swap(static_cast(*this), static_cast(b)); + swap(size_, b.size_); + swap(states_, b.states_); + swap(elements_, b.elements_); + } + + friend void swap(AtomicQueueB2& a, AtomicQueueB2& b) noexcept { + a.swap(b); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +struct RetryDecorator : Queue { + using T = typename Queue::value_type; + + using Queue::Queue; + + void push(T element) noexcept { + while(!this->try_push(element)) + spin_loop_pause(); + } + + T pop() noexcept { + T element; + while(!this->try_pop(element)) + spin_loop_pause(); + return element; + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace atomic_queue + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // ATOMIC_QUEUE_ATOMIC_QUEUE_H_INCLUDED diff --git a/include/public/atomic_queue/atomic_queue_mutex.h b/include/public/atomic_queue/atomic_queue_mutex.h new file mode 100644 index 000000000..ea6731f7c --- /dev/null +++ b/include/public/atomic_queue/atomic_queue_mutex.h @@ -0,0 +1,92 @@ +/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ +#ifndef ATOMIC_QUEUE_ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED +#define ATOMIC_QUEUE_ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED + +// Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file LICENSE. + +#include "atomic_queue.h" +#include "spinlock.h" + +#include +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace atomic_queue { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +struct ScopedLockType { + using type = typename M::scoped_lock; +}; + +template<> +struct ScopedLockType { + using type = std::unique_lock; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class AtomicQueueMutexT { + static constexpr unsigned size_ = MINIMIZE_CONTENTION ? details::round_up_to_power_of_2(SIZE) : SIZE; + + Mutex mutex_; + alignas(CACHE_LINE_SIZE) unsigned head_ = 0; + alignas(CACHE_LINE_SIZE) unsigned tail_ = 0; + alignas(CACHE_LINE_SIZE) T q_[size_] = {}; + + static constexpr int SHUFFLE_BITS = details::GetIndexShuffleBits::value; + + using ScopedLock = typename ScopedLockType::type; + +public: + using value_type = T; + + template + bool try_push(U&& element) noexcept { + ScopedLock lock(mutex_); + if(ATOMIC_QUEUE_LIKELY(head_ - tail_ < size_)) { + q_[details::remap_index(head_ % size_)] = std::forward(element); + ++head_; + return true; + } + return false; + } + + bool try_pop(T& element) noexcept { + ScopedLock lock(mutex_); + if(ATOMIC_QUEUE_LIKELY(head_ != tail_)) { + element = std::move(q_[details::remap_index(tail_ % size_)]); + ++tail_; + return true; + } + return false; + } + + bool was_empty() const noexcept { + return static_cast(head_ - tail_) <= 0; + } + + bool was_full() const noexcept { + return static_cast(head_ - tail_) >= static_cast(size_); + } +}; + +template +using AtomicQueueMutex = AtomicQueueMutexT; + +template +using AtomicQueueSpinlock = AtomicQueueMutexT; + +// template +// using AtomicQueueSpinlockHle = AtomicQueueMutexT; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace atomic_queue + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // ATOMIC_QUEUE_ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED diff --git a/include/public/atomic_queue/barrier.h b/include/public/atomic_queue/barrier.h new file mode 100644 index 000000000..23ca0dccc --- /dev/null +++ b/include/public/atomic_queue/barrier.h @@ -0,0 +1,38 @@ +/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ +#ifndef BARRIER_H_INCLUDED +#define BARRIER_H_INCLUDED + +// Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file LICENSE. + +#include "defs.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace atomic_queue { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class Barrier { + std::atomic counter_ = {}; + +public: + void wait() noexcept { + counter_.fetch_add(1, std::memory_order_acquire); + while(counter_.load(std::memory_order_relaxed)) + spin_loop_pause(); + } + + void release(unsigned expected_counter) noexcept { + while(expected_counter != counter_.load(std::memory_order_relaxed)) + spin_loop_pause(); + counter_.store(0, std::memory_order_release); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace atomic_queue + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // BARRIER_H_INCLUDED diff --git a/include/public/atomic_queue/defs.h b/include/public/atomic_queue/defs.h new file mode 100644 index 000000000..d459209c7 --- /dev/null +++ b/include/public/atomic_queue/defs.h @@ -0,0 +1,93 @@ +/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ +#ifndef ATOMIC_QUEUE_DEFS_H_INCLUDED +#define ATOMIC_QUEUE_DEFS_H_INCLUDED + +// Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file LICENSE. + +#include + +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) +#include +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 64; +static inline void spin_loop_pause() noexcept { + _mm_pause(); +} +} // namespace atomic_queue +#elif defined(__arm__) || defined(__aarch64__) +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 64; +static inline void spin_loop_pause() noexcept { +#if (defined(__ARM_ARCH_6K__) || \ + defined(__ARM_ARCH_6Z__) || \ + defined(__ARM_ARCH_6ZK__) || \ + defined(__ARM_ARCH_6T2__) || \ + defined(__ARM_ARCH_7__) || \ + defined(__ARM_ARCH_7A__) || \ + defined(__ARM_ARCH_7R__) || \ + defined(__ARM_ARCH_7M__) || \ + defined(__ARM_ARCH_7S__) || \ + defined(__ARM_ARCH_8A__) || \ + defined(__aarch64__)) + asm volatile ("yield" ::: "memory"); +#else + asm volatile ("nop" ::: "memory"); +#endif +} +} // namespace atomic_queue +#elif defined(__ppc64__) || defined(__powerpc64__) +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 128; // TODO: Review that this is the correct value. +static inline void spin_loop_pause() noexcept { + asm volatile("or 31,31,31 # very low priority"); // TODO: Review and benchmark that this is the right instruction. +} +} // namespace atomic_queue +#elif defined(__s390x__) +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 256; // TODO: Review that this is the correct value. +static inline void spin_loop_pause() noexcept {} // TODO: Find the right instruction to use here, if any. +} // namespace atomic_queue +#elif defined(__riscv) +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 64; +static inline void spin_loop_pause() noexcept { + asm volatile (".insn i 0x0F, 0, x0, x0, 0x010"); +} +} // namespace atomic_queue +#else +#warning "Unknown CPU architecture. Using L1 cache line size of 64 bytes and no spinloop pause instruction." +namespace atomic_queue { +constexpr int CACHE_LINE_SIZE = 64; // TODO: Review that this is the correct value. +static inline void spin_loop_pause() noexcept {} +} // namespace atomic_queue +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace atomic_queue { + +#if defined(__GNUC__) || defined(__clang__) +#define ATOMIC_QUEUE_LIKELY(expr) __builtin_expect(static_cast(expr), 1) +#define ATOMIC_QUEUE_UNLIKELY(expr) __builtin_expect(static_cast(expr), 0) +#define ATOMIC_QUEUE_NOINLINE __attribute__((noinline)) +#else +#define ATOMIC_QUEUE_LIKELY(expr) (expr) +#define ATOMIC_QUEUE_UNLIKELY(expr) (expr) +#define ATOMIC_QUEUE_NOINLINE +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +auto constexpr A = std::memory_order_acquire; +auto constexpr R = std::memory_order_release; +auto constexpr X = std::memory_order_relaxed; +auto constexpr C = std::memory_order_seq_cst; +auto constexpr AR = std::memory_order_acq_rel; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace atomic_queue + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // ATOMIC_QUEUE_DEFS_H_INCLUDED diff --git a/include/public/atomic_queue/spinlock.h b/include/public/atomic_queue/spinlock.h new file mode 100644 index 000000000..802ea4676 --- /dev/null +++ b/include/public/atomic_queue/spinlock.h @@ -0,0 +1,203 @@ +/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ +#ifndef ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED +#define ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED + +// Copyright (c) 2019 Maxim Egorushkin. MIT License. See the full licence in file LICENSE. + +#include "defs.h" + +#include +#include +#include + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace atomic_queue { + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class Spinlock { + pthread_spinlock_t s_; + +public: + using scoped_lock = std::lock_guard; + + Spinlock() noexcept { + if(ATOMIC_QUEUE_UNLIKELY(::pthread_spin_init(&s_, 0))) + std::abort(); + } + + Spinlock(Spinlock const&) = delete; + Spinlock& operator=(Spinlock const&) = delete; + + ~Spinlock() noexcept { + ::pthread_spin_destroy(&s_); + } + + void lock() noexcept { + if(ATOMIC_QUEUE_UNLIKELY(::pthread_spin_lock(&s_))) + std::abort(); + } + + void unlock() noexcept { + if(ATOMIC_QUEUE_UNLIKELY(::pthread_spin_unlock(&s_))) + std::abort(); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class TicketSpinlock { + alignas(CACHE_LINE_SIZE) std::atomic ticket_{0}; + alignas(CACHE_LINE_SIZE) std::atomic next_{0}; + +public: + class LockGuard { + TicketSpinlock* const m_; + unsigned const ticket_; + public: + LockGuard(TicketSpinlock& m) noexcept + : m_(&m) + , ticket_(m.lock()) + {} + + LockGuard(LockGuard const&) = delete; + LockGuard& operator=(LockGuard const&) = delete; + + ~LockGuard() noexcept { + m_->unlock(ticket_); + } + }; + + using scoped_lock = LockGuard; + + TicketSpinlock() noexcept = default; + TicketSpinlock(TicketSpinlock const&) = delete; + TicketSpinlock& operator=(TicketSpinlock const&) = delete; + + ATOMIC_QUEUE_NOINLINE unsigned lock() noexcept { + auto ticket = ticket_.fetch_add(1, std::memory_order_relaxed); + for(;;) { + auto position = ticket - next_.load(std::memory_order_acquire); + if(ATOMIC_QUEUE_LIKELY(!position)) + break; + do + spin_loop_pause(); + while(--position); + } + return ticket; + } + + void unlock() noexcept { + unlock(next_.load(std::memory_order_relaxed) + 1); + } + + void unlock(unsigned ticket) noexcept { + next_.store(ticket + 1, std::memory_order_release); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class UnfairSpinlock { + std::atomic lock_{0}; + +public: + using scoped_lock = std::lock_guard; + + UnfairSpinlock(UnfairSpinlock const&) = delete; + UnfairSpinlock& operator=(UnfairSpinlock const&) = delete; + + void lock() noexcept { + for(;;) { + if(!lock_.load(std::memory_order_relaxed) && !lock_.exchange(1, std::memory_order_acquire)) + return; + spin_loop_pause(); + } + } + + void unlock() noexcept { + lock_.store(0, std::memory_order_release); + } +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// class SpinlockHle { +// int lock_ = 0; + +// #ifdef __gcc__ +// static constexpr int HLE_ACQUIRE = __ATOMIC_HLE_ACQUIRE; +// static constexpr int HLE_RELEASE = __ATOMIC_HLE_RELEASE; +// #else +// static constexpr int HLE_ACQUIRE = 0; +// static constexpr int HLE_RELEASE = 0; +// #endif + +// public: +// using scoped_lock = std::lock_guard; + +// SpinlockHle(SpinlockHle const&) = delete; +// SpinlockHle& operator=(SpinlockHle const&) = delete; + +// void lock() noexcept { +// for(int expected = 0; +// !__atomic_compare_exchange_n(&lock_, &expected, 1, false, __ATOMIC_ACQUIRE | HLE_ACQUIRE, __ATOMIC_RELAXED); +// expected = 0) +// spin_loop_pause(); +// } + +// void unlock() noexcept { +// __atomic_store_n(&lock_, 0, __ATOMIC_RELEASE | HLE_RELEASE); +// } +// }; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// class AdaptiveMutex { +// pthread_mutex_t m_; + +// public: +// using scoped_lock = std::lock_guard; + +// AdaptiveMutex() noexcept { +// pthread_mutexattr_t a; +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutexattr_init(&a))) +// std::abort(); +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutexattr_settype(&a, PTHREAD_MUTEX_ADAPTIVE_NP))) +// std::abort(); +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutex_init(&m_, &a))) +// std::abort(); +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutexattr_destroy(&a))) +// std::abort(); +// m_.__data.__spins = 32767; +// } + +// AdaptiveMutex(AdaptiveMutex const&) = delete; +// AdaptiveMutex& operator=(AdaptiveMutex const&) = delete; + +// ~AdaptiveMutex() noexcept { +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutex_destroy(&m_))) +// std::abort(); +// } + +// void lock() noexcept { +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutex_lock(&m_))) +// std::abort(); +// } + +// void unlock() noexcept { +// if(ATOMIC_QUEUE_UNLIKELY(::pthread_mutex_unlock(&m_))) +// std::abort(); +// } +// }; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace atomic_queue + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#endif // ATOMIC_QUEUE_SPIN_LOCK_H_INCLUDED diff --git a/include/public/gsl/byte b/include/public/gsl/byte new file mode 100644 index 000000000..f92a91c92 --- /dev/null +++ b/include/public/gsl/byte @@ -0,0 +1,213 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_BYTE_H +#define GSL_BYTE_H + +// +// make suppress attributes work for some compilers +// Hopefully temporary until suppression standardization occurs +// +#if defined(__clang__) +#define GSL_SUPPRESS(x) [[gsl::suppress("x")]] +#else +#if defined(_MSC_VER) && !defined(__INTEL_COMPILER) && !defined(__NVCC__) +#define GSL_SUPPRESS(x) [[gsl::suppress(x)]] +#else +#define GSL_SUPPRESS(x) +#endif // _MSC_VER +#endif // __clang__ + +#include + +// VS2017 15.8 added support for the __cpp_lib_byte definition +// To do: drop _HAS_STD_BYTE when support for pre 15.8 expires +#ifdef _MSC_VER + +#pragma warning(push) + +// Turn MSVC /analyze rules that generate too much noise. TODO: fix in the tool. +#pragma warning(disable : 26493) // don't use c-style casts // TODO: MSVC suppression in templates + // does not always work + +#ifndef GSL_USE_STD_BYTE +// this tests if we are under MSVC and the standard lib has std::byte and it is enabled +#if (defined(_HAS_STD_BYTE) && _HAS_STD_BYTE) || \ + (defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603) + +#define GSL_USE_STD_BYTE 1 + +#else // (defined(_HAS_STD_BYTE) && _HAS_STD_BYTE) || (defined(__cpp_lib_byte) && __cpp_lib_byte >= + // 201603) + +#define GSL_USE_STD_BYTE 0 + +#endif // (defined(_HAS_STD_BYTE) && _HAS_STD_BYTE) || (defined(__cpp_lib_byte) && __cpp_lib_byte >= + // 201603) +#endif // GSL_USE_STD_BYTE + +#else // _MSC_VER + +#ifndef GSL_USE_STD_BYTE +#include /* __cpp_lib_byte */ +// this tests if we are under GCC or Clang with enough -std=c++1z power to get us std::byte +// also check if libc++ version is sufficient (> 5.0) or libstdc++ actually contains std::byte +#if defined(__cplusplus) && (__cplusplus >= 201703L) && \ + (defined(__cpp_lib_byte) && (__cpp_lib_byte >= 201603) || \ + defined(_LIBCPP_VERSION) && (_LIBCPP_VERSION >= 5000)) + +#define GSL_USE_STD_BYTE 1 + +#else // defined(__cplusplus) && (__cplusplus >= 201703L) && + // (defined(__cpp_lib_byte) && (__cpp_lib_byte >= 201603) || + // defined(_LIBCPP_VERSION) && (_LIBCPP_VERSION >= 5000)) + +#define GSL_USE_STD_BYTE 0 + +#endif // defined(__cplusplus) && (__cplusplus >= 201703L) && + // (defined(__cpp_lib_byte) && (__cpp_lib_byte >= 201603) || + // defined(_LIBCPP_VERSION) && (_LIBCPP_VERSION >= 5000)) +#endif // GSL_USE_STD_BYTE + +#endif // _MSC_VER + +// Use __may_alias__ attribute on gcc and clang +#if defined __clang__ || (defined(__GNUC__) && __GNUC__ > 5) +#define byte_may_alias __attribute__((__may_alias__)) +#else // defined __clang__ || defined __GNUC__ +#define byte_may_alias +#endif // defined __clang__ || defined __GNUC__ + +#if GSL_USE_STD_BYTE +#include +#endif + +namespace gsl +{ +#if GSL_USE_STD_BYTE + +using std::byte; +using std::to_integer; + +#else // GSL_USE_STD_BYTE + +// This is a simple definition for now that allows +// use of byte within span<> to be standards-compliant +enum class byte_may_alias byte : unsigned char +{ +}; + +template ::value>> +constexpr byte& operator<<=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte operator<<(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) << shift); +} + +template ::value>> +constexpr byte& operator>>=(byte& b, IntegerType shift) noexcept +{ + return b = byte(static_cast(b) >> shift); +} + +template ::value>> +constexpr byte operator>>(byte b, IntegerType shift) noexcept +{ + return byte(static_cast(b) >> shift); +} + +constexpr byte& operator|=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) | static_cast(r)); +} + +constexpr byte operator|(byte l, byte r) noexcept +{ + return byte(static_cast(l) | static_cast(r)); +} + +constexpr byte& operator&=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) & static_cast(r)); +} + +constexpr byte operator&(byte l, byte r) noexcept +{ + return byte(static_cast(l) & static_cast(r)); +} + +constexpr byte& operator^=(byte& l, byte r) noexcept +{ + return l = byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator^(byte l, byte r) noexcept +{ + return byte(static_cast(l) ^ static_cast(r)); +} + +constexpr byte operator~(byte b) noexcept { return byte(~static_cast(b)); } + +template ::value>> +constexpr IntegerType to_integer(byte b) noexcept +{ + return static_cast(b); +} + +#endif // GSL_USE_STD_BYTE + +template +constexpr byte to_byte_impl(T t) noexcept +{ + static_assert( + E, "gsl::to_byte(t) must be provided an unsigned char, otherwise data loss may occur. " + "If you are calling to_byte with an integer contant use: gsl::to_byte() version."); + return static_cast(t); +} +template <> +// NOTE: need suppression since c++14 does not allow "return {t}" +// GSL_SUPPRESS(type.4) // NO-FORMAT: attribute // TODO: suppression does not work +constexpr byte to_byte_impl(unsigned char t) noexcept +{ + return byte(t); +} + +template +constexpr byte to_byte(T t) noexcept +{ + return to_byte_impl::value, T>(t); +} + +template +constexpr byte to_byte() noexcept +{ + static_assert(I >= 0 && I <= 255, + "gsl::byte only has 8 bits of storage, values must be in range 0-255"); + return static_cast(I); +} + +} // namespace gsl + +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER + +#endif // GSL_BYTE_H diff --git a/include/public/gsl/span b/include/public/gsl/span new file mode 100644 index 000000000..49e1502ab --- /dev/null +++ b/include/public/gsl/span @@ -0,0 +1,844 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_SPAN_H +#define GSL_SPAN_H + +#include "assert" // for Expects +#include "byte" // for byte +#include "span_ext" // for span specialization of gsl::at and other span-related extensions +#include "util" // for narrow_cast + +#include // for array +#include // for ptrdiff_t, size_t, nullptr_t +#include // for reverse_iterator, distance, random_access_... +#include // for pointer_traits +#include // for enable_if_t, declval, is_convertible, inte... + +#if defined(__has_include) && __has_include() +#include +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#pragma warning(push) + +// turn off some warnings that are noisy about our Expects statements +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning( \ + disable : 4146) // unary minus operator applied to unsigned type, result still unsigned +#pragma warning(disable : 4702) // unreachable code + +// Turn MSVC /analyze rules that generate too much noise. TODO: fix in the tool. +#pragma warning(disable : 26495) // uninitalized member when constructor calls constructor +#pragma warning(disable : 26446) // parser bug does not allow attributes on some templates + +#endif // _MSC_VER + +// See if we have enough C++17 power to use a static constexpr data member +// without needing an out-of-line definition +#if !(defined(__cplusplus) && (__cplusplus >= 201703L)) +#define GSL_USE_STATIC_CONSTEXPR_WORKAROUND +#endif // !(defined(__cplusplus) && (__cplusplus >= 201703L)) + +// GCC 7 does not like the signed unsigned missmatch (size_t ptrdiff_t) +// While there is a conversion from signed to unsigned, it happens at +// compiletime, so the compiler wouldn't have to warn indiscriminately, but +// could check if the source value actually doesn't fit into the target type +// and only warn in those cases. +#if defined(__GNUC__) && __GNUC__ > 6 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif + +namespace gsl +{ + +// implementation details +namespace details +{ + template + struct is_span_oracle : std::false_type + { + }; + + template + struct is_span_oracle> : std::true_type + { + }; + + template + struct is_span : public is_span_oracle> + { + }; + + template + struct is_std_array_oracle : std::false_type + { + }; + + template + struct is_std_array_oracle> : std::true_type + { + }; + + template + struct is_std_array : is_std_array_oracle> + { + }; + + template + struct is_allowed_extent_conversion + : std::integral_constant + { + }; + + template + struct is_allowed_element_type_conversion + : std::integral_constant::value> + { + }; + + template + class span_iterator + { + public: +#if defined(__cpp_lib_ranges) || (defined(_MSVC_STL_VERSION) && defined(__cpp_lib_concepts)) + using iterator_concept = std::contiguous_iterator_tag; +#endif // __cpp_lib_ranges + using iterator_category = std::random_access_iterator_tag; + using value_type = std::remove_cv_t; + using difference_type = std::ptrdiff_t; + using pointer = Type*; + using reference = Type&; + +#ifdef _MSC_VER + using _Unchecked_type = pointer; +#endif // _MSC_VER + constexpr span_iterator() = default; + + constexpr span_iterator(pointer begin, pointer end, pointer current) + : begin_(begin), end_(end), current_(current) + {} + + constexpr operator span_iterator() const noexcept + { + return {begin_, end_, current_}; + } + + constexpr reference operator*() const noexcept + { + Expects(begin_ && end_); + Expects(begin_ <= current_ && current_ < end_); + return *current_; + } + + constexpr pointer operator->() const noexcept + { + Expects(begin_ && end_); + Expects(begin_ <= current_ && current_ < end_); + return current_; + } + constexpr span_iterator& operator++() noexcept + { + Expects(begin_ && current_ && end_); + Expects(current_ < end_); + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + ++current_; + return *this; + } + + constexpr span_iterator operator++(int) noexcept + { + span_iterator ret = *this; + ++*this; + return ret; + } + + constexpr span_iterator& operator--() noexcept + { + Expects(begin_ && end_); + Expects(begin_ < current_); + --current_; + return *this; + } + + constexpr span_iterator operator--(int) noexcept + { + span_iterator ret = *this; + --*this; + return ret; + } + + constexpr span_iterator& operator+=(const difference_type n) noexcept + { + if (n != 0) Expects(begin_ && current_ && end_); + if (n > 0) Expects(end_ - current_ >= n); + if (n < 0) Expects(current_ - begin_ >= -n); + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + current_ += n; + return *this; + } + + constexpr span_iterator operator+(const difference_type n) const noexcept + { + span_iterator ret = *this; + ret += n; + return ret; + } + + friend constexpr span_iterator operator+(const difference_type n, + const span_iterator& rhs) noexcept + { + return rhs + n; + } + + constexpr span_iterator& operator-=(const difference_type n) noexcept + { + if (n != 0) Expects(begin_ && current_ && end_); + if (n > 0) Expects(current_ - begin_ >= n); + if (n < 0) Expects(end_ - current_ >= -n); + current_ -= n; + return *this; + } + + constexpr span_iterator operator-(const difference_type n) const noexcept + { + span_iterator ret = *this; + ret -= n; + return ret; + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr difference_type operator-(const span_iterator& rhs) const noexcept + { + Expects(begin_ == rhs.begin_ && end_ == rhs.end_); + return current_ - rhs.current_; + } + + constexpr reference operator[](const difference_type n) const noexcept + { + return *(*this + n); + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator==(const span_iterator& rhs) const noexcept + { + Expects(begin_ == rhs.begin_ && end_ == rhs.end_); + return current_ == rhs.current_; + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator!=(const span_iterator& rhs) const noexcept + { + return !(*this == rhs); + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator<(const span_iterator& rhs) const noexcept + { + Expects(begin_ == rhs.begin_ && end_ == rhs.end_); + return current_ < rhs.current_; + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator>(const span_iterator& rhs) const noexcept + { + return rhs < *this; + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator<=(const span_iterator& rhs) const noexcept + { + return !(rhs < *this); + } + + template < + class Type2, + std::enable_if_t, value_type>::value, int> = 0> + constexpr bool operator>=(const span_iterator& rhs) const noexcept + { + return !(*this < rhs); + } + +#ifdef _MSC_VER + // MSVC++ iterator debugging support; allows STL algorithms in 15.8+ + // to unwrap span_iterator to a pointer type after a range check in STL + // algorithm calls + friend constexpr void _Verify_range(span_iterator lhs, span_iterator rhs) noexcept + { // test that [lhs, rhs) forms a valid range inside an STL algorithm + Expects(lhs.begin_ == rhs.begin_ // range spans have to match + && lhs.end_ == rhs.end_ && + lhs.current_ <= rhs.current_); // range must not be transposed + } + + constexpr void _Verify_offset(const difference_type n) const noexcept + { // test that *this + n is within the range of this call + if (n != 0) Expects(begin_ && current_ && end_); + if (n > 0) Expects(end_ - current_ >= n); + if (n < 0) Expects(current_ - begin_ >= -n); + } + + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + constexpr pointer _Unwrapped() const noexcept + { // after seeking *this to a high water mark, or using one of the + // _Verify_xxx functions above, unwrap this span_iterator to a raw + // pointer + return current_; + } + + // Tell the STL that span_iterator should not be unwrapped if it can't + // validate in advance, even in release / optimized builds: +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const bool _Unwrap_when_unverified = false; +#else + static constexpr bool _Unwrap_when_unverified = false; +#endif + // clang-format off + GSL_SUPPRESS(con.3) // NO-FORMAT: attribute // TODO: false positive + // clang-format on + constexpr void _Seek_to(const pointer p) noexcept + { // adjust the position of *this to previously verified location p + // after _Unwrapped + current_ = p; + } +#endif + + pointer begin_ = nullptr; + pointer end_ = nullptr; + pointer current_ = nullptr; + + template + friend struct std::pointer_traits; + }; +}} // namespace gsl::details + +namespace std +{ +template +struct pointer_traits<::gsl::details::span_iterator> +{ + using pointer = ::gsl::details::span_iterator; + using element_type = Type; + using difference_type = ptrdiff_t; + + static constexpr element_type* to_address(const pointer i) noexcept { return i.current_; } +}; +} // namespace std + +namespace gsl { namespace details { + template + class extent_type + { + public: + using size_type = std::size_t; + + constexpr extent_type() noexcept = default; + + constexpr explicit extent_type(extent_type); + + constexpr explicit extent_type(size_type size) { Expects(size == Ext); } + + constexpr size_type size() const noexcept { return Ext; } + + private: +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const size_type size_ = Ext; // static size equal to Ext +#else + static constexpr size_type size_ = Ext; // static size equal to Ext +#endif + }; + + template <> + class extent_type + { + public: + using size_type = std::size_t; + + template + constexpr explicit extent_type(extent_type ext) : size_(ext.size()) + {} + + constexpr explicit extent_type(size_type size) : size_(size) + { + Expects(size != dynamic_extent); + } + + constexpr size_type size() const noexcept { return size_; } + + private: + size_type size_; + }; + + template + constexpr extent_type::extent_type(extent_type ext) + { + Expects(ext.size() == Ext); + } + + template + struct calculate_subspan_type + { + using type = span; + }; +} // namespace details + +// [span], class template span +template +class span +{ +public: + // constants and types + using element_type = ElementType; + using value_type = std::remove_cv_t; + using size_type = std::size_t; + using pointer = element_type*; + using const_pointer = const element_type*; + using reference = element_type&; + using const_reference = const element_type&; + using difference_type = std::ptrdiff_t; + + using iterator = details::span_iterator; + using reverse_iterator = std::reverse_iterator; + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) + static constexpr const size_type extent{Extent}; +#else + static constexpr size_type extent{Extent}; +#endif + + // [span.cons], span constructors, copy, assignment, and destructor + template " SFINAE, since "std::enable_if_t" is ill-formed when Extent is greater than 0. + class = std::enable_if_t<(Dependent || + details::is_allowed_extent_conversion<0, Extent>::value)>> + constexpr span() noexcept : storage_(nullptr, details::extent_type<0>()) + {} + + template = 0> + constexpr explicit span(pointer ptr, size_type count) noexcept : storage_(ptr, count) + { + Expects(count == Extent); + } + + template = 0> + constexpr span(pointer ptr, size_type count) noexcept : storage_(ptr, count) + {} + + template = 0> + constexpr explicit span(pointer firstElem, pointer lastElem) noexcept + : storage_(firstElem, narrow_cast(lastElem - firstElem)) + { + Expects(lastElem - firstElem == static_cast(Extent)); + } + + template = 0> + constexpr span(pointer firstElem, pointer lastElem) noexcept + : storage_(firstElem, narrow_cast(lastElem - firstElem)) + {} + + template ::value, int> = 0> + constexpr span(element_type (&arr)[N]) noexcept + : storage_(KnownNotNull{arr}, details::extent_type()) + {} + + template < + class T, std::size_t N, + std::enable_if_t<(details::is_allowed_extent_conversion::value && + details::is_allowed_element_type_conversion::value), + int> = 0> + constexpr span(std::array& arr) noexcept + : storage_(KnownNotNull{arr.data()}, details::extent_type()) + {} + + template ::value && + details::is_allowed_element_type_conversion::value), + int> = 0> + constexpr span(const std::array& arr) noexcept + : storage_(KnownNotNull{arr.data()}, details::extent_type()) + {} + + // NB: the SFINAE on these constructors uses .data() as an incomplete/imperfect proxy for the + // requirement on Container to be a contiguous sequence container. + template ::value && + !details::is_std_array::value && + std::is_pointer().data())>::value && + std::is_convertible< + std::remove_pointer_t().data())> (*)[], + element_type (*)[]>::value, + int> = 0> + constexpr explicit span(Container& cont) noexcept : span(cont.data(), cont.size()) + {} + + template ::value && + !details::is_std_array::value && + std::is_pointer().data())>::value && + std::is_convertible< + std::remove_pointer_t().data())> (*)[], + element_type (*)[]>::value, + int> = 0> + constexpr span(Container& cont) noexcept : span(cont.data(), cont.size()) + {} + + template < + std::size_t MyExtent = Extent, class Container, + std::enable_if_t< + MyExtent != dynamic_extent && std::is_const::value && + !details::is_span::value && !details::is_std_array::value && + std::is_pointer().data())>::value && + std::is_convertible< + std::remove_pointer_t().data())> (*)[], + element_type (*)[]>::value, + int> = 0> + constexpr explicit span(const Container& cont) noexcept : span(cont.data(), cont.size()) + {} + + template < + std::size_t MyExtent = Extent, class Container, + std::enable_if_t< + MyExtent == dynamic_extent && std::is_const::value && + !details::is_span::value && !details::is_std_array::value && + std::is_pointer().data())>::value && + std::is_convertible< + std::remove_pointer_t().data())> (*)[], + element_type (*)[]>::value, + int> = 0> + constexpr span(const Container& cont) noexcept : span(cont.data(), cont.size()) + {} + + constexpr span(const span& other) noexcept = default; + + template ::value, + int> = 0> + constexpr span(const span& other) noexcept + : storage_(other.data(), details::extent_type(other.size())) + {} + + template ::value, + int> = 0> + constexpr explicit span(const span& other) noexcept + : storage_(other.data(), details::extent_type(other.size())) + {} + + ~span() noexcept = default; + constexpr span& operator=(const span& other) noexcept = default; + + // [span.sub], span subviews + template + constexpr span first() const noexcept + { + Expects(Count <= size()); + return span{data(), Count}; + } + + template + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + constexpr span last() const noexcept + { + Expects(Count <= size()); + return span{data() + (size() - Count), Count}; + } + + template + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + constexpr auto subspan() const noexcept -> + typename details::calculate_subspan_type::type + { + Expects((size() >= Offset) && (Count == dynamic_extent || (Count <= size() - Offset))); + using type = + typename details::calculate_subspan_type::type; + return type{data() + Offset, Count == dynamic_extent ? size() - Offset : Count}; + } + + constexpr span first(size_type count) const noexcept + { + Expects(count <= size()); + return {data(), count}; + } + + constexpr span last(size_type count) const noexcept + { + Expects(count <= size()); + return make_subspan(size() - count, dynamic_extent, subspan_selector{}); + } + + constexpr span + subspan(size_type offset, size_type count = dynamic_extent) const noexcept + { + return make_subspan(offset, count, subspan_selector{}); + } + + // [span.obs], span observers + constexpr size_type size() const noexcept { return storage_.size(); } + + constexpr size_type size_bytes() const noexcept + { + Expects(size() < dynamic_extent / sizeof(element_type)); + return size() * sizeof(element_type); + } + + constexpr bool empty() const noexcept { return size() == 0; } + + // [span.elem], span element access + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + constexpr reference operator[](size_type idx) const noexcept + { + Expects(idx < size()); + return data()[idx]; + } + + constexpr reference front() const noexcept + { + Expects(size() > 0); + return data()[0]; + } + + constexpr reference back() const noexcept + { + Expects(size() > 0); + return data()[size() - 1]; + } + + constexpr pointer data() const noexcept { return storage_.data(); } + + // [span.iter], span iterator support + constexpr iterator begin() const noexcept + { + const auto data = storage_.data(); + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + return {data, data + size(), data}; + } + + constexpr iterator end() const noexcept + { + const auto data = storage_.data(); + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + const auto endData = data + storage_.size(); + return {data, endData, endData}; + } + + constexpr reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; } + constexpr reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; } + +#ifdef _MSC_VER + // Tell MSVC how to unwrap spans in range-based-for + constexpr pointer _Unchecked_begin() const noexcept { return data(); } + constexpr pointer _Unchecked_end() const noexcept + { + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + return data() + size(); + } +#endif // _MSC_VER + +private: + // Needed to remove unnecessary null check in subspans + struct KnownNotNull + { + pointer p; + }; + + // this implementation detail class lets us take advantage of the + // empty base class optimization to pay for only storage of a single + // pointer in the case of fixed-size spans + template + class storage_type : public ExtentType + { + public: + // KnownNotNull parameter is needed to remove unnecessary null check + // in subspans and constructors from arrays + template + constexpr storage_type(KnownNotNull data, OtherExtentType ext) + : ExtentType(ext), data_(data.p) + {} + + template + constexpr storage_type(pointer data, OtherExtentType ext) : ExtentType(ext), data_(data) + { + Expects(data || ExtentType::size() == 0); + } + + constexpr pointer data() const noexcept { return data_; } + + private: + pointer data_; + }; + + storage_type> storage_; + + // The rest is needed to remove unnecessary null check + // in subspans and constructors from arrays + constexpr span(KnownNotNull ptr, size_type count) noexcept : storage_(ptr, count) {} + + template + class subspan_selector + { + }; + + template + constexpr span + make_subspan(size_type offset, size_type count, subspan_selector) const noexcept + { + const span tmp(*this); + return tmp.subspan(offset, count); + } + + // clang-format off + GSL_SUPPRESS(bounds.1) // NO-FORMAT: attribute + // clang-format on + constexpr span + make_subspan(size_type offset, size_type count, subspan_selector) const noexcept + { + Expects(size() >= offset); + + if (count == dynamic_extent) { return {KnownNotNull{data() + offset}, size() - offset}; } + + Expects(size() - offset >= count); + return {KnownNotNull{data() + offset}, count}; + } +}; + +#if (defined(__cpp_deduction_guides) && (__cpp_deduction_guides >= 201611L)) + +// Deduction Guides +template +span(Type (&)[Extent]) -> span; + +template +span(std::array&) -> span; + +template +span(const std::array&) -> span; + +template ().data())>> +span(Container&) -> span; + +template ().data())>> +span(const Container&) -> span; + +#endif // ( defined(__cpp_deduction_guides) && (__cpp_deduction_guides >= 201611L) ) + +#if defined(GSL_USE_STATIC_CONSTEXPR_WORKAROUND) +template +constexpr const typename span::size_type span::extent; +#endif + +namespace details +{ + // if we only supported compilers with good constexpr support then + // this pair of classes could collapse down to a constexpr function + + // we should use a narrow_cast<> to go to std::size_t, but older compilers may not see it as + // constexpr + // and so will fail compilation of the template + template + struct calculate_byte_size : std::integral_constant + { + static_assert(Extent < dynamic_extent / sizeof(ElementType), "Size is too big."); + }; + + template + struct calculate_byte_size + : std::integral_constant + { + }; +} // namespace details + +// [span.objectrep], views of object representation +template +span::value> +as_bytes(span s) noexcept +{ + using type = span::value>; + + // clang-format off + GSL_SUPPRESS(type.1) // NO-FORMAT: attribute + // clang-format on + return type{reinterpret_cast(s.data()), s.size_bytes()}; +} + +template ::value, int> = 0> +span::value> +as_writable_bytes(span s) noexcept +{ + using type = span::value>; + + // clang-format off + GSL_SUPPRESS(type.1) // NO-FORMAT: attribute + // clang-format on + return type{reinterpret_cast(s.data()), s.size_bytes()}; +} + +} // namespace gsl + +#if defined(_MSC_VER) && !defined(__clang__) + +#pragma warning(pop) +#endif // _MSC_VER + +#if defined(__GNUC__) && __GNUC__ > 6 +#pragma GCC diagnostic pop +#endif // __GNUC__ > 6 + +#endif // GSL_SPAN_H diff --git a/include/public/gsl/span_ext b/include/public/gsl/span_ext new file mode 100644 index 000000000..516cc991d --- /dev/null +++ b/include/public/gsl/span_ext @@ -0,0 +1,212 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2015 Microsoft Corporation. All rights reserved. +// +// This code is licensed under the MIT License (MIT). +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef GSL_SPAN_EXT_H +#define GSL_SPAN_EXT_H + +/////////////////////////////////////////////////////////////////////////////// +// +// File: span_ext +// Purpose: continue offering features that have been cut from the official +// implementation of span. +// While modernizing gsl::span a number of features needed to be removed to +// be compliant with the design of std::span +// +/////////////////////////////////////////////////////////////////////////////// + +#include "assert" // GSL_KERNEL_MODE +#include "util" // for narrow_cast, narrow + +#include // for ptrdiff_t, size_t +#include + +#ifndef GSL_KERNEL_MODE +#include // for lexicographical_compare +#endif // GSL_KERNEL_MODE + +namespace gsl +{ + +// [span.views.constants], constants +GSL_INLINE constexpr const std::size_t dynamic_extent = narrow_cast(-1); + +template +class span; + +// std::equal and std::lexicographical_compare are not /kernel compatible +// so all comparison operators must be removed for kernel mode. +#ifndef GSL_KERNEL_MODE + +// [span.comparison], span comparison operators +template +constexpr bool operator==(span l, span r) +{ + return std::equal(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator!=(span l, span r) +{ + return !(l == r); +} + +template +constexpr bool operator<(span l, span r) +{ + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); +} + +template +constexpr bool operator<=(span l, span r) +{ + return !(l > r); +} + +template +constexpr bool operator>(span l, span r) +{ + return r < l; +} + +template +constexpr bool operator>=(span l, span r) +{ + return !(l < r); +} + +#endif // GSL_KERNEL_MODE + +// +// make_span() - Utility functions for creating spans +// +template +constexpr span make_span(ElementType* ptr, typename span::size_type count) +{ + return span(ptr, count); +} + +template +constexpr span make_span(ElementType* firstElem, ElementType* lastElem) +{ + return span(firstElem, lastElem); +} + +template +constexpr span make_span(ElementType (&arr)[N]) noexcept +{ + return span(arr); +} + +template +constexpr span make_span(Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(const Container& cont) +{ + return span(cont); +} + +template +constexpr span make_span(Ptr& cont, std::size_t count) +{ + return span(cont, count); +} + +template +constexpr span make_span(Ptr& cont) +{ + return span(cont); +} + +// Specialization of gsl::at for span +template +constexpr ElementType& at(span s, index i) +{ + // No bounds checking here because it is done in span::operator[] called below + Ensures(i >= 0); + return s[narrow_cast(i)]; +} + +// [span.obs] Free observer functions +template +constexpr std::ptrdiff_t ssize(const span& s) noexcept +{ + return static_cast(s.size()); +} + +// [span.iter] Free functions for begin/end functions +template +constexpr typename span::iterator +begin(const span& s) noexcept +{ + return s.begin(); +} + +template +constexpr typename span::iterator +end(const span& s) noexcept +{ + return s.end(); +} + +template +constexpr typename span::reverse_iterator +rbegin(const span& s) noexcept +{ + return s.rbegin(); +} + +template +constexpr typename span::reverse_iterator +rend(const span& s) noexcept +{ + return s.rend(); +} + +template +constexpr typename span::iterator +cbegin(const span& s) noexcept +{ + return s.begin(); +} + +template +constexpr typename span::iterator +cend(const span& s) noexcept +{ + return s.end(); +} + +template +constexpr typename span::reverse_iterator +crbegin(const span& s) noexcept +{ + return s.rbegin(); +} + +template +constexpr typename span::reverse_iterator +crend(const span& s) noexcept +{ + return s.rend(); +} + +} // namespace gsl + +#endif // GSL_SPAN_EXT_H diff --git a/include/public/gsl/util b/include/public/gsl/util index a215bad10..75c78ade4 100644 --- a/include/public/gsl/util +++ b/include/public/gsl/util @@ -17,7 +17,7 @@ #ifndef GSL_UTIL_H #define GSL_UTIL_H -#include // for Expects +#include "assert" // for Expects #include #include // for ptrdiff_t, size_t @@ -45,6 +45,12 @@ #define GSL_NODISCARD #endif // defined(__cplusplus) && (__cplusplus >= 201703L) +#if defined(__cpp_inline_variables) +#define GSL_INLINE inline +#else +#define GSL_INLINE +#endif + namespace gsl { // @@ -59,40 +65,29 @@ template class final_action { public: - static_assert(!std::is_reference::value && !std::is_const::value && - !std::is_volatile::value, - "Final_action should store its callable by value"); + explicit final_action(const F& ff) noexcept : f{ff} { } + explicit final_action(F&& ff) noexcept : f{std::move(ff)} { } - explicit final_action(F f) noexcept : f_(std::move(f)) {} + ~final_action() noexcept { if (invoke) f(); } final_action(final_action&& other) noexcept - : f_(std::move(other.f_)), invoke_(std::exchange(other.invoke_, false)) - {} + : f(std::move(other.f)), invoke(std::exchange(other.invoke, false)) + { } - final_action(const final_action&) = delete; - final_action& operator=(const final_action&) = delete; - final_action& operator=(final_action&&) = delete; - - // clang-format off - GSL_SUPPRESS(f.6) // NO-FORMAT: attribute // terminate if throws - // clang-format on - ~final_action() noexcept - { - if (invoke_) f_(); - } + final_action(const final_action&) = delete; + void operator=(const final_action&) = delete; + void operator=(final_action&&) = delete; private: - F f_; - bool invoke_{true}; + F f; + bool invoke = true; }; // finally() - convenience function to generate a final_action template -GSL_NODISCARD final_action::type>::type> -finally(F&& f) noexcept +GSL_NODISCARD auto finally(F&& f) noexcept { - return final_action::type>::type>( - std::forward(f)); + return final_action>{std::forward(f)}; } // narrow_cast(): a searchable way to do narrowing casts of values diff --git a/src/Sound.cpp b/src/Sound.cpp index ea2734207..f12889ab0 100644 --- a/src/Sound.cpp +++ b/src/Sound.cpp @@ -1,89 +1,58 @@ -// Copyright 2019-2022 Jan Niklas Hasse +// Copyright 2019-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt #include "Sound.hpp" #include "audio.hpp" -#include "SoundParams.hpp" +#include "audio/constants.hpp" +#include "audio/effect/loop.hpp" +#include "audio/effect/pitch.hpp" +#include "audio/effect/volume.hpp" +#include "audio/Track.hpp" #include "jngl/debug.hpp" -#ifdef __APPLE__ -// OpenAL is deprecated in favor of AVAudioEngine -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#endif +#include namespace jngl { struct Sound::Impl { - ALuint buffer = 0; - ALuint source = 0; + std::shared_ptr track; + std::shared_ptr stream; + std::shared_ptr volumeControl; }; -float Sound::masterVolume = 1.0f; - -Sound::Sound(const SoundParams& params, std::vector& bufferData) -: impl(std::make_unique()) { - alGenBuffers(1, &impl->buffer); - checkAlError(); - alGenSources(1, &impl->source); - checkAlError(); - alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); - checkAlError(); - alSource3f(impl->source, AL_POSITION, 0.0f, 0.0f, 0.0f); - checkAlError(); - alBufferData(impl->buffer, params.format, &bufferData[0], - static_cast(bufferData.size()), params.freq); - checkAlError(); - alSourcei(impl->source, AL_BUFFER, impl->buffer); - checkAlError(); - alSourcePlay(impl->source); - checkAlError(); - setVolume(masterVolume); +Sound::Sound(std::vector& bufferData, long frequency) +: impl(new Impl{ load_raw(bufferData) }) { + auto stream = impl->track->stream(); + if (frequency != jngl::audio::frequency) { + stream = + audio::pitch(std::move(stream), static_cast(frequency) / jngl::audio::frequency); + } + impl->stream = impl->volumeControl = audio::volume(std::move(stream)); } Sound::~Sound() { - alSourceStop(impl->source); - checkAlError(); - ALint processedBuffers = 0; - alGetSourcei(impl->source, AL_BUFFERS_PROCESSED, &processedBuffers); - checkAlError(); - if (processedBuffers == 1) { - alSourceUnqueueBuffers(impl->source, processedBuffers, &impl->buffer); - checkAlError(); - } - alDeleteSources(1, &impl->source); - checkAlError(); - alDeleteBuffers(1, &impl->buffer); - checkAlError(); } bool Sound::isPlaying() const { - ALint state; - alGetSourcei(impl->source, AL_SOURCE_STATE, &state); - checkAlError(); - return state == AL_PLAYING; + return impl->stream->isPlaying(); } -void Sound::loop() { - alSourcei(impl->source, AL_LOOPING, AL_TRUE); - checkAlError(); +bool Sound::isLooping() const { + return impl->stream != impl->volumeControl; } -bool Sound::isStopped() const { - ALint state; - alGetSourcei(impl->source, AL_SOURCE_STATE, &state); - checkAlError(); - return state == AL_STOPPED; +void Sound::loop() { + assert(!isLooping()); + impl->stream = audio::loop(impl->volumeControl); } -void Sound::SetPitch(float p) { - alSourcef(impl->source, AL_PITCH, p); - checkAlError(); +void Sound::setVolume(float v) { + impl->volumeControl->gain(v); } -void Sound::setVolume(float v) { - alSourcef(impl->source, AL_GAIN, v); - checkAlError(); +std::shared_ptr Sound::getStream() { + return impl->stream; } } // namespace jngl diff --git a/src/Sound.hpp b/src/Sound.hpp index f4a71eb7c..766e0a91f 100644 --- a/src/Sound.hpp +++ b/src/Sound.hpp @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Jan Niklas Hasse +// Copyright 2019-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt #pragma once @@ -7,23 +7,23 @@ #include namespace jngl { + +struct Stream; struct SoundParams; class Sound { public: - Sound(const SoundParams&, std::vector& bufferData); + Sound(std::vector& bufferData, long frequency); Sound(const Sound&) = delete; Sound& operator=(const Sound&) = delete; Sound(Sound&&) = default; Sound& operator=(Sound&&) = default; ~Sound(); bool isPlaying() const; + bool isLooping() const; void loop(); - bool isStopped() const; - void SetPitch(float p); void setVolume(float v); - - static float masterVolume; + std::shared_ptr getStream(); private: struct Impl; diff --git a/src/SoundParams.hpp b/src/SoundParams.hpp deleted file mode 100644 index 84c8f9211..000000000 --- a/src/SoundParams.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019-2020 Jan Niklas Hasse -// For conditions of distribution and use, see copyright notice in LICENSE.txt -#pragma once - -#ifdef __APPLE__ -#include -#include -#else -#include -#include -#endif -#define OV_EXCLUDE_STATIC_CALLBACKS -#include - -namespace jngl { - -struct SoundParams { - ALenum format; - ALsizei freq; -}; - -} // namespace jngl diff --git a/src/audio.cpp b/src/audio.cpp deleted file mode 100644 index c34d3af32..000000000 --- a/src/audio.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2009-2020 Jan Niklas Hasse -// For conditions of distribution and use, see copyright notice in LICENSE.txt - -#include "audio.hpp" - -#include "Sound.hpp" - -namespace jngl { - -bool isOpenALInstalled() { - return true; -} - -float getVolume() { - return Sound::masterVolume; -} - -} // namespace jngl diff --git a/src/audio.hpp b/src/audio.hpp index 76ae283a0..5798e278a 100644 --- a/src/audio.hpp +++ b/src/audio.hpp @@ -1,8 +1,12 @@ -// Copyright 2010-2020 Jan Niklas Hasse +// Copyright 2010-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt #pragma once +#include "audio/mixer.hpp" + +#include + namespace jngl { void checkAlError(); @@ -12,5 +16,6 @@ void resumeAudioDevice(); class Audio; Audio& GetAudio(); +std::shared_ptr getMixer(); } // namespace jngl diff --git a/src/audio/Stream.hpp b/src/audio/Stream.hpp new file mode 100644 index 000000000..0ebef809f --- /dev/null +++ b/src/audio/Stream.hpp @@ -0,0 +1,24 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include +#include +#include + +namespace jngl { + +struct Stream { + // Return value less than sample count means end of stream + // Must be called from mixing thread + virtual std::size_t read(float* data, std::size_t sample_count) = 0; + + virtual void rewind() = 0; + virtual bool isPlaying() const = 0; + + virtual ~Stream() = default; +}; + +} // namespace jngl diff --git a/src/audio/Track.hpp b/src/audio/Track.hpp new file mode 100644 index 000000000..5314bb698 --- /dev/null +++ b/src/audio/Track.hpp @@ -0,0 +1,28 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include +#include +#include + +namespace jngl { + +struct Stream; + +struct Track { + virtual std::shared_ptr stream() const = 0; + virtual std::optional length() const = 0; + + virtual ~Track() = default; +}; + +std::shared_ptr load_raw(gsl::span samples); +std::shared_ptr load_raw(std::vector samples); + +std::shared_ptr load_ogg(gsl::span data); +std::shared_ptr load_ogg(std::vector data); + +} // namespace jngl diff --git a/src/audio/android/engine.cpp b/src/audio/android/engine.cpp new file mode 100644 index 000000000..d73e62b87 --- /dev/null +++ b/src/audio/android/engine.cpp @@ -0,0 +1,85 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +#include "../engine.hpp" +#include "../constants.hpp" +#include "../Stream.hpp" +#include "../../jngl/debug.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace jngl::audio { + +struct engine::Impl { + std::shared_ptr output; + + Impl(std::shared_ptr output); + + class Callback : public oboe::AudioStreamCallback { + public: + Callback(Impl& self) : self(self) { + } + + private: + oboe::DataCallbackResult onAudioReady(oboe::AudioStream*, void* data, + int32_t len) override { + float* dst = reinterpret_cast(data); + self.output->read(dst, len * 2); + return oboe::DataCallbackResult::Continue; + } + + Impl& self; + }; + + Callback callback; + oboe::AudioStreamBuilder builder; + std::shared_ptr oboeStream; +}; + +engine::Impl::Impl(std::shared_ptr output) +: output(std::move(output)), callback(*this) +{ + builder.setDirection(oboe::Direction::Output); + builder.setPerformanceMode(oboe::PerformanceMode::LowLatency); + builder.setSharingMode(oboe::SharingMode::Exclusive); + builder.setFormat(oboe::AudioFormat::Float); + builder.setChannelCount(oboe::ChannelCount::Stereo); + builder.setSampleRate(frequency); + // desired.samples = 256; + builder.setCallback(&callback); + const auto result = builder.openStream(oboeStream); + if (result != oboe::Result::OK) { + throw std::runtime_error(oboe::convertToText(result)); + } + + oboeStream->requestStart(); +} + +engine::engine(std::shared_ptr output) : impl(std::make_unique(std::move(output))) { +} + +engine::~engine() = default; + +void engine::setPause(bool pause) { + if (pause) { + if (impl->oboeStream) { + impl->oboeStream->close(); + impl->oboeStream.reset(); + } + } else { + if (!impl->oboeStream) { + const auto result = impl->builder.openStream(impl->oboeStream); + if (result == oboe::Result::OK) { + impl->oboeStream->requestStart(); + } else { + debugLn(std::string("WARNING: ") + oboe::convertToText(result)); + } + } + } +} +} // namespace jngl::audio diff --git a/src/audio/constants.hpp b/src/audio/constants.hpp new file mode 100644 index 000000000..00cdc8fe6 --- /dev/null +++ b/src/audio/constants.hpp @@ -0,0 +1,31 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include +#include + +namespace jngl::audio { + +constexpr int frequency = 44100; +constexpr float inv_frequency = 1.f / frequency; + +inline std::int64_t seconds_to_samples(float seconds) { + return static_cast(2 * std::round(seconds * frequency)); +} + +inline float samples_to_seconds(std::int64_t samples) { + return static_cast(samples) * 0.5f * inv_frequency; +} + +inline float to_db(float amplitude) { + return 10.f * std::log10(amplitude); +} + +inline float from_db(float db) { + return std::pow(10.f, db / 10.f); +} + +} // namespace jngl::audio diff --git a/src/audio/duration.hpp b/src/audio/duration.hpp new file mode 100644 index 000000000..e67c9ef1a --- /dev/null +++ b/src/audio/duration.hpp @@ -0,0 +1,59 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "constants.hpp" + +#include +#include + +namespace jngl::audio { + +struct duration { + duration() : samples_{ 0 } { + } + + /* NOLINT */ duration(std::int64_t samples) : samples_{ samples } { + } + + /* NOLINT */ duration(std::size_t samples) : samples_{ static_cast(samples) } { + } + + /* NOLINT */ duration(float seconds) : samples_{ seconds_to_samples(seconds) } { + } + + duration(duration const& other) = default; + + float seconds() const { + return samples_to_seconds(samples_); + } + + std::int64_t samples() const { + return samples_; + } + + duration& operator+=(duration const& other) { + samples_ += other.samples_; + return *this; + } + + duration& operator-=(duration const& other) { + samples_ -= other.samples_; + return *this; + } + + friend duration operator+(duration const& d1, duration const& d2) { + return duration{ d1.samples() + d2.samples() }; + } + + friend duration operator-(duration const& d1, duration const& d2) { + return duration{ d1.samples() - d2.samples() }; + } + +private: + std::int64_t samples_; +}; + +} // namespace jngl::audio diff --git a/src/audio/effect/loop.cpp b/src/audio/effect/loop.cpp new file mode 100644 index 000000000..74fac5a6e --- /dev/null +++ b/src/audio/effect/loop.cpp @@ -0,0 +1,52 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "loop.hpp" + +#include "../Stream.hpp" + +namespace jngl::audio { + +namespace { + +struct loop_impl : Stream { + loop_impl(std::shared_ptr stream, std::optional count) + : stream_(std::move(stream)), count_(count) { + } + + std::size_t read(float* data, std::size_t sample_count) override { + std::size_t result = 0; + while (result < sample_count && (!count_ || repeated_ < *count_)) { + auto need = sample_count - result; + auto count = stream_->read(data + result, need); + result += count; + if (count == 0) { + ++repeated_; + stream_->rewind(); + } + } + return result; + } + + void rewind() override { + repeated_ = 0; + } + + bool isPlaying() const override { + return true; + } + +private: + std::shared_ptr stream_; + std::optional count_; + std::size_t repeated_ = 0; +}; + +} // namespace + +std::shared_ptr loop(std::shared_ptr stream, std::optional count) { + return std::make_shared(std::move(stream), count); +} + +} // namespace jngl::audio diff --git a/src/audio/effect/loop.hpp b/src/audio/effect/loop.hpp new file mode 100644 index 000000000..74938a460 --- /dev/null +++ b/src/audio/effect/loop.hpp @@ -0,0 +1,18 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include +#include + +namespace jngl { +struct Stream; + +namespace audio { + +std::shared_ptr loop(std::shared_ptr stream, std::optional count = {}); + +} // namespace audio +} // namespace jngl diff --git a/src/audio/effect/pause.cpp b/src/audio/effect/pause.cpp new file mode 100644 index 000000000..a74290ea4 --- /dev/null +++ b/src/audio/effect/pause.cpp @@ -0,0 +1,82 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "pause.hpp" + +#include +#include + +namespace jngl::audio { + +namespace { + +struct pause_control_impl : pause_control { + pause_control_impl(std::shared_ptr stream, bool paused, duration length) + : stream_(std::move(stream)), paused_{ paused }, length_(length), + level_(paused ? 0 : length_.samples()) { + } + + bool paused() const override { + return paused_.load(); + } + + bool paused(bool value) override { + return paused_.exchange(value); + } + + std::size_t read(float* data, std::size_t sample_count) override { + bool const paused = paused_.load(); + + if (paused) { + auto result = stream_->read(data, std::min(sample_count, level_)); + + for (std::size_t i = 0; i < result; i += 2) { + float gain = static_cast(level_) / length_.samples(); + data[i + 0] *= gain; + data[i + 1] *= gain; + level_ -= 2; + } + + std::fill(data + result, data + sample_count, 0.f); + return sample_count; + } + auto result = stream_->read(data, sample_count); + + auto const max_level = static_cast(length_.samples()); + + for (std::size_t i = 0; i < result; i += 2) { + float gain = static_cast(level_) / max_level; + data[i + 0] *= gain; + data[i + 1] *= gain; + + if (level_ < max_level) { + level_ += 2; + } + } + + return result; + } + + void rewind() override { + return stream_->rewind(); + } + + bool isPlaying() const override { + return paused_ ? false : stream_->isPlaying(); + } + +private: + std::shared_ptr stream_; + std::atomic paused_; + duration length_; + std::size_t level_; +}; + +} // namespace + +std::shared_ptr pause(std::shared_ptr stream, bool paused, duration length) { + return std::make_shared(std::move(stream), paused, length); +} + +} // namespace jngl::audio diff --git a/src/audio/effect/pause.hpp b/src/audio/effect/pause.hpp new file mode 100644 index 000000000..684bf0fa3 --- /dev/null +++ b/src/audio/effect/pause.hpp @@ -0,0 +1,27 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "../Stream.hpp" +#include "../duration.hpp" + +namespace jngl::audio { + +struct pause_control : Stream { + virtual bool paused() const = 0; + virtual bool paused(bool value) = 0; + + virtual void pause() { + paused(true); + } + virtual void resume() { + paused(false); + } +}; + +std::shared_ptr pause(std::shared_ptr stream, bool paused = false, + duration length = 0.01f); + +} // namespace jngl::audio diff --git a/src/audio/effect/pitch.cpp b/src/audio/effect/pitch.cpp new file mode 100644 index 000000000..5a78bece1 --- /dev/null +++ b/src/audio/effect/pitch.cpp @@ -0,0 +1,111 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "pitch.hpp" + +#include +#include +#include + +#if __cplusplus < 202002L +float lerp(float a, float b, float t) { + return a + t * (b - a); +} +#else +using std::lerp; +#endif + +namespace jngl::audio { + +namespace { + +struct pitch_control_impl : pitch_control { + pitch_control_impl(std::shared_ptr stream, float ratio) + : stream_(std::move(stream)), ratio(1.f / ratio) { + } + + // N.B. resampler ratio is in terms of sampling frequency, while + // pitch control ratio is in terms of sound frequency, which + // is the opposite + + float pitch() const override { + return 1.f / ratio; + } + + float pitch(float ratio) override { + return this->ratio = 1.f / ratio; + } + + std::size_t read(float* data, std::size_t sample_count) override { + assert(sample_count % 2 == 0); + std::size_t result = 0; + + if (sourceBufferSize == 0) { + sourceBufferSize = stream_->read(sourceBuffer, MAX_SOURCE_BUFFER_SIZE); + if (sourceBufferSize == 0) { return 0; } + assert(sourceBufferSize % 2 == 0); + } + + while (result < sample_count) { + // assert(2 * (position + 1) <= sourceBufferSize); + if (2 * (position + 1) >= sourceBufferSize) { + static_assert(MAX_SOURCE_BUFFER_SIZE % 2 == 0); + sourceBuffer[0] = sourceBuffer[sourceBufferSize - 2]; + sourceBuffer[1] = sourceBuffer[sourceBufferSize - 1]; + position = 0; + sourceBufferSize = stream_->read(sourceBuffer + 2, MAX_SOURCE_BUFFER_SIZE - 2) + 2; + if (sourceBufferSize == 2) { + break; + } + assert(sourceBufferSize % 2 == 0); + } + data[result] = + lerp(sourceBuffer[2 * position], sourceBuffer[2 * (position + 1)], positionFrac); + ++result; + data[result] = lerp(sourceBuffer[2 * position + 1], + sourceBuffer[2 * (position + 1) + 1], positionFrac); + ++result; + + positionFrac += 1.f / ratio; + while (positionFrac > 1.f) { + ++position; + positionFrac -= 1.f; + } + } + played_ += result; + return result; + } + + void rewind() override { + stream_->rewind(); + position = 0; + sourceBufferSize = 0; + } + + bool isPlaying() const override { + return stream_->isPlaying(); + } + +private: + std::shared_ptr stream_; + + constexpr static size_t MAX_SOURCE_BUFFER_SIZE = 996; + size_t position = 0; + size_t sourceBufferSize = 0; + float sourceBuffer[MAX_SOURCE_BUFFER_SIZE] = { 0 }; + + float positionFrac = 0; //< 0 = left sample, 1 = right sample + + float ratio; // 1. / ratio + + std::atomic played_{ 0 }; +}; + +} // namespace + +std::shared_ptr pitch(std::shared_ptr stream, float ratio) { + return std::make_shared(std::move(stream), ratio); +} + +} // namespace jngl::audio diff --git a/src/audio/effect/pitch.hpp b/src/audio/effect/pitch.hpp new file mode 100644 index 000000000..f5bb14fc7 --- /dev/null +++ b/src/audio/effect/pitch.hpp @@ -0,0 +1,18 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "../Stream.hpp" + +namespace jngl::audio { + +struct pitch_control : Stream { + virtual float pitch() const = 0; + virtual float pitch(float ratio) = 0; +}; + +std::shared_ptr pitch(std::shared_ptr stream, float ratio = 1.f); + +} // namespace jngl::audio diff --git a/src/audio/effect/volume.cpp b/src/audio/effect/volume.cpp new file mode 100644 index 000000000..a6d7b3068 --- /dev/null +++ b/src/audio/effect/volume.cpp @@ -0,0 +1,108 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "volume.hpp" +#include "volume_base.hpp" + +namespace jngl::audio { + +namespace { + +struct volume_control_impl : volume_control { + volume_control_impl(std::shared_ptr stream, float gain, float smoothness) + : base_(gain, gain, smoothness), stream_(std::move(stream)) { + } + + float gain() const override { + return base_.gain_left(); + } + float gain(float value) override { + base_.gain_left(value); + return base_.gain_right(value); + } + + float smoothness() const override { + return base_.smoothness(); + } + float smoothness(float value) override { + return base_.smoothness(value); + } + + void rewind() override { + return stream_->rewind(); + } + + bool isPlaying() const override { + return stream_->isPlaying(); + } + + std::size_t read(float* data, std::size_t sample_count) override { + auto result = stream_->read(data, sample_count); + base_.apply(data, result); + return result; + } + +private: + volume_base base_; + std::shared_ptr stream_; +}; + +struct volume_control_stereo_impl : volume_control_stereo { + volume_control_stereo_impl(std::shared_ptr stream, float gain_left, float gain_right, + float smoothness) + : base_(gain_left, gain_right, smoothness), stream_(std::move(stream)) { + } + + float gain_left() const override { + return base_.gain_left(); + } + float gain_right() const override { + return base_.gain_right(); + } + float gain_left(float value) override { + return base_.gain_left(value); + } + float gain_right(float value) override { + return base_.gain_right(value); + } + + float smoothness() const override { + return base_.smoothness(); + } + float smoothness(float value) override { + return base_.smoothness(value); + } + + void rewind() override { + return stream_->rewind(); + } + + bool isPlaying() const override { + return stream_->isPlaying(); + } + + std::size_t read(float* data, std::size_t sample_count) override { + auto result = stream_->read(data, sample_count); + base_.apply(data, result); + return result; + } + +private: + volume_base base_; + std::shared_ptr stream_; +}; + +} // namespace + +std::shared_ptr volume(std::shared_ptr stream, float gain, float smoothness) { + return std::make_shared(std::move(stream), gain, smoothness); +} + +std::shared_ptr volume_stereo(std::shared_ptr stream, float gain_left, + float gain_right, float smoothness) { + return std::make_shared(std::move(stream), gain_left, gain_right, + smoothness); +} + +} // namespace jngl::audio diff --git a/src/audio/effect/volume.hpp b/src/audio/effect/volume.hpp new file mode 100644 index 000000000..c45fa4f56 --- /dev/null +++ b/src/audio/effect/volume.hpp @@ -0,0 +1,34 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "../Stream.hpp" + +namespace jngl::audio { + +struct volume_control : Stream { + virtual float gain() const = 0; + virtual float gain(float value) = 0; + + virtual float smoothness() const = 0; + virtual float smoothness(float value) = 0; +}; + +struct volume_control_stereo : Stream { + virtual float gain_left() const = 0; + virtual float gain_right() const = 0; + virtual float gain_left(float value) = 0; + virtual float gain_right(float value) = 0; + + virtual float smoothness() const = 0; + virtual float smoothness(float value) = 0; +}; + +std::shared_ptr volume(std::shared_ptr stream, float gain = 1.f, float smoothness = 0.f); +std::shared_ptr volume_stereo(std::shared_ptr stream, float gain_left = 1.f, + float gain_right = 1.f, + float smoothness = 0.f); + +} // namespace jngl::audio diff --git a/src/audio/effect/volume_base.cpp b/src/audio/effect/volume_base.cpp new file mode 100644 index 000000000..cd7835f14 --- /dev/null +++ b/src/audio/effect/volume_base.cpp @@ -0,0 +1,41 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "volume_base.hpp" +#include "../constants.hpp" +#include "../smooth.hpp" + +#include + +namespace jngl::audio { + +volume_base::volume_base(float gain_left, float gain_right, float smoothness) +: gain_{ gain_left, gain_right }, smoothness_multiplier_{ smoothness_to_multiplier(smoothness) }, + real_gain_{ gain_left, gain_right } { +} + +float volume_base::smoothness() const { + return multiplier_to_smoothness(smoothness_multiplier_.load()); +} + +float volume_base::smoothness(float value) { + auto old = smoothness_multiplier_.exchange(smoothness_to_multiplier(value)); + return multiplier_to_smoothness(old); +} + +void volume_base::apply(float* data, std::size_t sample_count) { + float gain[2] = { gain_[0].load(), gain_[1].load() }; + float smoothness_multiplier = smoothness_multiplier_.load(); + + auto end = data + sample_count; + for (auto p = data; p < end;) { + *p++ *= real_gain_[0]; + *p++ *= real_gain_[1]; + + smooth_update(real_gain_[0], gain[0], smoothness_multiplier); + smooth_update(real_gain_[1], gain[1], smoothness_multiplier); + } +} + +} // namespace jngl::audio diff --git a/src/audio/effect/volume_base.hpp b/src/audio/effect/volume_base.hpp new file mode 100644 index 000000000..c84e6a9f7 --- /dev/null +++ b/src/audio/effect/volume_base.hpp @@ -0,0 +1,41 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "../Stream.hpp" + +#include + +namespace jngl::audio { + +struct volume_base { + volume_base(float gain_left, float gain_right, float smoothness); + + float gain_left() const { + return gain_[0].load(); + } + float gain_right() const { + return gain_[1].load(); + } + + float gain_left(float value) { + return gain_[0].exchange(value); + } + float gain_right(float value) { + return gain_[1].exchange(value); + } + + float smoothness() const; + float smoothness(float value); + + void apply(float* data, std::size_t sample_count); + +private: + std::atomic gain_[2]; + std::atomic smoothness_multiplier_; + float real_gain_[2]; +}; + +} // namespace jngl::audio diff --git a/src/audio/engine.hpp b/src/audio/engine.hpp new file mode 100644 index 000000000..2892e5424 --- /dev/null +++ b/src/audio/engine.hpp @@ -0,0 +1,28 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include +#include +#include + +namespace jngl { +struct Stream; + +namespace audio { + +struct engine { + explicit engine(std::shared_ptr output); + ~engine(); + + void setPause(bool); + +private: + struct Impl; + std::unique_ptr impl; +}; + +} // namespace audio +} // namespace jngl diff --git a/src/audio/ios/Engine.mm b/src/audio/ios/Engine.mm new file mode 100644 index 000000000..f5ebf8c3a --- /dev/null +++ b/src/audio/ios/Engine.mm @@ -0,0 +1,105 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +#include "../engine.hpp" + +#include "../constants.hpp" +#include "../Stream.hpp" + +#include +#include +#include + +namespace jngl::audio { + +constexpr AudioUnitElement OUTPUT_ELEMENT = 0; + +struct engine::Impl { + Impl(std::shared_ptr output) : output(std::move(output)) { + AudioComponentDescription desc{}; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + AudioComponent comp = AudioComponentFindNext(nullptr, &desc); + if (comp == nullptr) { + throw std::runtime_error("Could not find audio component"); + } + + OSStatus err = AudioComponentInstanceNew(comp, &audioUnit); + if (err != noErr) { + throw std::runtime_error("Could not create component instance: " + std::to_string(err)); + } + + AudioStreamBasicDescription streamFormat{}; + UInt32 size = sizeof(streamFormat); + err = AudioUnitGetProperty(audioUnit, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, OUTPUT_ELEMENT, &streamFormat, &size); + if (err != noErr || size != sizeof(streamFormat)) { + throw std::runtime_error("AudioUnitGetProperty failed"); + } + + streamFormat.mSampleRate = frequency; + streamFormat.mChannelsPerFrame = 2; // stereo + streamFormat.mFramesPerPacket = 1; + streamFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kLinearPCMFormatFlagIsPacked | + kLinearPCMFormatFlagIsFloat; + streamFormat.mFormatID = kAudioFormatLinearPCM; + streamFormat.mBitsPerChannel = 32; + streamFormat.mBytesPerFrame = + streamFormat.mChannelsPerFrame * streamFormat.mBitsPerChannel / 8; + streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame * streamFormat.mFramesPerPacket; + + err = + AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, + OUTPUT_ELEMENT, &streamFormat, sizeof(streamFormat)); + if (err != noErr) { + throw std::runtime_error("AudioUnitSetProperty failed"); + } + + AURenderCallbackStruct input{}; + input.inputProc = callback; + input.inputProcRefCon = this; + + err = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, OUTPUT_ELEMENT, &input, + sizeof(AURenderCallbackStruct)); + if (err != noErr) { + throw std::runtime_error("AudioUnitSetProperty failed"); + } + + err = AudioUnitInitialize(audioUnit); + if (err != noErr) { + throw std::runtime_error("AudioUnitInitialize failed"); + } + } + + static OSStatus callback(void* refCon, AudioUnitRenderActionFlags* ioActionFlags, + const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, + UInt32 inNumberFrames, AudioBufferList* ioData) noexcept { + for (size_t i = 0; i < ioData->mNumberBuffers; ++i) { + auto& buffer = ioData->mBuffers[i]; + static_cast(refCon)->output->read(static_cast(buffer.mData), + buffer.mDataByteSize / sizeof(float)); + } + return noErr; + } + + std::shared_ptr output; + AudioUnit audioUnit; +}; + +engine::engine(std::shared_ptr output) : impl(std::make_unique(std::move(output))) { + setPause(false); +} + +engine::~engine() = default; + +void engine::setPause(bool pause) { + const OSStatus err = + pause ? AudioOutputUnitStop(impl->audioUnit) : AudioOutputUnitStart(impl->audioUnit); + assert(err == noErr); +} + +} // namespace jngl::audio diff --git a/src/audio/mixer.cpp b/src/audio/mixer.cpp new file mode 100644 index 000000000..a683002a1 --- /dev/null +++ b/src/audio/mixer.cpp @@ -0,0 +1,113 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "mixer.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace jngl { + +struct Mixer::Impl { + atomic_queue::AtomicQueue streamsToRemoveOnMainThread; + atomic_queue::AtomicQueue newStreams; + atomic_queue::AtomicQueue streamsToStop; +}; + +Mixer::Mixer() : impl(std::make_unique()) { +} +Mixer::~Mixer() = default; + +void Mixer::gc() { + Stream* tmp; + while (impl->streamsToRemoveOnMainThread.try_pop(tmp)) { + auto it = std::find_if(streamsOnMainThread.begin(), streamsOnMainThread.end(), + [tmp](const auto& stream) { return stream.get() == tmp; }); + assert(it != streamsOnMainThread.end()); + streamsOnMainThread.erase(it); + } +} + +void Mixer::remove(Stream* stream) { + gc(); + impl->streamsToStop.push(stream); +} + +void Mixer::rewind() { + assert(false); +} + +bool Mixer::isPlaying() const { + for (const auto& stream : streamsOnMainThread) { + if (stream->isPlaying()) { + return true; + } + } + return false; +} + +void Mixer::add(std::shared_ptr stream) { + gc(); + impl->newStreams.push(stream.get()); + streamsOnMainThread.emplace_back(std::move(stream)); +} + +size_t Mixer::read(float* data, size_t sample_count) { + if (sample_count > BUFFER_SIZE) { + size_t first = read(data, BUFFER_SIZE); + return first + read(data + BUFFER_SIZE, sample_count - BUFFER_SIZE); + } + + Stream* tmp; + while (numberOfActiveStreams < MAX_ACTIVE_STREAMS && impl->newStreams.try_pop(tmp)) { + activeStreams[numberOfActiveStreams++] = tmp; + } + while (impl->streamsToStop.try_pop(tmp)) { + auto it = std::find(activeStreams, activeStreams + numberOfActiveStreams, tmp); + if (it != activeStreams + numberOfActiveStreams) { + *it = activeStreams[--numberOfActiveStreams]; // move the last element to the one to be + // erased + impl->streamsToRemoveOnMainThread.push(tmp); + } + } + + std::fill(data, data + sample_count, 0.f); + + for (auto it = activeStreams; it != activeStreams + numberOfActiveStreams;) { + auto& stream = *it; + if (!stream) { + continue; + } + + auto read = stream->read(buffer, sample_count); + + { + auto begin = buffer; + auto end = begin + read; + auto dst = data; + for (; begin < end;) { + *dst++ += *begin++; + } + } + + if (read < sample_count) { + impl->streamsToRemoveOnMainThread.push(*it); + *it = activeStreams[--numberOfActiveStreams]; // move the last element to the one to be + // erased + } else { + ++it; + } + } + + played_.fetch_add(sample_count); + + return sample_count; +} + +} // namespace jngl diff --git a/src/audio/mixer.hpp b/src/audio/mixer.hpp new file mode 100644 index 000000000..533f7f39a --- /dev/null +++ b/src/audio/mixer.hpp @@ -0,0 +1,46 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "Stream.hpp" + +#include +#include +#include + +namespace jngl { + +class Mixer : public Stream { +public: + Mixer(); + ~Mixer() override; + + void add(std::shared_ptr stream); + void remove(Stream*); + + void rewind() override; + bool isPlaying() const override; + size_t read(float*, size_t) override; + +private: + void gc(); + + struct Impl; + std::unique_ptr impl; + + size_t numberOfActiveStreams = 0; + + constexpr static size_t BUFFER_SIZE = 996; + float buffer[BUFFER_SIZE] = { 0 }; + + constexpr static size_t MAX_ACTIVE_STREAMS = 256; + Stream* activeStreams[MAX_ACTIVE_STREAMS] = { nullptr }; //< only used on mixer thread + + std::atomic played_{ 0 }; + + std::vector> streamsOnMainThread; +}; + +} // namespace jngl diff --git a/src/audio/sdl/engine.cpp b/src/audio/sdl/engine.cpp new file mode 100644 index 000000000..0049b35df --- /dev/null +++ b/src/audio/sdl/engine.cpp @@ -0,0 +1,159 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "../engine.hpp" + +#include "../../jngl/debug.hpp" +#include "../constants.hpp" +#include "../Stream.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace jngl::audio { + +struct engine::Impl { + explicit Impl(std::shared_ptr output) { + try { + backend = std::make_unique(output); + } catch (std::exception& e) { + debugLn(e.what()); + backend = std::make_unique(std::move(output)); + } + } + struct Backend { + virtual ~Backend() = default; + virtual void setPause(bool) = 0; + }; + std::unique_ptr backend; + + struct DummyImpl : public Backend { + explicit DummyImpl(std::shared_ptr output) + : output(std::move(output)), thread( + [this]() + { + const int slowdown = 10; + std::array buffer; + while (!quit) { + do { + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / slowdown)); + } while (pause && !quit); + const auto read = this->output->read(buffer.data(), buffer.size()); + assert(read == buffer.size()); + // to debug sound on the terminal: + // float average = 0; + // for (float f : buffer) { + // average += std::fabs(f / buffer.size()); + // } + // if (average < 0.1f) { + // debug(' '); + // } else if (average < 0.2f) { + // debug("▁"); + // } else if (average < 0.3f) { + // debug("▂"); + // } else if (average < 0.4f) { + // debug("▃"); + // } else if (average < 0.5f) { + // debug("▄"); + // } else if (average < 0.6f) { + // debug("▅"); + // } else if (average < 0.7f) { + // debug("▆"); + // } else if (average < 0.8f) { + // debug("▇"); + // } else { + // debug("█"); + // } + } + }) { + } + ~DummyImpl() override { + quit = true; + thread.join(); + } + void setPause(bool pause) override { + this->pause = pause; + } + + std::shared_ptr output; + std::atomic_bool pause{ false }; + std::atomic_bool quit{ false }; + std::thread thread; + }; + + struct SdlImpl : public Backend { + std::shared_ptr sdl_init; + + SDL_AudioDeviceID device; + + std::vector buffer; + + std::shared_ptr output; + + explicit SdlImpl(std::shared_ptr output) : output(std::move(output)) { + if (SDL_Init(SDL_INIT_AUDIO) < 0) { + throw std::runtime_error(SDL_GetError()); + } + SDL_AudioSpec desired, obtained; + desired.freq = frequency; + desired.channels = 2; + desired.format = AUDIO_S16SYS; + desired.samples = 256; + desired.callback = &callback; + desired.userdata = this; + if (device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device == 0) { + throw std::runtime_error(SDL_GetError()); + } + + // log::info() << "Initialized audio: " << static_cast(obtained.channels) << " + // channels, " << obtained.freq << " Hz, " << obtained.samples << " samples"; + + buffer.resize(obtained.samples * obtained.channels); + SDL_PauseAudioDevice(device, 0); + } + ~SdlImpl() override { + SDL_CloseAudioDevice(device); + } + + static void callback(void* userdata, std::uint8_t* dst_u8, int len) { + static std::string const profiler_str = "audio"; + // prof::profiler prof(profiler_str); + + auto self = static_cast(userdata); + std::int16_t* dst = reinterpret_cast(dst_u8); + + std::size_t const size = len / 2; + std::size_t read = 0; + read = self->output->read(self->buffer.data(), size); + std::fill(self->buffer.data() + read, self->buffer.data() + size, 0.f); + + for (auto s : self->buffer) { + *dst++ = static_cast( + std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f)); + } + } + + void setPause(bool pause) override { + SDL_PauseAudioDevice(device, pause ? 1 : 0); + } + }; +}; + +engine::engine(std::shared_ptr output) : impl(std::make_unique(std::move(output))) { +} + +engine::~engine() = default; + +void engine::setPause(bool pause) { + impl->backend->setPause(pause); +} +} // namespace jngl::audio diff --git a/src/audio/smooth.hpp b/src/audio/smooth.hpp new file mode 100644 index 000000000..6a52c7783 --- /dev/null +++ b/src/audio/smooth.hpp @@ -0,0 +1,28 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#pragma once + +#include "constants.hpp" + +#include + +namespace jngl::audio { + +inline float smoothness_to_multiplier(float smoothness) { + if (smoothness == 0.f) return 1.f; + return -std::expm1(-inv_frequency / smoothness); +} + +inline float multiplier_to_smoothness(float multiplier) { + if (multiplier == 1.f) return 0.f; + + return -inv_frequency / std::log1p(-multiplier); +} + +inline void smooth_update(float& value, float target_value, float smoothness_multiplier) { + value += (target_value - value) * smoothness_multiplier; +} + +} // namespace jngl::audio diff --git a/src/audio/track_raw.cpp b/src/audio/track_raw.cpp new file mode 100644 index 000000000..ca654e9da --- /dev/null +++ b/src/audio/track_raw.cpp @@ -0,0 +1,89 @@ +// Copyright 2023 Jan Niklas Hasse +// For conditions of distribution and use, see copyright notice in LICENSE.txt +// Based on the audio implementation of the psemek engine, see +// https://lisyarus.github.io/blog/programming/2022/10/15/audio-mixing.html +#include "Track.hpp" + +#include "Stream.hpp" + +#include +#include + +namespace jngl { + +namespace { + +struct data_holder { + std::vector storage; + gsl::span samples; + + explicit data_holder(gsl::span samples) : samples(samples) { + } + + explicit data_holder(std::vector storage) + : storage(std::move(storage)), samples(this->storage) { + } +}; + +struct raw_stream_impl : Stream { + explicit raw_stream_impl(std::shared_ptr data_holder) + : data_holder_(std::move(data_holder)) { + } + + std::size_t read(float* data, std::size_t sample_count) override { + auto played = played_.load(); + + auto count = std::min(data_holder_->samples.size() - played, sample_count); + std::copy(data_holder_->samples.begin() + played, + data_holder_->samples.begin() + played + count, data); + played_.fetch_add(count); + return count; + } + + void rewind() override { + played_ = 0; + } + + bool isPlaying() const override { + return data_holder_->samples.size() - played_ > 0; + } + +private: + std::shared_ptr data_holder_; + std::atomic played_{ 0 }; +}; + +struct raw_track_impl : Track { + explicit raw_track_impl(std::shared_ptr data_holder) + : data_holder_(std::move(data_holder)) { + } + + std::shared_ptr stream() const override { + return std::make_shared(data_holder_); + } + + std::optional length() const override { + return data_holder_->samples.size(); + } + +private: + std::shared_ptr data_holder_; +}; + +} // namespace + +std::shared_ptr load_raw(gsl::span samples) { + if ((samples.size() % 2) != 0) { + throw std::runtime_error("bad sample count"); + } + return std::make_shared(std::make_shared(samples)); +} + +std::shared_ptr load_raw(std::vector samples) { + if ((samples.size() % 2) != 0) { + throw std::runtime_error("bad sample count"); + } + return std::make_shared(std::make_shared(std::move(samples))); +} + +} // namespace jngl diff --git a/src/jngl/SoundFile.cpp b/src/jngl/SoundFile.cpp index f8d787f31..f8422f75d 100644 --- a/src/jngl/SoundFile.cpp +++ b/src/jngl/SoundFile.cpp @@ -1,30 +1,26 @@ -// Copyright 2019-2022 Jan Niklas Hasse +// Copyright 2019-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt #include "SoundFile.hpp" #include "../Sound.hpp" -#include "../SoundParams.hpp" #include "../audio.hpp" +#include "../audio/effect/pitch.hpp" +#include "../audio/effect/volume.hpp" +#include "../audio/engine.hpp" +#include "../audio/mixer.hpp" #include "../main.hpp" #include "debug.hpp" #include -#include -#include #include #include +#define OV_EXCLUDE_STATIC_CALLBACKS +#include + #ifdef ANDROID #include "../android/fopen.hpp" - -#define AL_ALEXT_PROTOTYPES 1 -#include -#endif - -#ifdef __APPLE__ -// OpenAL is deprecated in favor of AVAudioEngine -#pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif namespace jngl { @@ -33,144 +29,109 @@ std::unordered_map> sounds; class Audio { public: - Audio() { - device_ = alcOpenDevice(nullptr); - if (device_ == nullptr) { - jngl::debugLn("Could not open audio device."); - return; - } - context_ = alcCreateContext(device_, nullptr); - if (context_ == nullptr) { - jngl::debugLn("Could not create audio context."); - return; - } - alcMakeContextCurrent(context_); + Audio() + : mixer(std::make_shared()), pitchControl(audio::pitch(mixer)), + volumeControl(volume(pitchControl)), engine(volumeControl) { } Audio(const Audio&) = delete; Audio& operator=(const Audio&) = delete; Audio(Audio&&) = delete; Audio& operator=(Audio&&) = delete; - ~Audio() { - sounds_.clear(); - sounds.clear(); - alcMakeContextCurrent(nullptr); - alcDestroyContext(context_); - alcCloseDevice(device_); - } - void checkAlError() { - if (context_ == nullptr) { - return; - } - switch (alGetError()) { - case AL_NO_ERROR: - break; - case AL_INVALID_NAME: - debugLn("Invalid name paramater passed to AL call."); - break; - case AL_INVALID_ENUM: - debugLn("Invalid enum parameter passed to AL call."); - break; - case AL_INVALID_VALUE: - debugLn("Invalid value parameter passed to AL call."); - break; - case AL_INVALID_OPERATION: - debugLn("Illegal AL call."); - break; - case AL_OUT_OF_MEMORY: - debugLn("Not enough memory."); - break; - default: - debugLn("Unknown OpenAL error."); - } - } - static bool IsStopped(std::shared_ptr& s) { - return s->isStopped(); - } + ~Audio() = default; void play(std::shared_ptr sound) { - sounds_.erase(std::remove_if(sounds_.begin(), sounds_.end(), IsStopped), sounds_.end()); + mixer->add(sound->getStream()); + sounds_.erase(std::remove_if(sounds_.begin(), sounds_.end(), + [](const auto& s) { return !s->isPlaying(); }), + sounds_.end()); sounds_.emplace_back(std::move(sound)); } void Stop(std::shared_ptr& sound) { - std::vector>::iterator i; - if ((i = std::find(sounds_.begin(), sounds_.end(), sound)) != sounds_.end()) { + mixer->remove(sound->getStream().get()); + if (auto i = std::find(sounds_.begin(), sounds_.end(), sound); i != sounds_.end()) { sounds_.erase(i); } } -#ifdef ALC_SOFT_pause_device void pauseDevice() { - alcDevicePauseSOFT(device_); + engine.setPause(true); } void resumeDevice() { - alcDeviceResumeSOFT(device_); + engine.setPause(false); + } + void setPitch(float pitch) { + pitchControl->pitch(pitch); + } + float getVolume() const { + return volumeControl->gain(); + } + void setVolume(float volume) { + volumeControl->gain(volume); + } + std::shared_ptr getMixer() { + return mixer; } -#endif private: std::vector> sounds_; - ALCdevice* device_ = nullptr; - ALCcontext* context_ = nullptr; + std::shared_ptr mixer; + std::shared_ptr pitchControl; + std::shared_ptr volumeControl; + audio::engine engine; }; -void checkAlError() { - GetAudio().checkAlError(); -} - -SoundFile::SoundFile(std::string filename, std::launch policy) -: params(std::make_unique()) { -#ifndef EMSCRIPTEN - loader = std::async(policy, [this, filename = std::move(filename)]() { -#endif - debug("Decoding "); - debug(filename); - debug(" ... "); +SoundFile::SoundFile(const std::string& filename, std::launch) { + debug("Decoding "); + debug(filename); + debug(" ... "); #ifdef _WIN32 - FILE* const f = fopen(filename.c_str(), "rb"); + FILE* const f = fopen(filename.c_str(), "rb"); #else - FILE* const f = fopen(filename.c_str(), "rbe"); + FILE* const f = fopen(filename.c_str(), "rbe"); #endif - if (f == nullptr) { - throw std::runtime_error("File not found (" + filename + ")."); - } + if (f == nullptr) { + throw std::runtime_error("File not found (" + filename + ")."); + } - OggVorbis_File oggFile; - if (ov_open(f, &oggFile, nullptr, 0) != 0) { - fclose(f); // If [and only if] an ov_open() call fails, the application must explicitly - // fclose() the FILE * pointer itself. - throw std::runtime_error("Could not open OGG file (" + filename + ")."); + OggVorbis_File oggFile; + if (ov_open(f, &oggFile, nullptr, 0) != 0) { + fclose(f); // If [and only if] an ov_open() call fails, the application must explicitly + // fclose() the FILE * pointer itself. + throw std::runtime_error("Could not open OGG file (" + filename + ")."); + } + Finally cleanup( + [&oggFile]() + { + ov_clear(&oggFile); /* calls fclose */ + }); + + const vorbis_info* const pInfo = ov_info(&oggFile, -1); + frequency = pInfo->rate; + + int bitStream; + while (true) { + float** buffer = nullptr; + auto samples_read = ov_read_float(&oggFile, &buffer, 1024, &bitStream); + if (samples_read == 0) { + break; + } + if (samples_read < 0) { + throw std::runtime_error("Error decoding OGG file (" + filename + ")."); } - Finally cleanup([&oggFile]() { ov_clear(&oggFile); /* calls fclose */ }); - const vorbis_info* const pInfo = ov_info(&oggFile, -1); - if (pInfo->channels == 1) { - params->format = AL_FORMAT_MONO16; - } else { - params->format = AL_FORMAT_STEREO16; + size_t start = buffer_.size(); + buffer_.resize(start + samples_read * 2); + for (std::size_t i = samples_read; i > 0;) { + i -= 1; + auto tmp = buffer[0][i]; + buffer_[start + i * 2 + 0] = tmp; + buffer_[start + i * 2 + 1] = buffer[pInfo->channels == 1 ? 0 : 1][i]; } - params->freq = static_cast(pInfo->rate); - - const int bufferSize = 32768; - std::array array{}; // 32 KB buffers - const int endian = 0; // 0 for Little-Endian, 1 for Big-Endian - int bitStream; - long bytes; // NOLINT - do { - bytes = ov_read(&oggFile, &array[0], bufferSize, endian, 2, 1, &bitStream); - - if (bytes < 0) { - throw std::runtime_error("Error decoding OGG file (" + filename + ")."); - } - - buffer_.insert(buffer_.end(), &array[0], &array[0] + bytes); - } while (bytes > 0); - - debug("OK ("); - debug(buffer_.size() / 1024. / 1024.); - debugLn(" MB)"); -#ifndef EMSCRIPTEN - }); -#endif + } + + debug("OK ("); + debug(buffer_.size() * sizeof(float) / 1024. / 1024.); + debugLn(" MB)"); } SoundFile::~SoundFile() = default; @@ -179,21 +140,14 @@ SoundFile::SoundFile(SoundFile&& other) noexcept { *this = std::move(other); } SoundFile& SoundFile::operator=(SoundFile&& other) noexcept { -#ifndef EMSCRIPTEN - other.load(); - assert(!other.loader); - load(); - assert(!loader); -#endif sound_ = std::move(other.sound_); - params = std::move(other.params); buffer_ = std::move(other.buffer_); + frequency = other.frequency; return *this; } void SoundFile::play() { - load(); - sound_ = std::make_shared(*params, buffer_); + sound_ = std::make_shared(buffer_, frequency); GetAudio().play(sound_); } void SoundFile::stop() { @@ -210,17 +164,14 @@ bool SoundFile::isPlaying() { } void SoundFile::loop() { - if (!isPlaying()) { - play(); + if (sound_ && sound_->isLooping()) { + return; } + sound_ = std::make_shared(buffer_, frequency); sound_->loop(); + GetAudio().play(sound_); } -void SoundFile::setPitch(float p) { - if (sound_) { - sound_->SetPitch(p); - } -} void SoundFile::setVolume(float v) { if (sound_) { sound_->setVolume(v); @@ -228,15 +179,6 @@ void SoundFile::setVolume(float v) { } void SoundFile::load() { -#ifndef EMSCRIPTEN - if (loader) { - if (!loader->valid()) { - throw std::runtime_error("Invalid SoundFile."); - } - loader->get(); // might throw - loader = std::nullopt; - } -#endif } std::shared_ptr getSoundFile(const std::string& filename, std::launch policy) { @@ -259,9 +201,7 @@ void stop(const std::string& filename) { Finally loadSound(const std::string& filename) { auto soundFile = getSoundFile(filename, std::launch::async); - return Finally([soundFile = std::move(soundFile)]() { - soundFile->load(); - }); + return Finally([soundFile = std::move(soundFile)]() { soundFile->load(); }); } bool isPlaying(const std::string& filename) { @@ -275,21 +215,17 @@ std::shared_ptr loop(const std::string& filename) { } void setPlaybackSpeed(float speed) { - auto end = sounds.end(); - for (auto i = sounds.begin(); i != end; ++i) { - i->second->setPitch(speed); - } + GetAudio().setPitch(speed); +} + +float getVolume() { + return GetAudio().getVolume(); } void setVolume(float volume) { - auto end = sounds.end(); - for (auto i = sounds.begin(); i != end; ++i) { - i->second->setVolume(volume); - } - Sound::masterVolume = volume; + GetAudio().setVolume(volume); } -#ifdef ALC_SOFT_pause_device void pauseAudioDevice() { GetAudio().pauseDevice(); } @@ -297,11 +233,14 @@ void pauseAudioDevice() { void resumeAudioDevice() { GetAudio().resumeDevice(); } -#endif Audio& GetAudio() { static Audio audio; return audio; } +std::shared_ptr getMixer() { + return GetAudio().getMixer(); +} + } // namespace jngl diff --git a/src/jngl/SoundFile.hpp b/src/jngl/SoundFile.hpp index b4b06c389..b0d7a48d0 100644 --- a/src/jngl/SoundFile.hpp +++ b/src/jngl/SoundFile.hpp @@ -1,4 +1,4 @@ -// Copyright 2019-2022 Jan Niklas Hasse +// Copyright 2019-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt /// Contains jngl::SoundFile class /// @file @@ -35,7 +35,7 @@ class SoundFile { /// \note /// If the file doesn't exist this will not throw, but calling SoundFile::play, SoundFile::loop /// or SoundFile::load will. - explicit SoundFile(std::string filename, std::launch policy = std::launch::async); + explicit SoundFile(const std::string& filename, std::launch policy = std::launch::async); ~SoundFile(); SoundFile(const SoundFile&) = delete; SoundFile& operator=(const SoundFile&) = delete; @@ -54,9 +54,6 @@ class SoundFile { /// Play the sound in a loop. Can also be stopped using stop() void loop(); - /// Set pitch in (0.0f, ∞]. Default is 1.0f - void setPitch(float); - /// Set volume in [0, ∞]. Default is 1.0f void setVolume(float v); @@ -67,11 +64,8 @@ class SoundFile { private: std::shared_ptr sound_; - std::unique_ptr params; - std::vector buffer_; -#ifndef EMSCRIPTEN - optional> loader; -#endif + std::vector buffer_; + long frequency = -1; }; } // namespace jngl diff --git a/src/jngl/Video.cpp b/src/jngl/Video.cpp index 507fe3d95..16a2fc3e8 100644 --- a/src/jngl/Video.cpp +++ b/src/jngl/Video.cpp @@ -3,10 +3,14 @@ #include "Video.hpp" +#include +#include #include #ifdef JNGL_VIDEO +#include "../audio/constants.hpp" +#include "../audio/effect/pitch.hpp" #include "../Sound.hpp" #include "../audio.hpp" #include "../main.hpp" @@ -22,15 +26,10 @@ #include #include #include -#ifdef __APPLE__ -#include -#else -#include -#endif namespace jngl { -class Video::Impl { +class Video::Impl : public Stream { public: explicit Impl(const std::string& filename) : decoder(THEORAPLAY_startDecodeFile((pathPrefix + filename).c_str(), BUFFER_SIZE, @@ -46,28 +45,13 @@ class Video::Impl { timePerFrame = 1. / video->fps; assert(timePerFrame > 0); - GetAudio(); - alGenSources(1, &source); - checkAlError(); - alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f); - checkAlError(); - alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); - checkAlError(); - while (!audio) { audio = THEORAPLAY_getAudio(decoder); } + } - switch (audio->channels) { - case 1: - format = AL_FORMAT_MONO16; - break; - case 2: - format = AL_FORMAT_STEREO16; - break; - default: - throw std::runtime_error("Unsupported number of channels."); - } + int getFrequency() const { + return audio->freq; } void draw() { @@ -250,53 +234,8 @@ class Video::Impl { if (!audio) { audio = THEORAPLAY_getAudio(decoder); } - alSourcef(source, AL_GAIN, getVolume()); while (audio) { - ALint processed; - alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); - checkAlError(); - - assert(processed >= 0); - - if (processed > 0) { - // Reuse a processed buffer - alSourceUnqueueBuffers(source, 1, &buffers.front()); - checkAlError(); - - queueAudio(buffers.front()); - buffers.pop_front(); - - ALint state; - alGetSourcei(source, AL_SOURCE_STATE, &state); - checkAlError(); - if (state != AL_PLAYING) { - while (true) { - alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); - checkAlError(); - if (processed == 0) { - break; - } - alSourceUnqueueBuffers(source, 1, &buffers.front()); - checkAlError(); - alDeleteBuffers(1, &buffers.front()); - checkAlError(); - buffers.pop_front(); - } - - alSourcePlay(source); - checkAlError(); - jngl::debugLn("WARNING: Audio buffer underrun!"); - } - } else { - ALuint buffer; - alGenBuffers(1, &buffer); - checkAlError(); - queueAudio(buffer); - if (buffers.size() == 1) { - alSourcePlay(source); - checkAlError(); - } - } + queueAudio(); } } if (shaderProgram) { @@ -317,17 +256,11 @@ class Video::Impl { } } - [[nodiscard]] bool finished() const { - return !THEORAPLAY_isDecoding(decoder); + [[nodiscard]] bool isPlaying() const override { + return THEORAPLAY_isDecoding(decoder); } - ~Impl() { - alDeleteSources(1, &source); - checkAlError(); - for (auto buffer : buffers) { - alDeleteBuffers(1, &buffer); - checkAlError(); - } + ~Impl() override { THEORAPLAY_stopDecode(decoder); } @@ -344,26 +277,50 @@ class Video::Impl { return startTime > 0; } - void queueAudio(ALuint buffer) { - auto pcm = std::make_unique(static_cast(audio->frames) * audio->channels); - for (int i = 0; i < audio->frames * audio->channels; ++i) { - const float sample = std::clamp(audio->samples[i], -1.f, 1.f); - pcm[i] = static_cast(sample * 32767.f); - } - - alBufferData(buffer, format, pcm.get(), - audio->frames * audio->channels * static_cast(sizeof(int16_t)), - audio->freq); - checkAlError(); - alSourceQueueBuffers(source, 1, &buffer); - checkAlError(); - - buffers.push_back(buffer); + void rewind() override { + assert(false); + } + void queueAudio() { + { + std::scoped_lock lock(audioBufferMutex); + if (audio->channels == 1) { + for (size_t i = 0; i < audio->frames; ++i) { + audioBuffer.push_back(audio->samples[i]); + audioBuffer.push_back(audio->samples[i]); + } + } else { + assert(audio->channels == 2); + audioBuffer.insert(audioBuffer.end(), audio->samples, + audio->samples + audio->frames * 2); + } + } THEORAPLAY_freeAudio(audio); audio = THEORAPLAY_getAudio(decoder); } + std::size_t read(float * data, std::size_t sample_count) override { + std::scoped_lock lock(audioBufferMutex); + if (audioBuffer.size() < sample_count) { + if (!isPlaying()) { + return 0; + } + if (started()) { + jngl::debugLn("WARNING: Audio buffer underrun!"); + } + std::memset(data, 0, sample_count); + return sample_count; + } + const auto begin = audioBuffer.begin(); + const auto end = begin + sample_count; + std::copy(begin, end, data); + audioBuffer.erase(begin, end); + return sample_count; + } + + std::mutex audioBufferMutex; + std::vector audioBuffer; + constexpr static unsigned int BUFFER_SIZE = 200; std::unique_ptr sound; @@ -372,9 +329,6 @@ class Video::Impl { const THEORAPLAY_AudioPacket* audio = nullptr; double startTime; double timePerFrame; - std::deque buffers; - ALuint source = 0; - ALenum format; std::unique_ptr shaderProgram; int modelviewUniform = -1; @@ -385,7 +339,12 @@ class Video::Impl { GLuint vertexBuffer = 0; }; -Video::Video(const std::string& filename) : impl(std::make_unique(filename)) { +Video::Video(const std::string& filename) : impl(std::make_shared(filename)) { + if (impl->getFrequency() != audio::frequency) { + getMixer()->add(audio::pitch(impl, float(impl->getFrequency()) / audio::frequency)); + } else { + getMixer()->add(impl); + } } Video::~Video() = default; @@ -403,7 +362,7 @@ int Video::getHeight() const { } bool Video::finished() const { - return impl->finished(); + return !impl->isPlaying(); } } // namespace jngl diff --git a/src/jngl/Video.hpp b/src/jngl/Video.hpp index 0c4313af3..949dbd519 100644 --- a/src/jngl/Video.hpp +++ b/src/jngl/Video.hpp @@ -1,4 +1,4 @@ -// Copyright 2018-2020 Jan Niklas Hasse +// Copyright 2018-2023 Jan Niklas Hasse // For conditions of distribution and use, see copyright notice in LICENSE.txt /// @file #pragma once @@ -50,7 +50,7 @@ class Video { private: class Impl; - std::unique_ptr impl; + std::shared_ptr impl; }; } // namespace jngl diff --git a/src/jngl/sound.hpp b/src/jngl/sound.hpp index fa3bcfc72..1d9b34e56 100644 --- a/src/jngl/sound.hpp +++ b/src/jngl/sound.hpp @@ -30,11 +30,10 @@ bool isPlaying(const std::string& filename); /// a pointer to the same SoundFile. std::shared_ptr loop(const std::string& filename); -[[deprecated("an OpenAL implementation will always be available")]] -bool isOpenALInstalled(); - +/// Set global pitch in (0.0f, ∞]. Default is 1.0f void setPlaybackSpeed(float speed); +/// Set global volume in [0, ∞]. Default is 1.0f void setVolume(float volume); } // namespace jngl diff --git a/src/main.cpp b/src/main.cpp index 9d46478ec..1462a6e95 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -557,7 +557,7 @@ bool getAntiAliasing() { return antiAliasingEnabled; } -Finally loadSound(const std::string&); // definied in audio.cpp +Finally loadSound(const std::string&); // definied in SoundFile.cpp Finally load(const std::string& filename) { if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".ogg") { diff --git a/src/test/test.cpp b/src/test/test.cpp index 187789a96..78e3b57c9 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -3,6 +3,8 @@ #include "../jngl.hpp" #include "../jngl/init.hpp" +#include "audio/engine.hpp" +#include "audio/mixer.hpp" #include #include @@ -167,7 +169,8 @@ class Test : public jngl::Work { jngl::setFullscreen(!jngl::getFullscreen()); } jngl::print("Press K to test key codes.", 5, 490); - jngl::print("Press P to play a sound, L to loop it.", 6, 510); + jngl::print("Press P to play a sound, L to loop it.", jngl::isPlaying("test.ogg") ? 20 : 6, + 510); jngl::print("Press G to load a Sprite asynchronously.", 6, 530); static int playbackSpeed = 100; jngl::setPlaybackSpeed(static_cast(playbackSpeed) / 100.0f);