Browse Source

* BugFix: Correct convexDecomp compilation by setting the LINUX flag when necessary.
* BugFix: Update OpenAL to correct a compilation error on Linux.

Robert MacGregor 3 years ago
parent
commit
7380161054
100 changed files with 18366 additions and 3559 deletions
  1. 8 3
      Engine/lib/CMakeLists.txt
  2. 4 0
      Engine/lib/convexDecomp/CMakeLists.txt
  3. 66 0
      Engine/lib/openal-soft/.github/workflows/ci.yml
  4. 2 1
      Engine/lib/openal-soft/.travis.yml
  5. 0 282
      Engine/lib/openal-soft/Alc/alcontext.h
  6. 0 250
      Engine/lib/openal-soft/Alc/backends/oboe.cpp
  7. 0 9
      Engine/lib/openal-soft/Alc/compat.h
  8. 0 793
      Engine/lib/openal-soft/Alc/voice.cpp
  9. 1 0
      Engine/lib/openal-soft/BSD-3Clause
  10. 240 130
      Engine/lib/openal-soft/CMakeLists.txt
  11. 58 0
      Engine/lib/openal-soft/ChangeLog
  12. 9 0
      Engine/lib/openal-soft/OpenALConfig.cmake.in
  13. 5 0
      Engine/lib/openal-soft/XCompile-Android.txt
  14. 832 34
      Engine/lib/openal-soft/al/auxeffectslot.cpp
  15. 214 5
      Engine/lib/openal-soft/al/auxeffectslot.h
  16. 352 122
      Engine/lib/openal-soft/al/buffer.cpp
  17. 13 2
      Engine/lib/openal-soft/al/buffer.h
  18. 1213 0
      Engine/lib/openal-soft/al/eax_api.cpp
  19. 1557 0
      Engine/lib/openal-soft/al/eax_api.h
  20. 324 0
      Engine/lib/openal-soft/al/eax_eax_call.cpp
  21. 117 0
      Engine/lib/openal-soft/al/eax_eax_call.h
  22. 3 0
      Engine/lib/openal-soft/al/eax_effect.cpp
  23. 44 0
      Engine/lib/openal-soft/al/eax_effect.h
  24. 63 0
      Engine/lib/openal-soft/al/eax_exception.cpp
  25. 25 0
      Engine/lib/openal-soft/al/eax_exception.h
  26. 71 0
      Engine/lib/openal-soft/al/eax_fx_slot_index.cpp
  27. 41 0
      Engine/lib/openal-soft/al/eax_fx_slot_index.h
  28. 84 0
      Engine/lib/openal-soft/al/eax_fx_slots.cpp
  29. 54 0
      Engine/lib/openal-soft/al/eax_fx_slots.h
  30. 21 0
      Engine/lib/openal-soft/al/eax_globals.cpp
  31. 22 0
      Engine/lib/openal-soft/al/eax_globals.h
  32. 36 0
      Engine/lib/openal-soft/al/eax_utils.cpp
  33. 132 0
      Engine/lib/openal-soft/al/eax_utils.h
  34. 3 0
      Engine/lib/openal-soft/al/eax_x_ram.cpp
  35. 38 0
      Engine/lib/openal-soft/al/eax_x_ram.h
  36. 36 17
      Engine/lib/openal-soft/al/effect.cpp
  37. 2 0
      Engine/lib/openal-soft/al/effect.h
  38. 440 1
      Engine/lib/openal-soft/al/effects/autowah.cpp
  39. 1082 1
      Engine/lib/openal-soft/al/effects/chorus.cpp
  40. 225 1
      Engine/lib/openal-soft/al/effects/compressor.cpp
  41. 2 2
      Engine/lib/openal-soft/al/effects/convolution.cpp
  42. 2 2
      Engine/lib/openal-soft/al/effects/dedicated.cpp
  43. 458 1
      Engine/lib/openal-soft/al/effects/distortion.cpp
  44. 459 1
      Engine/lib/openal-soft/al/effects/echo.cpp
  45. 66 0
      Engine/lib/openal-soft/al/effects/effects.cpp
  46. 13 0
      Engine/lib/openal-soft/al/effects/effects.h
  47. 753 1
      Engine/lib/openal-soft/al/effects/equalizer.cpp
  48. 352 1
      Engine/lib/openal-soft/al/effects/fshifter.cpp
  49. 349 1
      Engine/lib/openal-soft/al/effects/modulator.cpp
  50. 60 1
      Engine/lib/openal-soft/al/effects/null.cpp
  51. 281 1
      Engine/lib/openal-soft/al/effects/pshifter.cpp
  52. 1987 1
      Engine/lib/openal-soft/al/effects/reverb.cpp
  53. 540 1
      Engine/lib/openal-soft/al/effects/vmorpher.cpp
  54. 3 3
      Engine/lib/openal-soft/al/error.cpp
  55. 26 29
      Engine/lib/openal-soft/al/event.cpp
  56. 2 2
      Engine/lib/openal-soft/al/extension.cpp
  57. 24 22
      Engine/lib/openal-soft/al/filter.cpp
  58. 1 1
      Engine/lib/openal-soft/al/filter.h
  59. 45 48
      Engine/lib/openal-soft/al/listener.cpp
  60. 0 7
      Engine/lib/openal-soft/al/listener.h
  61. 302 196
      Engine/lib/openal-soft/al/source.cpp
  62. 681 6
      Engine/lib/openal-soft/al/source.h
  63. 94 25
      Engine/lib/openal-soft/al/state.cpp
  64. 418 346
      Engine/lib/openal-soft/alc/alc.cpp
  65. 68 82
      Engine/lib/openal-soft/alc/alconfig.cpp
  66. 1 3
      Engine/lib/openal-soft/alc/alconfig.h
  67. 278 283
      Engine/lib/openal-soft/alc/alu.cpp
  68. 38 0
      Engine/lib/openal-soft/alc/alu.h
  69. 86 85
      Engine/lib/openal-soft/alc/backends/alsa.cpp
  70. 2 2
      Engine/lib/openal-soft/alc/backends/alsa.h
  71. 16 17
      Engine/lib/openal-soft/alc/backends/base.cpp
  72. 12 8
      Engine/lib/openal-soft/alc/backends/base.h
  73. 378 120
      Engine/lib/openal-soft/alc/backends/coreaudio.cpp
  74. 2 2
      Engine/lib/openal-soft/alc/backends/coreaudio.h
  75. 39 64
      Engine/lib/openal-soft/alc/backends/dsound.cpp
  76. 2 2
      Engine/lib/openal-soft/alc/backends/dsound.h
  77. 263 125
      Engine/lib/openal-soft/alc/backends/jack.cpp
  78. 2 2
      Engine/lib/openal-soft/alc/backends/jack.h
  79. 6 7
      Engine/lib/openal-soft/alc/backends/loopback.cpp
  80. 2 2
      Engine/lib/openal-soft/alc/backends/loopback.h
  81. 5 5
      Engine/lib/openal-soft/alc/backends/null.cpp
  82. 2 2
      Engine/lib/openal-soft/alc/backends/null.h
  83. 384 0
      Engine/lib/openal-soft/alc/backends/oboe.cpp
  84. 2 2
      Engine/lib/openal-soft/alc/backends/oboe.h
  85. 23 14
      Engine/lib/openal-soft/alc/backends/opensl.cpp
  86. 2 2
      Engine/lib/openal-soft/alc/backends/opensl.h
  87. 14 10
      Engine/lib/openal-soft/alc/backends/oss.cpp
  88. 2 2
      Engine/lib/openal-soft/alc/backends/oss.h
  89. 2008 0
      Engine/lib/openal-soft/alc/backends/pipewire.cpp
  90. 23 0
      Engine/lib/openal-soft/alc/backends/pipewire.h
  91. 28 23
      Engine/lib/openal-soft/alc/backends/portaudio.cpp
  92. 2 2
      Engine/lib/openal-soft/alc/backends/portaudio.h
  93. 78 131
      Engine/lib/openal-soft/alc/backends/pulseaudio.cpp
  94. 2 2
      Engine/lib/openal-soft/alc/backends/pulseaudio.h
  95. 37 32
      Engine/lib/openal-soft/alc/backends/sdl2.cpp
  96. 2 2
      Engine/lib/openal-soft/alc/backends/sdl2.h
  97. 160 144
      Engine/lib/openal-soft/alc/backends/sndio.cpp
  98. 2 2
      Engine/lib/openal-soft/alc/backends/sndio.h
  99. 38 29
      Engine/lib/openal-soft/alc/backends/solaris.cpp
  100. 2 2
      Engine/lib/openal-soft/alc/backends/solaris.h

+ 8 - 3
Engine/lib/CMakeLists.txt

@@ -1,8 +1,5 @@
 # Ask CMake to perform builds in a temporary directory for all of these.
 # We also use EXCLUDE_FROM_ALL to ensure we only build and install what we want
-set(BUILD_SHARED_LIBS off CACHE STRING "")
-add_subdirectory(assimp ${CMAKE_BINARY_DIR}/temp/assimp EXCLUDE_FROM_ALL)
-
 set(SDL_SHARED on CACHE BOOL "" FORCE)
 add_subdirectory(sdl ${CMAKE_BINARY_DIR}/temp/sdl2 EXCLUDE_FROM_ALL)
 
@@ -16,6 +13,14 @@ set(PNG_BUILD_ZLIB on CACHE BOOL "" FORCE)
 get_filename_component(ZLIB_ROOT "zlib" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
 set(ZLIB_ROOT "${ZLIB_ROOT}" CACHE STRING "ZLib root location" FORCE)
 
+# Assimp depends on zlib
+set(BUILD_SHARED_LIBS off CACHE STRING "")
+set(ASSIMP_BUILD_ZLIB on CACHE STRING "")
+set(ASSIMP_HUNTER_ENABLED off CACHE STRING "")
+add_subdirectory(assimp ${CMAKE_BINARY_DIR}/temp/assimp EXCLUDE_FROM_ALL)
+
+
+
 add_subdirectory(lpng ${CMAKE_BINARY_DIR}/temp/lpng EXCLUDE_FROM_ALL)
 
 add_subdirectory(ljpeg ${CMAKE_BINARY_DIR}/temp/ljpeg EXCLUDE_FROM_ALL)

+ 4 - 0
Engine/lib/convexDecomp/CMakeLists.txt

@@ -1,3 +1,7 @@
 file(GLOB CONVEX_DECOMP_SOURCES "*.cpp")
 add_library(convexDecomp STATIC ${CONVEX_DECOMP_SOURCES})
 target_include_directories(convexDecomp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+if (UNIX AND NOT APPLE)
+    target_compile_definitions(convexDecomp PUBLIC LINUX)
+endif (UNIX AND NOT APPLE)

+ 66 - 0
Engine/lib/openal-soft/.github/workflows/ci.yml

@@ -0,0 +1,66 @@
+name: CI
+
+on: [push]
+
+jobs:
+  build:
+    name: ${{matrix.config.name}}
+    runs-on: ${{matrix.config.os}}
+    strategy:
+      fail-fast: false
+      matrix:
+        config:
+        - {
+            name: "Visual Studio 64-bit",
+            os: windows-latest,
+            cmake_opts: "-A x64 \
+              -DALSOFT_BUILD_ROUTER=ON \
+              -DALSOFT_REQUIRE_WINMM=ON \
+              -DALSOFT_REQUIRE_DSOUND=ON \
+              -DALSOFT_REQUIRE_WASAPI=ON",
+            build_type: "Release"
+          }
+        - {
+            name: "macOS",
+            os: macos-latest,
+            cmake_opts: "-DALSOFT_REQUIRE_COREAUDIO=ON",
+            build_type: "Release"
+          }
+        - {
+            name: "Linux",
+            os: ubuntu-latest,
+            cmake_opts: "-DALSOFT_REQUIRE_RTKIT=ON \
+              -DALSOFT_REQUIRE_ALSA=ON \
+              -DALSOFT_REQUIRE_OSS=ON \
+              -DALSOFT_REQUIRE_PORTAUDIO=ON \
+              -DALSOFT_REQUIRE_PULSEAUDIO=ON \
+              -DALSOFT_REQUIRE_JACK=ON",
+            deps_cmdline: "sudo apt update && sudo apt-get install -qq \
+              libpulse-dev \
+              portaudio19-dev \
+              libasound2-dev \
+              libjack-dev \
+              qtbase5-dev \
+              libdbus-1-dev",
+            build_type: "Release"
+          }
+
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Install Dependencies
+      shell: bash
+      run: |
+        if [[ ! -z "${{matrix.config.deps_cmdline}}" ]]; then
+          eval ${{matrix.config.deps_cmdline}}
+        fi
+
+    - name: Configure
+      shell: bash
+      run: |
+        cmake -B build -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} ${{matrix.config.cmake_opts}} .
+
+    - name: Build
+      shell: bash
+      run: |
+        cmake --build build --config ${{matrix.config.build_type}}

+ 2 - 1
Engine/lib/openal-soft/.travis.yml

@@ -26,7 +26,8 @@ install:
         portaudio19-dev \
         libasound2-dev \
         libjack-dev \
-        qtbase5-dev
+        qtbase5-dev \
+        libdbus-1-dev
     fi
   - >
     if [[ "${TRAVIS_OS_NAME}" == "linux" && "${BUILD_ANDROID}" == "true" ]]; then

+ 0 - 282
Engine/lib/openal-soft/Alc/alcontext.h

@@ -1,282 +0,0 @@
-#ifndef ALCONTEXT_H
-#define ALCONTEXT_H
-
-#include <array>
-#include <atomic>
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-#include <mutex>
-#include <thread>
-#include <utility>
-
-#include "AL/al.h"
-#include "AL/alc.h"
-
-#include "al/listener.h"
-#include "almalloc.h"
-#include "alnumeric.h"
-#include "alu.h"
-#include "atomic.h"
-#include "inprogext.h"
-#include "intrusive_ptr.h"
-#include "threads.h"
-#include "vecmat.h"
-#include "vector.h"
-
-struct ALeffectslot;
-struct ALsource;
-struct EffectSlot;
-struct EffectSlotProps;
-struct RingBuffer;
-struct Voice;
-struct VoiceChange;
-struct VoicePropsItem;
-
-
-enum class DistanceModel : unsigned char {
-    Disable,
-    Inverse, InverseClamped,
-    Linear, LinearClamped,
-    Exponent, ExponentClamped,
-
-    Default = InverseClamped
-};
-
-
-struct WetBuffer {
-    bool mInUse;
-    al::FlexArray<FloatBufferLine, 16> mBuffer;
-
-    WetBuffer(size_t count) : mBuffer{count} { }
-
-    DEF_FAM_NEWDEL(WetBuffer, mBuffer)
-};
-using WetBufferPtr = std::unique_ptr<WetBuffer>;
-
-
-struct ContextProps {
-    float DopplerFactor;
-    float DopplerVelocity;
-    float SpeedOfSound;
-    bool SourceDistanceModel;
-    DistanceModel mDistanceModel;
-
-    std::atomic<ContextProps*> next;
-
-    DEF_NEWDEL(ContextProps)
-};
-
-struct ListenerProps {
-    std::array<float,3> Position;
-    std::array<float,3> Velocity;
-    std::array<float,3> OrientAt;
-    std::array<float,3> OrientUp;
-    float Gain;
-    float MetersPerUnit;
-
-    std::atomic<ListenerProps*> next;
-
-    DEF_NEWDEL(ListenerProps)
-};
-
-struct ContextParams {
-    /* Pointer to the most recent property values that are awaiting an update. */
-    std::atomic<ContextProps*> ContextUpdate{nullptr};
-    std::atomic<ListenerProps*> ListenerUpdate{nullptr};
-
-    alu::Matrix Matrix{alu::Matrix::Identity()};
-    alu::Vector Velocity{};
-
-    float Gain{1.0f};
-    float MetersPerUnit{1.0f};
-
-    float DopplerFactor{1.0f};
-    float SpeedOfSound{343.3f}; /* in units per sec! */
-
-    bool SourceDistanceModel{false};
-    DistanceModel mDistanceModel{};
-};
-
-
-struct SourceSubList {
-    uint64_t FreeMask{~0_u64};
-    ALsource *Sources{nullptr}; /* 64 */
-
-    SourceSubList() noexcept = default;
-    SourceSubList(const SourceSubList&) = delete;
-    SourceSubList(SourceSubList&& rhs) noexcept : FreeMask{rhs.FreeMask}, Sources{rhs.Sources}
-    { rhs.FreeMask = ~0_u64; rhs.Sources = nullptr; }
-    ~SourceSubList();
-
-    SourceSubList& operator=(const SourceSubList&) = delete;
-    SourceSubList& operator=(SourceSubList&& rhs) noexcept
-    { std::swap(FreeMask, rhs.FreeMask); std::swap(Sources, rhs.Sources); return *this; }
-};
-
-struct EffectSlotSubList {
-    uint64_t FreeMask{~0_u64};
-    ALeffectslot *EffectSlots{nullptr}; /* 64 */
-
-    EffectSlotSubList() noexcept = default;
-    EffectSlotSubList(const EffectSlotSubList&) = delete;
-    EffectSlotSubList(EffectSlotSubList&& rhs) noexcept
-      : FreeMask{rhs.FreeMask}, EffectSlots{rhs.EffectSlots}
-    { rhs.FreeMask = ~0_u64; rhs.EffectSlots = nullptr; }
-    ~EffectSlotSubList();
-
-    EffectSlotSubList& operator=(const EffectSlotSubList&) = delete;
-    EffectSlotSubList& operator=(EffectSlotSubList&& rhs) noexcept
-    { std::swap(FreeMask, rhs.FreeMask); std::swap(EffectSlots, rhs.EffectSlots); return *this; }
-};
-
-struct ALCcontext : public al::intrusive_ref<ALCcontext> {
-    const al::intrusive_ptr<ALCdevice> mDevice;
-
-    /* Counter for the pre-mixing updates, in 31.1 fixed point (lowest bit
-     * indicates if updates are currently happening).
-     */
-    RefCount mUpdateCount{0u};
-    std::atomic<bool> mHoldUpdates{false};
-
-    float mGainBoost{1.0f};
-
-    /* Linked lists of unused property containers, free to use for future
-     * updates.
-     */
-    std::atomic<ContextProps*> mFreeContextProps{nullptr};
-    std::atomic<ListenerProps*> mFreeListenerProps{nullptr};
-    std::atomic<VoicePropsItem*> mFreeVoiceProps{nullptr};
-    std::atomic<EffectSlotProps*> mFreeEffectslotProps{nullptr};
-
-    /* The voice change tail is the beginning of the "free" elements, up to and
-     * *excluding* the current. If tail==current, there's no free elements and
-     * new ones need to be allocated. The current voice change is the element
-     * last processed, and any after are pending.
-     */
-    VoiceChange *mVoiceChangeTail{};
-    std::atomic<VoiceChange*> mCurrentVoiceChange{};
-
-    void allocVoiceChanges(size_t addcount);
-
-
-    ContextParams mParams;
-
-    using VoiceArray = al::FlexArray<Voice*>;
-    std::atomic<VoiceArray*> mVoices{};
-    std::atomic<size_t> mActiveVoiceCount{};
-
-    void allocVoices(size_t addcount);
-    al::span<Voice*> getVoicesSpan() const noexcept
-    {
-        return {mVoices.load(std::memory_order_relaxed)->data(),
-            mActiveVoiceCount.load(std::memory_order_relaxed)};
-    }
-    al::span<Voice*> getVoicesSpanAcquired() const noexcept
-    {
-        return {mVoices.load(std::memory_order_acquire)->data(),
-            mActiveVoiceCount.load(std::memory_order_acquire)};
-    }
-
-
-    using EffectSlotArray = al::FlexArray<EffectSlot*>;
-    std::atomic<EffectSlotArray*> mActiveAuxSlots{nullptr};
-
-    std::thread mEventThread;
-    al::semaphore mEventSem;
-    std::unique_ptr<RingBuffer> mAsyncEvents;
-    std::atomic<uint> mEnabledEvts{0u};
-
-    /* Asynchronous voice change actions are processed as a linked list of
-     * VoiceChange objects by the mixer, which is atomically appended to.
-     * However, to avoid allocating each object individually, they're allocated
-     * in clusters that are stored in a vector for easy automatic cleanup.
-     */
-    using VoiceChangeCluster = std::unique_ptr<VoiceChange[]>;
-    al::vector<VoiceChangeCluster> mVoiceChangeClusters;
-
-    using VoiceCluster = std::unique_ptr<Voice[]>;
-    al::vector<VoiceCluster> mVoiceClusters;
-
-    /* Wet buffers used by effect slots. */
-    al::vector<WetBufferPtr> mWetBuffers;
-
-
-    std::atomic_flag mPropsClean;
-    std::atomic<bool> mDeferUpdates{false};
-
-    std::mutex mPropLock;
-
-    std::atomic<ALenum> mLastError{AL_NO_ERROR};
-
-    DistanceModel mDistanceModel{DistanceModel::Default};
-    bool mSourceDistanceModel{false};
-
-    float mDopplerFactor{1.0f};
-    float mDopplerVelocity{1.0f};
-    float mSpeedOfSound{SpeedOfSoundMetersPerSec};
-
-    std::mutex mEventCbLock;
-    ALEVENTPROCSOFT mEventCb{};
-    void *mEventParam{nullptr};
-
-    ALlistener mListener{};
-
-    al::vector<SourceSubList> mSourceList;
-    ALuint mNumSources{0};
-    std::mutex mSourceLock;
-
-    al::vector<EffectSlotSubList> mEffectSlotList;
-    ALuint mNumEffectSlots{0u};
-    std::mutex mEffectSlotLock;
-
-    /* Default effect slot */
-    std::unique_ptr<ALeffectslot> mDefaultSlot;
-
-    const char *mExtensionList{nullptr};
-
-
-    ALCcontext(al::intrusive_ptr<ALCdevice> device);
-    ALCcontext(const ALCcontext&) = delete;
-    ALCcontext& operator=(const ALCcontext&) = delete;
-    ~ALCcontext();
-
-    void init();
-    /**
-     * Removes the context from its device and removes it from being current on
-     * the running thread or globally. Returns true if other contexts still
-     * exist on the device.
-     */
-    bool deinit();
-
-    /**
-     * Defers/suspends updates for the given context's listener and sources.
-     * This does *NOT* stop mixing, but rather prevents certain property
-     * changes from taking effect.
-     */
-    void deferUpdates() noexcept { mDeferUpdates.exchange(true, std::memory_order_acq_rel); }
-
-    /** Resumes update processing after being deferred. */
-    void processUpdates();
-
-    [[gnu::format(printf,3,4)]] void setError(ALenum errorCode, const char *msg, ...);
-
-    DEF_NEWDEL(ALCcontext)
-};
-
-#define SETERR_RETURN(ctx, err, retval, ...) do {                             \
-    (ctx)->setError((err), __VA_ARGS__);                                      \
-    return retval;                                                            \
-} while(0)
-
-
-using ContextRef = al::intrusive_ptr<ALCcontext>;
-
-ContextRef GetContextRef(void);
-
-void UpdateContextProps(ALCcontext *context);
-
-
-extern bool TrapALError;
-
-#endif /* ALCONTEXT_H */

+ 0 - 250
Engine/lib/openal-soft/Alc/backends/oboe.cpp

@@ -1,250 +0,0 @@
-
-#include "config.h"
-
-#include "oboe.h"
-
-#include <cassert>
-#include <cstring>
-
-#include "alu.h"
-#include "core/logging.h"
-
-#include "oboe/Oboe.h"
-
-
-namespace {
-
-constexpr char device_name[] = "Oboe Default";
-
-
-struct OboePlayback final : public BackendBase, public oboe::AudioStreamCallback {
-    OboePlayback(ALCdevice *device) : BackendBase{device} { }
-
-    oboe::ManagedStream mStream;
-
-    oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
-        int32_t numFrames) override;
-
-    void open(const char *name) override;
-    bool reset() override;
-    void start() override;
-    void stop() override;
-};
-
-
-oboe::DataCallbackResult OboePlayback::onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
-    int32_t numFrames)
-{
-    assert(numFrames > 0);
-    const int32_t numChannels{oboeStream->getChannelCount()};
-
-    if UNLIKELY(numChannels > 2 && mDevice->FmtChans == DevFmtStereo)
-    {
-        /* If the device is only mixing stereo but there's more than two
-         * output channels, there are unused channels that need to be silenced.
-         */
-        if(mStream->getFormat() == oboe::AudioFormat::Float)
-            memset(audioData, 0, static_cast<uint32_t>(numFrames*numChannels)*sizeof(float));
-        else
-            memset(audioData, 0, static_cast<uint32_t>(numFrames*numChannels)*sizeof(int16_t));
-    }
-
-    mDevice->renderSamples(audioData, static_cast<uint32_t>(numFrames),
-        static_cast<uint32_t>(numChannels));
-    return oboe::DataCallbackResult::Continue;
-}
-
-
-void OboePlayback::open(const char *name)
-{
-    if(!name)
-        name = device_name;
-    else if(std::strcmp(name, device_name) != 0)
-        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
-            name};
-
-    /* Open a basic output stream, just to ensure it can work. */
-    oboe::Result result{oboe::AudioStreamBuilder{}.setDirection(oboe::Direction::Output)
-        ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
-        ->openManagedStream(mStream)};
-    if(result != oboe::Result::OK)
-        throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s",
-            oboe::convertToText(result)};
-
-    mDevice->DeviceName = name;
-}
-
-bool OboePlayback::reset()
-{
-    oboe::AudioStreamBuilder builder;
-    builder.setDirection(oboe::Direction::Output);
-    builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
-    /* Don't let Oboe convert. We should be able to handle anything it gives
-     * back.
-     */
-    builder.setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None);
-    builder.setChannelConversionAllowed(false);
-    builder.setFormatConversionAllowed(false);
-    builder.setCallback(this);
-
-    if(mDevice->Flags.test(FrequencyRequest))
-        builder.setSampleRate(static_cast<int32_t>(mDevice->Frequency));
-    if(mDevice->Flags.test(ChannelsRequest))
-    {
-        /* Only use mono or stereo at user request. There's no telling what
-         * other counts may be inferred as.
-         */
-        builder.setChannelCount((mDevice->FmtChans==DevFmtMono) ? oboe::ChannelCount::Mono
-            : (mDevice->FmtChans==DevFmtStereo) ? oboe::ChannelCount::Stereo
-            : oboe::ChannelCount::Unspecified);
-    }
-    if(mDevice->Flags.test(SampleTypeRequest))
-    {
-        oboe::AudioFormat format{oboe::AudioFormat::Unspecified};
-        switch(mDevice->FmtType)
-        {
-        case DevFmtByte:
-        case DevFmtUByte:
-        case DevFmtShort:
-        case DevFmtUShort:
-            format = oboe::AudioFormat::I16;
-            break;
-        case DevFmtInt:
-        case DevFmtUInt:
-        case DevFmtFloat:
-            format = oboe::AudioFormat::Float;
-            break;
-        }
-        builder.setFormat(format);
-    }
-
-    oboe::Result result{builder.openManagedStream(mStream)};
-    /* If the format failed, try asking for the defaults. */
-    while(result == oboe::Result::ErrorInvalidFormat)
-    {
-        if(builder.getFormat() != oboe::AudioFormat::Unspecified)
-            builder.setFormat(oboe::AudioFormat::Unspecified);
-        else if(builder.getSampleRate() != oboe::kUnspecified)
-            builder.setSampleRate(oboe::kUnspecified);
-        else if(builder.getChannelCount() != oboe::ChannelCount::Unspecified)
-            builder.setChannelCount(oboe::ChannelCount::Unspecified);
-        else
-            break;
-        result = builder.openManagedStream(mStream);
-    }
-    if(result != oboe::Result::OK)
-        throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s",
-            oboe::convertToText(result)};
-    TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get()));
-
-    switch(mStream->getChannelCount())
-    {
-    case oboe::ChannelCount::Mono:
-        mDevice->FmtChans = DevFmtMono;
-        break;
-    case oboe::ChannelCount::Stereo:
-        mDevice->FmtChans = DevFmtStereo;
-        break;
-    /* Other potential configurations. Could be wrong, but better than failing.
-     * Assume WFX channel order.
-     */
-    case 4:
-        mDevice->FmtChans = DevFmtQuad;
-        break;
-    case 6:
-        mDevice->FmtChans = DevFmtX51Rear;
-        break;
-    case 7:
-        mDevice->FmtChans = DevFmtX61;
-        break;
-    case 8:
-        mDevice->FmtChans = DevFmtX71;
-        break;
-    default:
-        if(mStream->getChannelCount() < 1)
-            throw al::backend_exception{al::backend_error::DeviceError,
-                "Got unhandled channel count: %d", mStream->getChannelCount()};
-        /* Assume first two channels are front left/right. We can do a stereo
-         * mix and keep the other channels silent.
-         */
-        mDevice->FmtChans = DevFmtStereo;
-        break;
-    }
-    setDefaultWFXChannelOrder();
-
-    switch(mStream->getFormat())
-    {
-    case oboe::AudioFormat::I16:
-        mDevice->FmtType = DevFmtShort;
-        break;
-    case oboe::AudioFormat::Float:
-        mDevice->FmtType = DevFmtFloat;
-        break;
-    case oboe::AudioFormat::Unspecified:
-    case oboe::AudioFormat::Invalid:
-        throw al::backend_exception{al::backend_error::DeviceError,
-            "Got unhandled sample type: %s", oboe::convertToText(mStream->getFormat())};
-    }
-    mDevice->Frequency = static_cast<uint32_t>(mStream->getSampleRate());
-
-    /* Ensure the period size is no less than 10ms. It's possible for FramesPerCallback to be 0
-     * indicating variable updates, but OpenAL should have a reasonable minimum update size set.
-     * FramesPerBurst may not necessarily be correct, but hopefully it can act as a minimum
-     * update size.
-     */
-    mDevice->UpdateSize = maxu(mDevice->Frequency / 100,
-        static_cast<uint32_t>(mStream->getFramesPerBurst()));
-    mDevice->BufferSize = maxu(mDevice->UpdateSize * 2,
-        static_cast<uint32_t>(mStream->getBufferSizeInFrames()));
-
-    return true;
-}
-
-void OboePlayback::start()
-{
-    const oboe::Result result{mStream->start()};
-    if(result != oboe::Result::OK)
-        throw al::backend_exception{al::backend_error::DeviceError, "Failed to start stream: %s",
-            oboe::convertToText(result)};
-}
-
-void OboePlayback::stop()
-{
-    oboe::Result result{mStream->stop()};
-    if(result != oboe::Result::OK)
-        throw al::backend_exception{al::backend_error::DeviceError, "Failed to stop stream: %s",
-            oboe::convertToText(result)};
-}
-
-} // namespace
-
-bool OboeBackendFactory::init() { return true; }
-
-bool OboeBackendFactory::querySupport(BackendType type)
-{ return type == BackendType::Playback; }
-
-std::string OboeBackendFactory::probe(BackendType type)
-{
-    switch(type)
-    {
-    case BackendType::Playback:
-        /* Includes null char. */
-        return std::string{device_name, sizeof(device_name)};
-    case BackendType::Capture:
-        break;
-    }
-    return std::string{};
-}
-
-BackendPtr OboeBackendFactory::createBackend(ALCdevice *device, BackendType type)
-{
-    if(type == BackendType::Playback)
-        return BackendPtr{new OboePlayback{device}};
-    return nullptr;
-}
-
-BackendFactory &OboeBackendFactory::getFactory()
-{
-    static OboeBackendFactory factory{};
-    return factory;
-}

+ 0 - 9
Engine/lib/openal-soft/Alc/compat.h

@@ -1,9 +0,0 @@
-#ifndef AL_COMPAT_H
-#define AL_COMPAT_H
-
-#include <string>
-
-struct PathNamePair { std::string path, fname; };
-const PathNamePair &GetProcBinary(void);
-
-#endif /* AL_COMPAT_H */

+ 0 - 793
Engine/lib/openal-soft/Alc/voice.cpp

@@ -1,793 +0,0 @@
-/**
- * OpenAL cross platform audio library
- * Copyright (C) 1999-2007 by authors.
- * This library is free software; you can redistribute it and/or
- *  modify it under the terms of the GNU Library General Public
- *  License as published by the Free Software Foundation; either
- *  version 2 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *  Library General Public License for more details.
- *
- * You should have received a copy of the GNU Library General Public
- *  License along with this library; if not, write to the
- *  Free Software Foundation, Inc.,
- *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- * Or go to http://www.gnu.org/copyleft/lgpl.html
- */
-
-#include "config.h"
-
-#include "voice.h"
-
-#include <algorithm>
-#include <array>
-#include <atomic>
-#include <cassert>
-#include <climits>
-#include <cstddef>
-#include <cstdint>
-#include <iterator>
-#include <memory>
-#include <new>
-#include <utility>
-
-#include "alcmain.h"
-#include "albyte.h"
-#include "alconfig.h"
-#include "alcontext.h"
-#include "alnumeric.h"
-#include "aloptional.h"
-#include "alspan.h"
-#include "alstring.h"
-#include "alu.h"
-#include "async_event.h"
-#include "buffer_storage.h"
-#include "core/cpu_caps.h"
-#include "core/devformat.h"
-#include "core/filters/biquad.h"
-#include "core/filters/nfc.h"
-#include "core/filters/splitter.h"
-#include "core/fmt_traits.h"
-#include "core/logging.h"
-#include "core/mixer/defs.h"
-#include "core/mixer/hrtfdefs.h"
-#include "hrtf.h"
-#include "inprogext.h"
-#include "opthelpers.h"
-#include "ringbuffer.h"
-#include "threads.h"
-#include "vector.h"
-#include "voice_change.h"
-
-struct CTag;
-#ifdef HAVE_SSE
-struct SSETag;
-#endif
-#ifdef HAVE_NEON
-struct NEONTag;
-#endif
-struct CopyTag;
-
-
-Resampler ResamplerDefault{Resampler::Linear};
-
-MixerFunc MixSamples{Mix_<CTag>};
-
-namespace {
-
-using HrtfMixerFunc = void(*)(const float *InSamples, float2 *AccumSamples, const uint IrSize,
-    const MixHrtfFilter *hrtfparams, const size_t BufferSize);
-using HrtfMixerBlendFunc = void(*)(const float *InSamples, float2 *AccumSamples,
-    const uint IrSize, const HrtfFilter *oldparams, const MixHrtfFilter *newparams,
-    const size_t BufferSize);
-
-HrtfMixerFunc MixHrtfSamples{MixHrtf_<CTag>};
-HrtfMixerBlendFunc MixHrtfBlendSamples{MixHrtfBlend_<CTag>};
-
-inline MixerFunc SelectMixer()
-{
-#ifdef HAVE_NEON
-    if((CPUCapFlags&CPU_CAP_NEON))
-        return Mix_<NEONTag>;
-#endif
-#ifdef HAVE_SSE
-    if((CPUCapFlags&CPU_CAP_SSE))
-        return Mix_<SSETag>;
-#endif
-    return Mix_<CTag>;
-}
-
-inline HrtfMixerFunc SelectHrtfMixer()
-{
-#ifdef HAVE_NEON
-    if((CPUCapFlags&CPU_CAP_NEON))
-        return MixHrtf_<NEONTag>;
-#endif
-#ifdef HAVE_SSE
-    if((CPUCapFlags&CPU_CAP_SSE))
-        return MixHrtf_<SSETag>;
-#endif
-    return MixHrtf_<CTag>;
-}
-
-inline HrtfMixerBlendFunc SelectHrtfBlendMixer()
-{
-#ifdef HAVE_NEON
-    if((CPUCapFlags&CPU_CAP_NEON))
-        return MixHrtfBlend_<NEONTag>;
-#endif
-#ifdef HAVE_SSE
-    if((CPUCapFlags&CPU_CAP_SSE))
-        return MixHrtfBlend_<SSETag>;
-#endif
-    return MixHrtfBlend_<CTag>;
-}
-
-} // namespace
-
-
-void aluInitMixer()
-{
-    if(auto resopt = ConfigValueStr(nullptr, nullptr, "resampler"))
-    {
-        struct ResamplerEntry {
-            const char name[16];
-            const Resampler resampler;
-        };
-        constexpr ResamplerEntry ResamplerList[]{
-            { "none", Resampler::Point },
-            { "point", Resampler::Point },
-            { "linear", Resampler::Linear },
-            { "cubic", Resampler::Cubic },
-            { "bsinc12", Resampler::BSinc12 },
-            { "fast_bsinc12", Resampler::FastBSinc12 },
-            { "bsinc24", Resampler::BSinc24 },
-            { "fast_bsinc24", Resampler::FastBSinc24 },
-        };
-
-        const char *str{resopt->c_str()};
-        if(al::strcasecmp(str, "bsinc") == 0)
-        {
-            WARN("Resampler option \"%s\" is deprecated, using bsinc12\n", str);
-            str = "bsinc12";
-        }
-        else if(al::strcasecmp(str, "sinc4") == 0 || al::strcasecmp(str, "sinc8") == 0)
-        {
-            WARN("Resampler option \"%s\" is deprecated, using cubic\n", str);
-            str = "cubic";
-        }
-
-        auto iter = std::find_if(std::begin(ResamplerList), std::end(ResamplerList),
-            [str](const ResamplerEntry &entry) -> bool
-            { return al::strcasecmp(str, entry.name) == 0; });
-        if(iter == std::end(ResamplerList))
-            ERR("Invalid resampler: %s\n", str);
-        else
-            ResamplerDefault = iter->resampler;
-    }
-
-    MixSamples = SelectMixer();
-    MixHrtfBlendSamples = SelectHrtfBlendMixer();
-    MixHrtfSamples = SelectHrtfMixer();
-}
-
-
-namespace {
-
-void SendSourceStoppedEvent(ALCcontext *context, uint id)
-{
-    RingBuffer *ring{context->mAsyncEvents.get()};
-    auto evt_vec = ring->getWriteVector();
-    if(evt_vec.first.len < 1) return;
-
-    AsyncEvent *evt{::new(evt_vec.first.buf) AsyncEvent{EventType_SourceStateChange}};
-    evt->u.srcstate.id = id;
-    evt->u.srcstate.state = VChangeState::Stop;
-
-    ring->writeAdvance(1);
-}
-
-
-const float *DoFilters(BiquadFilter &lpfilter, BiquadFilter &hpfilter, float *dst,
-    const al::span<const float> src, int type)
-{
-    switch(type)
-    {
-    case AF_None:
-        lpfilter.clear();
-        hpfilter.clear();
-        break;
-
-    case AF_LowPass:
-        lpfilter.process(src, dst);
-        hpfilter.clear();
-        return dst;
-    case AF_HighPass:
-        lpfilter.clear();
-        hpfilter.process(src, dst);
-        return dst;
-
-    case AF_BandPass:
-        DualBiquad{lpfilter, hpfilter}.process(src, dst);
-        return dst;
-    }
-    return src.data();
-}
-
-
-void LoadSamples(float *RESTRICT dst, const al::byte *src, const size_t srcstep, FmtType srctype,
-    const size_t samples) noexcept
-{
-#define HANDLE_FMT(T)  case T: al::LoadSampleArray<T>(dst, src, srcstep, samples); break
-    switch(srctype)
-    {
-    HANDLE_FMT(FmtUByte);
-    HANDLE_FMT(FmtShort);
-    HANDLE_FMT(FmtFloat);
-    HANDLE_FMT(FmtDouble);
-    HANDLE_FMT(FmtMulaw);
-    HANDLE_FMT(FmtAlaw);
-    }
-#undef HANDLE_FMT
-}
-
-float *LoadBufferStatic(VoiceBufferItem *buffer, VoiceBufferItem *&bufferLoopItem,
-    const size_t numChannels, const FmtType sampleType, const size_t sampleSize, const size_t chan,
-    size_t dataPosInt, al::span<float> srcBuffer)
-{
-    const uint LoopStart{buffer->mLoopStart};
-    const uint LoopEnd{buffer->mLoopEnd};
-    ASSUME(LoopEnd > LoopStart);
-
-    /* If current pos is beyond the loop range, do not loop */
-    if(!bufferLoopItem || dataPosInt >= LoopEnd)
-    {
-        bufferLoopItem = nullptr;
-
-        /* Load what's left to play from the buffer */
-        const size_t DataRem{minz(srcBuffer.size(), buffer->mSampleLen-dataPosInt)};
-
-        const al::byte *Data{buffer->mSamples + (dataPosInt*numChannels + chan)*sampleSize};
-        LoadSamples(srcBuffer.data(), Data, numChannels, sampleType, DataRem);
-        srcBuffer = srcBuffer.subspan(DataRem);
-    }
-    else
-    {
-        /* Load what's left of this loop iteration */
-        const size_t DataRem{minz(srcBuffer.size(), LoopEnd-dataPosInt)};
-
-        const al::byte *Data{buffer->mSamples + (dataPosInt*numChannels + chan)*sampleSize};
-        LoadSamples(srcBuffer.data(), Data, numChannels, sampleType, DataRem);
-        srcBuffer = srcBuffer.subspan(DataRem);
-
-        /* Load any repeats of the loop we can to fill the buffer. */
-        const auto LoopSize = static_cast<size_t>(LoopEnd - LoopStart);
-        while(!srcBuffer.empty())
-        {
-            const size_t DataSize{minz(srcBuffer.size(), LoopSize)};
-
-            Data = buffer->mSamples + (LoopStart*numChannels + chan)*sampleSize;
-
-            LoadSamples(srcBuffer.data(), Data, numChannels, sampleType, DataSize);
-            srcBuffer = srcBuffer.subspan(DataSize);
-        }
-    }
-    return srcBuffer.begin();
-}
-
-float *LoadBufferCallback(VoiceBufferItem *buffer, const size_t numChannels,
-    const FmtType sampleType, const size_t sampleSize, const size_t chan,
-    size_t numCallbackSamples, al::span<float> srcBuffer)
-{
-    /* Load what's left to play from the buffer */
-    const size_t DataRem{minz(srcBuffer.size(), numCallbackSamples)};
-
-    const al::byte *Data{buffer->mSamples + chan*sampleSize};
-    LoadSamples(srcBuffer.data(), Data, numChannels, sampleType, DataRem);
-    srcBuffer = srcBuffer.subspan(DataRem);
-
-    return srcBuffer.begin();
-}
-
-float *LoadBufferQueue(VoiceBufferItem *buffer, VoiceBufferItem *bufferLoopItem,
-    const size_t numChannels, const FmtType sampleType, const size_t sampleSize, const size_t chan,
-    size_t dataPosInt, al::span<float> srcBuffer)
-{
-    /* Crawl the buffer queue to fill in the temp buffer */
-    while(buffer && !srcBuffer.empty())
-    {
-        if(dataPosInt >= buffer->mSampleLen)
-        {
-            dataPosInt -= buffer->mSampleLen;
-            buffer = buffer->mNext.load(std::memory_order_acquire);
-            if(!buffer) buffer = bufferLoopItem;
-            continue;
-        }
-
-        const size_t DataSize{minz(srcBuffer.size(), buffer->mSampleLen-dataPosInt)};
-
-        const al::byte *Data{buffer->mSamples + (dataPosInt*numChannels + chan)*sampleSize};
-        LoadSamples(srcBuffer.data(), Data, numChannels, sampleType, DataSize);
-        srcBuffer = srcBuffer.subspan(DataSize);
-        if(srcBuffer.empty()) break;
-
-        dataPosInt = 0;
-        buffer = buffer->mNext.load(std::memory_order_acquire);
-        if(!buffer) buffer = bufferLoopItem;
-    }
-
-    return srcBuffer.begin();
-}
-
-
-void DoHrtfMix(const float *samples, const uint DstBufferSize, DirectParams &parms,
-    const float TargetGain, const uint Counter, uint OutPos, const uint IrSize,
-    ALCdevice *Device)
-{
-    auto &HrtfSamples = Device->HrtfSourceData;
-    /* Source HRTF mixing needs to include the direct delay so it remains
-     * aligned with the direct mix's HRTF filtering.
-     */
-    float2 *AccumSamples{Device->HrtfAccumData + HrtfDirectDelay};
-
-    /* Copy the HRTF history and new input samples into a temp buffer. */
-    auto src_iter = std::copy(parms.Hrtf.History.begin(), parms.Hrtf.History.end(),
-        std::begin(HrtfSamples));
-    std::copy_n(samples, DstBufferSize, src_iter);
-    /* Copy the last used samples back into the history buffer for later. */
-    std::copy_n(std::begin(HrtfSamples) + DstBufferSize, parms.Hrtf.History.size(),
-        parms.Hrtf.History.begin());
-
-    /* If fading and this is the first mixing pass, fade between the IRs. */
-    uint fademix{0u};
-    if(Counter && OutPos == 0)
-    {
-        fademix = minu(DstBufferSize, Counter);
-
-        float gain{TargetGain};
-
-        /* The new coefficients need to fade in completely since they're
-         * replacing the old ones. To keep the gain fading consistent,
-         * interpolate between the old and new target gains given how much of
-         * the fade time this mix handles.
-         */
-        if(Counter > fademix)
-        {
-            const float a{static_cast<float>(fademix) / static_cast<float>(Counter)};
-            gain = lerp(parms.Hrtf.Old.Gain, TargetGain, a);
-        }
-        MixHrtfFilter hrtfparams;
-        hrtfparams.Coeffs = &parms.Hrtf.Target.Coeffs;
-        hrtfparams.Delay = parms.Hrtf.Target.Delay;
-        hrtfparams.Gain = 0.0f;
-        hrtfparams.GainStep = gain / static_cast<float>(fademix);
-
-        MixHrtfBlendSamples(HrtfSamples, AccumSamples+OutPos, IrSize, &parms.Hrtf.Old, &hrtfparams,
-            fademix);
-        /* Update the old parameters with the result. */
-        parms.Hrtf.Old = parms.Hrtf.Target;
-        parms.Hrtf.Old.Gain = gain;
-        OutPos += fademix;
-    }
-
-    if(fademix < DstBufferSize)
-    {
-        const uint todo{DstBufferSize - fademix};
-        float gain{TargetGain};
-
-        /* Interpolate the target gain if the gain fading lasts longer than
-         * this mix.
-         */
-        if(Counter > DstBufferSize)
-        {
-            const float a{static_cast<float>(todo) / static_cast<float>(Counter-fademix)};
-            gain = lerp(parms.Hrtf.Old.Gain, TargetGain, a);
-        }
-
-        MixHrtfFilter hrtfparams;
-        hrtfparams.Coeffs = &parms.Hrtf.Target.Coeffs;
-        hrtfparams.Delay = parms.Hrtf.Target.Delay;
-        hrtfparams.Gain = parms.Hrtf.Old.Gain;
-        hrtfparams.GainStep = (gain - parms.Hrtf.Old.Gain) / static_cast<float>(todo);
-        MixHrtfSamples(HrtfSamples+fademix, AccumSamples+OutPos, IrSize, &hrtfparams, todo);
-        /* Store the now-current gain for next time. */
-        parms.Hrtf.Old.Gain = gain;
-    }
-}
-
-void DoNfcMix(const al::span<const float> samples, FloatBufferLine *OutBuffer, DirectParams &parms,
-    const float *TargetGains, const uint Counter, const uint OutPos, ALCdevice *Device)
-{
-    using FilterProc = void (NfcFilter::*)(const al::span<const float>, float*);
-    static constexpr FilterProc NfcProcess[MaxAmbiOrder+1]{
-        nullptr, &NfcFilter::process1, &NfcFilter::process2, &NfcFilter::process3};
-
-    float *CurrentGains{parms.Gains.Current.data()};
-    MixSamples(samples, {OutBuffer, 1u}, CurrentGains, TargetGains, Counter, OutPos);
-    ++OutBuffer;
-    ++CurrentGains;
-    ++TargetGains;
-
-    const al::span<float> nfcsamples{Device->NfcSampleData, samples.size()};
-    size_t order{1};
-    while(const size_t chancount{Device->NumChannelsPerOrder[order]})
-    {
-        (parms.NFCtrlFilter.*NfcProcess[order])(samples, nfcsamples.data());
-        MixSamples(nfcsamples, {OutBuffer, chancount}, CurrentGains, TargetGains, Counter, OutPos);
-        OutBuffer += chancount;
-        CurrentGains += chancount;
-        TargetGains += chancount;
-        if(++order == MaxAmbiOrder+1)
-            break;
-    }
-}
-
-} // namespace
-
-void Voice::mix(const State vstate, ALCcontext *Context, const uint SamplesToDo)
-{
-    static constexpr std::array<float,MAX_OUTPUT_CHANNELS> SilentTarget{};
-
-    ASSUME(SamplesToDo > 0);
-
-    /* Get voice info */
-    uint DataPosInt{mPosition.load(std::memory_order_relaxed)};
-    uint DataPosFrac{mPositionFrac.load(std::memory_order_relaxed)};
-    VoiceBufferItem *BufferListItem{mCurrentBuffer.load(std::memory_order_relaxed)};
-    VoiceBufferItem *BufferLoopItem{mLoopBuffer.load(std::memory_order_relaxed)};
-    const FmtType SampleType{mFmtType};
-    const uint SampleSize{mSampleSize};
-    const uint increment{mStep};
-    if UNLIKELY(increment < 1)
-    {
-        /* If the voice is supposed to be stopping but can't be mixed, just
-         * stop it before bailing.
-         */
-        if(vstate == Stopping)
-            mPlayState.store(Stopped, std::memory_order_release);
-        return;
-    }
-
-    ASSUME(SampleSize > 0);
-
-    const size_t FrameSize{mChans.size() * SampleSize};
-    ASSUME(FrameSize > 0);
-
-    ALCdevice *Device{Context->mDevice.get()};
-    const uint NumSends{Device->NumAuxSends};
-    const uint IrSize{Device->mIrSize};
-
-    ResamplerFunc Resample{(increment == MixerFracOne && DataPosFrac == 0) ?
-                           Resample_<CopyTag,CTag> : mResampler};
-
-    uint Counter{(mFlags&VoiceIsFading) ? SamplesToDo : 0};
-    if(!Counter)
-    {
-        /* No fading, just overwrite the old/current params. */
-        for(auto &chandata : mChans)
-        {
-            {
-                DirectParams &parms = chandata.mDryParams;
-                if(!(mFlags&VoiceHasHrtf))
-                    parms.Gains.Current = parms.Gains.Target;
-                else
-                    parms.Hrtf.Old = parms.Hrtf.Target;
-            }
-            for(uint send{0};send < NumSends;++send)
-            {
-                if(mSend[send].Buffer.empty())
-                    continue;
-
-                SendParams &parms = chandata.mWetParams[send];
-                parms.Gains.Current = parms.Gains.Target;
-            }
-        }
-    }
-
-    float fadeCoeff{1.0f}, fadeGain{1.0f};
-    if UNLIKELY(vstate == Stopping)
-    {
-        /* Calculate the multiplier for fading the resampled signal by -60dB
-         * over 1ms.
-         */
-        fadeCoeff = std::pow(0.001f, 1000.0f/static_cast<float>(Device->Frequency));
-    }
-
-    uint buffers_done{0u};
-    uint OutPos{0u};
-    do {
-        /* Figure out how many buffer samples will be needed */
-        uint DstBufferSize{SamplesToDo - OutPos};
-        uint SrcBufferSize;
-
-        if(increment <= MixerFracOne)
-        {
-            /* Calculate the last written dst sample pos. */
-            uint64_t DataSize64{DstBufferSize - 1};
-            /* Calculate the last read src sample pos. */
-            DataSize64 = (DataSize64*increment + DataPosFrac) >> MixerFracBits;
-            /* +1 to get the src sample count, include padding. */
-            DataSize64 += 1 + MaxResamplerPadding;
-
-            /* Result is guaranteed to be <= BufferLineSize+MaxResamplerPadding
-             * since we won't use more src samples than dst samples+padding.
-             */
-            SrcBufferSize = static_cast<uint>(DataSize64);
-        }
-        else
-        {
-            uint64_t DataSize64{DstBufferSize};
-            /* Calculate the end src sample pos, include padding. */
-            DataSize64 = (DataSize64*increment + DataPosFrac) >> MixerFracBits;
-            DataSize64 += MaxResamplerPadding;
-
-            if(DataSize64 <= BufferLineSize + MaxResamplerPadding)
-                SrcBufferSize = static_cast<uint>(DataSize64);
-            else
-            {
-                /* If the source size got saturated, we can't fill the desired
-                 * dst size. Figure out how many samples we can actually mix.
-                 */
-                SrcBufferSize = BufferLineSize + MaxResamplerPadding;
-
-                DataSize64 = SrcBufferSize - MaxResamplerPadding;
-                DataSize64 = ((DataSize64<<MixerFracBits) - DataPosFrac) / increment;
-                if(DataSize64 < DstBufferSize)
-                {
-                    /* Some mixers require being 16-byte aligned, so also limit
-                     * to a multiple of 4 samples to maintain alignment.
-                     */
-                    DstBufferSize = static_cast<uint>(DataSize64) & ~3u;
-                }
-            }
-        }
-
-        if((mFlags&(VoiceIsCallback|VoiceCallbackStopped)) == VoiceIsCallback && BufferListItem)
-        {
-            /* Exclude resampler pre-padding from the needed size. */
-            const uint toLoad{SrcBufferSize - (MaxResamplerPadding>>1)};
-            if(toLoad > mNumCallbackSamples)
-            {
-                const size_t byteOffset{mNumCallbackSamples*FrameSize};
-                const size_t needBytes{toLoad*FrameSize - byteOffset};
-
-                const int gotBytes{BufferListItem->mCallback(BufferListItem->mUserData,
-                    &BufferListItem->mSamples[byteOffset], static_cast<int>(needBytes))};
-                if(gotBytes < 1)
-                    mFlags |= VoiceCallbackStopped;
-                else if(static_cast<uint>(gotBytes) < needBytes)
-                {
-                    mFlags |= VoiceCallbackStopped;
-                    mNumCallbackSamples += static_cast<uint>(static_cast<uint>(gotBytes) /
-                        FrameSize);
-                }
-                else
-                    mNumCallbackSamples = toLoad;
-            }
-        }
-
-        const float fadeVal{fadeGain};
-        const size_t num_chans{mChans.size()};
-        size_t chan_idx{0};
-        ASSUME(DstBufferSize > 0);
-        for(auto &chandata : mChans)
-        {
-            const al::span<float> SrcData{Device->SourceData, SrcBufferSize};
-
-            /* Load the previous samples into the source data first, then load
-             * what we can from the buffer queue.
-             */
-            auto srciter = std::copy_n(chandata.mPrevSamples.begin(), MaxResamplerPadding>>1,
-                SrcData.begin());
-
-            if UNLIKELY(!BufferListItem)
-                srciter = std::copy(chandata.mPrevSamples.begin()+(MaxResamplerPadding>>1),
-                    chandata.mPrevSamples.end(), srciter);
-            else if((mFlags&VoiceIsStatic))
-                srciter = LoadBufferStatic(BufferListItem, BufferLoopItem, num_chans, SampleType,
-                    SampleSize, chan_idx, DataPosInt, {srciter, SrcData.end()});
-            else if((mFlags&VoiceIsCallback))
-                srciter = LoadBufferCallback(BufferListItem, num_chans, SampleType, SampleSize,
-                    chan_idx, mNumCallbackSamples, {srciter, SrcData.end()});
-            else
-                srciter = LoadBufferQueue(BufferListItem, BufferLoopItem, num_chans, SampleType,
-                    SampleSize, chan_idx, DataPosInt, {srciter, SrcData.end()});
-
-            if UNLIKELY(srciter != SrcData.end())
-            {
-                /* If the source buffer wasn't filled, copy the last sample for
-                 * the remaining buffer. Ideally it should have ended with
-                 * silence, but if not the gain fading should help avoid clicks
-                 * from sudden amplitude changes.
-                 */
-                const float sample{*(srciter-1)};
-                std::fill(srciter, SrcData.end(), sample);
-            }
-
-            /* Store the last source samples used for next time. */
-            std::copy_n(&SrcData[(increment*DstBufferSize + DataPosFrac)>>MixerFracBits],
-                chandata.mPrevSamples.size(), chandata.mPrevSamples.begin());
-
-            /* Resample, then apply ambisonic upsampling as needed. */
-            float *ResampledData{Resample(&mResampleState, &SrcData[MaxResamplerPadding>>1],
-                DataPosFrac, increment, {Device->ResampledData, DstBufferSize})};
-            if((mFlags&VoiceIsAmbisonic))
-                chandata.mAmbiSplitter.processHfScale({ResampledData, DstBufferSize},
-                    chandata.mAmbiScale);
-
-            if UNLIKELY(vstate == Stopping)
-            {
-                fadeGain = fadeVal;
-                for(float &sample : al::span<float>{ResampledData, DstBufferSize})
-                {
-                    fadeGain *= fadeCoeff;
-                    sample *= fadeGain;
-                }
-            }
-
-            /* Now filter and mix to the appropriate outputs. */
-            float (&FilterBuf)[BufferLineSize] = Device->FilteredData;
-            {
-                DirectParams &parms = chandata.mDryParams;
-                const float *samples{DoFilters(parms.LowPass, parms.HighPass, FilterBuf,
-                    {ResampledData, DstBufferSize}, mDirect.FilterType)};
-
-                if((mFlags&VoiceHasHrtf))
-                {
-                    const float TargetGain{UNLIKELY(vstate == Stopping) ? 0.0f :
-                        parms.Hrtf.Target.Gain};
-                    DoHrtfMix(samples, DstBufferSize, parms, TargetGain, Counter, OutPos, IrSize,
-                        Device);
-                }
-                else if((mFlags&VoiceHasNfc))
-                {
-                    const float *TargetGains{UNLIKELY(vstate == Stopping) ? SilentTarget.data()
-                        : parms.Gains.Target.data()};
-                    DoNfcMix({samples, DstBufferSize}, mDirect.Buffer.data(), parms, TargetGains,
-                        Counter, OutPos, Device);
-                }
-                else
-                {
-                    const float *TargetGains{UNLIKELY(vstate == Stopping) ? SilentTarget.data()
-                        : parms.Gains.Target.data()};
-                    MixSamples({samples, DstBufferSize}, mDirect.Buffer,
-                        parms.Gains.Current.data(), TargetGains, Counter, OutPos);
-                }
-            }
-
-            for(uint send{0};send < NumSends;++send)
-            {
-                if(mSend[send].Buffer.empty())
-                    continue;
-
-                SendParams &parms = chandata.mWetParams[send];
-                const float *samples{DoFilters(parms.LowPass, parms.HighPass, FilterBuf,
-                    {ResampledData, DstBufferSize}, mSend[send].FilterType)};
-
-                const float *TargetGains{UNLIKELY(vstate == Stopping) ? SilentTarget.data()
-                    : parms.Gains.Target.data()};
-                MixSamples({samples, DstBufferSize}, mSend[send].Buffer,
-                    parms.Gains.Current.data(), TargetGains, Counter, OutPos);
-            }
-
-            ++chan_idx;
-        }
-        /* Update positions */
-        DataPosFrac += increment*DstBufferSize;
-        const uint SrcSamplesDone{DataPosFrac>>MixerFracBits};
-        DataPosInt  += SrcSamplesDone;
-        DataPosFrac &= MixerFracMask;
-
-        OutPos += DstBufferSize;
-        Counter = maxu(DstBufferSize, Counter) - DstBufferSize;
-
-        if UNLIKELY(!BufferListItem)
-        {
-            /* Do nothing extra when there's no buffers. */
-        }
-        else if((mFlags&VoiceIsStatic))
-        {
-            if(BufferLoopItem)
-            {
-                /* Handle looping static source */
-                const uint LoopStart{BufferListItem->mLoopStart};
-                const uint LoopEnd{BufferListItem->mLoopEnd};
-                if(DataPosInt >= LoopEnd)
-                {
-                    assert(LoopEnd > LoopStart);
-                    DataPosInt = ((DataPosInt-LoopStart)%(LoopEnd-LoopStart)) + LoopStart;
-                }
-            }
-            else
-            {
-                /* Handle non-looping static source */
-                if(DataPosInt >= BufferListItem->mSampleLen)
-                {
-                    BufferListItem = nullptr;
-                    break;
-                }
-            }
-        }
-        else if((mFlags&VoiceIsCallback))
-        {
-            if(SrcSamplesDone < mNumCallbackSamples)
-            {
-                const size_t byteOffset{SrcSamplesDone*FrameSize};
-                const size_t byteEnd{mNumCallbackSamples*FrameSize};
-                al::byte *data{BufferListItem->mSamples};
-                std::copy(data+byteOffset, data+byteEnd, data);
-                mNumCallbackSamples -= SrcSamplesDone;
-            }
-            else
-            {
-                BufferListItem = nullptr;
-                mNumCallbackSamples = 0;
-            }
-        }
-        else
-        {
-            /* Handle streaming source */
-            do {
-                if(BufferListItem->mSampleLen > DataPosInt)
-                    break;
-
-                DataPosInt -= BufferListItem->mSampleLen;
-
-                ++buffers_done;
-                BufferListItem = BufferListItem->mNext.load(std::memory_order_relaxed);
-                if(!BufferListItem) BufferListItem = BufferLoopItem;
-            } while(BufferListItem);
-        }
-    } while(OutPos < SamplesToDo);
-
-    mFlags |= VoiceIsFading;
-
-    /* Don't update positions and buffers if we were stopping. */
-    if UNLIKELY(vstate == Stopping)
-    {
-        mPlayState.store(Stopped, std::memory_order_release);
-        return;
-    }
-
-    /* Capture the source ID in case it's reset for stopping. */
-    const uint SourceID{mSourceID.load(std::memory_order_relaxed)};
-
-    /* Update voice info */
-    mPosition.store(DataPosInt, std::memory_order_relaxed);
-    mPositionFrac.store(DataPosFrac, std::memory_order_relaxed);
-    mCurrentBuffer.store(BufferListItem, std::memory_order_relaxed);
-    if(!BufferListItem)
-    {
-        mLoopBuffer.store(nullptr, std::memory_order_relaxed);
-        mSourceID.store(0u, std::memory_order_relaxed);
-    }
-    std::atomic_thread_fence(std::memory_order_release);
-
-    /* Send any events now, after the position/buffer info was updated. */
-    const uint enabledevt{Context->mEnabledEvts.load(std::memory_order_acquire)};
-    if(buffers_done > 0 && (enabledevt&EventType_BufferCompleted))
-    {
-        RingBuffer *ring{Context->mAsyncEvents.get()};
-        auto evt_vec = ring->getWriteVector();
-        if(evt_vec.first.len > 0)
-        {
-            AsyncEvent *evt{::new(evt_vec.first.buf) AsyncEvent{EventType_BufferCompleted}};
-            evt->u.bufcomp.id = SourceID;
-            evt->u.bufcomp.count = buffers_done;
-            ring->writeAdvance(1);
-        }
-    }
-
-    if(!BufferListItem)
-    {
-        /* If the voice just ended, set it to Stopping so the next render
-         * ensures any residual noise fades to 0 amplitude.
-         */
-        mPlayState.store(Stopping, std::memory_order_release);
-        if((enabledevt&EventType_SourceStateChange))
-            SendSourceStoppedEvent(Context, SourceID);
-    }
-}

+ 1 - 0
Engine/lib/openal-soft/BSD-3Clause

@@ -1,6 +1,7 @@
 Portions of this software are licensed under the BSD 3-Clause license.
 
 Copyright (c) 2015, Archontis Politis
+Copyright (c) 2019, Anis A. Hireche
 Copyright (c) 2019, Christopher Robinson
 All rights reserved.
 

+ 240 - 130
Engine/lib/openal-soft/CMakeLists.txt

@@ -2,21 +2,24 @@
 
 cmake_minimum_required(VERSION 3.0.2)
 
-# The workaround for try_compile failing with code signing
-# since cmake-3.18.2, not required
-set(CMAKE_TRY_COMPILE_PLATFORM_VARIABLES
-    "CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED"
-    "CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED")
-set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO)
-set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO)
-
-# Fix compile failure with armv7 deployment target >= 11.0, xcode clang will
-# report:
-# error: invalid iOS deployment version '--target=armv7-apple-ios13.6',
-# iOS 10 is the maximum deployment target for 32-bit targets
-# If CMAKE_OSX_DEPLOYMENT_TARGET is not defined, cmake will choose latest
-# deployment target
+if(APPLE)
+    # The workaround for try_compile failing with code signing
+    # since cmake-3.18.2, not required
+    set(CMAKE_TRY_COMPILE_PLATFORM_VARIABLES
+        ${CMAKE_TRY_COMPILE_PLATFORM_VARIABLES}
+        "CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED"
+        "CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED")
+    set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED NO)
+    set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO)
+endif()
+
 if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
+    # Fix compile failure with armv7 deployment target >= 11.0, xcode clang
+    # will report:
+    # error: invalid iOS deployment version '--target=armv7-apple-ios13.6',
+    # iOS 10 is the maximum deployment target for 32-bit targets
+    # If CMAKE_OSX_DEPLOYMENT_TARGET is not defined, cmake will choose latest
+    # deployment target
     if("${CMAKE_OSX_ARCHITECTURES}" MATCHES ".*armv7.*")
         if(NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET
             OR NOT CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS "11.0")
@@ -69,8 +72,11 @@ include(CheckCXXCompilerFlag)
 include(CheckCSourceCompiles)
 include(CheckCXXSourceCompiles)
 include(CheckStructHasMember)
+include(CMakePackageConfigHelpers)
 include(GNUInstallDirs)
 
+find_package(PkgConfig)
+
 
 option(ALSOFT_DLOPEN  "Check for the dlopen API for loading optional libs"  ON)
 
@@ -89,6 +95,8 @@ option(ALSOFT_INSTALL_EXAMPLES "Install example programs (alplay, alstream, ...)
 option(ALSOFT_INSTALL_UTILS "Install utility programs (openal-info, alsoft-config, ...)" ON)
 option(ALSOFT_UPDATE_BUILD_VERSION "Update git build version info" ON)
 
+option(ALSOFT_EAX "Enable legacy EAX extensions" ${WIN32})
+
 if(DEFINED SHARE_INSTALL_DIR)
     message(WARNING "SHARE_INSTALL_DIR is deprecated.  Use the variables provided by the GNUInstallDirs module instead")
     set(CMAKE_INSTALL_DATADIR "${SHARE_INSTALL_DIR}")
@@ -115,7 +123,7 @@ set(LINKER_FLAGS )
 set(EXTRA_LIBS )
 
 if(WIN32)
-    set(CPP_DEFS ${CPP_DEFS} _WIN32)
+    set(CPP_DEFS ${CPP_DEFS} _WIN32 NOMINMAX)
     if(MINGW)
         set(CPP_DEFS ${CPP_DEFS} __USE_MINGW_ANSI_STDIO)
     endif()
@@ -142,7 +150,7 @@ if(NOT LIBTYPE)
 endif()
 
 set(LIB_MAJOR_VERSION "1")
-set(LIB_MINOR_VERSION "21")
+set(LIB_MINOR_VERSION "22")
 set(LIB_REVISION "0")
 set(LIB_VERSION "${LIB_MAJOR_VERSION}.${LIB_MINOR_VERSION}.${LIB_REVISION}")
 set(LIB_VERSION_NUM ${LIB_MAJOR_VERSION},${LIB_MINOR_VERSION},${LIB_REVISION},0)
@@ -196,20 +204,22 @@ else()
 endif()
 unset(OLD_REQUIRED_LIBRARIES)
 
-# Include liblog for Android logging
-check_library_exists(log __android_log_print "" HAVE_LIBLOG)
-if(HAVE_LIBLOG)
-    set(EXTRA_LIBS log ${EXTRA_LIBS})
-    set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} log)
+if(ANDROID)
+    # Include liblog for Android logging
+    check_library_exists(log __android_log_print "" HAVE_LIBLOG)
+    if(HAVE_LIBLOG)
+        set(EXTRA_LIBS log ${EXTRA_LIBS})
+        set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} log)
+    endif()
 endif()
 
 if(MSVC)
-    set(CPP_DEFS ${CPP_DEFS} _CRT_SECURE_NO_WARNINGS NOMINMAX)
+    set(CPP_DEFS ${CPP_DEFS} _CRT_SECURE_NO_WARNINGS)
     check_cxx_compiler_flag(/permissive- HAVE_PERMISSIVE_SWITCH)
     if(HAVE_PERMISSIVE_SWITCH)
         set(C_FLAGS ${C_FLAGS} $<$<COMPILE_LANGUAGE:CXX>:/permissive->)
     endif()
-    set(C_FLAGS ${C_FLAGS} /W4 /w14640 /wd4065 /wd4268 /wd4324 /wd5030)
+    set(C_FLAGS ${C_FLAGS} /W4 /w14640 /wd4065 /wd4127 /wd4268 /wd4324 /wd5030)
     # Remove /W3, which is added by default, since we set /W4. Some build
     # generators with MSVC complain about both /W3 and /W4 being specified.
     foreach(flag_var  CMAKE_C_FLAGS CMAKE_CXX_FLAGS)
@@ -345,17 +355,17 @@ endif()
 
 set(SSE2_SWITCH "")
 
-set(OLD_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
 if(NOT MSVC)
+    set(OLD_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
     # Yes GCC, really don't accept command line options you don't support
     set(CMAKE_REQUIRED_FLAGS "${OLD_REQUIRED_FLAGS} -Werror")
+    check_c_compiler_flag(-msse2 HAVE_MSSE2_SWITCH)
+    if(HAVE_MSSE2_SWITCH)
+        set(SSE2_SWITCH "-msse2")
+    endif()
+    set(CMAKE_REQUIRED_FLAGS ${OLD_REQUIRED_FLAGS})
+    unset(OLD_REQUIRED_FLAGS)
 endif()
-check_c_compiler_flag(-msse2 HAVE_MSSE2_SWITCH)
-if(HAVE_MSSE2_SWITCH)
-    set(SSE2_SWITCH "-msse2")
-endif()
-set(CMAKE_REQUIRED_FLAGS ${OLD_REQUIRED_FLAGS})
-unset(OLD_REQUIRED_FLAGS)
 
 check_include_file(xmmintrin.h HAVE_XMMINTRIN_H)
 check_include_file(emmintrin.h HAVE_EMMINTRIN_H)
@@ -369,20 +379,25 @@ set(HAVE_SSE3       0)
 set(HAVE_SSE4_1     0)
 set(HAVE_NEON       0)
 
-# Check for SSE+SSE2 support
+# Check for SSE support
 option(ALSOFT_REQUIRE_SSE "Require SSE support" OFF)
-option(ALSOFT_REQUIRE_SSE2 "Require SSE2 support" OFF)
-if(HAVE_XMMINTRIN_H AND HAVE_EMMINTRIN_H)
+if(HAVE_XMMINTRIN_H)
     option(ALSOFT_CPUEXT_SSE "Enable SSE support" ON)
-    option(ALSOFT_CPUEXT_SSE2 "Enable SSE2 support" ON)
-    if(ALSOFT_CPUEXT_SSE AND ALSOFT_CPUEXT_SSE2)
+    if(ALSOFT_CPUEXT_SSE)
         set(HAVE_SSE 1)
-        set(HAVE_SSE2 1)
     endif()
 endif()
 if(ALSOFT_REQUIRE_SSE AND NOT HAVE_SSE)
     message(FATAL_ERROR "Failed to enabled required SSE CPU extensions")
 endif()
+
+option(ALSOFT_REQUIRE_SSE2 "Require SSE2 support" OFF)
+if(HAVE_EMMINTRIN_H)
+    option(ALSOFT_CPUEXT_SSE2 "Enable SSE2 support" ON)
+    if(HAVE_SSE AND ALSOFT_CPUEXT_SSE2)
+        set(HAVE_SSE2 1)
+    endif()
+endif()
 if(ALSOFT_REQUIRE_SSE2 AND NOT HAVE_SSE2)
     message(FATAL_ERROR "Failed to enable required SSE2 CPU extensions")
 endif()
@@ -435,7 +450,14 @@ set(FPMATH_SET "0")
 if(CMAKE_SIZEOF_VOID_P MATCHES "4" AND HAVE_SSE2)
     option(ALSOFT_ENABLE_SSE2_CODEGEN "Enable SSE2 code generation instead of x87 for 32-bit targets." TRUE)
     if(ALSOFT_ENABLE_SSE2_CODEGEN)
-        if(SSE2_SWITCH OR NOT MSVC)
+        if(MSVC)
+            check_c_compiler_flag("/arch:SSE2" HAVE_ARCH_SSE2)
+            if(HAVE_ARCH_SSE2)
+                set(SSE_FLAGS ${SSE_FLAGS} "/arch:SSE2")
+                set(C_FLAGS ${C_FLAGS} ${SSE_FLAGS})
+                set(FPMATH_SET 2)
+            endif()
+        elseif(SSE2_SWITCH)
             check_c_compiler_flag("${SSE2_SWITCH} -mfpmath=sse" HAVE_MFPMATH_SSE_2)
             if(HAVE_MFPMATH_SSE_2)
                 set(SSE_FLAGS ${SSE_FLAGS} ${SSE2_SWITCH} -mfpmath=sse)
@@ -447,13 +469,6 @@ if(CMAKE_SIZEOF_VOID_P MATCHES "4" AND HAVE_SSE2)
             # OSs don't guarantee this on 32-bit, so externally-callable
             # functions need to ensure an aligned stack.
             set(EXPORT_DECL "${EXPORT_DECL} __attribute__((force_align_arg_pointer))")
-        elseif(MSVC)
-            check_c_compiler_flag("/arch:SSE2" HAVE_ARCH_SSE2)
-            if(HAVE_ARCH_SSE2)
-                set(SSE_FLAGS ${SSE_FLAGS} "/arch:SSE2")
-                set(C_FLAGS ${C_FLAGS} ${SSE_FLAGS})
-                set(FPMATH_SET 2)
-            endif()
         endif()
     endif()
 endif()
@@ -551,51 +566,11 @@ if(NOT WIN32)
         check_symbol_exists(pthread_setname_np "pthread.h;pthread_np.h" HAVE_PTHREAD_SETNAME_NP)
         if(NOT HAVE_PTHREAD_SETNAME_NP)
             check_symbol_exists(pthread_set_name_np "pthread.h;pthread_np.h" HAVE_PTHREAD_SET_NAME_NP)
-        else()
-            check_c_source_compiles("
-#include <pthread.h>
-#include <pthread_np.h>
-int main()
-{
-    pthread_setname_np(\"testname\");
-    return 0;
-}"
-                PTHREAD_SETNAME_NP_ONE_PARAM
-            )
-            check_c_source_compiles("
-#include <pthread.h>
-#include <pthread_np.h>
-int main()
-{
-    pthread_setname_np(pthread_self(), \"%s\", \"testname\");
-    return 0;
-}"
-                PTHREAD_SETNAME_NP_THREE_PARAMS
-            )
         endif()
     else()
         check_symbol_exists(pthread_setname_np pthread.h HAVE_PTHREAD_SETNAME_NP)
         if(NOT HAVE_PTHREAD_SETNAME_NP)
             check_symbol_exists(pthread_set_name_np pthread.h HAVE_PTHREAD_SET_NAME_NP)
-        else()
-            check_c_source_compiles("
-#include <pthread.h>
-int main()
-{
-    pthread_setname_np(\"testname\");
-    return 0;
-}"
-                PTHREAD_SETNAME_NP_ONE_PARAM
-            )
-            check_c_source_compiles("
-#include <pthread.h>
-int main()
-{
-    pthread_setname_np(pthread_self(), \"%s\", \"testname\");
-    return 0;
-}"
-                PTHREAD_SETNAME_NP_THREE_PARAMS
-            )
         endif()
     endif()
 endif()
@@ -615,17 +590,19 @@ set(COMMON_OBJS
     common/alfstream.h
     common/almalloc.cpp
     common/almalloc.h
+    common/alnumbers.h
     common/alnumeric.h
     common/aloptional.h
     common/alspan.h
     common/alstring.cpp
     common/alstring.h
     common/atomic.h
+    common/comptr.h
     common/dynload.cpp
     common/dynload.h
     common/intrusive_ptr.h
-    common/math_defs.h
     common/opthelpers.h
+    common/phase_shifter.h
     common/polyphase_resampler.cpp
     common/polyphase_resampler.h
     common/pragmadefs.h
@@ -642,17 +619,32 @@ set(COMMON_OBJS
 set(CORE_OBJS
     core/ambdec.cpp
     core/ambdec.h
+    core/ambidefs.cpp
     core/ambidefs.h
+    core/async_event.h
+    core/bformatdec.cpp
+    core/bformatdec.h
     core/bs2b.cpp
     core/bs2b.h
     core/bsinc_defs.h
     core/bsinc_tables.cpp
     core/bsinc_tables.h
     core/bufferline.h
+    core/buffer_storage.cpp
+    core/buffer_storage.h
+    core/context.cpp
+    core/context.h
+    core/converter.cpp
+    core/converter.h
     core/cpu_caps.cpp
     core/cpu_caps.h
     core/devformat.cpp
     core/devformat.h
+    core/device.cpp
+    core/device.h
+    core/effects/base.h
+    core/effectslot.cpp
+    core/effectslot.h
     core/except.cpp
     core/except.h
     core/filters/biquad.h
@@ -665,12 +657,57 @@ set(CORE_OBJS
     core/fmt_traits.h
     core/fpu_ctrl.cpp
     core/fpu_ctrl.h
+    core/front_stablizer.h
+    core/helpers.cpp
+    core/helpers.h
+    core/hrtf.cpp
+    core/hrtf.h
     core/logging.cpp
     core/logging.h
     core/mastering.cpp
     core/mastering.h
+    core/mixer.cpp
+    core/mixer.h
+    core/resampler_limits.h
     core/uhjfilter.cpp
     core/uhjfilter.h
+    core/uiddefs.cpp
+    core/voice.cpp
+    core/voice.h
+    core/voice_change.h)
+
+set(HAVE_RTKIT 0)
+option(ALSOFT_REQUIRE_RTKIT "Require RTKit/D-Bus support" FALSE)
+find_package(DBus1 QUIET)
+if(DBus1_FOUND)
+    option(ALSOFT_RTKIT "Enable RTKit support" ON)
+    if(ALSOFT_RTKIT)
+        set(HAVE_RTKIT 1)
+        set(CORE_OBJS ${CORE_OBJS} core/dbus_wrap.cpp core/dbus_wrap.h core/rtkit.cpp core/rtkit.h)
+        if(WIN32 OR HAVE_DLFCN_H)
+            set(INC_PATHS ${INC_PATHS} ${DBus1_INCLUDE_DIRS})
+            set(CPP_DEFS ${CPP_DEFS} ${DBus1_DEFINITIONS})
+        else()
+            set(EXTRA_LIBS ${EXTRA_LIBS} ${DBus1_LIBRARIES})
+        endif()
+    endif()
+else()
+    set(MISSING_VARS "")
+    if(NOT DBus1_INCLUDE_DIRS)
+        set(MISSING_VARS "${MISSING_VARS} DBus1_INCLUDE_DIRS")
+    endif()
+    if(NOT DBus1_LIBRARIES)
+        set(MISSING_VARS "${MISSING_VARS} DBus1_LIBRARIES")
+    endif()
+    message(STATUS "Could NOT find DBus1 (missing:${MISSING_VARS})")
+    unset(MISSING_VARS)
+endif()
+if(ALSOFT_REQUIRE_RTKIT AND NOT HAVE_RTKIT)
+    message(FATAL_ERROR "Failed to enabled required RTKit support")
+endif()
+
+# Default mixers, always available
+set(CORE_OBJS ${CORE_OBJS}
     core/mixer/defs.h
     core/mixer/hrtfbase.h
     core/mixer/hrtfdefs.h
@@ -691,6 +728,7 @@ set(OPENAL_OBJS
     al/effects/dedicated.cpp
     al/effects/distortion.cpp
     al/effects/echo.cpp
+	al/effects/effects.cpp
     al/effects/effects.h
     al/effects/equalizer.cpp
     al/effects/fshifter.cpp
@@ -714,22 +752,14 @@ set(OPENAL_OBJS
 # ALC and related routines
 set(ALC_OBJS
     alc/alc.cpp
-    alc/alcmain.h
     alc/alu.cpp
     alc/alu.h
     alc/alconfig.cpp
     alc/alconfig.h
-    alc/alcontext.h
-    alc/async_event.h
-    alc/bformatdec.cpp
-    alc/bformatdec.h
-    alc/buffer_storage.cpp
-    alc/buffer_storage.h
-    alc/compat.h
-    alc/converter.cpp
-    alc/converter.h
-    alc/effectslot.cpp
-    alc/effectslot.h
+    alc/context.cpp
+    alc/context.h
+    alc/device.cpp
+    alc/device.h
     alc/effects/base.h
     alc/effects/autowah.cpp
     alc/effects/chorus.cpp
@@ -745,22 +775,42 @@ set(ALC_OBJS
     alc/effects/pshifter.cpp
     alc/effects/reverb.cpp
     alc/effects/vmorpher.cpp
-    alc/front_stablizer.h
-    alc/helpers.cpp
-    alc/hrtf.cpp
-    alc/hrtf.h
     alc/inprogext.h
-    alc/panning.cpp
-    alc/uiddefs.cpp
-    alc/voice.cpp
-    alc/voice.h
-    alc/voice_change.h)
+    alc/panning.cpp)
+
+if (ALSOFT_EAX)
+    set(OPENAL_OBJS
+        ${OPENAL_OBJS}
+        al/eax_api.cpp
+        al/eax_api.h
+        al/eax_eax_call.cpp
+        al/eax_eax_call.h
+        al/eax_effect.cpp
+        al/eax_effect.h
+        al/eax_exception.cpp
+        al/eax_exception.h
+        al/eax_fx_slot_index.cpp
+        al/eax_fx_slot_index.h
+        al/eax_fx_slots.cpp
+        al/eax_fx_slots.h
+        al/eax_globals.cpp
+        al/eax_globals.h
+        al/eax_utils.cpp
+        al/eax_utils.h
+        al/eax_x_ram.cpp
+        al/eax_x_ram.h
+)
+endif ()
 
 # Include SIMD mixers
 set(CPU_EXTS "Default")
+if(HAVE_SSE)
+    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, SSE")
+endif()
 if(HAVE_SSE2)
-    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse.cpp core/mixer/mixer_sse2.cpp)
-    set(CPU_EXTS "${CPU_EXTS}, SSE, SSE2")
+    set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse2.cpp)
+    set(CPU_EXTS "${CPU_EXTS}, SSE2")
 endif()
 if(HAVE_SSE3)
     set(CORE_OBJS  ${CORE_OBJS} core/mixer/mixer_sse3.cpp)
@@ -778,6 +828,7 @@ endif()
 
 set(HAVE_ALSA       0)
 set(HAVE_OSS        0)
+set(HAVE_PIPEWIRE   0)
 set(HAVE_SOLARIS    0)
 set(HAVE_SNDIO      0)
 set(HAVE_DSOUND     0)
@@ -849,6 +900,25 @@ if(ALSOFT_REQUIRE_OSS AND NOT HAVE_OSS)
     message(FATAL_ERROR "Failed to enabled required OSS backend")
 endif()
 
+# Check PipeWire backend
+option(ALSOFT_REQUIRE_PIPEWIRE "Require PipeWire backend" OFF)
+if(PkgConfig_FOUND)
+    pkg_check_modules(PIPEWIRE libpipewire-0.3)
+    if(PIPEWIRE_FOUND)
+        option(ALSOFT_BACKEND_PIPEWIRE "Enable PipeWire backend" ON)
+        if(ALSOFT_BACKEND_PIPEWIRE)
+            set(HAVE_PIPEWIRE 1)
+            set(BACKENDS  "${BACKENDS} PipeWire${IS_LINKED},")
+            set(ALC_OBJS  ${ALC_OBJS} alc/backends/pipewire.cpp alc/backends/pipewire.h)
+            add_backend_libs(${PIPEWIRE_LIBRARIES})
+            set(INC_PATHS ${INC_PATHS} ${PIPEWIRE_INCLUDE_DIRS})
+        endif()
+    endif()
+endif()
+if(ALSOFT_REQUIRE_PIPEWIRE AND NOT HAVE_PIPEWIRE)
+    message(FATAL_ERROR "Failed to enabled required PipeWire backend")
+endif()
+
 # Check Solaris backend
 option(ALSOFT_REQUIRE_SOLARIS "Require Solaris backend" OFF)
 find_package(AudioIO)
@@ -1148,10 +1218,16 @@ if(ALSOFT_EMBED_HRTF_DATA)
 endif()
 
 
-if(ALSOFT_UTILS AND NOT ALSOFT_NO_CONFIG_UTIL)
-    find_package(Qt5Widgets)
+if(ALSOFT_UTILS)
+    find_package(MySOFA)
+    if(NOT ALSOFT_NO_CONFIG_UTIL)
+        find_package(Qt5Widgets QUIET)
+        if(NOT Qt5Widgets_FOUND)
+            message(STATUS "Could NOT find Qt5Widgets")
+        endif()
+    endif()
 endif()
-if(ALSOFT_EXAMPLES)
+if(ALSOFT_UTILS OR ALSOFT_EXAMPLES)
     find_package(SndFile)
     find_package(SDL2)
     if(SDL2_FOUND)
@@ -1212,6 +1288,7 @@ if(LIBTYPE STREQUAL "STATIC")
     add_library(${IMPL_TARGET} STATIC ${COMMON_OBJS} ${OPENAL_OBJS} ${ALC_OBJS} ${CORE_OBJS})
     target_compile_definitions(${IMPL_TARGET} PUBLIC AL_LIBTYPE_STATIC)
     target_link_libraries(${IMPL_TARGET} PRIVATE ${LINKER_FLAGS} ${EXTRA_LIBS} ${MATH_LIB})
+
     if(WIN32)
         # This option is for static linking OpenAL Soft into another project
         # that already defines the IDs. It is up to that project to ensure all
@@ -1232,13 +1309,14 @@ else()
             router/al.cpp
         )
         target_compile_definitions(OpenAL
-            PRIVATE AL_BUILD_LIBRARY AL_ALEXT_PROTOTYPES ${CPP_DEFS})
+            PRIVATE AL_BUILD_LIBRARY AL_ALEXT_PROTOTYPES "ALC_API=${EXPORT_DECL}"
+            "AL_API=${EXPORT_DECL}" ${CPP_DEFS})
         target_compile_options(OpenAL PRIVATE ${C_FLAGS})
         target_link_libraries(OpenAL PRIVATE common ${LINKER_FLAGS})
         target_include_directories(OpenAL
           PUBLIC
             $<BUILD_INTERFACE:${OpenAL_SOURCE_DIR}/include>
-            $<INSTALL_INTERFACE:include>
+            $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
           PRIVATE
             ${OpenAL_SOURCE_DIR}/common
             ${OpenAL_BINARY_DIR}
@@ -1303,12 +1381,14 @@ endif()
 target_include_directories(${IMPL_TARGET}
   PUBLIC
     $<BUILD_INTERFACE:${OpenAL_SOURCE_DIR}/include>
-    $<INSTALL_INTERFACE:include>
+  INTERFACE
+    $<BUILD_INTERFACE:${OpenAL_SOURCE_DIR}/include/AL>
+    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/AL>
   PRIVATE
     ${INC_PATHS}
     ${OpenAL_BINARY_DIR}
     ${OpenAL_SOURCE_DIR}
-    ${OpenAL_SOURCE_DIR}/alc
     ${OpenAL_SOURCE_DIR}/common
 )
 
@@ -1317,7 +1397,8 @@ set_target_properties(${IMPL_TARGET} PROPERTIES OUTPUT_NAME ${LIBNAME}
     SOVERSION ${LIB_MAJOR_VERSION}
 )
 target_compile_definitions(${IMPL_TARGET}
-    PRIVATE AL_BUILD_LIBRARY AL_ALEXT_PROTOTYPES ${CPP_DEFS})
+    PRIVATE AL_BUILD_LIBRARY AL_ALEXT_PROTOTYPES "ALC_API=${EXPORT_DECL}" "AL_API=${EXPORT_DECL}"
+    ${CPP_DEFS})
 target_compile_options(${IMPL_TARGET} PRIVATE ${C_FLAGS})
 
 if(TARGET build_version)
@@ -1351,25 +1432,35 @@ if(HAS_ROUTER)
     message(STATUS "Building DLL router")
 endif()
 
-message(STATUS "
-Building OpenAL with support for the following backends:
-    ${BACKENDS}
-
-Building with support for CPU extensions:
-    ${CPU_EXTS}
-")
+message(STATUS "")
+message(STATUS "Building OpenAL with support for the following backends:")
+message(STATUS "   ${BACKENDS}")
+message(STATUS "")
+message(STATUS "Building with support for CPU extensions:")
+message(STATUS "    ${CPU_EXTS}")
+message(STATUS "")
 if(FPMATH_SET)
-    message(STATUS "Building with SSE${FPMATH_SET} codegen
-")
+    message(STATUS "Building with SSE${FPMATH_SET} codegen")
+    message(STATUS "")
+endif()
+
+if(ALSOFT_EAX)
+    message(STATUS "Building with legacy EAX extension support")
+    message(STATUS "")
 endif()
 
 if(ALSOFT_EMBED_HRTF_DATA)
-    message(STATUS "Embedding HRTF datasets
-")
+    message(STATUS "Embedding HRTF datasets")
+    message(STATUS "")
 endif()
 
+# An alias for sub-project builds
+add_library(OpenAL::OpenAL ALIAS OpenAL)
+
 # Install main library
 if(ALSOFT_INSTALL)
+    configure_package_config_file(OpenALConfig.cmake.in OpenALConfig.cmake
+        INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/OpenAL)
     install(TARGETS OpenAL EXPORT OpenAL
         RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
         LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
@@ -1378,15 +1469,17 @@ if(ALSOFT_INSTALL)
         INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ${CMAKE_INSTALL_INCLUDEDIR}/AL)
     export(TARGETS OpenAL
         NAMESPACE OpenAL::
-        FILE OpenALConfig.cmake)
+        FILE OpenALTargets.cmake)
     install(EXPORT OpenAL
         DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/OpenAL
         NAMESPACE OpenAL::
-        FILE OpenALConfig.cmake)
+        FILE OpenALTargets.cmake)
     install(DIRECTORY include/AL
         DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
     install(FILES "${OpenAL_BINARY_DIR}/openal.pc"
         DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
+    install(FILES "${OpenAL_BINARY_DIR}/OpenALConfig.cmake"
+        DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/OpenAL")
     if(TARGET soft_oal)
         install(TARGETS soft_oal
             RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
@@ -1429,7 +1522,24 @@ if(ALSOFT_UTILS)
         set(EXTRA_INSTALLS ${EXTRA_INSTALLS} openal-info)
     endif()
 
-    find_package(MySOFA)
+    if(SNDFILE_FOUND)
+        add_executable(uhjdecoder utils/uhjdecoder.cpp)
+        target_compile_definitions(uhjdecoder PRIVATE ${CPP_DEFS})
+        target_include_directories(uhjdecoder
+            PRIVATE ${OpenAL_BINARY_DIR} ${OpenAL_SOURCE_DIR}/common)
+        target_compile_options(uhjdecoder PRIVATE ${C_FLAGS})
+        target_link_libraries(uhjdecoder PUBLIC common
+            PRIVATE ${LINKER_FLAGS} SndFile::SndFile ${UNICODE_FLAG})
+
+        add_executable(uhjencoder utils/uhjencoder.cpp)
+        target_compile_definitions(uhjencoder PRIVATE ${CPP_DEFS})
+        target_include_directories(uhjencoder
+            PRIVATE ${OpenAL_BINARY_DIR} ${OpenAL_SOURCE_DIR}/common)
+        target_compile_options(uhjencoder PRIVATE ${C_FLAGS})
+        target_link_libraries(uhjencoder PUBLIC common
+            PRIVATE ${LINKER_FLAGS} SndFile::SndFile ${UNICODE_FLAG})
+    endif()
+
     if(MYSOFA_FOUND)
         set(SOFA_SUPPORT_SRCS
             utils/sofa-support.cpp

+ 58 - 0
Engine/lib/openal-soft/ChangeLog

@@ -1,3 +1,57 @@
+openal-soft-1.22.0:
+
+    Implemented the ALC_SOFT_reopen_device extension. This allows for moving
+    devices to different outputs without losing object state.
+
+    Implemented the ALC_SOFT_output_mode extension.
+
+    Implemented the AL_SOFT_callback_buffer extension.
+
+    Implemented the AL_SOFT_UHJ extension. This supports native UHJ buffer
+    formats and Super Stereo processing.
+
+    Implemented the legacy EAX extensions. Enabled by default only on Windows.
+
+    Improved sound positioning stability when a source is near the listener.
+
+    Improved the default 5.1 output decoder.
+
+    Improved the high frequency response for the HRTF second-order ambisonic
+    decoder.
+
+    Improved SoundIO capture behavior.
+
+    Fixed UHJ output on NEON-capable CPUs.
+
+    Fixed redundant effect updates when setting an effect property to the
+    current value.
+
+    Fixed WASAPI capture using really low sample rates, and sources with very
+    high pitch shifts when using a bsinc resampler.
+
+    Added a PipeWire backend.
+
+    Added enumeration for the JACK and CoreAudio backends.
+
+    Added optional support for RTKit to get real-time priority. Only used as a
+    backup when pthread_setschedparam fails.
+
+    Added an option for JACK playback to render directly in the real-time
+    processing callback. For lower playback latency, on by default.
+
+    Added an option for custom JACK devices.
+
+    Added utilities to encode and decode UHJ audio files. Files are decoded to
+    the .amb format, and are encoded from libsndfile-compatible formats.
+
+    Added an in-progress extension to hold sources in a playing state when a
+    device disconnects. Allows devices to be reset or reopened and have sources
+    resume from where they left off.
+
+    Lowered the priority of the JACK backend. To avoid it getting picked when
+    PipeWire is providing JACK compatibility, since the JACK backend is less
+    robust with auto-configuration.
+
 openal-soft-1.21.1:
 
     Improved alext.h's detection of standard types.
@@ -5,6 +59,8 @@ openal-soft-1.21.1:
     Improved slightly the local source position when the listener and source
     are near each other.
 
+    Improved click/pop prevention for sounds that stop prematurely.
+
     Fixed compilation for Windows ARM targets with MSVC.
 
     Fixed ARM NEON detection on Windows.
@@ -19,6 +75,8 @@ openal-soft-1.21.1:
 
     Fixed missing source stop events when stopping a paused source.
 
+    Added capture support to the experimental Oboe backend.
+
 openal-soft-1.21.0:
 
     Updated library codebase to C++14.

+ 9 - 0
Engine/lib/openal-soft/OpenALConfig.cmake.in

@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.1)
+
+include("${CMAKE_CURRENT_LIST_DIR}/OpenALTargets.cmake")
+
+set(OPENAL_FOUND ON)
+set(OPENAL_INCLUDE_DIR $<TARGET_PROPERTY:OpenAL::OpenAL,INTERFACE_INCLUDE_DIRECTORIES>)
+set(OPENAL_LIBRARY $<LINK_ONLY:OpenAL::OpenAL>)
+set(OPENAL_DEFINITIONS $<TARGET_PROPERTY:OpenAL::OpenAL,INTERFACE_COMPILE_DEFINITIONS>)
+set(OPENAL_VERSION_STRING @PACKAGE_VERSION@)

+ 5 - 0
Engine/lib/openal-soft/XCompile-Android.txt

@@ -7,5 +7,10 @@
 # cmake .. -DANDROID_STL=c++_shared \
 #     -DCMAKE_TOOLCHAIN_FILE=${ndk_root}/build/cmake/android.toolchain.cmake
 #
+# Certain NDK versions may also need to use the lld linker to avoid errors
+# about missing liblog.so and libOpenSLES.so. That can be done by:
+# cmake .. -DANDROID_LD=lld \
+#     -DCMAKE_TOOLCHAIN_FILE=${ndk_root}/build/cmake/android.toolchain.cmake
+#
 
 MESSAGE(FATAL_ERROR "Use the toolchain provided by the Android NDK")

+ 832 - 34
Engine/lib/openal-soft/al/auxeffectslot.cpp

@@ -36,20 +36,24 @@
 #include "AL/efx.h"
 
 #include "albit.h"
-#include "alcmain.h"
-#include "alcontext.h"
+#include "alc/alu.h"
+#include "alc/context.h"
+#include "alc/device.h"
+#include "alc/inprogext.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "alspan.h"
-#include "alu.h"
 #include "buffer.h"
 #include "core/except.h"
 #include "core/fpu_ctrl.h"
 #include "core/logging.h"
 #include "effect.h"
-#include "inprogext.h"
 #include "opthelpers.h"
 
+#ifdef ALSOFT_EAX
+#include "eax_exception.h"
+#include "eax_utils.h"
+#endif // ALSOFT_EAX
 
 namespace {
 
@@ -277,8 +281,9 @@ ALeffectslot *AllocEffectSlot(ALCcontext *context)
         { return entry.FreeMask != 0; });
     auto lidx = static_cast<ALuint>(std::distance(context->mEffectSlotList.begin(), sublist));
     auto slidx = static_cast<ALuint>(al::countr_zero(sublist->FreeMask));
+    ASSUME(slidx < 64);
 
-    ALeffectslot *slot{::new(sublist->EffectSlots + slidx) ALeffectslot{}};
+    ALeffectslot *slot{al::construct_at(sublist->EffectSlots + slidx)};
     aluInitEffectPanning(&slot->mSlot, context);
 
     /* Add 1 to avoid source ID 0. */
@@ -303,13 +308,15 @@ void FreeEffectSlot(ALCcontext *context, ALeffectslot *slot)
 }
 
 
-#define DO_UPDATEPROPS() do {                                                 \
-    if(!context->mDeferUpdates.load(std::memory_order_acquire)                \
-        && slot->mState == SlotState::Playing)                                \
-        slot->updateProps(context.get());                                     \
-    else                                                                      \
-        slot->PropsClean.clear(std::memory_order_release);                    \
-} while(0)
+inline void UpdateProps(ALeffectslot *slot, ALCcontext *context)
+{
+    if(!context->mDeferUpdates && slot->mState == SlotState::Playing)
+    {
+        slot->updateProps(context);
+        return;
+    }
+    slot->mPropsDirty = true;
+}
 
 } // namespace
 
@@ -325,7 +332,7 @@ START_API_FUNC
     if UNLIKELY(n <= 0) return;
 
     std::unique_lock<std::mutex> slotlock{context->mEffectSlotLock};
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     if(static_cast<ALuint>(n) > device->AuxiliaryEffectSlotMax-context->mNumEffectSlots)
     {
         context->setError(AL_OUT_OF_MEMORY, "Exceeding %u effect slot limit (%u + %d)",
@@ -460,7 +467,7 @@ START_API_FUNC
     if(slot->mState == SlotState::Playing)
         return;
 
-    slot->PropsClean.test_and_set(std::memory_order_acq_rel);
+    slot->mPropsDirty = false;
     slot->updateProps(context.get());
 
     AddActiveEffectSlots({&slot, 1}, context.get());
@@ -491,7 +498,7 @@ START_API_FUNC
 
         if(slot->mState != SlotState::Playing)
         {
-            slot->PropsClean.test_and_set(std::memory_order_acq_rel);
+            slot->mPropsDirty = false;
             slot->updateProps(context.get());
         }
         slots[i] = slot;
@@ -571,14 +578,19 @@ START_API_FUNC
     switch(param)
     {
     case AL_EFFECTSLOT_EFFECT:
-        device = context->mDevice.get();
+        device = context->mALDevice.get();
 
         {
             std::lock_guard<std::mutex> ___{device->EffectLock};
             ALeffect *effect{value ? LookupEffect(device, static_cast<ALuint>(value)) : nullptr};
-            if(!(value == 0 || effect != nullptr))
-                SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid effect ID %u", value);
-            err = slot->initEffect(effect, context.get());
+            if(effect)
+                err = slot->initEffect(effect->type, effect->Props, context.get());
+            else
+            {
+                if(value != 0)
+                    SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid effect ID %u", value);
+                err = slot->initEffect(AL_EFFECT_NULL, EffectProps{}, context.get());
+            }
         }
         if UNLIKELY(err != AL_NO_ERROR)
         {
@@ -587,8 +599,12 @@ START_API_FUNC
         }
         if UNLIKELY(slot->mState == SlotState::Initial)
         {
+            slot->mPropsDirty = false;
+            slot->updateProps(context.get());
+
             AddActiveEffectSlots({&slot, 1}, context.get());
             slot->mState = SlotState::Playing;
+            return;
         }
         break;
 
@@ -596,6 +612,8 @@ START_API_FUNC
         if(!(value == AL_TRUE || value == AL_FALSE))
             SETERR_RETURN(context, AL_INVALID_VALUE,,
                 "Effect slot auxiliary send auto out of range");
+        if UNLIKELY(slot->AuxSendAuto == !!value)
+            return;
         slot->AuxSendAuto = !!value;
         break;
 
@@ -603,6 +621,8 @@ START_API_FUNC
         target = LookupEffectSlot(context.get(), static_cast<ALuint>(value));
         if(value && !target)
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid effect slot target ID");
+        if UNLIKELY(slot->Target == target)
+            return;
         if(target)
         {
             ALeffectslot *checker{target};
@@ -631,12 +651,20 @@ START_API_FUNC
         break;
 
     case AL_BUFFER:
-        device = context->mDevice.get();
+        device = context->mALDevice.get();
 
         if(slot->mState == SlotState::Playing)
             SETERR_RETURN(context, AL_INVALID_OPERATION,,
                 "Setting buffer on playing effect slot %u", slot->id);
 
+        if(ALbuffer *buffer{slot->Buffer})
+        {
+            if UNLIKELY(buffer->id == static_cast<ALuint>(value))
+                return;
+        }
+        else if UNLIKELY(value == 0)
+            return;
+
         {
             std::lock_guard<std::mutex> ___{device->BufferLock};
             ALbuffer *buffer{};
@@ -668,7 +696,7 @@ START_API_FUNC
         SETERR_RETURN(context, AL_INVALID_ENUM,, "Invalid effect slot integer property 0x%04x",
             param);
     }
-    DO_UPDATEPROPS();
+    UpdateProps(slot, context.get());
 }
 END_API_FUNC
 
@@ -720,6 +748,8 @@ START_API_FUNC
     case AL_EFFECTSLOT_GAIN:
         if(!(value >= 0.0f && value <= 1.0f))
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Effect slot gain out of range");
+        if UNLIKELY(slot->Gain == value)
+            return;
         slot->Gain = value;
         break;
 
@@ -727,7 +757,7 @@ START_API_FUNC
         SETERR_RETURN(context, AL_INVALID_ENUM,, "Invalid effect slot float property 0x%04x",
             param);
     }
-    DO_UPDATEPROPS();
+    UpdateProps(slot, context.get());
 }
 END_API_FUNC
 
@@ -884,10 +914,8 @@ END_API_FUNC
 
 ALeffectslot::ALeffectslot()
 {
-    PropsClean.test_and_set(std::memory_order_relaxed);
-
     EffectStateFactory *factory{getFactoryByType(EffectSlotType::None)};
-    assert(factory != nullptr);
+    if(!factory) throw std::runtime_error{"Failed to get null effect factory"};
 
     al::intrusive_ptr<EffectState> state{factory->create()};
     Effect.State = state;
@@ -915,9 +943,10 @@ ALeffectslot::~ALeffectslot()
         mSlot.mEffectState->release();
 }
 
-ALenum ALeffectslot::initEffect(ALeffect *effect, ALCcontext *context)
+ALenum ALeffectslot::initEffect(ALenum effectType, const EffectProps &effectProps,
+    ALCcontext *context)
 {
-    EffectSlotType newtype{EffectSlotTypeFromEnum(effect ? effect->type : AL_EFFECT_NULL)};
+    EffectSlotType newtype{EffectSlotTypeFromEnum(effectType)};
     if(newtype != Effect.Type)
     {
         EffectStateFactory *factory{getFactoryByType(newtype)};
@@ -928,7 +957,7 @@ ALenum ALeffectslot::initEffect(ALeffect *effect, ALCcontext *context)
         }
         al::intrusive_ptr<EffectState> state{factory->create()};
 
-        ALCdevice *device{context->mDevice.get()};
+        ALCdevice *device{context->mALDevice.get()};
         std::unique_lock<std::mutex> statelock{device->StateLock};
         state->mOutTarget = device->Dry.Buffer;
         {
@@ -937,12 +966,12 @@ ALenum ALeffectslot::initEffect(ALeffect *effect, ALCcontext *context)
         }
 
         Effect.Type = newtype;
-        Effect.Props = effect ? effect->Props : EffectProps{};
+        Effect.Props = effectProps;
 
         Effect.State = std::move(state);
     }
-    else if(effect)
-        Effect.Props = effect->Props;
+    else if(newtype != EffectSlotType::None)
+        Effect.Props = effectProps;
 
     /* Remove state references from old effect slot property updates. */
     EffectSlotProps *props{context->mFreeEffectslotProps.load()};
@@ -994,17 +1023,20 @@ void ALeffectslot::updateProps(ALCcontext *context)
 void UpdateAllEffectSlotProps(ALCcontext *context)
 {
     std::lock_guard<std::mutex> _{context->mEffectSlotLock};
+#ifdef ALSOFT_EAX
+    if(context->has_eax())
+        context->eax_commit_fx_slots();
+#endif
     for(auto &sublist : context->mEffectSlotList)
     {
         uint64_t usemask{~sublist.FreeMask};
         while(usemask)
         {
             const int idx{al::countr_zero(usemask)};
-            ALeffectslot *slot{sublist.EffectSlots + idx};
             usemask &= ~(1_u64 << idx);
+            ALeffectslot *slot{sublist.EffectSlots + idx};
 
-            if(slot->mState != SlotState::Stopped
-                && slot->PropsClean.test_and_set(std::memory_order_acq_rel))
+            if(slot->mState != SlotState::Stopped && std::exchange(slot->mPropsDirty, false))
                 slot->updateProps(context);
         }
     }
@@ -1023,3 +1055,769 @@ EffectSlotSubList::~EffectSlotSubList()
     al_free(EffectSlots);
     EffectSlots = nullptr;
 }
+
+#ifdef ALSOFT_EAX
+namespace {
+
+class EaxFxSlotException :
+    public EaxException
+{
+public:
+	explicit EaxFxSlotException(
+		const char* message)
+		:
+		EaxException{"EAX_FX_SLOT", message}
+	{
+	}
+}; // EaxFxSlotException
+
+
+} // namespace
+
+
+void ALeffectslot::eax_initialize(
+    ALCcontext& al_context,
+    EaxFxSlotIndexValue index)
+{
+    eax_al_context_ = &al_context;
+
+    if (index >= EAX_MAX_FXSLOTS)
+    {
+        eax_fail("Index out of range.");
+    }
+
+    eax_fx_slot_index_ = index;
+
+    eax_initialize_eax();
+    eax_initialize_lock();
+    eax_initialize_effects();
+}
+
+const EAX50FXSLOTPROPERTIES& ALeffectslot::eax_get_eax_fx_slot() const noexcept
+{
+    return eax_eax_fx_slot_;
+}
+
+void ALeffectslot::eax_ensure_is_unlocked() const
+{
+    if (eax_is_locked_)
+        eax_fail("Locked.");
+}
+
+void ALeffectslot::eax_validate_fx_slot_effect(
+    const GUID& eax_effect_id)
+{
+    eax_ensure_is_unlocked();
+
+    if (eax_effect_id != EAX_NULL_GUID &&
+        eax_effect_id != EAX_REVERB_EFFECT &&
+        eax_effect_id != EAX_AGCCOMPRESSOR_EFFECT &&
+        eax_effect_id != EAX_AUTOWAH_EFFECT &&
+        eax_effect_id != EAX_CHORUS_EFFECT &&
+        eax_effect_id != EAX_DISTORTION_EFFECT &&
+        eax_effect_id != EAX_ECHO_EFFECT &&
+        eax_effect_id != EAX_EQUALIZER_EFFECT &&
+        eax_effect_id != EAX_FLANGER_EFFECT &&
+        eax_effect_id != EAX_FREQUENCYSHIFTER_EFFECT &&
+        eax_effect_id != EAX_VOCALMORPHER_EFFECT &&
+        eax_effect_id != EAX_PITCHSHIFTER_EFFECT &&
+        eax_effect_id != EAX_RINGMODULATOR_EFFECT)
+    {
+        eax_fail("Unsupported EAX effect GUID.");
+    }
+}
+
+void ALeffectslot::eax_validate_fx_slot_volume(
+    long eax_volume)
+{
+    eax_validate_range<EaxFxSlotException>(
+        "Volume",
+        eax_volume,
+        EAXFXSLOT_MINVOLUME,
+        EAXFXSLOT_MAXVOLUME);
+}
+
+void ALeffectslot::eax_validate_fx_slot_lock(
+    long eax_lock)
+{
+    eax_ensure_is_unlocked();
+
+    eax_validate_range<EaxFxSlotException>(
+        "Lock",
+        eax_lock,
+        EAXFXSLOT_MINLOCK,
+        EAXFXSLOT_MAXLOCK);
+}
+
+void ALeffectslot::eax_validate_fx_slot_flags(
+    unsigned long eax_flags,
+    int eax_version)
+{
+    eax_validate_range<EaxFxSlotException>(
+        "Flags",
+        eax_flags,
+        0UL,
+        ~(eax_version == 4 ? EAX40FXSLOTFLAGS_RESERVED : EAX50FXSLOTFLAGS_RESERVED));
+}
+
+void ALeffectslot::eax_validate_fx_slot_occlusion(
+    long eax_occlusion)
+{
+    eax_validate_range<EaxFxSlotException>(
+        "Occlusion",
+        eax_occlusion,
+        EAXFXSLOT_MINOCCLUSION,
+        EAXFXSLOT_MAXOCCLUSION);
+}
+
+void ALeffectslot::eax_validate_fx_slot_occlusion_lf_ratio(
+    float eax_occlusion_lf_ratio)
+{
+    eax_validate_range<EaxFxSlotException>(
+        "Occlusion LF Ratio",
+        eax_occlusion_lf_ratio,
+        EAXFXSLOT_MINOCCLUSIONLFRATIO,
+        EAXFXSLOT_MAXOCCLUSIONLFRATIO);
+}
+
+void ALeffectslot::eax_validate_fx_slot_all(
+    const EAX40FXSLOTPROPERTIES& fx_slot,
+    int eax_version)
+{
+    eax_validate_fx_slot_effect(fx_slot.guidLoadEffect);
+    eax_validate_fx_slot_volume(fx_slot.lVolume);
+    eax_validate_fx_slot_lock(fx_slot.lLock);
+    eax_validate_fx_slot_flags(fx_slot.ulFlags, eax_version);
+}
+
+void ALeffectslot::eax_validate_fx_slot_all(
+    const EAX50FXSLOTPROPERTIES& fx_slot,
+    int eax_version)
+{
+    eax_validate_fx_slot_all(static_cast<const EAX40FXSLOTPROPERTIES&>(fx_slot), eax_version);
+
+    eax_validate_fx_slot_occlusion(fx_slot.lOcclusion);
+    eax_validate_fx_slot_occlusion_lf_ratio(fx_slot.flOcclusionLFRatio);
+}
+
+void ALeffectslot::eax_set_fx_slot_effect(
+    const GUID& eax_effect_id)
+{
+    if (eax_eax_fx_slot_.guidLoadEffect == eax_effect_id)
+    {
+        return;
+    }
+
+    eax_eax_fx_slot_.guidLoadEffect = eax_effect_id;
+
+    eax_set_fx_slot_effect();
+}
+
+void ALeffectslot::eax_set_fx_slot_volume(
+    long eax_volume)
+{
+    if (eax_eax_fx_slot_.lVolume == eax_volume)
+    {
+        return;
+    }
+
+    eax_eax_fx_slot_.lVolume = eax_volume;
+
+    eax_set_fx_slot_volume();
+}
+
+void ALeffectslot::eax_set_fx_slot_lock(
+    long eax_lock)
+{
+    if (eax_eax_fx_slot_.lLock == eax_lock)
+    {
+        return;
+    }
+
+    eax_eax_fx_slot_.lLock = eax_lock;
+}
+
+void ALeffectslot::eax_set_fx_slot_flags(
+    unsigned long eax_flags)
+{
+    if (eax_eax_fx_slot_.ulFlags == eax_flags)
+    {
+        return;
+    }
+
+    eax_eax_fx_slot_.ulFlags = eax_flags;
+
+    eax_set_fx_slot_flags();
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_occlusion(
+    long eax_occlusion)
+{
+    if (eax_eax_fx_slot_.lOcclusion == eax_occlusion)
+    {
+        return false;
+    }
+
+    eax_eax_fx_slot_.lOcclusion = eax_occlusion;
+
+    return true;
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_occlusion_lf_ratio(
+    float eax_occlusion_lf_ratio)
+{
+    if (eax_eax_fx_slot_.flOcclusionLFRatio == eax_occlusion_lf_ratio)
+    {
+        return false;
+    }
+
+    eax_eax_fx_slot_.flOcclusionLFRatio = eax_occlusion_lf_ratio;
+
+    return true;
+}
+
+void ALeffectslot::eax_set_fx_slot_all(
+    const EAX40FXSLOTPROPERTIES& eax_fx_slot)
+{
+    eax_set_fx_slot_effect(eax_fx_slot.guidLoadEffect);
+    eax_set_fx_slot_volume(eax_fx_slot.lVolume);
+    eax_set_fx_slot_lock(eax_fx_slot.lLock);
+    eax_set_fx_slot_flags(eax_fx_slot.ulFlags);
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_all(
+    const EAX50FXSLOTPROPERTIES& eax_fx_slot)
+{
+    eax_set_fx_slot_all(static_cast<const EAX40FXSLOTPROPERTIES&>(eax_fx_slot));
+
+    const auto is_occlusion_modified = eax_set_fx_slot_occlusion(eax_fx_slot.lOcclusion);
+    const auto is_occlusion_lf_ratio_modified = eax_set_fx_slot_occlusion_lf_ratio(eax_fx_slot.flOcclusionLFRatio);
+
+    return is_occlusion_modified || is_occlusion_lf_ratio_modified;
+}
+
+void ALeffectslot::eax_unlock_legacy() noexcept
+{
+    assert(eax_fx_slot_index_ < 2);
+    eax_is_locked_ = false;
+    eax_eax_fx_slot_.lLock = EAXFXSLOT_UNLOCKED;
+}
+
+[[noreturn]]
+void ALeffectslot::eax_fail(
+    const char* message)
+{
+    throw EaxFxSlotException{message};
+}
+
+GUID ALeffectslot::eax_get_eax_default_effect_guid() const noexcept
+{
+    switch (eax_fx_slot_index_)
+    {
+        case 0: return EAX_REVERB_EFFECT;
+        case 1: return EAX_CHORUS_EFFECT;
+        default: return EAX_NULL_GUID;
+    }
+}
+
+long ALeffectslot::eax_get_eax_default_lock() const noexcept
+{
+    return eax_fx_slot_index_ < 2 ? EAXFXSLOT_LOCKED : EAXFXSLOT_UNLOCKED;
+}
+
+void ALeffectslot::eax_set_eax_fx_slot_defaults()
+{
+    eax_eax_fx_slot_.guidLoadEffect = eax_get_eax_default_effect_guid();
+    eax_eax_fx_slot_.lVolume = EAXFXSLOT_DEFAULTVOLUME;
+    eax_eax_fx_slot_.lLock = eax_get_eax_default_lock();
+    eax_eax_fx_slot_.ulFlags = EAX40FXSLOT_DEFAULTFLAGS;
+    eax_eax_fx_slot_.lOcclusion = EAXFXSLOT_DEFAULTOCCLUSION;
+    eax_eax_fx_slot_.flOcclusionLFRatio = EAXFXSLOT_DEFAULTOCCLUSIONLFRATIO;
+}
+
+void ALeffectslot::eax_initialize_eax()
+{
+    eax_set_eax_fx_slot_defaults();
+}
+
+void ALeffectslot::eax_initialize_lock()
+{
+    eax_is_locked_ = (eax_fx_slot_index_ < 2);
+}
+
+void ALeffectslot::eax_initialize_effects()
+{
+    eax_set_fx_slot_effect();
+}
+
+void ALeffectslot::eax_get_fx_slot_all(
+    const EaxEaxCall& eax_call) const
+{
+    switch (eax_call.get_version())
+    {
+        case 4:
+            eax_call.set_value<EaxFxSlotException, EAX40FXSLOTPROPERTIES>(eax_eax_fx_slot_);
+            break;
+
+        case 5:
+            eax_call.set_value<EaxFxSlotException, EAX50FXSLOTPROPERTIES>(eax_eax_fx_slot_);
+            break;
+
+        default:
+            eax_fail("Unsupported EAX version.");
+    }
+}
+
+void ALeffectslot::eax_get_fx_slot(
+    const EaxEaxCall& eax_call) const
+{
+    switch (eax_call.get_property_id())
+    {
+        case EAXFXSLOT_ALLPARAMETERS:
+            eax_get_fx_slot_all(eax_call);
+            break;
+
+        case EAXFXSLOT_LOADEFFECT:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.guidLoadEffect);
+            break;
+
+        case EAXFXSLOT_VOLUME:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.lVolume);
+            break;
+
+        case EAXFXSLOT_LOCK:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.lLock);
+            break;
+
+        case EAXFXSLOT_FLAGS:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.ulFlags);
+            break;
+
+        case EAXFXSLOT_OCCLUSION:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.lOcclusion);
+            break;
+
+        case EAXFXSLOT_OCCLUSIONLFRATIO:
+            eax_call.set_value<EaxFxSlotException>(eax_eax_fx_slot_.flOcclusionLFRatio);
+            break;
+
+        default:
+            eax_fail("Unsupported FX slot property id.");
+    }
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_get(
+    const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_set_id())
+    {
+        case EaxEaxCallPropertySetId::fx_slot:
+            eax_get_fx_slot(eax_call);
+            break;
+
+        case EaxEaxCallPropertySetId::fx_slot_effect:
+            eax_dispatch_effect(eax_call);
+            break;
+
+        default:
+            eax_fail("Unsupported property id.");
+    }
+
+    return false;
+}
+
+void ALeffectslot::eax_set_fx_slot_effect(
+    ALenum al_effect_type)
+{
+    if(!IsValidEffectType(al_effect_type))
+        eax_fail("Unsupported effect.");
+
+    eax_effect_ = nullptr;
+    eax_effect_ = eax_create_eax_effect(al_effect_type);
+
+    eax_set_effect_slot_effect(*eax_effect_);
+}
+
+void ALeffectslot::eax_set_fx_slot_effect()
+{
+    auto al_effect_type = ALenum{};
+
+    if (false)
+    {
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_NULL_GUID)
+    {
+        al_effect_type = AL_EFFECT_NULL;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_AUTOWAH_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_AUTOWAH;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_CHORUS_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_CHORUS;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_AGCCOMPRESSOR_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_COMPRESSOR;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_DISTORTION_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_DISTORTION;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_REVERB_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_EAXREVERB;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_ECHO_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_ECHO;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_EQUALIZER_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_EQUALIZER;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_FLANGER_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_FLANGER;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_FREQUENCYSHIFTER_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_FREQUENCY_SHIFTER;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_PITCHSHIFTER_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_PITCH_SHIFTER;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_RINGMODULATOR_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_RING_MODULATOR;
+    }
+    else if (eax_eax_fx_slot_.guidLoadEffect == EAX_VOCALMORPHER_EFFECT)
+    {
+        al_effect_type = AL_EFFECT_VOCAL_MORPHER;
+    }
+    else
+    {
+        eax_fail("Unsupported effect.");
+    }
+
+    eax_set_fx_slot_effect(al_effect_type);
+}
+
+void ALeffectslot::eax_set_efx_effect_slot_gain()
+{
+    const auto gain = level_mb_to_gain(
+        static_cast<float>(clamp(
+            eax_eax_fx_slot_.lVolume,
+            EAXFXSLOT_MINVOLUME,
+            EAXFXSLOT_MAXVOLUME)));
+
+    eax_set_effect_slot_gain(gain);
+}
+
+void ALeffectslot::eax_set_fx_slot_volume()
+{
+    eax_set_efx_effect_slot_gain();
+}
+
+void ALeffectslot::eax_set_effect_slot_send_auto()
+{
+    eax_set_effect_slot_send_auto((eax_eax_fx_slot_.ulFlags & EAXFXSLOTFLAGS_ENVIRONMENT) != 0);
+}
+
+void ALeffectslot::eax_set_fx_slot_flags()
+{
+    eax_set_effect_slot_send_auto();
+}
+
+void ALeffectslot::eax_set_fx_slot_effect(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_effect_id =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX40FXSLOTPROPERTIES::guidLoadEffect)>();
+
+    eax_validate_fx_slot_effect(eax_effect_id);
+    eax_set_fx_slot_effect(eax_effect_id);
+}
+
+void ALeffectslot::eax_set_fx_slot_volume(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_volume =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX40FXSLOTPROPERTIES::lVolume)>();
+
+    eax_validate_fx_slot_volume(eax_volume);
+    eax_set_fx_slot_volume(eax_volume);
+}
+
+void ALeffectslot::eax_set_fx_slot_lock(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_lock =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX40FXSLOTPROPERTIES::lLock)>();
+
+    eax_validate_fx_slot_lock(eax_lock);
+    eax_set_fx_slot_lock(eax_lock);
+}
+
+void ALeffectslot::eax_set_fx_slot_flags(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_flags =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX40FXSLOTPROPERTIES::ulFlags)>();
+
+    eax_validate_fx_slot_flags(eax_flags, eax_call.get_version());
+    eax_set_fx_slot_flags(eax_flags);
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_occlusion(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_occlusion =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX50FXSLOTPROPERTIES::lOcclusion)>();
+
+    eax_validate_fx_slot_occlusion(eax_occlusion);
+
+    return eax_set_fx_slot_occlusion(eax_occlusion);
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_occlusion_lf_ratio(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eax_occlusion_lf_ratio =
+        eax_call.get_value<EaxFxSlotException, const decltype(EAX50FXSLOTPROPERTIES::flOcclusionLFRatio)>();
+
+    eax_validate_fx_slot_occlusion_lf_ratio(eax_occlusion_lf_ratio);
+
+    return eax_set_fx_slot_occlusion_lf_ratio(eax_occlusion_lf_ratio);
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set_fx_slot_all(
+    const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_version())
+    {
+        case 4:
+            {
+                const auto& eax_all =
+                    eax_call.get_value<EaxFxSlotException, const EAX40FXSLOTPROPERTIES>();
+
+                eax_validate_fx_slot_all(eax_all, eax_call.get_version());
+                eax_set_fx_slot_all(eax_all);
+
+                return false;
+            }
+
+        case 5:
+            {
+                const auto& eax_all =
+                    eax_call.get_value<EaxFxSlotException, const EAX50FXSLOTPROPERTIES>();
+
+                eax_validate_fx_slot_all(eax_all, eax_call.get_version());
+                return eax_set_fx_slot_all(eax_all);
+            }
+
+        default:
+            eax_fail("Unsupported EAX version.");
+    }
+}
+
+bool ALeffectslot::eax_set_fx_slot(
+    const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_id())
+    {
+        case EAXFXSLOT_NONE:
+            return false;
+
+        case EAXFXSLOT_ALLPARAMETERS:
+            return eax_set_fx_slot_all(eax_call);
+
+        case EAXFXSLOT_LOADEFFECT:
+            eax_set_fx_slot_effect(eax_call);
+            return false;
+
+        case EAXFXSLOT_VOLUME:
+            eax_set_fx_slot_volume(eax_call);
+            return false;
+
+        case EAXFXSLOT_LOCK:
+            eax_set_fx_slot_lock(eax_call);
+            return false;
+
+        case EAXFXSLOT_FLAGS:
+            eax_set_fx_slot_flags(eax_call);
+            return false;
+
+        case EAXFXSLOT_OCCLUSION:
+            return eax_set_fx_slot_occlusion(eax_call);
+
+        case EAXFXSLOT_OCCLUSIONLFRATIO:
+            return eax_set_fx_slot_occlusion_lf_ratio(eax_call);
+
+
+        default:
+            eax_fail("Unsupported FX slot property id.");
+    }
+}
+
+// [[nodiscard]]
+bool ALeffectslot::eax_set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_set_id())
+    {
+        case EaxEaxCallPropertySetId::fx_slot:
+            return eax_set_fx_slot(eax_call);
+
+        case EaxEaxCallPropertySetId::fx_slot_effect:
+            eax_dispatch_effect(eax_call);
+            break;
+
+        default:
+            eax_fail("Unsupported property id.");
+    }
+
+    return false;
+}
+
+void ALeffectslot::eax_dispatch_effect(const EaxEaxCall& eax_call)
+{ if(eax_effect_) eax_effect_->dispatch(eax_call); }
+
+void ALeffectslot::eax_apply_deferred()
+{
+    /* The other FXSlot properties (volume, effect, etc) aren't deferred? */
+
+    auto is_changed = false;
+    if(eax_effect_)
+        is_changed = eax_effect_->apply_deferred();
+    if(is_changed)
+        eax_set_effect_slot_effect(*eax_effect_);
+}
+
+
+void ALeffectslot::eax_set_effect_slot_effect(EaxEffect &effect)
+{
+#define EAX_PREFIX "[EAX_SET_EFFECT_SLOT_EFFECT] "
+
+    const auto error = initEffect(effect.al_effect_type_, effect.al_effect_props_, eax_al_context_);
+    if (error != AL_NO_ERROR)
+    {
+        ERR(EAX_PREFIX "%s\n", "Failed to initialize an effect.");
+        return;
+    }
+
+    if (mState == SlotState::Initial)
+    {
+        mPropsDirty = false;
+        updateProps(eax_al_context_);
+
+        auto effect_slot_ptr = this;
+
+        AddActiveEffectSlots({&effect_slot_ptr, 1}, eax_al_context_);
+        mState = SlotState::Playing;
+
+        return;
+    }
+
+    UpdateProps(this, eax_al_context_);
+
+#undef EAX_PREFIX
+}
+
+void ALeffectslot::eax_set_effect_slot_send_auto(
+    bool is_send_auto)
+{
+    if(AuxSendAuto == is_send_auto)
+        return;
+
+    AuxSendAuto = is_send_auto;
+    UpdateProps(this, eax_al_context_);
+}
+
+void ALeffectslot::eax_set_effect_slot_gain(
+    ALfloat gain)
+{
+#define EAX_PREFIX "[EAX_SET_EFFECT_SLOT_GAIN] "
+
+    if(gain == Gain)
+        return;
+    if(gain < 0.0f || gain > 1.0f)
+        ERR(EAX_PREFIX "Gain out of range (%f)\n", gain);
+
+    Gain = clampf(gain, 0.0f, 1.0f);
+    UpdateProps(this, eax_al_context_);
+
+#undef EAX_PREFIX
+}
+
+
+void ALeffectslot::EaxDeleter::operator()(ALeffectslot* effect_slot)
+{
+    assert(effect_slot);
+    eax_delete_al_effect_slot(*effect_slot->eax_al_context_, *effect_slot);
+}
+
+
+EaxAlEffectSlotUPtr eax_create_al_effect_slot(
+    ALCcontext& context)
+{
+#define EAX_PREFIX "[EAX_MAKE_EFFECT_SLOT] "
+
+    std::unique_lock<std::mutex> effect_slot_lock{context.mEffectSlotLock};
+
+    auto& device = *context.mALDevice;
+
+    if (context.mNumEffectSlots == device.AuxiliaryEffectSlotMax)
+    {
+        ERR(EAX_PREFIX "%s\n", "Out of memory.");
+        return nullptr;
+    }
+
+    if (!EnsureEffectSlots(&context, 1))
+    {
+        ERR(EAX_PREFIX "%s\n", "Failed to ensure.");
+        return nullptr;
+    }
+
+    auto effect_slot = EaxAlEffectSlotUPtr{AllocEffectSlot(&context)};
+    if (!effect_slot)
+    {
+        ERR(EAX_PREFIX "%s\n", "Failed to allocate.");
+        return nullptr;
+    }
+
+    return effect_slot;
+
+#undef EAX_PREFIX
+}
+
+void eax_delete_al_effect_slot(
+    ALCcontext& context,
+    ALeffectslot& effect_slot)
+{
+#define EAX_PREFIX "[EAX_DELETE_EFFECT_SLOT] "
+
+    std::lock_guard<std::mutex> effect_slot_lock{context.mEffectSlotLock};
+
+    if (ReadRef(effect_slot.ref) != 0)
+    {
+        ERR(EAX_PREFIX "Deleting in-use effect slot %u.\n", effect_slot.id);
+        return;
+    }
+
+    auto effect_slot_ptr = &effect_slot;
+
+    RemoveActiveEffectSlots({&effect_slot_ptr, 1}, &context);
+    FreeEffectSlot(&context, &effect_slot);
+
+#undef EAX_PREFIX
+}
+#endif // ALSOFT_EAX

+ 214 - 5
Engine/lib/openal-soft/al/auxeffectslot.h

@@ -8,14 +8,22 @@
 #include "AL/alc.h"
 #include "AL/efx.h"
 
-#include "alcmain.h"
+#include "alc/device.h"
+#include "alc/effects/base.h"
 #include "almalloc.h"
 #include "atomic.h"
-#include "effectslot.h"
-#include "effects/base.h"
+#include "core/effectslot.h"
 #include "intrusive_ptr.h"
 #include "vector.h"
 
+#ifdef ALSOFT_EAX
+#include <memory>
+
+#include "eax_eax_call.h"
+#include "eax_effect.h"
+#include "eax_fx_slot_index.h"
+#endif // ALSOFT_EAX
+
 struct ALbuffer;
 struct ALeffect;
 struct WetBuffer;
@@ -40,7 +48,7 @@ struct ALeffectslot {
         al::intrusive_ptr<EffectState> State;
     } Effect;
 
-    std::atomic_flag PropsClean;
+    bool mPropsDirty{true};
 
     SlotState mState{SlotState::Initial};
 
@@ -56,13 +64,214 @@ struct ALeffectslot {
     ALeffectslot& operator=(const ALeffectslot&) = delete;
     ~ALeffectslot();
 
-    ALenum initEffect(ALeffect *effect, ALCcontext *context);
+    ALenum initEffect(ALenum effectType, const EffectProps &effectProps, ALCcontext *context);
     void updateProps(ALCcontext *context);
 
     /* This can be new'd for the context's default effect slot. */
     DEF_NEWDEL(ALeffectslot)
+
+
+#ifdef ALSOFT_EAX
+public:
+    void eax_initialize(
+        ALCcontext& al_context,
+        EaxFxSlotIndexValue index);
+
+    const EAX50FXSLOTPROPERTIES& eax_get_eax_fx_slot() const noexcept;
+
+
+    // [[nodiscard]]
+    bool eax_dispatch(const EaxEaxCall& eax_call)
+    { return eax_call.is_get() ? eax_get(eax_call) : eax_set(eax_call); }
+
+
+    void eax_unlock_legacy() noexcept;
+
+    void eax_commit() { eax_apply_deferred(); }
+
+private:
+    ALCcontext* eax_al_context_{};
+
+    EaxFxSlotIndexValue eax_fx_slot_index_{};
+
+    EAX50FXSLOTPROPERTIES eax_eax_fx_slot_{};
+
+    EaxEffectUPtr eax_effect_{};
+    bool eax_is_locked_{};
+
+
+    [[noreturn]]
+    static void eax_fail(
+        const char* message);
+
+
+    GUID eax_get_eax_default_effect_guid() const noexcept;
+    long eax_get_eax_default_lock() const noexcept;
+
+    void eax_set_eax_fx_slot_defaults();
+
+    void eax_initialize_eax();
+
+    void eax_initialize_lock();
+
+
+    void eax_initialize_effects();
+
+
+    void eax_get_fx_slot_all(
+        const EaxEaxCall& eax_call) const;
+
+    void eax_get_fx_slot(
+        const EaxEaxCall& eax_call) const;
+
+    // [[nodiscard]]
+    bool eax_get(
+        const EaxEaxCall& eax_call);
+
+
+    void eax_set_fx_slot_effect(
+        ALenum effect_type);
+
+    void eax_set_fx_slot_effect();
+
+
+    void eax_set_efx_effect_slot_gain();
+
+    void eax_set_fx_slot_volume();
+
+
+    void eax_set_effect_slot_send_auto();
+
+    void eax_set_fx_slot_flags();
+
+
+    void eax_ensure_is_unlocked() const;
+
+
+    void eax_validate_fx_slot_effect(
+        const GUID& eax_effect_id);
+
+    void eax_validate_fx_slot_volume(
+        long eax_volume);
+
+    void eax_validate_fx_slot_lock(
+        long eax_lock);
+
+    void eax_validate_fx_slot_flags(
+        unsigned long eax_flags,
+        int eax_version);
+
+    void eax_validate_fx_slot_occlusion(
+        long eax_occlusion);
+
+    void eax_validate_fx_slot_occlusion_lf_ratio(
+        float eax_occlusion_lf_ratio);
+
+    void eax_validate_fx_slot_all(
+        const EAX40FXSLOTPROPERTIES& fx_slot,
+        int eax_version);
+
+    void eax_validate_fx_slot_all(
+        const EAX50FXSLOTPROPERTIES& fx_slot,
+        int eax_version);
+
+
+    void eax_set_fx_slot_effect(
+        const GUID& eax_effect_id);
+
+    void eax_set_fx_slot_volume(
+        long eax_volume);
+
+    void eax_set_fx_slot_lock(
+        long eax_lock);
+
+    void eax_set_fx_slot_flags(
+        unsigned long eax_flags);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_occlusion(
+        long eax_occlusion);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_occlusion_lf_ratio(
+        float eax_occlusion_lf_ratio);
+
+    void eax_set_fx_slot_all(
+        const EAX40FXSLOTPROPERTIES& eax_fx_slot);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_all(
+        const EAX50FXSLOTPROPERTIES& eax_fx_slot);
+
+
+    void eax_set_fx_slot_effect(
+        const EaxEaxCall& eax_call);
+
+    void eax_set_fx_slot_volume(
+        const EaxEaxCall& eax_call);
+
+    void eax_set_fx_slot_lock(
+        const EaxEaxCall& eax_call);
+
+    void eax_set_fx_slot_flags(
+        const EaxEaxCall& eax_call);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_occlusion(
+        const EaxEaxCall& eax_call);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_occlusion_lf_ratio(
+        const EaxEaxCall& eax_call);
+
+    // [[nodiscard]]
+    bool eax_set_fx_slot_all(
+        const EaxEaxCall& eax_call);
+
+    bool eax_set_fx_slot(
+        const EaxEaxCall& eax_call);
+
+    void eax_apply_deferred();
+
+    // [[nodiscard]]
+    bool eax_set(
+        const EaxEaxCall& eax_call);
+
+
+    void eax_dispatch_effect(
+        const EaxEaxCall& eax_call);
+
+
+    // `alAuxiliaryEffectSloti(effect_slot, AL_EFFECTSLOT_EFFECT, effect)`
+    void eax_set_effect_slot_effect(EaxEffect &effect);
+
+    // `alAuxiliaryEffectSloti(effect_slot, AL_EFFECTSLOT_AUXILIARY_SEND_AUTO, value)`
+    void eax_set_effect_slot_send_auto(bool is_send_auto);
+
+    // `alAuxiliaryEffectSlotf(effect_slot, AL_EFFECTSLOT_GAIN, gain)`
+    void eax_set_effect_slot_gain(ALfloat gain);
+
+public:
+    class EaxDeleter {
+    public:
+        void operator()(ALeffectslot *effect_slot);
+    }; // EaxAlEffectSlotDeleter
+#endif // ALSOFT_EAX
 };
 
 void UpdateAllEffectSlotProps(ALCcontext *context);
 
+#ifdef ALSOFT_EAX
+
+using EaxAlEffectSlotUPtr = std::unique_ptr<ALeffectslot, ALeffectslot::EaxDeleter>;
+
+
+EaxAlEffectSlotUPtr eax_create_al_effect_slot(
+    ALCcontext& context);
+
+void eax_delete_al_effect_slot(
+    ALCcontext& context,
+    ALeffectslot& effect_slot);
+#endif // ALSOFT_EAX
+
 #endif

+ 352 - 122
Engine/lib/openal-soft/al/buffer.cpp

@@ -35,6 +35,7 @@
 #include <mutex>
 #include <new>
 #include <numeric>
+#include <stdexcept>
 #include <utility>
 
 #include "AL/al.h"
@@ -43,16 +44,23 @@
 
 #include "albit.h"
 #include "albyte.h"
-#include "alcmain.h"
-#include "alcontext.h"
+#include "alc/context.h"
+#include "alc/device.h"
+#include "alc/inprogext.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "aloptional.h"
 #include "atomic.h"
 #include "core/except.h"
-#include "inprogext.h"
+#include "core/logging.h"
+#include "core/voice.h"
 #include "opthelpers.h"
 
+#ifdef ALSOFT_EAX
+#include "eax_globals.h"
+#include "eax_x_ram.h"
+#endif // ALSOFT_EAX
+
 
 namespace {
 
@@ -110,10 +118,10 @@ void DecodeIMA4Block(int16_t *dst, const al::byte *src, size_t numchans, size_t
 
     for(size_t c{0};c < numchans;c++)
     {
-        sample[c] = al::to_integer<int>(src[0]) | (al::to_integer<int>(src[1])<<8);
+        sample[c] = src[0] | (src[1]<<8);
         sample[c] = (sample[c]^0x8000) - 32768;
         src += 2;
-        index[c] = al::to_integer<int>(src[0]) | (al::to_integer<int>(src[1])<<8);
+        index[c] = src[0] | (src[1]<<8);
         index[c] = clampi((index[c]^0x8000) - 32768, 0, 88);
         src += 2;
 
@@ -126,8 +134,8 @@ void DecodeIMA4Block(int16_t *dst, const al::byte *src, size_t numchans, size_t
         {
             for(size_t c{0};c < numchans;c++)
             {
-                code[c] = al::to_integer<ALuint>(src[0]) | (al::to_integer<ALuint>(src[1])<< 8) |
-                    (al::to_integer<ALuint>(src[2])<<16) | (al::to_integer<ALuint>(src[3])<<24);
+                code[c] = ALuint{src[0]} | (ALuint{src[1]}<< 8) | (ALuint{src[2]}<<16)
+                    | (ALuint{src[3]}<<24);
                 src += 4;
             }
         }
@@ -156,25 +164,23 @@ void DecodeMSADPCMBlock(int16_t *dst, const al::byte *src, size_t numchans, size
 
     for(size_t c{0};c < numchans;c++)
     {
-        blockpred[c] = std::min<ALubyte>(al::to_integer<ALubyte>(src[0]), 6);
+        blockpred[c] = std::min<ALubyte>(src[0], 6);
         ++src;
     }
     for(size_t c{0};c < numchans;c++)
     {
-        delta[c] = al::to_integer<int>(src[0]) | (al::to_integer<int>(src[1])<<8);
+        delta[c] = src[0] | (src[1]<<8);
         delta[c] = (delta[c]^0x8000) - 32768;
         src += 2;
     }
     for(size_t c{0};c < numchans;c++)
     {
-        samples[c][0] = static_cast<ALshort>(al::to_integer<int>(src[0]) |
-            (al::to_integer<int>(src[1])<<8));
+        samples[c][0] = static_cast<ALshort>(src[0] | (src[1]<<8));
         src += 2;
     }
     for(size_t c{0};c < numchans;c++)
     {
-        samples[c][1] = static_cast<ALshort>(al::to_integer<int>(src[0]) |
-            (al::to_integer<int>(src[1])<<8));
+        samples[c][1] = static_cast<ALshort>(src[0] | (src[1]<<8));
         src += 2;
     }
 
@@ -198,13 +204,13 @@ void DecodeMSADPCMBlock(int16_t *dst, const al::byte *src, size_t numchans, size
 
             int pred{(samples[c][0]*MSADPCMAdaptionCoeff[blockpred[c]][0] +
                 samples[c][1]*MSADPCMAdaptionCoeff[blockpred[c]][1]) / 256};
-            pred += (al::to_integer<int>(nibble^0x08) - 0x08) * delta[c];
+            pred += ((nibble^0x08) - 0x08) * delta[c];
             pred  = clampi(pred, -32768, 32767);
 
             samples[c][1] = samples[c][0];
             samples[c][0] = static_cast<int16_t>(pred);
 
-            delta[c] = (MSADPCMAdaption[al::to_integer<ALubyte>(nibble)] * delta[c]) / 256;
+            delta[c] = (MSADPCMAdaption[nibble] * delta[c]) / 256;
             delta[c] = maxi(16, delta[c]);
 
             *(dst++) = static_cast<int16_t>(pred);
@@ -271,6 +277,9 @@ ALuint ChannelsFromUserFmt(UserFmtChannels chans, ALuint ambiorder) noexcept
     case UserFmtX71: return 8;
     case UserFmtBFormat2D: return (ambiorder*2) + 1;
     case UserFmtBFormat3D: return (ambiorder+1) * (ambiorder+1);
+    case UserFmtUHJ2: return 2;
+    case UserFmtUHJ3: return 3;
+    case UserFmtUHJ4: return 4;
     }
     return 0;
 }
@@ -310,11 +319,82 @@ ALenum EnumFromAmbiScaling(AmbiScaling scale)
     {
     case AmbiScaling::FuMa: return AL_FUMA_SOFT;
     case AmbiScaling::SN3D: return AL_SN3D_SOFT;
-    case AmbiScaling::N3D: return AL_SN3D_SOFT;
+    case AmbiScaling::N3D: return AL_N3D_SOFT;
+    case AmbiScaling::UHJ: break;
     }
     throw std::runtime_error{"Invalid AmbiScaling: "+std::to_string(int(scale))};
 }
 
+al::optional<FmtChannels> FmtFromUserFmt(UserFmtChannels chans)
+{
+    switch(chans)
+    {
+    case UserFmtMono: return al::make_optional(FmtMono);
+    case UserFmtStereo: return al::make_optional(FmtStereo);
+    case UserFmtRear: return al::make_optional(FmtRear);
+    case UserFmtQuad: return al::make_optional(FmtQuad);
+    case UserFmtX51: return al::make_optional(FmtX51);
+    case UserFmtX61: return al::make_optional(FmtX61);
+    case UserFmtX71: return al::make_optional(FmtX71);
+    case UserFmtBFormat2D: return al::make_optional(FmtBFormat2D);
+    case UserFmtBFormat3D: return al::make_optional(FmtBFormat3D);
+    case UserFmtUHJ2: return al::make_optional(FmtUHJ2);
+    case UserFmtUHJ3: return al::make_optional(FmtUHJ3);
+    case UserFmtUHJ4: return al::make_optional(FmtUHJ4);
+    }
+    return al::nullopt;
+}
+al::optional<FmtType> FmtFromUserFmt(UserFmtType type)
+{
+    switch(type)
+    {
+    case UserFmtUByte: return al::make_optional(FmtUByte);
+    case UserFmtShort: return al::make_optional(FmtShort);
+    case UserFmtFloat: return al::make_optional(FmtFloat);
+    case UserFmtDouble: return al::make_optional(FmtDouble);
+    case UserFmtMulaw: return al::make_optional(FmtMulaw);
+    case UserFmtAlaw: return al::make_optional(FmtAlaw);
+    /* ADPCM not handled here. */
+    case UserFmtIMA4: break;
+    case UserFmtMSADPCM: break;
+    }
+    return al::nullopt;
+}
+
+
+#ifdef ALSOFT_EAX
+bool eax_x_ram_check_availability(const ALCdevice &device, const ALbuffer &buffer,
+    const ALuint newsize) noexcept
+{
+    ALuint freemem{device.eax_x_ram_free_size};
+    /* If the buffer is currently in "hardware", add its memory to the free
+     * pool since it'll be "replaced".
+     */
+    if(buffer.eax_x_ram_is_hardware)
+        freemem += buffer.OriginalSize;
+    return freemem >= newsize;
+}
+
+void eax_x_ram_apply(ALCdevice &device, ALbuffer &buffer) noexcept
+{
+    if(buffer.eax_x_ram_is_hardware)
+        return;
+
+    if(device.eax_x_ram_free_size >= buffer.OriginalSize)
+    {
+        device.eax_x_ram_free_size -= buffer.OriginalSize;
+        buffer.eax_x_ram_is_hardware = true;
+    }
+}
+
+void eax_x_ram_clear(ALCdevice& al_device, ALbuffer& al_buffer)
+{
+    if(al_buffer.eax_x_ram_is_hardware)
+        al_device.eax_x_ram_free_size += al_buffer.OriginalSize;
+    al_buffer.eax_x_ram_is_hardware = false;
+}
+#endif // ALSOFT_EAX
+
 
 constexpr ALbitfieldSOFT INVALID_STORAGE_MASK{~unsigned(AL_MAP_READ_BIT_SOFT |
     AL_MAP_WRITE_BIT_SOFT | AL_MAP_PERSISTENT_BIT_SOFT | AL_PRESERVE_DATA_BIT_SOFT)};
@@ -352,13 +432,12 @@ ALbuffer *AllocBuffer(ALCdevice *device)
 {
     auto sublist = std::find_if(device->BufferList.begin(), device->BufferList.end(),
         [](const BufferSubList &entry) noexcept -> bool
-        { return entry.FreeMask != 0; }
-    );
-
+        { return entry.FreeMask != 0; });
     auto lidx = static_cast<ALuint>(std::distance(device->BufferList.begin(), sublist));
     auto slidx = static_cast<ALuint>(al::countr_zero(sublist->FreeMask));
+    ASSUME(slidx < 64);
 
-    ALbuffer *buffer{::new (sublist->Buffers + slidx) ALbuffer{}};
+    ALbuffer *buffer{al::construct_at(sublist->Buffers + slidx)};
 
     /* Add 1 to avoid buffer ID 0. */
     buffer->id = ((lidx<<6) | slidx) + 1;
@@ -370,6 +449,10 @@ ALbuffer *AllocBuffer(ALCdevice *device)
 
 void FreeBuffer(ALCdevice *device, ALbuffer *buffer)
 {
+#ifdef ALSOFT_EAX
+    eax_x_ram_clear(*device, *buffer);
+#endif // ALSOFT_EAX
+
     const ALuint id{buffer->id - 1};
     const size_t lidx{id >> 6};
     const ALuint slidx{id & 0x3f};
@@ -453,46 +536,26 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
                       ALBuf->id);
 
     /* Currently no channel configurations need to be converted. */
-    FmtChannels DstChannels{FmtMono};
-    switch(SrcChannels)
-    {
-    case UserFmtMono: DstChannels = FmtMono; break;
-    case UserFmtStereo: DstChannels = FmtStereo; break;
-    case UserFmtRear: DstChannels = FmtRear; break;
-    case UserFmtQuad: DstChannels = FmtQuad; break;
-    case UserFmtX51: DstChannels = FmtX51; break;
-    case UserFmtX61: DstChannels = FmtX61; break;
-    case UserFmtX71: DstChannels = FmtX71; break;
-    case UserFmtBFormat2D: DstChannels = FmtBFormat2D; break;
-    case UserFmtBFormat3D: DstChannels = FmtBFormat3D; break;
-    }
-    if UNLIKELY(static_cast<long>(SrcChannels) != static_cast<long>(DstChannels))
+    auto DstChannels = FmtFromUserFmt(SrcChannels);
+    if UNLIKELY(!DstChannels)
         SETERR_RETURN(context, AL_INVALID_ENUM, , "Invalid format");
 
-    /* IMA4 and MSADPCM convert to 16-bit short. */
-    FmtType DstType{FmtUByte};
-    switch(SrcType)
-    {
-    case UserFmtUByte: DstType = FmtUByte; break;
-    case UserFmtShort: DstType = FmtShort; break;
-    case UserFmtFloat: DstType = FmtFloat; break;
-    case UserFmtDouble: DstType = FmtDouble; break;
-    case UserFmtAlaw: DstType = FmtAlaw; break;
-    case UserFmtMulaw: DstType = FmtMulaw; break;
-    case UserFmtIMA4: DstType = FmtShort; break;
-    case UserFmtMSADPCM: DstType = FmtShort; break;
-    }
-
-    /* TODO: Currently we can only map samples when they're not converted. To
+    /* IMA4 and MSADPCM convert to 16-bit short.
+     *
+     * TODO: Currently we can only map samples when they're not converted. To
      * allow it would need some kind of double-buffering to hold onto a copy of
      * the original data.
      */
     if((access&MAP_READ_WRITE_FLAGS))
     {
-        if UNLIKELY(static_cast<long>(SrcType) != static_cast<long>(DstType))
+        if UNLIKELY(SrcType == UserFmtIMA4 || SrcType == UserFmtMSADPCM)
             SETERR_RETURN(context, AL_INVALID_VALUE,, "%s samples cannot be mapped",
                 NameFromUserFmtType(SrcType));
     }
+    auto DstType = (SrcType == UserFmtIMA4 || SrcType == UserFmtMSADPCM)
+        ? al::make_optional(FmtShort) : FmtFromUserFmt(SrcType);
+    if UNLIKELY(!DstType)
+        SETERR_RETURN(context, AL_INVALID_ENUM, , "Invalid format");
 
     const ALuint unpackalign{ALBuf->UnpackAlign};
     const ALuint align{SanitizeAlignment(SrcType, unpackalign)};
@@ -500,13 +563,13 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
         SETERR_RETURN(context, AL_INVALID_VALUE,, "Invalid unpack alignment %u for %s samples",
             unpackalign, NameFromUserFmtType(SrcType));
 
-    const ALuint ambiorder{(DstChannels == FmtBFormat2D || DstChannels == FmtBFormat3D) ?
-        ALBuf->UnpackAmbiOrder : 0};
+    const ALuint ambiorder{IsBFormat(*DstChannels) ? ALBuf->UnpackAmbiOrder :
+        (IsUHJ(*DstChannels) ? 1 : 0)};
 
     if((access&AL_PRESERVE_DATA_BIT_SOFT))
     {
         /* Can only preserve data with the same format and alignment. */
-        if UNLIKELY(ALBuf->mChannels != DstChannels || ALBuf->OriginalType != SrcType)
+        if UNLIKELY(ALBuf->mChannels != *DstChannels || ALBuf->OriginalType != SrcType)
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Preserving data of mismatched format");
         if UNLIKELY(ALBuf->OriginalAlign != align)
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Preserving data of mismatched alignment");
@@ -534,13 +597,23 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
     /* Convert the sample frames to the number of bytes needed for internal
      * storage.
      */
-    ALuint NumChannels{ChannelsFromFmt(DstChannels, ambiorder)};
-    ALuint FrameSize{NumChannels * BytesFromFmt(DstType)};
+    ALuint NumChannels{ChannelsFromFmt(*DstChannels, ambiorder)};
+    ALuint FrameSize{NumChannels * BytesFromFmt(*DstType)};
     if UNLIKELY(frames > std::numeric_limits<size_t>::max()/FrameSize)
         SETERR_RETURN(context, AL_OUT_OF_MEMORY,,
             "Buffer size overflow, %d frames x %d bytes per frame", frames, FrameSize);
     size_t newsize{static_cast<size_t>(frames) * FrameSize};
 
+#ifdef ALSOFT_EAX
+    if(ALBuf->eax_x_ram_mode == AL_STORAGE_HARDWARE)
+    {
+        ALCdevice &device = *context->mALDevice;
+        if(!eax_x_ram_check_availability(device, *ALBuf, size))
+            SETERR_RETURN(context, AL_OUT_OF_MEMORY,,
+                "Out of X-RAM memory (avail: %u, needed: %u)", device.eax_x_ram_free_size, size);
+    }
+#endif
+
     /* Round up to the next 16-byte multiple. This could reallocate only when
      * increasing or the new size is less than half the current, but then the
      * buffer's AL_SIZE would not be very reliable for accounting buffer memory
@@ -561,7 +634,7 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
 
     if(SrcType == UserFmtIMA4)
     {
-        assert(DstType == FmtShort);
+        assert(*DstType == FmtShort);
         if(SrcData != nullptr && !ALBuf->mData.empty())
             Convert_int16_ima4(reinterpret_cast<int16_t*>(ALBuf->mData.data()), SrcData,
                 NumChannels, frames, align);
@@ -569,7 +642,7 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
     }
     else if(SrcType == UserFmtMSADPCM)
     {
-        assert(DstType == FmtShort);
+        assert(*DstType == FmtShort);
         if(SrcData != nullptr && !ALBuf->mData.empty())
             Convert_int16_msadpcm(reinterpret_cast<int16_t*>(ALBuf->mData.data()), SrcData,
                 NumChannels, frames, align);
@@ -577,7 +650,7 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
     }
     else
     {
-        assert(static_cast<long>(SrcType) == static_cast<long>(DstType));
+        assert(DstType.has_value());
         if(SrcData != nullptr && !ALBuf->mData.empty())
             std::copy_n(SrcData, frames*FrameSize, ALBuf->mData.begin());
         ALBuf->OriginalAlign = 1;
@@ -588,8 +661,8 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
     ALBuf->Access = access;
 
     ALBuf->mSampleRate = static_cast<ALuint>(freq);
-    ALBuf->mChannels = DstChannels;
-    ALBuf->mType = DstType;
+    ALBuf->mChannels = *DstChannels;
+    ALBuf->mType = *DstType;
     ALBuf->mAmbiOrder = ambiorder;
 
     ALBuf->mCallback = nullptr;
@@ -598,11 +671,16 @@ void LoadData(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq, ALuint size,
     ALBuf->mSampleLen = frames;
     ALBuf->mLoopStart = 0;
     ALBuf->mLoopEnd = ALBuf->mSampleLen;
+
+#ifdef ALSOFT_EAX
+    if(eax_g_is_enabled && ALBuf->eax_x_ram_mode != AL_STORAGE_ACCESSIBLE)
+        eax_x_ram_apply(*context->mALDevice, *ALBuf);
+#endif
 }
 
 /** Prepares the buffer to use the specified callback, using the specified format. */
 void PrepareCallback(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq,
-    UserFmtChannels SrcChannels, UserFmtType SrcType, LPALBUFFERCALLBACKTYPESOFT callback,
+    UserFmtChannels SrcChannels, UserFmtType SrcType, ALBUFFERCALLBACKTYPESOFT callback,
     void *userptr)
 {
     if UNLIKELY(ReadRef(ALBuf->ref) != 0 || ALBuf->MappedAccess != 0)
@@ -610,43 +688,25 @@ void PrepareCallback(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq,
             ALBuf->id);
 
     /* Currently no channel configurations need to be converted. */
-    FmtChannels DstChannels{FmtMono};
-    switch(SrcChannels)
-    {
-    case UserFmtMono: DstChannels = FmtMono; break;
-    case UserFmtStereo: DstChannels = FmtStereo; break;
-    case UserFmtRear: DstChannels = FmtRear; break;
-    case UserFmtQuad: DstChannels = FmtQuad; break;
-    case UserFmtX51: DstChannels = FmtX51; break;
-    case UserFmtX61: DstChannels = FmtX61; break;
-    case UserFmtX71: DstChannels = FmtX71; break;
-    case UserFmtBFormat2D: DstChannels = FmtBFormat2D; break;
-    case UserFmtBFormat3D: DstChannels = FmtBFormat3D; break;
-    }
-    if UNLIKELY(static_cast<long>(SrcChannels) != static_cast<long>(DstChannels))
+    auto DstChannels = FmtFromUserFmt(SrcChannels);
+    if UNLIKELY(!DstChannels)
         SETERR_RETURN(context, AL_INVALID_ENUM,, "Invalid format");
 
     /* IMA4 and MSADPCM convert to 16-bit short. Not supported with callbacks. */
-    FmtType DstType{FmtUByte};
-    switch(SrcType)
-    {
-    case UserFmtUByte: DstType = FmtUByte; break;
-    case UserFmtShort: DstType = FmtShort; break;
-    case UserFmtFloat: DstType = FmtFloat; break;
-    case UserFmtDouble: DstType = FmtDouble; break;
-    case UserFmtAlaw: DstType = FmtAlaw; break;
-    case UserFmtMulaw: DstType = FmtMulaw; break;
-    case UserFmtIMA4: DstType = FmtShort; break;
-    case UserFmtMSADPCM: DstType = FmtShort; break;
-    }
-    if UNLIKELY(static_cast<long>(SrcType) != static_cast<long>(DstType))
+    auto DstType = FmtFromUserFmt(SrcType);
+    if UNLIKELY(!DstType)
         SETERR_RETURN(context, AL_INVALID_ENUM,, "Unsupported callback format");
 
-    const ALuint ambiorder{(DstChannels == FmtBFormat2D || DstChannels == FmtBFormat3D) ?
-        ALBuf->UnpackAmbiOrder : 0};
+    const ALuint ambiorder{IsBFormat(*DstChannels) ? ALBuf->UnpackAmbiOrder :
+        (IsUHJ(*DstChannels) ? 1 : 0)};
 
-    al::vector<al::byte,16>(FrameSizeFromFmt(DstChannels, DstType, ambiorder) *
-        size_t{BufferLineSize + (MaxResamplerPadding>>1)}).swap(ALBuf->mData);
+    static constexpr uint line_size{BufferLineSize + MaxPostVoiceLoad};
+    al::vector<al::byte,16>(FrameSizeFromFmt(*DstChannels, *DstType, ambiorder) *
+        size_t{line_size}).swap(ALBuf->mData);
+
+#ifdef ALSOFT_EAX
+    eax_x_ram_clear(*context->mALDevice, *ALBuf);
+#endif
 
     ALBuf->mCallback = callback;
     ALBuf->mUserData = userptr;
@@ -657,8 +717,8 @@ void PrepareCallback(ALCcontext *context, ALbuffer *ALBuf, ALsizei freq,
     ALBuf->Access = 0;
 
     ALBuf->mSampleRate = static_cast<ALuint>(freq);
-    ALBuf->mChannels = DstChannels;
-    ALBuf->mType = DstType;
+    ALBuf->mChannels = *DstChannels;
+    ALBuf->mType = *DstType;
     ALBuf->mAmbiOrder = ambiorder;
 
     ALBuf->mSampleLen = 0;
@@ -675,7 +735,7 @@ al::optional<DecompResult> DecomposeUserFormat(ALenum format)
         UserFmtChannels channels;
         UserFmtType type;
     };
-    static const std::array<FormatMap,46> UserFmtList{{
+    static const std::array<FormatMap,55> UserFmtList{{
         { AL_FORMAT_MONO8,             UserFmtMono, UserFmtUByte   },
         { AL_FORMAT_MONO16,            UserFmtMono, UserFmtShort   },
         { AL_FORMAT_MONO_FLOAT32,      UserFmtMono, UserFmtFloat   },
@@ -731,6 +791,18 @@ al::optional<DecompResult> DecomposeUserFormat(ALenum format)
         { AL_FORMAT_BFORMAT3D_16,      UserFmtBFormat3D, UserFmtShort },
         { AL_FORMAT_BFORMAT3D_FLOAT32, UserFmtBFormat3D, UserFmtFloat },
         { AL_FORMAT_BFORMAT3D_MULAW,   UserFmtBFormat3D, UserFmtMulaw },
+
+        { AL_FORMAT_UHJ2CHN8_SOFT,        UserFmtUHJ2, UserFmtUByte },
+        { AL_FORMAT_UHJ2CHN16_SOFT,       UserFmtUHJ2, UserFmtShort },
+        { AL_FORMAT_UHJ2CHN_FLOAT32_SOFT, UserFmtUHJ2, UserFmtFloat },
+
+        { AL_FORMAT_UHJ3CHN8_SOFT,        UserFmtUHJ3, UserFmtUByte },
+        { AL_FORMAT_UHJ3CHN16_SOFT,       UserFmtUHJ3, UserFmtShort },
+        { AL_FORMAT_UHJ3CHN_FLOAT32_SOFT, UserFmtUHJ3, UserFmtFloat },
+
+        { AL_FORMAT_UHJ4CHN8_SOFT,        UserFmtUHJ4, UserFmtUByte },
+        { AL_FORMAT_UHJ4CHN16_SOFT,       UserFmtUHJ4, UserFmtShort },
+        { AL_FORMAT_UHJ4CHN_FLOAT32_SOFT, UserFmtUHJ4, UserFmtFloat },
     }};
 
     for(const auto &fmt : UserFmtList)
@@ -754,7 +826,7 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Generating %d buffers", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     if(!EnsureBuffers(device, static_cast<ALuint>(n)))
     {
@@ -794,7 +866,7 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Deleting %d buffers", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     /* First try to find any buffers that are invalid or in-use. */
@@ -834,7 +906,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if LIKELY(context)
     {
-        ALCdevice *device{context->mDevice.get()};
+        ALCdevice *device{context->mALDevice.get()};
         std::lock_guard<std::mutex> _{device->BufferLock};
         if(!buffer || LookupBuffer(device, buffer))
             return AL_TRUE;
@@ -855,7 +927,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -877,8 +949,10 @@ START_API_FUNC
         if UNLIKELY(!usrfmt)
             context->setError(AL_INVALID_ENUM, "Invalid format 0x%04x", format);
         else
+        {
             LoadData(context.get(), albuf, freq, static_cast<ALuint>(size), usrfmt->channels,
                 usrfmt->type, static_cast<const al::byte*>(data), flags);
+        }
     }
 }
 END_API_FUNC
@@ -889,7 +963,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return nullptr;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -942,7 +1016,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -965,7 +1039,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -997,7 +1071,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -1127,7 +1201,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1147,7 +1221,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1166,7 +1240,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1188,7 +1262,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -1250,7 +1324,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1283,7 +1357,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -1321,7 +1395,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -1343,7 +1417,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1371,7 +1445,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
@@ -1393,7 +1467,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     ALbuffer *albuf = LookupBuffer(device, buffer);
     if UNLIKELY(!albuf)
@@ -1450,7 +1524,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
         context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
@@ -1488,7 +1562,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     ALbuffer *albuf = LookupBuffer(device, buffer);
     if UNLIKELY(!albuf)
@@ -1510,13 +1584,13 @@ END_API_FUNC
 
 
 AL_API void AL_APIENTRY alBufferCallbackSOFT(ALuint buffer, ALenum format, ALsizei freq,
-    LPALBUFFERCALLBACKTYPESOFT callback, ALvoid *userptr, ALbitfieldSOFT flags)
+    ALBUFFERCALLBACKTYPESOFT callback, ALvoid *userptr)
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
 
     ALbuffer *albuf = LookupBuffer(device, buffer);
@@ -1526,8 +1600,6 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Invalid sample rate %d", freq);
     else if UNLIKELY(callback == nullptr)
         context->setError(AL_INVALID_VALUE, "NULL callback");
-    else if UNLIKELY(flags != 0)
-        context->setError(AL_INVALID_VALUE, "Invalid callback flags 0x%x", flags);
     else
     {
         auto usrfmt = DecomposeUserFormat(format);
@@ -1546,7 +1618,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     ALbuffer *albuf = LookupBuffer(device, buffer);
     if UNLIKELY(!albuf)
@@ -1574,7 +1646,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
         context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
@@ -1602,7 +1674,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->BufferLock};
     if UNLIKELY(LookupBuffer(device, buffer) == nullptr)
         context->setError(AL_INVALID_NAME, "Invalid buffer ID %u", buffer);
@@ -1630,3 +1702,161 @@ BufferSubList::~BufferSubList()
     al_free(Buffers);
     Buffers = nullptr;
 }
+
+
+#ifdef ALSOFT_EAX
+FORCE_ALIGN ALboolean AL_APIENTRY EAXSetBufferMode(ALsizei n, const ALuint* buffers, ALint value)
+START_API_FUNC
+{
+#define EAX_PREFIX "[EAXSetBufferMode] "
+
+    const auto context = ContextRef{GetContextRef()};
+    if(!context)
+    {
+        ERR(EAX_PREFIX "%s\n", "No current context.");
+        return ALC_FALSE;
+    }
+
+    if(!eax_g_is_enabled)
+    {
+        context->setError(AL_INVALID_OPERATION, EAX_PREFIX "%s", "EAX not enabled.");
+        return ALC_FALSE;
+    }
+
+    switch(value)
+    {
+    case AL_STORAGE_AUTOMATIC:
+    case AL_STORAGE_HARDWARE:
+    case AL_STORAGE_ACCESSIBLE:
+        break;
+
+    default:
+        context->setError(AL_INVALID_ENUM, EAX_PREFIX "Unsupported X-RAM mode 0x%x", value);
+        return ALC_FALSE;
+    }
+
+    if(n == 0)
+        return ALC_TRUE;
+
+    if(n < 0)
+    {
+        context->setError(AL_INVALID_VALUE, EAX_PREFIX "Buffer count %d out of range", n);
+        return ALC_FALSE;
+    }
+
+    if(!buffers)
+    {
+        context->setError(AL_INVALID_VALUE, EAX_PREFIX "%s", "Null AL buffers");
+        return ALC_FALSE;
+    }
+
+    auto device = context->mALDevice.get();
+    std::lock_guard<std::mutex> device_lock{device->BufferLock};
+    size_t total_needed{0};
+
+    // Validate the buffers.
+    //
+    for(auto i = 0;i < n;++i)
+    {
+        const auto buffer = buffers[i];
+        if(buffer == AL_NONE)
+            continue;
+
+        const auto al_buffer = LookupBuffer(device, buffer);
+        if (!al_buffer)
+        {
+            ERR(EAX_PREFIX "Invalid buffer ID %u.\n", buffer);
+            return ALC_FALSE;
+        }
+
+        /* TODO: Is the store location allowed to change for in-use buffers, or
+         * only when not set/queued on a source?
+         */
+
+        if(value == AL_STORAGE_HARDWARE && !al_buffer->eax_x_ram_is_hardware)
+        {
+            /* FIXME: This doesn't account for duplicate buffers. When the same
+             * buffer ID is specified multiple times in the provided list, it
+             * counts each instance as more memory that needs to fit in X-RAM.
+             */
+            if(unlikely(std::numeric_limits<size_t>::max()-al_buffer->OriginalSize < total_needed))
+            {
+                context->setError(AL_OUT_OF_MEMORY, EAX_PREFIX "Buffer size overflow (%u + %zu)\n",
+                    al_buffer->OriginalSize, total_needed);
+                return ALC_FALSE;
+            }
+            total_needed += al_buffer->OriginalSize;
+        }
+    }
+    if(total_needed > device->eax_x_ram_free_size)
+    {
+        context->setError(AL_INVALID_ENUM, EAX_PREFIX "Out of X-RAM memory (need: %zu, avail: %u)",
+            total_needed, device->eax_x_ram_free_size);
+        return ALC_FALSE;
+    }
+
+    // Update the mode.
+    //
+    for(auto i = 0;i < n;++i)
+    {
+        const auto buffer = buffers[i];
+        if(buffer == AL_NONE)
+            continue;
+
+        const auto al_buffer = LookupBuffer(device, buffer);
+        assert(al_buffer);
+
+        if(value != AL_STORAGE_ACCESSIBLE)
+            eax_x_ram_apply(*device, *al_buffer);
+        else
+            eax_x_ram_clear(*device, *al_buffer);
+        al_buffer->eax_x_ram_mode = value;
+    }
+
+    return AL_TRUE;
+
+#undef EAX_PREFIX
+}
+END_API_FUNC
+
+FORCE_ALIGN ALenum AL_APIENTRY EAXGetBufferMode(ALuint buffer, ALint* pReserved)
+START_API_FUNC
+{
+#define EAX_PREFIX "[EAXGetBufferMode] "
+
+    const auto context = ContextRef{GetContextRef()};
+    if(!context)
+    {
+        ERR(EAX_PREFIX "%s\n", "No current context.");
+        return AL_NONE;
+    }
+
+    if(!eax_g_is_enabled)
+    {
+        context->setError(AL_INVALID_OPERATION, EAX_PREFIX "%s", "EAX not enabled.");
+        return AL_NONE;
+    }
+
+    if(pReserved)
+    {
+        context->setError(AL_INVALID_VALUE, EAX_PREFIX "%s", "Non-null reserved parameter");
+        return AL_NONE;
+    }
+
+    auto device = context->mALDevice.get();
+    std::lock_guard<std::mutex> device_lock{device->BufferLock};
+
+    const auto al_buffer = LookupBuffer(device, buffer);
+    if(!al_buffer)
+    {
+        context->setError(AL_INVALID_NAME, EAX_PREFIX "Invalid buffer ID %u", buffer);
+        return AL_NONE;
+    }
+
+    return al_buffer->eax_x_ram_mode;
+
+#undef EAX_PREFIX
+}
+END_API_FUNC
+
+#endif // ALSOFT_EAX

+ 13 - 2
Engine/lib/openal-soft/al/buffer.h

@@ -6,12 +6,15 @@
 #include "AL/al.h"
 
 #include "albyte.h"
+#include "alc/inprogext.h"
 #include "almalloc.h"
 #include "atomic.h"
-#include "buffer_storage.h"
-#include "inprogext.h"
+#include "core/buffer_storage.h"
 #include "vector.h"
 
+#ifdef ALSOFT_EAX
+#include "eax_x_ram.h"
+#endif // ALSOFT_EAX
 
 /* User formats */
 enum UserFmtType : unsigned char {
@@ -35,6 +38,9 @@ enum UserFmtChannels : unsigned char {
     UserFmtX71 = FmtX71,
     UserFmtBFormat2D = FmtBFormat2D,
     UserFmtBFormat3D = FmtBFormat3D,
+    UserFmtUHJ2 = FmtUHJ2,
+    UserFmtUHJ3 = FmtUHJ3,
+    UserFmtUHJ4 = FmtUHJ4,
 };
 
 
@@ -65,6 +71,11 @@ struct ALbuffer : public BufferStorage {
     ALuint id{0};
 
     DISABLE_ALLOC()
+
+#ifdef ALSOFT_EAX
+    ALenum eax_x_ram_mode{AL_STORAGE_AUTOMATIC};
+    bool eax_x_ram_is_hardware{};
+#endif // ALSOFT_EAX
 };
 
 #endif

+ 1213 - 0
Engine/lib/openal-soft/al/eax_api.cpp

@@ -0,0 +1,1213 @@
+//
+// EAX API.
+//
+// Based on headers `eax[2-5].h` included in Doom 3 source code:
+// https://github.com/id-Software/DOOM-3/tree/master/neo/openal/include
+//
+
+#include "config.h"
+
+#include <algorithm>
+
+#include "al/eax_api.h"
+
+
+const GUID DSPROPSETID_EAX_ReverbProperties =
+{
+    0x4A4E6FC1,
+    0xC341,
+    0x11D1,
+    {0xB7, 0x3A, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
+};
+
+const GUID DSPROPSETID_EAXBUFFER_ReverbProperties =
+{
+    0x4A4E6FC0,
+    0xC341,
+    0x11D1,
+    {0xB7, 0x3A, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
+};
+
+const GUID DSPROPSETID_EAX20_ListenerProperties =
+{
+    0x306A6A8,
+    0xB224,
+    0x11D2,
+    {0x99, 0xE5, 0x00, 0x00, 0xE8, 0xD8, 0xC7, 0x22}
+};
+
+const GUID DSPROPSETID_EAX20_BufferProperties =
+{
+    0x306A6A7,
+    0xB224,
+    0x11D2,
+    {0x99, 0xE5, 0x00, 0x00, 0xE8, 0xD8, 0xC7, 0x22}
+};
+
+const GUID DSPROPSETID_EAX30_ListenerProperties =
+{
+    0xA8FA6882,
+    0xB476,
+    0x11D3,
+    {0xBD, 0xB9, 0x00, 0xC0, 0xF0, 0x2D, 0xDF, 0x87}
+};
+
+const GUID DSPROPSETID_EAX30_BufferProperties =
+{
+    0xA8FA6881,
+    0xB476,
+    0x11D3,
+    {0xBD, 0xB9, 0x00, 0xC0, 0xF0, 0x2D, 0xDF, 0x87}
+};
+
+const GUID EAX_NULL_GUID =
+{
+    0x00000000,
+    0x0000,
+    0x0000,
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+};
+
+const GUID EAX_PrimaryFXSlotID =
+{
+    0xF317866D,
+    0x924C,
+    0x450C,
+    {0x86, 0x1B, 0xE6, 0xDA, 0xA2, 0x5E, 0x7C, 0x20}
+};
+
+const GUID EAXPROPERTYID_EAX40_Context =
+{
+    0x1D4870AD,
+    0xDEF,
+    0x43C0,
+    {0xA4, 0xC, 0x52, 0x36, 0x32, 0x29, 0x63, 0x42}
+};
+
+const GUID EAXPROPERTYID_EAX50_Context =
+{
+    0x57E13437,
+    0xB932,
+    0x4AB2,
+    {0xB8, 0xBD, 0x52, 0x66, 0xC1, 0xA8, 0x87, 0xEE}
+};
+
+const GUID EAXPROPERTYID_EAX40_FXSlot0 =
+{
+    0xC4D79F1E,
+    0xF1AC,
+    0x436B,
+    {0xA8, 0x1D, 0xA7, 0x38, 0xE7, 0x04, 0x54, 0x69}
+};
+
+const GUID EAXPROPERTYID_EAX50_FXSlot0 =
+{
+    0x91F9590F,
+    0xC388,
+    0x407A,
+    {0x84, 0xB0, 0x1B, 0xAE, 0xE, 0xF7, 0x1A, 0xBC}
+};
+
+const GUID EAXPROPERTYID_EAX40_FXSlot1 =
+{
+    0x8C00E96,
+    0x74BE,
+    0x4491,
+    {0x93, 0xAA, 0xE8, 0xAD, 0x35, 0xA4, 0x91, 0x17}
+};
+
+const GUID EAXPROPERTYID_EAX50_FXSlot1 =
+{
+    0x8F5F7ACA,
+    0x9608,
+    0x4965,
+    {0x81, 0x37, 0x82, 0x13, 0xC7, 0xB9, 0xD9, 0xDE}
+};
+
+const GUID EAXPROPERTYID_EAX40_FXSlot2 =
+{
+    0x1D433B88,
+    0xF0F6,
+    0x4637,
+    {0x91, 0x9F, 0x60, 0xE7, 0xE0, 0x6B, 0x5E, 0xDD}
+};
+
+const GUID EAXPROPERTYID_EAX50_FXSlot2 =
+{
+    0x3C0F5252,
+    0x9834,
+    0x46F0,
+    {0xA1, 0xD8, 0x5B, 0x95, 0xC4, 0xA0, 0xA, 0x30}
+};
+
+const GUID EAXPROPERTYID_EAX40_FXSlot3 =
+{
+    0xEFFF08EA,
+    0xC7D8,
+    0x44AB,
+    {0x93, 0xAD, 0x6D, 0xBD, 0x5F, 0x91, 0x00, 0x64}
+};
+
+const GUID EAXPROPERTYID_EAX50_FXSlot3 =
+{
+    0xE2EB0EAA,
+    0xE806,
+    0x45E7,
+    {0x9F, 0x86, 0x06, 0xC1, 0x57, 0x1A, 0x6F, 0xA3}
+};
+
+const GUID EAXPROPERTYID_EAX40_Source =
+{
+    0x1B86B823,
+    0x22DF,
+    0x4EAE,
+    {0x8B, 0x3C, 0x12, 0x78, 0xCE, 0x54, 0x42, 0x27}
+};
+
+const GUID EAXPROPERTYID_EAX50_Source =
+{
+    0x5EDF82F0,
+    0x24A7,
+    0x4F38,
+    {0x8E, 0x64, 0x2F, 0x09, 0xCA, 0x05, 0xDE, 0xE1}
+};
+
+const GUID EAX_REVERB_EFFECT =
+{
+    0xCF95C8F,
+    0xA3CC,
+    0x4849,
+    {0xB0, 0xB6, 0x83, 0x2E, 0xCC, 0x18, 0x22, 0xDF}
+};
+
+const GUID EAX_AGCCOMPRESSOR_EFFECT =
+{
+    0xBFB7A01E,
+    0x7825,
+    0x4039,
+    {0x92, 0x7F, 0x03, 0xAA, 0xBD, 0xA0, 0xC5, 0x60}
+};
+
+const GUID EAX_AUTOWAH_EFFECT =
+{
+    0xEC3130C0,
+    0xAC7A,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_CHORUS_EFFECT =
+{
+    0xDE6D6FE0,
+    0xAC79,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_DISTORTION_EFFECT =
+{
+    0x975A4CE0,
+    0xAC7E,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_ECHO_EFFECT =
+{
+    0xE9F1BC0,
+    0xAC82,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_EQUALIZER_EFFECT =
+{
+    0x65F94CE0,
+    0x9793,
+    0x11D3,
+    {0x93, 0x9D, 0x00, 0xC0, 0xF0, 0x2D, 0xD6, 0xF0}
+};
+
+const GUID EAX_FLANGER_EFFECT =
+{
+    0xA70007C0,
+    0x7D2,
+    0x11D3,
+    {0x9B, 0x1E, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_FREQUENCYSHIFTER_EFFECT =
+{
+    0xDC3E1880,
+    0x9212,
+    0x11D3,
+    {0x93, 0x9D, 0x00, 0xC0, 0xF0, 0x2D, 0xD6, 0xF0}
+};
+
+const GUID EAX_VOCALMORPHER_EFFECT =
+{
+    0xE41CF10C,
+    0x3383,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_PITCHSHIFTER_EFFECT =
+{
+    0xE7905100,
+    0xAFB2,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+const GUID EAX_RINGMODULATOR_EFFECT =
+{
+    0xB89FE60,
+    0xAFB5,
+    0x11D2,
+    {0x88, 0xDD, 0x00, 0xA0, 0x24, 0xD1, 0x3C, 0xE1}
+};
+
+
+bool operator==(
+    const EAX40CONTEXTPROPERTIES& lhs,
+    const EAX40CONTEXTPROPERTIES& rhs) noexcept
+{
+    return
+        lhs.guidPrimaryFXSlotID == rhs.guidPrimaryFXSlotID &&
+        lhs.flDistanceFactor == rhs.flDistanceFactor &&
+        lhs.flAirAbsorptionHF == rhs.flAirAbsorptionHF &&
+        lhs.flHFReference == rhs.flHFReference;
+}
+
+bool operator==(
+    const EAX50CONTEXTPROPERTIES& lhs,
+    const EAX50CONTEXTPROPERTIES& rhs) noexcept
+{
+    return
+        static_cast<const EAX40CONTEXTPROPERTIES&>(lhs) == static_cast<const EAX40CONTEXTPROPERTIES&>(rhs) &&
+        lhs.flMacroFXFactor == rhs.flMacroFXFactor;
+}
+
+
+const GUID EAXCONTEXT_DEFAULTPRIMARYFXSLOTID = EAXPROPERTYID_EAX40_FXSlot0;
+
+bool operator==(
+    const EAX40FXSLOTPROPERTIES& lhs,
+    const EAX40FXSLOTPROPERTIES& rhs) noexcept
+{
+    return
+        lhs.guidLoadEffect == rhs.guidLoadEffect &&
+        lhs.lVolume == rhs.lVolume &&
+        lhs.lLock == rhs.lLock &&
+        lhs.ulFlags == rhs.ulFlags;
+}
+
+bool operator==(
+    const EAX50FXSLOTPROPERTIES& lhs,
+    const EAX50FXSLOTPROPERTIES& rhs) noexcept
+{
+    return
+        static_cast<const EAX40FXSLOTPROPERTIES&>(lhs) == static_cast<const EAX40FXSLOTPROPERTIES&>(rhs) &&
+        lhs.lOcclusion == rhs.lOcclusion &&
+        lhs.flOcclusionLFRatio == rhs.flOcclusionLFRatio;
+}
+
+const EAX50ACTIVEFXSLOTS EAX40SOURCE_DEFAULTACTIVEFXSLOTID = EAX50ACTIVEFXSLOTS
+{{
+    EAX_NULL_GUID,
+    EAXPROPERTYID_EAX40_FXSlot0,
+}};
+
+bool operator==(
+    const EAX50ACTIVEFXSLOTS& lhs,
+    const EAX50ACTIVEFXSLOTS& rhs) noexcept
+{
+    return std::equal(
+        std::cbegin(lhs.guidActiveFXSlots),
+        std::cend(lhs.guidActiveFXSlots),
+        std::begin(rhs.guidActiveFXSlots));
+}
+
+bool operator!=(
+    const EAX50ACTIVEFXSLOTS& lhs,
+    const EAX50ACTIVEFXSLOTS& rhs) noexcept
+{
+    return !(lhs == rhs);
+}
+
+
+const EAX50ACTIVEFXSLOTS EAX50SOURCE_3DDEFAULTACTIVEFXSLOTID = EAX50ACTIVEFXSLOTS
+{{
+    EAX_NULL_GUID,
+    EAX_PrimaryFXSlotID,
+    EAX_NULL_GUID,
+    EAX_NULL_GUID,
+}};
+
+
+const EAX50ACTIVEFXSLOTS EAX50SOURCE_2DDEFAULTACTIVEFXSLOTID = EAX50ACTIVEFXSLOTS
+{{
+    EAX_NULL_GUID,
+    EAX_NULL_GUID,
+    EAX_NULL_GUID,
+    EAX_NULL_GUID,
+}};
+
+bool operator==(
+    const EAXREVERBPROPERTIES& lhs,
+    const EAXREVERBPROPERTIES& rhs) noexcept
+{
+    return
+        lhs.ulEnvironment == rhs.ulEnvironment &&
+        lhs.flEnvironmentSize == rhs.flEnvironmentSize &&
+        lhs.flEnvironmentDiffusion == rhs.flEnvironmentDiffusion &&
+        lhs.lRoom == rhs.lRoom &&
+        lhs.lRoomHF == rhs.lRoomHF &&
+        lhs.lRoomLF == rhs.lRoomLF &&
+        lhs.flDecayTime == rhs.flDecayTime &&
+        lhs.flDecayHFRatio == rhs.flDecayHFRatio &&
+        lhs.flDecayLFRatio == rhs.flDecayLFRatio &&
+        lhs.lReflections == rhs.lReflections &&
+        lhs.flReflectionsDelay == rhs.flReflectionsDelay &&
+        lhs.vReflectionsPan == rhs.vReflectionsPan &&
+        lhs.lReverb == rhs.lReverb &&
+        lhs.flReverbDelay == rhs.flReverbDelay &&
+        lhs.vReverbPan == rhs.vReverbPan &&
+        lhs.flEchoTime == rhs.flEchoTime &&
+        lhs.flEchoDepth == rhs.flEchoDepth &&
+        lhs.flModulationTime == rhs.flModulationTime &&
+        lhs.flModulationDepth == rhs.flModulationDepth &&
+        lhs.flAirAbsorptionHF == rhs.flAirAbsorptionHF &&
+        lhs.flHFReference == rhs.flHFReference &&
+        lhs.flLFReference == rhs.flLFReference &&
+        lhs.flRoomRolloffFactor == rhs.flRoomRolloffFactor &&
+        lhs.ulFlags == rhs.ulFlags;
+}
+
+bool operator!=(
+    const EAXREVERBPROPERTIES& lhs,
+    const EAXREVERBPROPERTIES& rhs) noexcept
+{
+    return !(lhs == rhs);
+}
+
+
+namespace {
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_GENERIC =
+{
+    EAXREVERB_DEFAULTENVIRONMENT,
+    EAXREVERB_DEFAULTENVIRONMENTSIZE,
+    EAXREVERB_DEFAULTENVIRONMENTDIFFUSION,
+    EAXREVERB_DEFAULTROOM,
+    EAXREVERB_DEFAULTROOMHF,
+    EAXREVERB_DEFAULTROOMLF,
+    EAXREVERB_DEFAULTDECAYTIME,
+    EAXREVERB_DEFAULTDECAYHFRATIO,
+    EAXREVERB_DEFAULTDECAYLFRATIO,
+    EAXREVERB_DEFAULTREFLECTIONS,
+    EAXREVERB_DEFAULTREFLECTIONSDELAY,
+    EAXREVERB_DEFAULTREFLECTIONSPAN,
+    EAXREVERB_DEFAULTREVERB,
+    EAXREVERB_DEFAULTREVERBDELAY,
+    EAXREVERB_DEFAULTREVERBPAN,
+    EAXREVERB_DEFAULTECHOTIME,
+    EAXREVERB_DEFAULTECHODEPTH,
+    EAXREVERB_DEFAULTMODULATIONTIME,
+    EAXREVERB_DEFAULTMODULATIONDEPTH,
+    EAXREVERB_DEFAULTAIRABSORPTIONHF,
+    EAXREVERB_DEFAULTHFREFERENCE,
+    EAXREVERB_DEFAULTLFREFERENCE,
+    EAXREVERB_DEFAULTROOMROLLOFFFACTOR,
+    EAXREVERB_DEFAULTFLAGS,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_PADDEDCELL =
+{
+    EAX_ENVIRONMENT_PADDEDCELL,
+    1.4F,
+    1.0F,
+    -1'000L,
+    -6'000L,
+    0L,
+    0.17F,
+    0.10F,
+    1.0F,
+    -1'204L,
+    0.001F,
+    EAXVECTOR{},
+    207L,
+    0.002F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_ROOM =
+{
+    EAX_ENVIRONMENT_ROOM,
+    1.9F,
+    1.0F,
+    -1'000L,
+    -454L,
+    0L,
+    0.40F,
+    0.83F,
+    1.0F,
+    -1'646L,
+    0.002F,
+    EAXVECTOR{},
+    53L,
+    0.003F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_BATHROOM =
+{
+    EAX_ENVIRONMENT_BATHROOM,
+    1.4F,
+    1.0F,
+    -1'000L,
+    -1'200L,
+    0L,
+    1.49F,
+    0.54F,
+    1.0F,
+    -370L,
+    0.007F,
+    EAXVECTOR{},
+    1'030L,
+    0.011F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_LIVINGROOM =
+{
+    EAX_ENVIRONMENT_LIVINGROOM,
+    2.5F,
+    1.0F,
+    -1'000L,
+    -6'000L,
+    0L,
+    0.50F,
+    0.10F,
+    1.0F,
+    -1'376,
+    0.003F,
+    EAXVECTOR{},
+    -1'104L,
+    0.004F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_STONEROOM =
+{
+    EAX_ENVIRONMENT_STONEROOM,
+    11.6F,
+    1.0F,
+    -1'000L,
+    -300L,
+    0L,
+    2.31F,
+    0.64F,
+    1.0F,
+    -711L,
+    0.012F,
+    EAXVECTOR{},
+    83L,
+    0.017F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_AUDITORIUM =
+{
+    EAX_ENVIRONMENT_AUDITORIUM,
+    21.6F,
+    1.0F,
+    -1'000L,
+    -476L,
+    0L,
+    4.32F,
+    0.59F,
+    1.0F,
+    -789L,
+    0.020F,
+    EAXVECTOR{},
+    -289L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_CONCERTHALL =
+{
+    EAX_ENVIRONMENT_CONCERTHALL,
+    19.6F,
+    1.0F,
+    -1'000L,
+    -500L,
+    0L,
+    3.92F,
+    0.70F,
+    1.0F,
+    -1'230L,
+    0.020F,
+    EAXVECTOR{},
+    -2L,
+    0.029F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_CAVE =
+{
+    EAX_ENVIRONMENT_CAVE,
+    14.6F,
+    1.0F,
+    -1'000L,
+    0L,
+    0L,
+    2.91F,
+    1.30F,
+    1.0F,
+    -602L,
+    0.015F,
+    EAXVECTOR{},
+    -302L,
+    0.022F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_ARENA =
+{
+    EAX_ENVIRONMENT_ARENA,
+    36.2F,
+    1.0F,
+    -1'000L,
+    -698L,
+    0L,
+    7.24F,
+    0.33F,
+    1.0F,
+    -1'166L,
+    0.020F,
+    EAXVECTOR{},
+    16L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_HANGAR =
+{
+    EAX_ENVIRONMENT_HANGAR,
+    50.3F,
+    1.0F,
+    -1'000L,
+    -1'000L,
+    0L,
+    10.05F,
+    0.23F,
+    1.0F,
+    -602L,
+    0.020F,
+    EAXVECTOR{},
+    198L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_CARPETTEDHALLWAY =
+{
+    EAX_ENVIRONMENT_CARPETEDHALLWAY,
+    1.9F,
+    1.0F,
+    -1'000L,
+    -4'000L,
+    0L,
+    0.30F,
+    0.10F,
+    1.0F,
+    -1'831L,
+    0.002F,
+    EAXVECTOR{},
+    -1'630L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_HALLWAY =
+{
+    EAX_ENVIRONMENT_HALLWAY,
+    1.8F,
+    1.0F,
+    -1'000L,
+    -300L,
+    0L,
+    1.49F,
+    0.59F,
+    1.0F,
+    -1'219L,
+    0.007F,
+    EAXVECTOR{},
+    441L,
+    0.011F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_STONECORRIDOR =
+{
+    EAX_ENVIRONMENT_STONECORRIDOR,
+    13.5F,
+    1.0F,
+    -1'000L,
+    -237L,
+    0L,
+    2.70F,
+    0.79F,
+    1.0F,
+    -1'214L,
+    0.013F,
+    EAXVECTOR{},
+    395L,
+    0.020F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_ALLEY =
+{
+    EAX_ENVIRONMENT_ALLEY,
+    7.5F,
+    0.300F,
+    -1'000L,
+    -270L,
+    0L,
+    1.49F,
+    0.86F,
+    1.0F,
+    -1'204L,
+    0.007F,
+    EAXVECTOR{},
+    -4L,
+    0.011F,
+    EAXVECTOR{},
+    0.125F,
+    0.950F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_FOREST =
+{
+    EAX_ENVIRONMENT_FOREST,
+    38.0F,
+    0.300F,
+    -1'000L,
+    -3'300L,
+    0L,
+    1.49F,
+    0.54F,
+    1.0F,
+    -2'560L,
+    0.162F,
+    EAXVECTOR{},
+    -229L,
+    0.088F,
+    EAXVECTOR{},
+    0.125F,
+    1.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_CITY =
+{
+    EAX_ENVIRONMENT_CITY,
+    7.5F,
+    0.500F,
+    -1'000L,
+    -800L,
+    0L,
+    1.49F,
+    0.67F,
+    1.0F,
+    -2'273L,
+    0.007F,
+    EAXVECTOR{},
+    -1'691L,
+    0.011F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_MOUNTAINS =
+{
+    EAX_ENVIRONMENT_MOUNTAINS,
+    100.0F,
+    0.270F,
+    -1'000L,
+    -2'500L,
+    0L,
+    1.49F,
+    0.21F,
+    1.0F,
+    -2'780L,
+    0.300F,
+    EAXVECTOR{},
+    -1'434L,
+    0.100F,
+    EAXVECTOR{},
+    0.250F,
+    1.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_QUARRY =
+{
+    EAX_ENVIRONMENT_QUARRY,
+    17.5F,
+    1.0F,
+    -1'000L,
+    -1'000L,
+    0L,
+    1.49F,
+    0.83F,
+    1.0F,
+    -10'000L,
+    0.061F,
+    EAXVECTOR{},
+    500L,
+    0.025F,
+    EAXVECTOR{},
+    0.125F,
+    0.700F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_PLAIN =
+{
+    EAX_ENVIRONMENT_PLAIN,
+    42.5F,
+    0.210F,
+    -1'000L,
+    -2'000L,
+    0L,
+    1.49F,
+    0.50F,
+    1.0F,
+    -2'466L,
+    0.179F,
+    EAXVECTOR{},
+    -1'926L,
+    0.100F,
+    EAXVECTOR{},
+    0.250F,
+    1.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_PARKINGLOT =
+{
+    EAX_ENVIRONMENT_PARKINGLOT,
+    8.3F,
+    1.0F,
+    -1'000L,
+    0L,
+    0L,
+    1.65F,
+    1.50F,
+    1.0F,
+    -1'363L,
+    0.008F,
+    EAXVECTOR{},
+    -1'153L,
+    0.012F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_SEWERPIPE =
+{
+    EAX_ENVIRONMENT_SEWERPIPE,
+    1.7F,
+    0.800F,
+    -1'000L,
+    -1'000L,
+    0L,
+    2.81F,
+    0.14F,
+    1.0F,
+    429L,
+    0.014F,
+    EAXVECTOR{},
+    1'023L,
+    0.021F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    0.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_UNDERWATER =
+{
+    EAX_ENVIRONMENT_UNDERWATER,
+    1.8F,
+    1.0F,
+    -1'000L,
+    -4'000L,
+    0L,
+    1.49F,
+    0.10F,
+    1.0F,
+    -449L,
+    0.007F,
+    EAXVECTOR{},
+    1'700L,
+    0.011F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    1.180F,
+    0.348F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x3FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_DRUGGED =
+{
+    EAX_ENVIRONMENT_DRUGGED,
+    1.9F,
+    0.500F,
+    -1'000L,
+    0L,
+    0L,
+    8.39F,
+    1.39F,
+    1.0F,
+    -115L,
+    0.002F,
+    EAXVECTOR{},
+    985L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    0.250F,
+    1.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_DIZZY =
+{
+    EAX_ENVIRONMENT_DIZZY,
+    1.8F,
+    0.600F,
+    -1'000L,
+    -400L,
+    0L,
+    17.23F,
+    0.56F,
+    1.0F,
+    -1'713L,
+    0.020F,
+    EAXVECTOR{},
+    -613L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    1.0F,
+    0.810F,
+    0.310F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+constexpr EAXREVERBPROPERTIES EAXREVERB_PRESET_PSYCHOTIC =
+{
+    EAX_ENVIRONMENT_PSYCHOTIC,
+    1.0F,
+    0.500F,
+    -1'000L,
+    -151L,
+    0L,
+    7.56F,
+    0.91F,
+    1.0F,
+    -626L,
+    0.020F,
+    EAXVECTOR{},
+    774L,
+    0.030F,
+    EAXVECTOR{},
+    0.250F,
+    0.0F,
+    4.0F,
+    1.0F,
+    -5.0F,
+    5'000.0F,
+    250.0F,
+    0.0F,
+    0x1FUL,
+};
+
+} // namespace
+
+const EaxReverbPresets EAXREVERB_PRESETS{{
+    EAXREVERB_PRESET_GENERIC,
+    EAXREVERB_PRESET_PADDEDCELL,
+    EAXREVERB_PRESET_ROOM,
+    EAXREVERB_PRESET_BATHROOM,
+    EAXREVERB_PRESET_LIVINGROOM,
+    EAXREVERB_PRESET_STONEROOM,
+    EAXREVERB_PRESET_AUDITORIUM,
+    EAXREVERB_PRESET_CONCERTHALL,
+    EAXREVERB_PRESET_CAVE,
+    EAXREVERB_PRESET_ARENA,
+    EAXREVERB_PRESET_HANGAR,
+    EAXREVERB_PRESET_CARPETTEDHALLWAY,
+    EAXREVERB_PRESET_HALLWAY,
+    EAXREVERB_PRESET_STONECORRIDOR,
+    EAXREVERB_PRESET_ALLEY,
+    EAXREVERB_PRESET_FOREST,
+    EAXREVERB_PRESET_CITY,
+    EAXREVERB_PRESET_MOUNTAINS,
+    EAXREVERB_PRESET_QUARRY,
+    EAXREVERB_PRESET_PLAIN,
+    EAXREVERB_PRESET_PARKINGLOT,
+    EAXREVERB_PRESET_SEWERPIPE,
+    EAXREVERB_PRESET_UNDERWATER,
+    EAXREVERB_PRESET_DRUGGED,
+    EAXREVERB_PRESET_DIZZY,
+    EAXREVERB_PRESET_PSYCHOTIC,
+}};
+
+namespace {
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_GENERIC = {EAX_ENVIRONMENT_GENERIC, 0.5F, 1.493F, 0.5F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_PADDEDCELL = {EAX_ENVIRONMENT_PADDEDCELL, 0.25F, 0.1F, 0.0F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_ROOM = {EAX_ENVIRONMENT_ROOM, 0.417F, 0.4F, 0.666F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_BATHROOM = {EAX_ENVIRONMENT_BATHROOM, 0.653F, 1.499F, 0.166F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_LIVINGROOM = {EAX_ENVIRONMENT_LIVINGROOM, 0.208F, 0.478F, 0.0F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_STONEROOM = {EAX_ENVIRONMENT_STONEROOM, 0.5F, 2.309F, 0.888F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_AUDITORIUM = {EAX_ENVIRONMENT_AUDITORIUM, 0.403F, 4.279F, 0.5F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_CONCERTHALL = {EAX_ENVIRONMENT_CONCERTHALL, 0.5F, 3.961F, 0.5F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_CAVE = {EAX_ENVIRONMENT_CAVE, 0.5F, 2.886F, 1.304F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_ARENA = {EAX_ENVIRONMENT_ARENA, 0.361F, 7.284F, 0.332F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_HANGAR = {EAX_ENVIRONMENT_HANGAR, 0.5F, 10.0F, 0.3F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_CARPETTEDHALLWAY = {EAX_ENVIRONMENT_CARPETEDHALLWAY, 0.153F, 0.259F, 2.0F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_HALLWAY = {EAX_ENVIRONMENT_HALLWAY, 0.361F, 1.493F, 0.0F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_STONECORRIDOR = {EAX_ENVIRONMENT_STONECORRIDOR, 0.444F, 2.697F, 0.638F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_ALLEY = {EAX_ENVIRONMENT_ALLEY, 0.25F, 1.752F, 0.776F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_FOREST = {EAX_ENVIRONMENT_FOREST, 0.111F, 3.145F, 0.472F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_CITY = {EAX_ENVIRONMENT_CITY, 0.111F, 2.767F, 0.224F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_MOUNTAINS = {EAX_ENVIRONMENT_MOUNTAINS, 0.194F, 7.841F, 0.472F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_QUARRY = {EAX_ENVIRONMENT_QUARRY, 1.0F, 1.499F, 0.5F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_PLAIN = {EAX_ENVIRONMENT_PLAIN, 0.097F, 2.767F, 0.224F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_PARKINGLOT = {EAX_ENVIRONMENT_PARKINGLOT, 0.208F, 1.652F, 1.5F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_SEWERPIPE = {EAX_ENVIRONMENT_SEWERPIPE, 0.652F, 2.886F, 0.25F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_UNDERWATER = {EAX_ENVIRONMENT_UNDERWATER, 1.0F, 1.499F, 0.0F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_DRUGGED = {EAX_ENVIRONMENT_DRUGGED, 0.875F, 8.392F, 1.388F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_DIZZY = {EAX_ENVIRONMENT_DIZZY, 0.139F, 17.234F, 0.666F};
+constexpr EAX_REVERBPROPERTIES EAX1REVERB_PRESET_PSYCHOTIC = {EAX_ENVIRONMENT_PSYCHOTIC, 0.486F, 7.563F, 0.806F};
+} // namespace
+
+const Eax1ReverbPresets EAX1REVERB_PRESETS{{
+    EAX1REVERB_PRESET_GENERIC,
+    EAX1REVERB_PRESET_PADDEDCELL,
+    EAX1REVERB_PRESET_ROOM,
+    EAX1REVERB_PRESET_BATHROOM,
+    EAX1REVERB_PRESET_LIVINGROOM,
+    EAX1REVERB_PRESET_STONEROOM,
+    EAX1REVERB_PRESET_AUDITORIUM,
+    EAX1REVERB_PRESET_CONCERTHALL,
+    EAX1REVERB_PRESET_CAVE,
+    EAX1REVERB_PRESET_ARENA,
+    EAX1REVERB_PRESET_HANGAR,
+    EAX1REVERB_PRESET_CARPETTEDHALLWAY,
+    EAX1REVERB_PRESET_HALLWAY,
+    EAX1REVERB_PRESET_STONECORRIDOR,
+    EAX1REVERB_PRESET_ALLEY,
+    EAX1REVERB_PRESET_FOREST,
+    EAX1REVERB_PRESET_CITY,
+    EAX1REVERB_PRESET_MOUNTAINS,
+    EAX1REVERB_PRESET_QUARRY,
+    EAX1REVERB_PRESET_PLAIN,
+    EAX1REVERB_PRESET_PARKINGLOT,
+    EAX1REVERB_PRESET_SEWERPIPE,
+    EAX1REVERB_PRESET_UNDERWATER,
+    EAX1REVERB_PRESET_DRUGGED,
+    EAX1REVERB_PRESET_DIZZY,
+    EAX1REVERB_PRESET_PSYCHOTIC,
+}};

+ 1557 - 0
Engine/lib/openal-soft/al/eax_api.h

@@ -0,0 +1,1557 @@
+#ifndef EAX_API_INCLUDED
+#define EAX_API_INCLUDED
+
+
+//
+// EAX API.
+//
+// Based on headers `eax[2-5].h` included in Doom 3 source code:
+// https://github.com/id-Software/DOOM-3/tree/master/neo/openal/include
+//
+
+
+#include <cfloat>
+#include <cstdint>
+#include <cstring>
+
+#include <array>
+
+#include "AL/al.h"
+
+
+#ifndef GUID_DEFINED
+#define GUID_DEFINED
+typedef struct _GUID
+{
+    std::uint32_t Data1;
+    std::uint16_t Data2;
+    std::uint16_t Data3;
+    std::uint8_t Data4[8];
+} GUID;
+
+inline bool operator==(const GUID& lhs, const GUID& rhs) noexcept
+{
+    return std::memcmp(&lhs, &rhs, sizeof(GUID)) == 0;
+}
+
+inline bool operator!=(const GUID& lhs, const GUID& rhs) noexcept
+{
+    return !(lhs == rhs);
+}
+#endif // GUID_DEFINED
+
+
+extern const GUID DSPROPSETID_EAX_ReverbProperties;
+
+enum DSPROPERTY_EAX_REVERBPROPERTY : unsigned int
+{
+    DSPROPERTY_EAX_ALL,
+    DSPROPERTY_EAX_ENVIRONMENT,
+    DSPROPERTY_EAX_VOLUME,
+    DSPROPERTY_EAX_DECAYTIME,
+    DSPROPERTY_EAX_DAMPING,
+}; // DSPROPERTY_EAX_REVERBPROPERTY
+
+struct EAX_REVERBPROPERTIES
+{
+    unsigned long environment;
+    float fVolume;
+    float fDecayTime_sec;
+    float fDamping;
+}; // EAX_REVERBPROPERTIES
+
+inline bool operator==(const EAX_REVERBPROPERTIES& lhs, const EAX_REVERBPROPERTIES& rhs) noexcept
+{
+    return std::memcmp(&lhs, &rhs, sizeof(EAX_REVERBPROPERTIES)) == 0;
+}
+
+extern const GUID DSPROPSETID_EAXBUFFER_ReverbProperties;
+
+enum DSPROPERTY_EAXBUFFER_REVERBPROPERTY : unsigned int
+{
+    DSPROPERTY_EAXBUFFER_ALL,
+    DSPROPERTY_EAXBUFFER_REVERBMIX,
+}; // DSPROPERTY_EAXBUFFER_REVERBPROPERTY
+
+struct EAXBUFFER_REVERBPROPERTIES
+{
+    float fMix;
+};
+
+inline bool operator==(const EAXBUFFER_REVERBPROPERTIES& lhs, const EAXBUFFER_REVERBPROPERTIES& rhs) noexcept
+{
+    return lhs.fMix == rhs.fMix;
+}
+
+constexpr auto EAX_BUFFER_MINREVERBMIX = 0.0F;
+constexpr auto EAX_BUFFER_MAXREVERBMIX = 1.0F;
+constexpr auto EAX_REVERBMIX_USEDISTANCE = -1.0F;
+
+
+extern const GUID DSPROPSETID_EAX20_ListenerProperties;
+
+enum DSPROPERTY_EAX20_LISTENERPROPERTY :
+    unsigned int
+{
+    DSPROPERTY_EAX20LISTENER_NONE,
+    DSPROPERTY_EAX20LISTENER_ALLPARAMETERS,
+    DSPROPERTY_EAX20LISTENER_ROOM,
+    DSPROPERTY_EAX20LISTENER_ROOMHF,
+    DSPROPERTY_EAX20LISTENER_ROOMROLLOFFFACTOR,
+    DSPROPERTY_EAX20LISTENER_DECAYTIME,
+    DSPROPERTY_EAX20LISTENER_DECAYHFRATIO,
+    DSPROPERTY_EAX20LISTENER_REFLECTIONS,
+    DSPROPERTY_EAX20LISTENER_REFLECTIONSDELAY,
+    DSPROPERTY_EAX20LISTENER_REVERB,
+    DSPROPERTY_EAX20LISTENER_REVERBDELAY,
+    DSPROPERTY_EAX20LISTENER_ENVIRONMENT,
+    DSPROPERTY_EAX20LISTENER_ENVIRONMENTSIZE,
+    DSPROPERTY_EAX20LISTENER_ENVIRONMENTDIFFUSION,
+    DSPROPERTY_EAX20LISTENER_AIRABSORPTIONHF,
+    DSPROPERTY_EAX20LISTENER_FLAGS
+}; // DSPROPERTY_EAX20_LISTENERPROPERTY
+
+struct EAX20LISTENERPROPERTIES
+{
+    long lRoom; // room effect level at low frequencies
+    long lRoomHF; // room effect high-frequency level re. low frequency level
+    float flRoomRolloffFactor; // like DS3D flRolloffFactor but for room effect
+    float flDecayTime; // reverberation decay time at low frequencies
+    float flDecayHFRatio; // high-frequency to low-frequency decay time ratio
+    long lReflections; // early reflections level relative to room effect
+    float flReflectionsDelay; // initial reflection delay time
+    long lReverb; // late reverberation level relative to room effect
+    float flReverbDelay; // late reverberation delay time relative to initial reflection
+    unsigned long dwEnvironment; // sets all listener properties
+    float flEnvironmentSize; // environment size in meters
+    float flEnvironmentDiffusion; // environment diffusion
+    float flAirAbsorptionHF; // change in level per meter at 5 kHz
+    unsigned long dwFlags; // modifies the behavior of properties
+}; // EAX20LISTENERPROPERTIES
+
+
+extern const GUID DSPROPSETID_EAX20_BufferProperties;
+
+
+enum DSPROPERTY_EAX20_BUFFERPROPERTY :
+    unsigned int
+{
+    DSPROPERTY_EAX20BUFFER_NONE,
+    DSPROPERTY_EAX20BUFFER_ALLPARAMETERS,
+    DSPROPERTY_EAX20BUFFER_DIRECT,
+    DSPROPERTY_EAX20BUFFER_DIRECTHF,
+    DSPROPERTY_EAX20BUFFER_ROOM,
+    DSPROPERTY_EAX20BUFFER_ROOMHF,
+    DSPROPERTY_EAX20BUFFER_ROOMROLLOFFFACTOR,
+    DSPROPERTY_EAX20BUFFER_OBSTRUCTION,
+    DSPROPERTY_EAX20BUFFER_OBSTRUCTIONLFRATIO,
+    DSPROPERTY_EAX20BUFFER_OCCLUSION,
+    DSPROPERTY_EAX20BUFFER_OCCLUSIONLFRATIO,
+    DSPROPERTY_EAX20BUFFER_OCCLUSIONROOMRATIO,
+    DSPROPERTY_EAX20BUFFER_OUTSIDEVOLUMEHF,
+    DSPROPERTY_EAX20BUFFER_AIRABSORPTIONFACTOR,
+    DSPROPERTY_EAX20BUFFER_FLAGS
+}; // DSPROPERTY_EAX20_BUFFERPROPERTY
+
+
+struct EAX20BUFFERPROPERTIES
+{
+    long lDirect; // direct path level
+    long lDirectHF; // direct path level at high frequencies
+    long lRoom; // room effect level
+    long lRoomHF; // room effect level at high frequencies
+    float flRoomRolloffFactor; // like DS3D flRolloffFactor but for room effect
+    long lObstruction; // main obstruction control (attenuation at high frequencies) 
+    float flObstructionLFRatio; // obstruction low-frequency level re. main control
+    long lOcclusion; // main occlusion control (attenuation at high frequencies)
+    float flOcclusionLFRatio; // occlusion low-frequency level re. main control
+    float flOcclusionRoomRatio; // occlusion room effect level re. main control
+    long lOutsideVolumeHF; // outside sound cone level at high frequencies
+    float flAirAbsorptionFactor; // multiplies DSPROPERTY_EAXLISTENER_AIRABSORPTIONHF
+    unsigned long dwFlags; // modifies the behavior of properties
+}; // EAX20BUFFERPROPERTIES
+
+
+extern const GUID DSPROPSETID_EAX30_ListenerProperties;
+
+extern const GUID DSPROPSETID_EAX30_BufferProperties;
+
+
+constexpr auto EAX_MAX_FXSLOTS = 4;
+
+constexpr auto EAX40_MAX_ACTIVE_FXSLOTS = 2;
+constexpr auto EAX50_MAX_ACTIVE_FXSLOTS = 4;
+
+
+constexpr auto EAX_OK = 0L;
+constexpr auto EAXERR_INVALID_OPERATION = -1L;
+constexpr auto EAXERR_INVALID_VALUE = -2L;
+constexpr auto EAXERR_NO_EFFECT_LOADED = -3L;
+constexpr auto EAXERR_UNKNOWN_EFFECT = -4L;
+constexpr auto EAXERR_INCOMPATIBLE_SOURCE_TYPE = -5L;
+constexpr auto EAXERR_INCOMPATIBLE_EAX_VERSION = -6L;
+
+
+extern const GUID EAX_NULL_GUID;
+
+extern const GUID EAX_PrimaryFXSlotID;
+
+
+struct EAXVECTOR
+{
+    float x;
+    float y;
+    float z;
+}; // EAXVECTOR
+
+inline bool operator==(const EAXVECTOR& lhs, const EAXVECTOR& rhs) noexcept
+{ return std::memcmp(&lhs, &rhs, sizeof(EAXVECTOR)) == 0; }
+
+inline bool operator!=(const EAXVECTOR& lhs, const EAXVECTOR& rhs) noexcept
+{ return !(lhs == rhs); }
+
+
+extern const GUID EAXPROPERTYID_EAX40_Context;
+
+extern const GUID EAXPROPERTYID_EAX50_Context;
+
+// EAX50
+enum :
+    unsigned long
+{
+    HEADPHONES = 0,
+    SPEAKERS_2,
+    SPEAKERS_4,
+    SPEAKERS_5,	// 5.1 speakers
+    SPEAKERS_6, // 6.1 speakers
+    SPEAKERS_7, // 7.1 speakers
+};
+
+// EAX50
+enum :
+    unsigned long
+{
+    EAX_40 = 5, // EAX 4.0
+    EAX_50 = 6, // EAX 5.0
+};
+
+constexpr auto EAXCONTEXT_MINEAXSESSION = EAX_40;
+constexpr auto EAXCONTEXT_MAXEAXSESSION = EAX_50;
+constexpr auto EAXCONTEXT_DEFAULTEAXSESSION = EAX_40;
+
+constexpr auto EAXCONTEXT_MINMAXACTIVESENDS = 2UL;
+constexpr auto EAXCONTEXT_MAXMAXACTIVESENDS = 4UL;
+constexpr auto EAXCONTEXT_DEFAULTMAXACTIVESENDS = 2UL;
+
+// EAX50
+struct EAXSESSIONPROPERTIES
+{
+    unsigned long ulEAXVersion;
+    unsigned long ulMaxActiveSends;
+}; // EAXSESSIONPROPERTIES
+
+enum EAXCONTEXT_PROPERTY :
+    unsigned int
+{
+    EAXCONTEXT_NONE = 0,
+    EAXCONTEXT_ALLPARAMETERS,
+    EAXCONTEXT_PRIMARYFXSLOTID,
+    EAXCONTEXT_DISTANCEFACTOR,
+    EAXCONTEXT_AIRABSORPTIONHF,
+    EAXCONTEXT_HFREFERENCE,
+    EAXCONTEXT_LASTERROR,
+
+    // EAX50
+    EAXCONTEXT_SPEAKERCONFIG,
+    EAXCONTEXT_EAXSESSION,
+    EAXCONTEXT_MACROFXFACTOR,
+}; // EAXCONTEXT_PROPERTY
+
+struct EAX40CONTEXTPROPERTIES
+{
+    GUID guidPrimaryFXSlotID;
+    float flDistanceFactor;
+    float flAirAbsorptionHF;
+    float flHFReference;
+}; // EAX40CONTEXTPROPERTIES
+
+struct EAX50CONTEXTPROPERTIES :
+    public EAX40CONTEXTPROPERTIES
+{
+    float flMacroFXFactor;
+}; // EAX40CONTEXTPROPERTIES
+
+
+bool operator==(
+    const EAX40CONTEXTPROPERTIES& lhs,
+    const EAX40CONTEXTPROPERTIES& rhs) noexcept;
+
+bool operator==(
+    const EAX50CONTEXTPROPERTIES& lhs,
+    const EAX50CONTEXTPROPERTIES& rhs) noexcept;
+
+
+constexpr auto EAXCONTEXT_MINDISTANCEFACTOR = FLT_MIN;
+constexpr auto EAXCONTEXT_MAXDISTANCEFACTOR = FLT_MAX;
+constexpr auto EAXCONTEXT_DEFAULTDISTANCEFACTOR = 1.0F;
+
+constexpr auto EAXCONTEXT_MINAIRABSORPTIONHF = -100.0F;
+constexpr auto EAXCONTEXT_MAXAIRABSORPTIONHF = 0.0F;
+constexpr auto EAXCONTEXT_DEFAULTAIRABSORPTIONHF = -5.0F;
+
+constexpr auto EAXCONTEXT_MINHFREFERENCE = 1000.0F;
+constexpr auto EAXCONTEXT_MAXHFREFERENCE = 20000.0F;
+constexpr auto EAXCONTEXT_DEFAULTHFREFERENCE = 5000.0F;
+
+constexpr auto EAXCONTEXT_MINMACROFXFACTOR = 0.0F;
+constexpr auto EAXCONTEXT_MAXMACROFXFACTOR = 1.0F;
+constexpr auto EAXCONTEXT_DEFAULTMACROFXFACTOR = 0.0F;
+
+
+extern const GUID EAXPROPERTYID_EAX40_FXSlot0;
+
+extern const GUID EAXPROPERTYID_EAX50_FXSlot0;
+
+extern const GUID EAXPROPERTYID_EAX40_FXSlot1;
+
+extern const GUID EAXPROPERTYID_EAX50_FXSlot1;
+
+extern const GUID EAXPROPERTYID_EAX40_FXSlot2;
+
+extern const GUID EAXPROPERTYID_EAX50_FXSlot2;
+
+extern const GUID EAXPROPERTYID_EAX40_FXSlot3;
+
+extern const GUID EAXPROPERTYID_EAX50_FXSlot3;
+
+extern const GUID EAXCONTEXT_DEFAULTPRIMARYFXSLOTID;
+
+enum EAXFXSLOT_PROPERTY :
+    unsigned int
+{
+    EAXFXSLOT_PARAMETER = 0,
+
+    EAXFXSLOT_NONE = 0x10000,
+    EAXFXSLOT_ALLPARAMETERS,
+    EAXFXSLOT_LOADEFFECT,
+    EAXFXSLOT_VOLUME,
+    EAXFXSLOT_LOCK,
+    EAXFXSLOT_FLAGS,
+
+    // EAX50
+    EAXFXSLOT_OCCLUSION,
+    EAXFXSLOT_OCCLUSIONLFRATIO,
+}; // EAXFXSLOT_PROPERTY
+
+constexpr auto EAXFXSLOTFLAGS_ENVIRONMENT = 0x00000001UL;
+// EAX50
+constexpr auto EAXFXSLOTFLAGS_UPMIX = 0x00000002UL;
+
+constexpr auto EAX40FXSLOTFLAGS_RESERVED = 0xFFFFFFFEUL; // reserved future use
+constexpr auto EAX50FXSLOTFLAGS_RESERVED = 0xFFFFFFFCUL; // reserved future use
+
+
+constexpr auto EAXFXSLOT_MINVOLUME = -10'000L;
+constexpr auto EAXFXSLOT_MAXVOLUME = 0L;
+constexpr auto EAXFXSLOT_DEFAULTVOLUME = 0L;
+
+constexpr auto EAXFXSLOT_MINLOCK = 0L;
+constexpr auto EAXFXSLOT_MAXLOCK = 1L;
+
+enum :
+    long
+{
+    EAXFXSLOT_UNLOCKED = 0,
+    EAXFXSLOT_LOCKED = 1
+};
+
+constexpr auto EAXFXSLOT_MINOCCLUSION = -10'000L;
+constexpr auto EAXFXSLOT_MAXOCCLUSION = 0L;
+constexpr auto EAXFXSLOT_DEFAULTOCCLUSION = 0L;
+
+constexpr auto EAXFXSLOT_MINOCCLUSIONLFRATIO = 0.0F;
+constexpr auto EAXFXSLOT_MAXOCCLUSIONLFRATIO = 1.0F;
+constexpr auto EAXFXSLOT_DEFAULTOCCLUSIONLFRATIO = 0.25F;
+
+constexpr auto EAX40FXSLOT_DEFAULTFLAGS = EAXFXSLOTFLAGS_ENVIRONMENT;
+
+constexpr auto EAX50FXSLOT_DEFAULTFLAGS =
+    EAXFXSLOTFLAGS_ENVIRONMENT |
+    EAXFXSLOTFLAGS_UPMIX; // ignored for reverb;
+
+struct EAX40FXSLOTPROPERTIES
+{
+    GUID guidLoadEffect;
+    long lVolume;
+    long lLock;
+    unsigned long ulFlags;
+}; // EAX40FXSLOTPROPERTIES
+
+struct EAX50FXSLOTPROPERTIES :
+    public EAX40FXSLOTPROPERTIES
+{
+    long lOcclusion;
+    float flOcclusionLFRatio;
+}; // EAX50FXSLOTPROPERTIES
+
+bool operator==(
+    const EAX40FXSLOTPROPERTIES& lhs,
+    const EAX40FXSLOTPROPERTIES& rhs) noexcept;
+
+bool operator==(
+    const EAX50FXSLOTPROPERTIES& lhs,
+    const EAX50FXSLOTPROPERTIES& rhs) noexcept;
+
+extern const GUID EAXPROPERTYID_EAX40_Source;
+
+extern const GUID EAXPROPERTYID_EAX50_Source;
+
+// Source object properties
+enum EAXSOURCE_PROPERTY :
+    unsigned int
+{
+    // EAX30
+
+    EAXSOURCE_NONE,
+    EAXSOURCE_ALLPARAMETERS,
+    EAXSOURCE_OBSTRUCTIONPARAMETERS,
+    EAXSOURCE_OCCLUSIONPARAMETERS,
+    EAXSOURCE_EXCLUSIONPARAMETERS,
+    EAXSOURCE_DIRECT,
+    EAXSOURCE_DIRECTHF,
+    EAXSOURCE_ROOM,
+    EAXSOURCE_ROOMHF,
+    EAXSOURCE_OBSTRUCTION,
+    EAXSOURCE_OBSTRUCTIONLFRATIO,
+    EAXSOURCE_OCCLUSION,
+    EAXSOURCE_OCCLUSIONLFRATIO,
+    EAXSOURCE_OCCLUSIONROOMRATIO,
+    EAXSOURCE_OCCLUSIONDIRECTRATIO,
+    EAXSOURCE_EXCLUSION,
+    EAXSOURCE_EXCLUSIONLFRATIO,
+    EAXSOURCE_OUTSIDEVOLUMEHF,
+    EAXSOURCE_DOPPLERFACTOR,
+    EAXSOURCE_ROLLOFFFACTOR,
+    EAXSOURCE_ROOMROLLOFFFACTOR,
+    EAXSOURCE_AIRABSORPTIONFACTOR,
+    EAXSOURCE_FLAGS,
+
+    // EAX40
+
+    EAXSOURCE_SENDPARAMETERS,
+    EAXSOURCE_ALLSENDPARAMETERS,
+    EAXSOURCE_OCCLUSIONSENDPARAMETERS,
+    EAXSOURCE_EXCLUSIONSENDPARAMETERS,
+    EAXSOURCE_ACTIVEFXSLOTID,
+
+    // EAX50
+
+    EAXSOURCE_MACROFXFACTOR,
+    EAXSOURCE_SPEAKERLEVELS,
+    EAXSOURCE_ALL2DPARAMETERS,
+}; // EAXSOURCE_PROPERTY
+
+
+constexpr auto EAXSOURCEFLAGS_DIRECTHFAUTO = 0x00000001UL; // relates to EAXSOURCE_DIRECTHF
+constexpr auto EAXSOURCEFLAGS_ROOMAUTO = 0x00000002UL; // relates to EAXSOURCE_ROOM
+constexpr auto EAXSOURCEFLAGS_ROOMHFAUTO = 0x00000004UL; // relates to EAXSOURCE_ROOMHF
+// EAX50
+constexpr auto EAXSOURCEFLAGS_3DELEVATIONFILTER = 0x00000008UL;
+// EAX50
+constexpr auto EAXSOURCEFLAGS_UPMIX = 0x00000010UL;
+// EAX50
+constexpr auto EAXSOURCEFLAGS_APPLYSPEAKERLEVELS = 0x00000020UL;
+
+constexpr auto EAX20SOURCEFLAGS_RESERVED = 0xFFFFFFF8UL; // reserved future use
+constexpr auto EAX50SOURCEFLAGS_RESERVED = 0xFFFFFFC0UL; // reserved future use
+
+
+constexpr auto EAXSOURCE_MINSEND = -10'000L;
+constexpr auto EAXSOURCE_MAXSEND = 0L;
+constexpr auto EAXSOURCE_DEFAULTSEND = 0L;
+
+constexpr auto EAXSOURCE_MINSENDHF = -10'000L;
+constexpr auto EAXSOURCE_MAXSENDHF = 0L;
+constexpr auto EAXSOURCE_DEFAULTSENDHF = 0L;
+
+constexpr auto EAXSOURCE_MINDIRECT = -10'000L;
+constexpr auto EAXSOURCE_MAXDIRECT = 1'000L;
+constexpr auto EAXSOURCE_DEFAULTDIRECT = 0L;
+
+constexpr auto EAXSOURCE_MINDIRECTHF = -10'000L;
+constexpr auto EAXSOURCE_MAXDIRECTHF = 0L;
+constexpr auto EAXSOURCE_DEFAULTDIRECTHF = 0L;
+
+constexpr auto EAXSOURCE_MINROOM = -10'000L;
+constexpr auto EAXSOURCE_MAXROOM = 1'000L;
+constexpr auto EAXSOURCE_DEFAULTROOM = 0L;
+
+constexpr auto EAXSOURCE_MINROOMHF = -10'000L;
+constexpr auto EAXSOURCE_MAXROOMHF = 0L;
+constexpr auto EAXSOURCE_DEFAULTROOMHF = 0L;
+
+constexpr auto EAXSOURCE_MINOBSTRUCTION = -10'000L;
+constexpr auto EAXSOURCE_MAXOBSTRUCTION = 0L;
+constexpr auto EAXSOURCE_DEFAULTOBSTRUCTION = 0L;
+
+constexpr auto EAXSOURCE_MINOBSTRUCTIONLFRATIO = 0.0F;
+constexpr auto EAXSOURCE_MAXOBSTRUCTIONLFRATIO = 1.0F;
+constexpr auto EAXSOURCE_DEFAULTOBSTRUCTIONLFRATIO = 0.0F;
+
+constexpr auto EAXSOURCE_MINOCCLUSION = -10'000L;
+constexpr auto EAXSOURCE_MAXOCCLUSION = 0L;
+constexpr auto EAXSOURCE_DEFAULTOCCLUSION = 0L;
+
+constexpr auto EAXSOURCE_MINOCCLUSIONLFRATIO = 0.0F;
+constexpr auto EAXSOURCE_MAXOCCLUSIONLFRATIO = 1.0F;
+constexpr auto EAXSOURCE_DEFAULTOCCLUSIONLFRATIO = 0.25F;
+
+constexpr auto EAXSOURCE_MINOCCLUSIONROOMRATIO = 0.0F;
+constexpr auto EAXSOURCE_MAXOCCLUSIONROOMRATIO = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTOCCLUSIONROOMRATIO = 1.5F;
+
+constexpr auto EAXSOURCE_MINOCCLUSIONDIRECTRATIO = 0.0F;
+constexpr auto EAXSOURCE_MAXOCCLUSIONDIRECTRATIO = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTOCCLUSIONDIRECTRATIO = 1.0F;
+
+constexpr auto EAXSOURCE_MINEXCLUSION = -10'000L;
+constexpr auto EAXSOURCE_MAXEXCLUSION = 0L;
+constexpr auto EAXSOURCE_DEFAULTEXCLUSION = 0L;
+
+constexpr auto EAXSOURCE_MINEXCLUSIONLFRATIO = 0.0F;
+constexpr auto EAXSOURCE_MAXEXCLUSIONLFRATIO = 1.0F;
+constexpr auto EAXSOURCE_DEFAULTEXCLUSIONLFRATIO = 1.0F;
+
+constexpr auto EAXSOURCE_MINOUTSIDEVOLUMEHF = -10'000L;
+constexpr auto EAXSOURCE_MAXOUTSIDEVOLUMEHF = 0L;
+constexpr auto EAXSOURCE_DEFAULTOUTSIDEVOLUMEHF = 0L;
+
+constexpr auto EAXSOURCE_MINDOPPLERFACTOR = 0.0F;
+constexpr auto EAXSOURCE_MAXDOPPLERFACTOR = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTDOPPLERFACTOR = 1.0F;
+
+constexpr auto EAXSOURCE_MINROLLOFFFACTOR = 0.0F;
+constexpr auto EAXSOURCE_MAXROLLOFFFACTOR = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTROLLOFFFACTOR = 0.0F;
+
+constexpr auto EAXSOURCE_MINROOMROLLOFFFACTOR = 0.0F;
+constexpr auto EAXSOURCE_MAXROOMROLLOFFFACTOR = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTROOMROLLOFFFACTOR = 0.0F;
+
+constexpr auto EAXSOURCE_MINAIRABSORPTIONFACTOR = 0.0F;
+constexpr auto EAXSOURCE_MAXAIRABSORPTIONFACTOR = 10.0F;
+constexpr auto EAXSOURCE_DEFAULTAIRABSORPTIONFACTOR = 0.0F;
+
+// EAX50
+
+constexpr auto EAXSOURCE_MINMACROFXFACTOR = 0.0F;
+constexpr auto EAXSOURCE_MAXMACROFXFACTOR = 1.0F;
+constexpr auto EAXSOURCE_DEFAULTMACROFXFACTOR = 1.0F;
+
+// EAX50
+
+constexpr auto EAXSOURCE_MINSPEAKERLEVEL = -10'000L;
+constexpr auto EAXSOURCE_MAXSPEAKERLEVEL = 0L;
+constexpr auto EAXSOURCE_DEFAULTSPEAKERLEVEL = -10'000L;
+
+constexpr auto EAXSOURCE_DEFAULTFLAGS =
+    EAXSOURCEFLAGS_DIRECTHFAUTO |
+    EAXSOURCEFLAGS_ROOMAUTO |
+    EAXSOURCEFLAGS_ROOMHFAUTO;
+
+enum :
+    long
+{
+    EAXSPEAKER_FRONT_LEFT = 1,
+    EAXSPEAKER_FRONT_CENTER = 2,
+    EAXSPEAKER_FRONT_RIGHT = 3,
+    EAXSPEAKER_SIDE_RIGHT = 4,
+    EAXSPEAKER_REAR_RIGHT = 5,
+    EAXSPEAKER_REAR_CENTER = 6,
+    EAXSPEAKER_REAR_LEFT = 7,
+    EAXSPEAKER_SIDE_LEFT = 8,
+    EAXSPEAKER_LOW_FREQUENCY = 9
+};
+
+// EAXSOURCEFLAGS_DIRECTHFAUTO, EAXSOURCEFLAGS_ROOMAUTO and EAXSOURCEFLAGS_ROOMHFAUTO are ignored for 2D sources
+// EAXSOURCEFLAGS_UPMIX is ignored for 3D sources
+constexpr auto EAX50SOURCE_DEFAULTFLAGS =
+    EAXSOURCEFLAGS_DIRECTHFAUTO |
+    EAXSOURCEFLAGS_ROOMAUTO |
+    EAXSOURCEFLAGS_ROOMHFAUTO |
+    EAXSOURCEFLAGS_UPMIX;
+
+struct EAX30SOURCEPROPERTIES
+{
+    long lDirect; // direct path level (at low and mid frequencies)
+    long lDirectHF; // relative direct path level at high frequencies
+    long lRoom; // room effect level (at low and mid frequencies)
+    long lRoomHF; // relative room effect level at high frequencies
+    long lObstruction; // main obstruction control (attenuation at high frequencies) 
+    float flObstructionLFRatio; // obstruction low-frequency level re. main control
+    long lOcclusion; // main occlusion control (attenuation at high frequencies)
+    float flOcclusionLFRatio; // occlusion low-frequency level re. main control
+    float flOcclusionRoomRatio; // relative occlusion control for room effect
+    float flOcclusionDirectRatio; // relative occlusion control for direct path
+    long lExclusion; // main exlusion control (attenuation at high frequencies)
+    float flExclusionLFRatio; // exclusion low-frequency level re. main control
+    long lOutsideVolumeHF; // outside sound cone level at high frequencies
+    float flDopplerFactor; // like DS3D flDopplerFactor but per source
+    float flRolloffFactor; // like DS3D flRolloffFactor but per source
+    float flRoomRolloffFactor; // like DS3D flRolloffFactor but for room effect
+    float flAirAbsorptionFactor; // multiplies EAXREVERB_AIRABSORPTIONHF
+    unsigned long ulFlags; // modifies the behavior of properties
+}; // EAX30SOURCEPROPERTIES
+
+struct EAX50SOURCEPROPERTIES :
+    public EAX30SOURCEPROPERTIES
+{
+    float flMacroFXFactor;
+}; // EAX50SOURCEPROPERTIES
+
+struct EAXSOURCEALLSENDPROPERTIES
+{
+    GUID guidReceivingFXSlotID;
+    long lSend; // send level (at low and mid frequencies)
+    long lSendHF; // relative send level at high frequencies
+    long lOcclusion;
+    float flOcclusionLFRatio;
+    float flOcclusionRoomRatio;
+    float flOcclusionDirectRatio;
+    long lExclusion;
+    float flExclusionLFRatio;
+}; // EAXSOURCEALLSENDPROPERTIES
+
+struct EAXSOURCE2DPROPERTIES
+{
+    long lDirect; // direct path level (at low and mid frequencies)
+    long lDirectHF; // relative direct path level at high frequencies
+    long lRoom; // room effect level (at low and mid frequencies)
+    long lRoomHF; // relative room effect level at high frequencies
+    unsigned long ulFlags; // modifies the behavior of properties
+}; // EAXSOURCE2DPROPERTIES
+
+struct EAXSPEAKERLEVELPROPERTIES
+{
+    long lSpeakerID;
+    long lLevel;
+}; // EAXSPEAKERLEVELPROPERTIES
+
+struct EAX40ACTIVEFXSLOTS
+{
+    GUID guidActiveFXSlots[EAX40_MAX_ACTIVE_FXSLOTS];
+}; // EAX40ACTIVEFXSLOTS
+
+struct EAX50ACTIVEFXSLOTS
+{
+    GUID guidActiveFXSlots[EAX50_MAX_ACTIVE_FXSLOTS];
+}; // EAX50ACTIVEFXSLOTS
+
+bool operator==(
+    const EAX50ACTIVEFXSLOTS& lhs,
+    const EAX50ACTIVEFXSLOTS& rhs) noexcept;
+
+bool operator!=(
+    const EAX50ACTIVEFXSLOTS& lhs,
+    const EAX50ACTIVEFXSLOTS& rhs) noexcept;
+
+// Use this structure for EAXSOURCE_OBSTRUCTIONPARAMETERS property.
+struct EAXOBSTRUCTIONPROPERTIES
+{
+    long lObstruction;
+    float flObstructionLFRatio;
+}; // EAXOBSTRUCTIONPROPERTIES
+
+// Use this structure for EAXSOURCE_OCCLUSIONPARAMETERS property.
+struct EAXOCCLUSIONPROPERTIES
+{
+    long lOcclusion;
+    float flOcclusionLFRatio;
+    float flOcclusionRoomRatio;
+    float flOcclusionDirectRatio;
+}; // EAXOCCLUSIONPROPERTIES
+
+// Use this structure for EAXSOURCE_EXCLUSIONPARAMETERS property.
+struct EAXEXCLUSIONPROPERTIES
+{
+    long lExclusion;
+    float flExclusionLFRatio;
+}; // EAXEXCLUSIONPROPERTIES
+
+// Use this structure for EAXSOURCE_SENDPARAMETERS properties.
+struct EAXSOURCESENDPROPERTIES
+{
+    GUID guidReceivingFXSlotID;
+    long lSend;
+    long lSendHF;
+}; // EAXSOURCESENDPROPERTIES
+
+// Use this structure for EAXSOURCE_OCCLUSIONSENDPARAMETERS 
+struct EAXSOURCEOCCLUSIONSENDPROPERTIES
+{
+    GUID guidReceivingFXSlotID;
+    long lOcclusion;
+    float flOcclusionLFRatio;
+    float flOcclusionRoomRatio;
+    float flOcclusionDirectRatio;
+}; // EAXSOURCEOCCLUSIONSENDPROPERTIES
+
+// Use this structure for EAXSOURCE_EXCLUSIONSENDPARAMETERS
+struct EAXSOURCEEXCLUSIONSENDPROPERTIES
+{
+    GUID guidReceivingFXSlotID;
+    long lExclusion;
+    float flExclusionLFRatio;
+}; // EAXSOURCEEXCLUSIONSENDPROPERTIES
+
+extern const EAX50ACTIVEFXSLOTS EAX40SOURCE_DEFAULTACTIVEFXSLOTID;
+
+extern const EAX50ACTIVEFXSLOTS EAX50SOURCE_3DDEFAULTACTIVEFXSLOTID;
+
+extern const EAX50ACTIVEFXSLOTS EAX50SOURCE_2DDEFAULTACTIVEFXSLOTID;
+
+
+// EAX Reverb Effect
+
+extern const GUID EAX_REVERB_EFFECT;
+
+// Reverb effect properties
+enum EAXREVERB_PROPERTY :
+    unsigned int
+{
+    EAXREVERB_NONE,
+    EAXREVERB_ALLPARAMETERS,
+    EAXREVERB_ENVIRONMENT,
+    EAXREVERB_ENVIRONMENTSIZE,
+    EAXREVERB_ENVIRONMENTDIFFUSION,
+    EAXREVERB_ROOM,
+    EAXREVERB_ROOMHF,
+    EAXREVERB_ROOMLF,
+    EAXREVERB_DECAYTIME,
+    EAXREVERB_DECAYHFRATIO,
+    EAXREVERB_DECAYLFRATIO,
+    EAXREVERB_REFLECTIONS,
+    EAXREVERB_REFLECTIONSDELAY,
+    EAXREVERB_REFLECTIONSPAN,
+    EAXREVERB_REVERB,
+    EAXREVERB_REVERBDELAY,
+    EAXREVERB_REVERBPAN,
+    EAXREVERB_ECHOTIME,
+    EAXREVERB_ECHODEPTH,
+    EAXREVERB_MODULATIONTIME,
+    EAXREVERB_MODULATIONDEPTH,
+    EAXREVERB_AIRABSORPTIONHF,
+    EAXREVERB_HFREFERENCE,
+    EAXREVERB_LFREFERENCE,
+    EAXREVERB_ROOMROLLOFFFACTOR,
+    EAXREVERB_FLAGS,
+}; // EAXREVERB_PROPERTY
+
+// used by EAXREVERB_ENVIRONMENT
+enum :
+    unsigned long
+{
+    EAX_ENVIRONMENT_GENERIC,
+    EAX_ENVIRONMENT_PADDEDCELL,
+    EAX_ENVIRONMENT_ROOM,
+    EAX_ENVIRONMENT_BATHROOM,
+    EAX_ENVIRONMENT_LIVINGROOM,
+    EAX_ENVIRONMENT_STONEROOM,
+    EAX_ENVIRONMENT_AUDITORIUM,
+    EAX_ENVIRONMENT_CONCERTHALL,
+    EAX_ENVIRONMENT_CAVE,
+    EAX_ENVIRONMENT_ARENA,
+    EAX_ENVIRONMENT_HANGAR,
+    EAX_ENVIRONMENT_CARPETEDHALLWAY,
+    EAX_ENVIRONMENT_HALLWAY,
+    EAX_ENVIRONMENT_STONECORRIDOR,
+    EAX_ENVIRONMENT_ALLEY,
+    EAX_ENVIRONMENT_FOREST,
+    EAX_ENVIRONMENT_CITY,
+    EAX_ENVIRONMENT_MOUNTAINS,
+    EAX_ENVIRONMENT_QUARRY,
+    EAX_ENVIRONMENT_PLAIN,
+    EAX_ENVIRONMENT_PARKINGLOT,
+    EAX_ENVIRONMENT_SEWERPIPE,
+    EAX_ENVIRONMENT_UNDERWATER,
+    EAX_ENVIRONMENT_DRUGGED,
+    EAX_ENVIRONMENT_DIZZY,
+    EAX_ENVIRONMENT_PSYCHOTIC,
+
+    EAX1_ENVIRONMENT_COUNT,
+
+    // EAX30
+    EAX_ENVIRONMENT_UNDEFINED = EAX1_ENVIRONMENT_COUNT,
+
+    EAX3_ENVIRONMENT_COUNT,
+};
+
+
+// reverberation decay time
+constexpr auto EAXREVERBFLAGS_DECAYTIMESCALE = 0x00000001UL;
+
+// reflection level
+constexpr auto EAXREVERBFLAGS_REFLECTIONSSCALE = 0x00000002UL;
+
+// initial reflection delay time
+constexpr auto EAXREVERBFLAGS_REFLECTIONSDELAYSCALE = 0x00000004UL;
+
+// reflections level
+constexpr auto EAXREVERBFLAGS_REVERBSCALE = 0x00000008UL;
+
+// late reverberation delay time
+constexpr auto EAXREVERBFLAGS_REVERBDELAYSCALE = 0x00000010UL;
+
+// echo time
+// EAX30+
+constexpr auto EAXREVERBFLAGS_ECHOTIMESCALE = 0x00000040UL;
+
+// modulation time
+// EAX30+
+constexpr auto EAXREVERBFLAGS_MODULATIONTIMESCALE = 0x00000080UL;
+
+// This flag limits high-frequency decay time according to air absorption.
+constexpr auto EAXREVERBFLAGS_DECAYHFLIMIT = 0x00000020UL;
+
+constexpr auto EAXREVERBFLAGS_RESERVED = 0xFFFFFF00UL; // reserved future use
+
+
+struct EAXREVERBPROPERTIES
+{
+    unsigned long ulEnvironment; // sets all reverb properties
+    float flEnvironmentSize; // environment size in meters
+    float flEnvironmentDiffusion; // environment diffusion
+    long lRoom; // room effect level (at mid frequencies)
+    long lRoomHF; // relative room effect level at high frequencies
+    long lRoomLF; // relative room effect level at low frequencies  
+    float flDecayTime; // reverberation decay time at mid frequencies
+    float flDecayHFRatio; // high-frequency to mid-frequency decay time ratio
+    float flDecayLFRatio; // low-frequency to mid-frequency decay time ratio   
+    long lReflections; // early reflections level relative to room effect
+    float flReflectionsDelay; // initial reflection delay time
+    EAXVECTOR vReflectionsPan; // early reflections panning vector
+    long lReverb; // late reverberation level relative to room effect
+    float flReverbDelay; // late reverberation delay time relative to initial reflection
+    EAXVECTOR vReverbPan; // late reverberation panning vector
+    float flEchoTime; // echo time
+    float flEchoDepth; // echo depth
+    float flModulationTime; // modulation time
+    float flModulationDepth; // modulation depth
+    float flAirAbsorptionHF; // change in level per meter at high frequencies
+    float flHFReference; // reference high frequency
+    float flLFReference; // reference low frequency 
+    float flRoomRolloffFactor; // like DS3D flRolloffFactor but for room effect
+    unsigned long ulFlags; // modifies the behavior of properties
+}; // EAXREVERBPROPERTIES
+
+bool operator==(
+    const EAXREVERBPROPERTIES& lhs,
+    const EAXREVERBPROPERTIES& rhs) noexcept;
+
+bool operator!=(
+    const EAXREVERBPROPERTIES& lhs,
+    const EAXREVERBPROPERTIES& rhs) noexcept;
+
+
+constexpr auto EAXREVERB_MINENVIRONMENT = static_cast<unsigned long>(EAX_ENVIRONMENT_GENERIC);
+constexpr auto EAX1REVERB_MAXENVIRONMENT = static_cast<unsigned long>(EAX_ENVIRONMENT_PSYCHOTIC);
+constexpr auto EAX30REVERB_MAXENVIRONMENT = static_cast<unsigned long>(EAX_ENVIRONMENT_UNDEFINED);
+constexpr auto EAXREVERB_DEFAULTENVIRONMENT = static_cast<unsigned long>(EAX_ENVIRONMENT_GENERIC);
+
+constexpr auto EAXREVERB_MINENVIRONMENTSIZE = 1.0F;
+constexpr auto EAXREVERB_MAXENVIRONMENTSIZE = 100.0F;
+constexpr auto EAXREVERB_DEFAULTENVIRONMENTSIZE = 7.5F;
+
+constexpr auto EAXREVERB_MINENVIRONMENTDIFFUSION = 0.0F;
+constexpr auto EAXREVERB_MAXENVIRONMENTDIFFUSION = 1.0F;
+constexpr auto EAXREVERB_DEFAULTENVIRONMENTDIFFUSION = 1.0F;
+
+constexpr auto EAXREVERB_MINROOM = -10'000L;
+constexpr auto EAXREVERB_MAXROOM = 0L;
+constexpr auto EAXREVERB_DEFAULTROOM = -1'000L;
+
+constexpr auto EAXREVERB_MINROOMHF = -10'000L;
+constexpr auto EAXREVERB_MAXROOMHF = 0L;
+constexpr auto EAXREVERB_DEFAULTROOMHF = -100L;
+
+constexpr auto EAXREVERB_MINROOMLF = -10'000L;
+constexpr auto EAXREVERB_MAXROOMLF = 0L;
+constexpr auto EAXREVERB_DEFAULTROOMLF = 0L;
+
+constexpr auto EAXREVERB_MINDECAYTIME = 0.1F;
+constexpr auto EAXREVERB_MAXDECAYTIME = 20.0F;
+constexpr auto EAXREVERB_DEFAULTDECAYTIME = 1.49F;
+
+constexpr auto EAXREVERB_MINDECAYHFRATIO = 0.1F;
+constexpr auto EAXREVERB_MAXDECAYHFRATIO = 2.0F;
+constexpr auto EAXREVERB_DEFAULTDECAYHFRATIO = 0.83F;
+
+constexpr auto EAXREVERB_MINDECAYLFRATIO = 0.1F;
+constexpr auto EAXREVERB_MAXDECAYLFRATIO = 2.0F;
+constexpr auto EAXREVERB_DEFAULTDECAYLFRATIO = 1.0F;
+
+constexpr auto EAXREVERB_MINREFLECTIONS = -10'000L;
+constexpr auto EAXREVERB_MAXREFLECTIONS = 1'000L;
+constexpr auto EAXREVERB_DEFAULTREFLECTIONS = -2'602L;
+
+constexpr auto EAXREVERB_MINREFLECTIONSDELAY = 0.0F;
+constexpr auto EAXREVERB_MAXREFLECTIONSDELAY = 0.3F;
+constexpr auto EAXREVERB_DEFAULTREFLECTIONSDELAY = 0.007F;
+
+constexpr auto EAXREVERB_DEFAULTREFLECTIONSPAN = EAXVECTOR{0.0F, 0.0F, 0.0F};
+
+constexpr auto EAXREVERB_MINREVERB = -10'000L;
+constexpr auto EAXREVERB_MAXREVERB = 2'000L;
+constexpr auto EAXREVERB_DEFAULTREVERB = 200L;
+
+constexpr auto EAXREVERB_MINREVERBDELAY = 0.0F;
+constexpr auto EAXREVERB_MAXREVERBDELAY = 0.1F;
+constexpr auto EAXREVERB_DEFAULTREVERBDELAY = 0.011F;
+
+constexpr auto EAXREVERB_DEFAULTREVERBPAN = EAXVECTOR{0.0F, 0.0F, 0.0F};
+
+constexpr auto EAXREVERB_MINECHOTIME = 0.075F;
+constexpr auto EAXREVERB_MAXECHOTIME = 0.25F;
+constexpr auto EAXREVERB_DEFAULTECHOTIME = 0.25F;
+
+constexpr auto EAXREVERB_MINECHODEPTH = 0.0F;
+constexpr auto EAXREVERB_MAXECHODEPTH = 1.0F;
+constexpr auto EAXREVERB_DEFAULTECHODEPTH = 0.0F;
+
+constexpr auto EAXREVERB_MINMODULATIONTIME = 0.04F;
+constexpr auto EAXREVERB_MAXMODULATIONTIME = 4.0F;
+constexpr auto EAXREVERB_DEFAULTMODULATIONTIME = 0.25F;
+
+constexpr auto EAXREVERB_MINMODULATIONDEPTH = 0.0F;
+constexpr auto EAXREVERB_MAXMODULATIONDEPTH = 1.0F;
+constexpr auto EAXREVERB_DEFAULTMODULATIONDEPTH = 0.0F;
+
+constexpr auto EAXREVERB_MINAIRABSORPTIONHF = -100.0F;
+constexpr auto EAXREVERB_MAXAIRABSORPTIONHF = 0.0F;
+constexpr auto EAXREVERB_DEFAULTAIRABSORPTIONHF = -5.0F;
+
+constexpr auto EAXREVERB_MINHFREFERENCE = 1'000.0F;
+constexpr auto EAXREVERB_MAXHFREFERENCE = 20'000.0F;
+constexpr auto EAXREVERB_DEFAULTHFREFERENCE = 5'000.0F;
+
+constexpr auto EAXREVERB_MINLFREFERENCE = 20.0F;
+constexpr auto EAXREVERB_MAXLFREFERENCE = 1'000.0F;
+constexpr auto EAXREVERB_DEFAULTLFREFERENCE = 250.0F;
+
+constexpr auto EAXREVERB_MINROOMROLLOFFFACTOR = 0.0F;
+constexpr auto EAXREVERB_MAXROOMROLLOFFFACTOR = 10.0F;
+constexpr auto EAXREVERB_DEFAULTROOMROLLOFFFACTOR = 0.0F;
+
+constexpr auto EAX1REVERB_MINVOLUME = 0.0F;
+constexpr auto EAX1REVERB_MAXVOLUME = 1.0F;
+
+constexpr auto EAX1REVERB_MINDAMPING = 0.0F;
+constexpr auto EAX1REVERB_MAXDAMPING = 2.0F;
+
+constexpr auto EAXREVERB_DEFAULTFLAGS =
+    EAXREVERBFLAGS_DECAYTIMESCALE |
+    EAXREVERBFLAGS_REFLECTIONSSCALE |
+    EAXREVERBFLAGS_REFLECTIONSDELAYSCALE |
+    EAXREVERBFLAGS_REVERBSCALE |
+    EAXREVERBFLAGS_REVERBDELAYSCALE |
+    EAXREVERBFLAGS_DECAYHFLIMIT;
+
+
+using EaxReverbPresets = std::array<EAXREVERBPROPERTIES, EAX1_ENVIRONMENT_COUNT>;
+extern const EaxReverbPresets EAXREVERB_PRESETS;
+
+
+using Eax1ReverbPresets = std::array<EAX_REVERBPROPERTIES, EAX1_ENVIRONMENT_COUNT>;
+extern const Eax1ReverbPresets EAX1REVERB_PRESETS;
+
+
+// AGC Compressor Effect
+
+extern const GUID EAX_AGCCOMPRESSOR_EFFECT;
+
+enum EAXAGCCOMPRESSOR_PROPERTY :
+    unsigned int
+{
+    EAXAGCCOMPRESSOR_NONE,
+    EAXAGCCOMPRESSOR_ALLPARAMETERS,
+    EAXAGCCOMPRESSOR_ONOFF,
+}; // EAXAGCCOMPRESSOR_PROPERTY
+
+struct EAXAGCCOMPRESSORPROPERTIES
+{
+    unsigned long ulOnOff; // Switch Compressor on or off
+}; // EAXAGCCOMPRESSORPROPERTIES
+
+
+constexpr auto EAXAGCCOMPRESSOR_MINONOFF = 0UL;
+constexpr auto EAXAGCCOMPRESSOR_MAXONOFF = 1UL;
+constexpr auto EAXAGCCOMPRESSOR_DEFAULTONOFF = EAXAGCCOMPRESSOR_MAXONOFF;
+
+
+// Autowah Effect
+
+extern const GUID EAX_AUTOWAH_EFFECT;
+
+enum EAXAUTOWAH_PROPERTY :
+    unsigned int
+{
+    EAXAUTOWAH_NONE,
+    EAXAUTOWAH_ALLPARAMETERS,
+    EAXAUTOWAH_ATTACKTIME,
+    EAXAUTOWAH_RELEASETIME,
+    EAXAUTOWAH_RESONANCE,
+    EAXAUTOWAH_PEAKLEVEL,
+}; // EAXAUTOWAH_PROPERTY
+
+struct EAXAUTOWAHPROPERTIES
+{
+    float flAttackTime; // Attack time (seconds)
+    float flReleaseTime; // Release time (seconds)
+    long lResonance; // Resonance (mB)
+    long lPeakLevel; // Peak level (mB)
+}; // EAXAUTOWAHPROPERTIES
+
+
+constexpr auto EAXAUTOWAH_MINATTACKTIME = 0.0001F;
+constexpr auto EAXAUTOWAH_MAXATTACKTIME = 1.0F;
+constexpr auto EAXAUTOWAH_DEFAULTATTACKTIME = 0.06F;
+
+constexpr auto EAXAUTOWAH_MINRELEASETIME = 0.0001F;
+constexpr auto EAXAUTOWAH_MAXRELEASETIME = 1.0F;
+constexpr auto EAXAUTOWAH_DEFAULTRELEASETIME = 0.06F;
+
+constexpr auto EAXAUTOWAH_MINRESONANCE = 600L;
+constexpr auto EAXAUTOWAH_MAXRESONANCE = 6000L;
+constexpr auto EAXAUTOWAH_DEFAULTRESONANCE = 6000L;
+
+constexpr auto EAXAUTOWAH_MINPEAKLEVEL = -9000L;
+constexpr auto EAXAUTOWAH_MAXPEAKLEVEL = 9000L;
+constexpr auto EAXAUTOWAH_DEFAULTPEAKLEVEL = 2100L;
+
+
+// Chorus Effect
+
+extern const GUID EAX_CHORUS_EFFECT;
+
+
+enum EAXCHORUS_PROPERTY :
+    unsigned int
+{
+    EAXCHORUS_NONE,
+    EAXCHORUS_ALLPARAMETERS,
+    EAXCHORUS_WAVEFORM,
+    EAXCHORUS_PHASE,
+    EAXCHORUS_RATE,
+    EAXCHORUS_DEPTH,
+    EAXCHORUS_FEEDBACK,
+    EAXCHORUS_DELAY,
+}; // EAXCHORUS_PROPERTY
+
+enum :
+    unsigned long
+{
+    EAX_CHORUS_SINUSOID,
+    EAX_CHORUS_TRIANGLE,
+};
+
+struct EAXCHORUSPROPERTIES
+{
+    unsigned long ulWaveform; // Waveform selector - see enum above
+    long lPhase; // Phase (Degrees)
+    float flRate; // Rate (Hz)
+    float flDepth; // Depth (0 to 1)
+    float flFeedback; // Feedback (-1 to 1)
+    float flDelay; // Delay (seconds)
+}; // EAXCHORUSPROPERTIES
+
+
+constexpr auto EAXCHORUS_MINWAVEFORM = 0UL;
+constexpr auto EAXCHORUS_MAXWAVEFORM = 1UL;
+constexpr auto EAXCHORUS_DEFAULTWAVEFORM = 1UL;
+
+constexpr auto EAXCHORUS_MINPHASE = -180L;
+constexpr auto EAXCHORUS_MAXPHASE = 180L;
+constexpr auto EAXCHORUS_DEFAULTPHASE = 90L;
+
+constexpr auto EAXCHORUS_MINRATE = 0.0F;
+constexpr auto EAXCHORUS_MAXRATE = 10.0F;
+constexpr auto EAXCHORUS_DEFAULTRATE = 1.1F;
+
+constexpr auto EAXCHORUS_MINDEPTH = 0.0F;
+constexpr auto EAXCHORUS_MAXDEPTH = 1.0F;
+constexpr auto EAXCHORUS_DEFAULTDEPTH = 0.1F;
+
+constexpr auto EAXCHORUS_MINFEEDBACK = -1.0F;
+constexpr auto EAXCHORUS_MAXFEEDBACK = 1.0F;
+constexpr auto EAXCHORUS_DEFAULTFEEDBACK = 0.25F;
+
+constexpr auto EAXCHORUS_MINDELAY = 0.0002F;
+constexpr auto EAXCHORUS_MAXDELAY = 0.016F;
+constexpr auto EAXCHORUS_DEFAULTDELAY = 0.016F;
+
+
+// Distortion Effect
+
+extern const GUID EAX_DISTORTION_EFFECT;
+
+enum EAXDISTORTION_PROPERTY :
+    unsigned int
+{
+    EAXDISTORTION_NONE,
+    EAXDISTORTION_ALLPARAMETERS,
+    EAXDISTORTION_EDGE,
+    EAXDISTORTION_GAIN,
+    EAXDISTORTION_LOWPASSCUTOFF,
+    EAXDISTORTION_EQCENTER,
+    EAXDISTORTION_EQBANDWIDTH,
+}; // EAXDISTORTION_PROPERTY
+
+
+struct EAXDISTORTIONPROPERTIES
+{
+    float flEdge; // Controls the shape of the distortion (0 to 1)
+    long lGain; // Controls the post distortion gain (mB)
+    float flLowPassCutOff; // Controls the cut-off of the filter pre-distortion (Hz)
+    float flEQCenter; // Controls the center frequency of the EQ post-distortion (Hz)
+    float flEQBandwidth; // Controls the bandwidth of the EQ post-distortion (Hz)
+}; // EAXDISTORTIONPROPERTIES
+
+
+constexpr auto EAXDISTORTION_MINEDGE = 0.0F;
+constexpr auto EAXDISTORTION_MAXEDGE = 1.0F;
+constexpr auto EAXDISTORTION_DEFAULTEDGE = 0.2F;
+
+constexpr auto EAXDISTORTION_MINGAIN = -6000L;
+constexpr auto EAXDISTORTION_MAXGAIN = 0L;
+constexpr auto EAXDISTORTION_DEFAULTGAIN = -2600L;
+
+constexpr auto EAXDISTORTION_MINLOWPASSCUTOFF = 80.0F;
+constexpr auto EAXDISTORTION_MAXLOWPASSCUTOFF = 24000.0F;
+constexpr auto EAXDISTORTION_DEFAULTLOWPASSCUTOFF = 8000.0F;
+
+constexpr auto EAXDISTORTION_MINEQCENTER = 80.0F;
+constexpr auto EAXDISTORTION_MAXEQCENTER = 24000.0F;
+constexpr auto EAXDISTORTION_DEFAULTEQCENTER = 3600.0F;
+
+constexpr auto EAXDISTORTION_MINEQBANDWIDTH = 80.0F;
+constexpr auto EAXDISTORTION_MAXEQBANDWIDTH = 24000.0F;
+constexpr auto EAXDISTORTION_DEFAULTEQBANDWIDTH = 3600.0F;
+
+
+// Echo Effect
+
+extern const GUID EAX_ECHO_EFFECT;
+
+
+enum EAXECHO_PROPERTY :
+    unsigned int
+{
+    EAXECHO_NONE,
+    EAXECHO_ALLPARAMETERS,
+    EAXECHO_DELAY,
+    EAXECHO_LRDELAY,
+    EAXECHO_DAMPING,
+    EAXECHO_FEEDBACK,
+    EAXECHO_SPREAD,
+}; // EAXECHO_PROPERTY
+
+
+struct EAXECHOPROPERTIES
+{
+    float flDelay; // Controls the initial delay time (seconds)
+    float flLRDelay; // Controls the delay time between the first and second taps (seconds)
+    float flDamping; // Controls a low-pass filter that dampens the echoes (0 to 1)
+    float flFeedback; // Controls the duration of echo repetition (0 to 1)
+    float flSpread; // Controls the left-right spread of the echoes
+}; // EAXECHOPROPERTIES
+
+
+constexpr auto EAXECHO_MINDAMPING = 0.0F;
+constexpr auto EAXECHO_MAXDAMPING = 0.99F;
+constexpr auto EAXECHO_DEFAULTDAMPING = 0.5F;
+
+constexpr auto EAXECHO_MINDELAY = 0.002F;
+constexpr auto EAXECHO_MAXDELAY = 0.207F;
+constexpr auto EAXECHO_DEFAULTDELAY = 0.1F;
+
+constexpr auto EAXECHO_MINLRDELAY = 0.0F;
+constexpr auto EAXECHO_MAXLRDELAY = 0.404F;
+constexpr auto EAXECHO_DEFAULTLRDELAY = 0.1F;
+
+constexpr auto EAXECHO_MINFEEDBACK = 0.0F;
+constexpr auto EAXECHO_MAXFEEDBACK = 1.0F;
+constexpr auto EAXECHO_DEFAULTFEEDBACK = 0.5F;
+
+constexpr auto EAXECHO_MINSPREAD = -1.0F;
+constexpr auto EAXECHO_MAXSPREAD = 1.0F;
+constexpr auto EAXECHO_DEFAULTSPREAD = -1.0F;
+
+
+// Equalizer Effect
+
+extern const GUID EAX_EQUALIZER_EFFECT;
+
+
+enum EAXEQUALIZER_PROPERTY :
+    unsigned int
+{
+    EAXEQUALIZER_NONE,
+    EAXEQUALIZER_ALLPARAMETERS,
+    EAXEQUALIZER_LOWGAIN,
+    EAXEQUALIZER_LOWCUTOFF,
+    EAXEQUALIZER_MID1GAIN,
+    EAXEQUALIZER_MID1CENTER,
+    EAXEQUALIZER_MID1WIDTH,
+    EAXEQUALIZER_MID2GAIN,
+    EAXEQUALIZER_MID2CENTER,
+    EAXEQUALIZER_MID2WIDTH,
+    EAXEQUALIZER_HIGHGAIN,
+    EAXEQUALIZER_HIGHCUTOFF,
+}; // EAXEQUALIZER_PROPERTY
+
+
+struct EAXEQUALIZERPROPERTIES
+{
+    long lLowGain; // (mB)
+    float flLowCutOff; // (Hz)
+    long lMid1Gain; // (mB)
+    float flMid1Center; // (Hz)
+    float flMid1Width; // (octaves)
+    long lMid2Gain; // (mB)
+    float flMid2Center; // (Hz)
+    float flMid2Width; // (octaves)
+    long lHighGain; // (mB)
+    float flHighCutOff; // (Hz)
+}; // EAXEQUALIZERPROPERTIES
+
+
+constexpr auto EAXEQUALIZER_MINLOWGAIN = -1800L;
+constexpr auto EAXEQUALIZER_MAXLOWGAIN = 1800L;
+constexpr auto EAXEQUALIZER_DEFAULTLOWGAIN = 0L;
+
+constexpr auto EAXEQUALIZER_MINLOWCUTOFF = 50.0F;
+constexpr auto EAXEQUALIZER_MAXLOWCUTOFF = 800.0F;
+constexpr auto EAXEQUALIZER_DEFAULTLOWCUTOFF = 200.0F;
+
+constexpr auto EAXEQUALIZER_MINMID1GAIN = -1800L;
+constexpr auto EAXEQUALIZER_MAXMID1GAIN = 1800L;
+constexpr auto EAXEQUALIZER_DEFAULTMID1GAIN = 0L;
+
+constexpr auto EAXEQUALIZER_MINMID1CENTER = 200.0F;
+constexpr auto EAXEQUALIZER_MAXMID1CENTER = 3000.0F;
+constexpr auto EAXEQUALIZER_DEFAULTMID1CENTER = 500.0F;
+
+constexpr auto EAXEQUALIZER_MINMID1WIDTH = 0.01F;
+constexpr auto EAXEQUALIZER_MAXMID1WIDTH = 1.0F;
+constexpr auto EAXEQUALIZER_DEFAULTMID1WIDTH = 1.0F;
+
+constexpr auto EAXEQUALIZER_MINMID2GAIN = -1800L;
+constexpr auto EAXEQUALIZER_MAXMID2GAIN = 1800L;
+constexpr auto EAXEQUALIZER_DEFAULTMID2GAIN = 0L;
+
+constexpr auto EAXEQUALIZER_MINMID2CENTER = 1000.0F;
+constexpr auto EAXEQUALIZER_MAXMID2CENTER = 8000.0F;
+constexpr auto EAXEQUALIZER_DEFAULTMID2CENTER = 3000.0F;
+
+constexpr auto EAXEQUALIZER_MINMID2WIDTH = 0.01F;
+constexpr auto EAXEQUALIZER_MAXMID2WIDTH = 1.0F;
+constexpr auto EAXEQUALIZER_DEFAULTMID2WIDTH = 1.0F;
+
+constexpr auto EAXEQUALIZER_MINHIGHGAIN = -1800L;
+constexpr auto EAXEQUALIZER_MAXHIGHGAIN = 1800L;
+constexpr auto EAXEQUALIZER_DEFAULTHIGHGAIN = 0L;
+
+constexpr auto EAXEQUALIZER_MINHIGHCUTOFF = 4000.0F;
+constexpr auto EAXEQUALIZER_MAXHIGHCUTOFF = 16000.0F;
+constexpr auto EAXEQUALIZER_DEFAULTHIGHCUTOFF = 6000.0F;
+
+
+// Flanger Effect
+
+extern const GUID EAX_FLANGER_EFFECT;
+
+enum EAXFLANGER_PROPERTY :
+    unsigned int
+{
+    EAXFLANGER_NONE,
+    EAXFLANGER_ALLPARAMETERS,
+    EAXFLANGER_WAVEFORM,
+    EAXFLANGER_PHASE,
+    EAXFLANGER_RATE,
+    EAXFLANGER_DEPTH,
+    EAXFLANGER_FEEDBACK,
+    EAXFLANGER_DELAY,
+}; // EAXFLANGER_PROPERTY
+
+enum :
+    unsigned long
+{
+    EAX_FLANGER_SINUSOID,
+    EAX_FLANGER_TRIANGLE,
+};
+
+struct EAXFLANGERPROPERTIES
+{
+    unsigned long ulWaveform; // Waveform selector - see enum above
+    long lPhase; // Phase (Degrees)
+    float flRate; // Rate (Hz)
+    float flDepth; // Depth (0 to 1)
+    float flFeedback; // Feedback (0 to 1)
+    float flDelay; // Delay (seconds)
+}; // EAXFLANGERPROPERTIES
+
+
+constexpr auto EAXFLANGER_MINWAVEFORM = 0UL;
+constexpr auto EAXFLANGER_MAXWAVEFORM = 1UL;
+constexpr auto EAXFLANGER_DEFAULTWAVEFORM = 1UL;
+
+constexpr auto EAXFLANGER_MINPHASE = -180L;
+constexpr auto EAXFLANGER_MAXPHASE = 180L;
+constexpr auto EAXFLANGER_DEFAULTPHASE = 0L;
+
+constexpr auto EAXFLANGER_MINRATE = 0.0F;
+constexpr auto EAXFLANGER_MAXRATE = 10.0F;
+constexpr auto EAXFLANGER_DEFAULTRATE = 0.27F;
+
+constexpr auto EAXFLANGER_MINDEPTH = 0.0F;
+constexpr auto EAXFLANGER_MAXDEPTH = 1.0F;
+constexpr auto EAXFLANGER_DEFAULTDEPTH = 1.0F;
+
+constexpr auto EAXFLANGER_MINFEEDBACK = -1.0F;
+constexpr auto EAXFLANGER_MAXFEEDBACK = 1.0F;
+constexpr auto EAXFLANGER_DEFAULTFEEDBACK = -0.5F;
+
+constexpr auto EAXFLANGER_MINDELAY = 0.0002F;
+constexpr auto EAXFLANGER_MAXDELAY = 0.004F;
+constexpr auto EAXFLANGER_DEFAULTDELAY = 0.002F;
+
+
+// Frequency Shifter Effect
+
+extern const GUID EAX_FREQUENCYSHIFTER_EFFECT;
+
+enum EAXFREQUENCYSHIFTER_PROPERTY :
+    unsigned int
+{
+    EAXFREQUENCYSHIFTER_NONE,
+    EAXFREQUENCYSHIFTER_ALLPARAMETERS,
+    EAXFREQUENCYSHIFTER_FREQUENCY,
+    EAXFREQUENCYSHIFTER_LEFTDIRECTION,
+    EAXFREQUENCYSHIFTER_RIGHTDIRECTION,
+}; // EAXFREQUENCYSHIFTER_PROPERTY
+
+enum :
+    unsigned long
+{
+    EAX_FREQUENCYSHIFTER_DOWN,
+    EAX_FREQUENCYSHIFTER_UP,
+    EAX_FREQUENCYSHIFTER_OFF
+};
+
+struct EAXFREQUENCYSHIFTERPROPERTIES
+{
+    float flFrequency; // (Hz)
+    unsigned long ulLeftDirection; // see enum above
+    unsigned long ulRightDirection; // see enum above
+}; // EAXFREQUENCYSHIFTERPROPERTIES
+
+
+constexpr auto EAXFREQUENCYSHIFTER_MINFREQUENCY = 0.0F;
+constexpr auto EAXFREQUENCYSHIFTER_MAXFREQUENCY = 24000.0F;
+constexpr auto EAXFREQUENCYSHIFTER_DEFAULTFREQUENCY = EAXFREQUENCYSHIFTER_MINFREQUENCY;
+
+constexpr auto EAXFREQUENCYSHIFTER_MINLEFTDIRECTION = 0UL;
+constexpr auto EAXFREQUENCYSHIFTER_MAXLEFTDIRECTION = 2UL;
+constexpr auto EAXFREQUENCYSHIFTER_DEFAULTLEFTDIRECTION = EAXFREQUENCYSHIFTER_MINLEFTDIRECTION;
+
+constexpr auto EAXFREQUENCYSHIFTER_MINRIGHTDIRECTION = 0UL;
+constexpr auto EAXFREQUENCYSHIFTER_MAXRIGHTDIRECTION = 2UL;
+constexpr auto EAXFREQUENCYSHIFTER_DEFAULTRIGHTDIRECTION = EAXFREQUENCYSHIFTER_MINRIGHTDIRECTION;
+
+
+// Vocal Morpher Effect
+
+extern const GUID EAX_VOCALMORPHER_EFFECT;
+
+enum EAXVOCALMORPHER_PROPERTY :
+    unsigned int
+{
+    EAXVOCALMORPHER_NONE,
+    EAXVOCALMORPHER_ALLPARAMETERS,
+    EAXVOCALMORPHER_PHONEMEA,
+    EAXVOCALMORPHER_PHONEMEACOARSETUNING,
+    EAXVOCALMORPHER_PHONEMEB,
+    EAXVOCALMORPHER_PHONEMEBCOARSETUNING,
+    EAXVOCALMORPHER_WAVEFORM,
+    EAXVOCALMORPHER_RATE,
+}; // EAXVOCALMORPHER_PROPERTY
+
+enum :
+    unsigned long
+{
+    A,
+    E,
+    I,
+    O,
+    U,
+    AA,
+    AE,
+    AH,
+    AO,
+    EH,
+    ER,
+    IH,
+    IY,
+    UH,
+    UW,
+    B,
+    D,
+    F,
+    G,
+    J,
+    K,
+    L,
+    M,
+    N,
+    P,
+    R,
+    S,
+    T,
+    V,
+    Z,
+};
+
+enum :
+    unsigned long
+{
+    EAX_VOCALMORPHER_SINUSOID,
+    EAX_VOCALMORPHER_TRIANGLE,
+    EAX_VOCALMORPHER_SAWTOOTH
+};
+
+// Use this structure for EAXVOCALMORPHER_ALLPARAMETERS
+struct EAXVOCALMORPHERPROPERTIES
+{
+    unsigned long ulPhonemeA; // see enum above
+    long lPhonemeACoarseTuning; // (semitones)
+    unsigned long ulPhonemeB; // see enum above
+    long lPhonemeBCoarseTuning; // (semitones)
+    unsigned long ulWaveform; // Waveform selector - see enum above
+    float flRate; // (Hz)
+}; // EAXVOCALMORPHERPROPERTIES
+
+
+constexpr auto EAXVOCALMORPHER_MINPHONEMEA = 0UL;
+constexpr auto EAXVOCALMORPHER_MAXPHONEMEA = 29UL;
+constexpr auto EAXVOCALMORPHER_DEFAULTPHONEMEA = EAXVOCALMORPHER_MINPHONEMEA;
+
+constexpr auto EAXVOCALMORPHER_MINPHONEMEACOARSETUNING = -24L;
+constexpr auto EAXVOCALMORPHER_MAXPHONEMEACOARSETUNING = 24L;
+constexpr auto EAXVOCALMORPHER_DEFAULTPHONEMEACOARSETUNING = 0L;
+
+constexpr auto EAXVOCALMORPHER_MINPHONEMEB = 0UL;
+constexpr auto EAXVOCALMORPHER_MAXPHONEMEB = 29UL;
+constexpr auto EAXVOCALMORPHER_DEFAULTPHONEMEB = 10UL;
+
+constexpr auto EAXVOCALMORPHER_MINPHONEMEBCOARSETUNING = -24L;
+constexpr auto EAXVOCALMORPHER_MAXPHONEMEBCOARSETUNING = 24L;
+constexpr auto EAXVOCALMORPHER_DEFAULTPHONEMEBCOARSETUNING = 0L;
+
+constexpr auto EAXVOCALMORPHER_MINWAVEFORM = 0UL;
+constexpr auto EAXVOCALMORPHER_MAXWAVEFORM = 2UL;
+constexpr auto EAXVOCALMORPHER_DEFAULTWAVEFORM = EAXVOCALMORPHER_MINWAVEFORM;
+
+constexpr auto EAXVOCALMORPHER_MINRATE = 0.0F;
+constexpr auto EAXVOCALMORPHER_MAXRATE = 10.0F;
+constexpr auto EAXVOCALMORPHER_DEFAULTRATE = 1.41F;
+
+
+// Pitch Shifter Effect
+
+extern const GUID EAX_PITCHSHIFTER_EFFECT;
+
+enum EAXPITCHSHIFTER_PROPERTY :
+    unsigned int
+{
+    EAXPITCHSHIFTER_NONE,
+    EAXPITCHSHIFTER_ALLPARAMETERS,
+    EAXPITCHSHIFTER_COARSETUNE,
+    EAXPITCHSHIFTER_FINETUNE,
+}; // EAXPITCHSHIFTER_PROPERTY
+
+struct EAXPITCHSHIFTERPROPERTIES
+{
+    long lCoarseTune; // Amount of pitch shift (semitones)
+    long lFineTune; // Amount of pitch shift (cents)
+}; // EAXPITCHSHIFTERPROPERTIES
+
+
+constexpr auto EAXPITCHSHIFTER_MINCOARSETUNE = -12L;
+constexpr auto EAXPITCHSHIFTER_MAXCOARSETUNE = 12L;
+constexpr auto EAXPITCHSHIFTER_DEFAULTCOARSETUNE = 12L;
+
+constexpr auto EAXPITCHSHIFTER_MINFINETUNE = -50L;
+constexpr auto EAXPITCHSHIFTER_MAXFINETUNE = 50L;
+constexpr auto EAXPITCHSHIFTER_DEFAULTFINETUNE = 0L;
+
+
+// Ring Modulator Effect
+
+extern const GUID EAX_RINGMODULATOR_EFFECT;
+
+enum EAXRINGMODULATOR_PROPERTY :
+    unsigned int
+{
+    EAXRINGMODULATOR_NONE,
+    EAXRINGMODULATOR_ALLPARAMETERS,
+    EAXRINGMODULATOR_FREQUENCY,
+    EAXRINGMODULATOR_HIGHPASSCUTOFF,
+    EAXRINGMODULATOR_WAVEFORM,
+}; // EAXRINGMODULATOR_PROPERTY
+
+enum :
+    unsigned long
+{
+    EAX_RINGMODULATOR_SINUSOID,
+    EAX_RINGMODULATOR_SAWTOOTH,
+    EAX_RINGMODULATOR_SQUARE,
+};
+
+// Use this structure for EAXRINGMODULATOR_ALLPARAMETERS
+struct EAXRINGMODULATORPROPERTIES
+{
+    float flFrequency; // Frequency of modulation (Hz)
+    float flHighPassCutOff; // Cut-off frequency of high-pass filter (Hz)
+    unsigned long ulWaveform; // Waveform selector - see enum above
+}; // EAXRINGMODULATORPROPERTIES
+
+
+constexpr auto EAXRINGMODULATOR_MINFREQUENCY = 0.0F;
+constexpr auto EAXRINGMODULATOR_MAXFREQUENCY = 8000.0F;
+constexpr auto EAXRINGMODULATOR_DEFAULTFREQUENCY = 440.0F;
+
+constexpr auto EAXRINGMODULATOR_MINHIGHPASSCUTOFF = 0.0F;
+constexpr auto EAXRINGMODULATOR_MAXHIGHPASSCUTOFF = 24000.0F;
+constexpr auto EAXRINGMODULATOR_DEFAULTHIGHPASSCUTOFF = 800.0F;
+
+constexpr auto EAXRINGMODULATOR_MINWAVEFORM = 0UL;
+constexpr auto EAXRINGMODULATOR_MAXWAVEFORM = 2UL;
+constexpr auto EAXRINGMODULATOR_DEFAULTWAVEFORM = EAXRINGMODULATOR_MINWAVEFORM;
+
+
+using LPEAXSET = ALenum(AL_APIENTRY*)(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size);
+
+using LPEAXGET = ALenum(AL_APIENTRY*)(
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size);
+
+
+#endif // !EAX_API_INCLUDED

+ 324 - 0
Engine/lib/openal-soft/al/eax_eax_call.cpp

@@ -0,0 +1,324 @@
+#include "config.h"
+
+#include "al/eax_eax_call.h"
+
+#include "al/eax_exception.h"
+
+
+namespace {
+
+constexpr auto deferred_flag = 0x80000000U;
+
+class EaxEaxCallException :
+    public EaxException
+{
+public:
+    explicit EaxEaxCallException(
+        const char* message)
+        :
+        EaxException{"EAX_EAX_CALL", message}
+    {
+    }
+}; // EaxEaxCallException
+
+} // namespace
+
+
+EaxEaxCall::EaxEaxCall(
+    bool is_get,
+    const GUID& property_set_guid,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size)
+    : is_get_{is_get}, version_{0}, property_set_id_{EaxEaxCallPropertySetId::none}
+    , property_id_{property_id & ~deferred_flag}, property_source_id_{property_source_id}
+    , property_buffer_{property_buffer}, property_size_{property_size}
+{
+    if (false)
+    {
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_Context)
+    {
+        version_ = 4;
+        property_set_id_ = EaxEaxCallPropertySetId::context;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_Context)
+    {
+        version_ = 5;
+        property_set_id_ = EaxEaxCallPropertySetId::context;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX20_ListenerProperties)
+    {
+        version_ = 2;
+        fx_slot_index_ = 0u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot_effect;
+        property_id_ = convert_eax_v2_0_listener_property_id(property_id_);
+    }
+    else if (property_set_guid == DSPROPSETID_EAX30_ListenerProperties)
+    {
+        version_ = 3;
+        fx_slot_index_ = 0u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot_effect;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot0)
+    {
+        version_ = 4;
+        fx_slot_index_ = 0u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot0)
+    {
+        version_ = 5;
+        fx_slot_index_ = 0u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot1)
+    {
+        version_ = 4;
+        fx_slot_index_ = 1u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot1)
+    {
+        version_ = 5;
+        fx_slot_index_ = 1u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot2)
+    {
+        version_ = 4;
+        fx_slot_index_ = 2u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot2)
+    {
+        version_ = 5;
+        fx_slot_index_ = 2u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_FXSlot3)
+    {
+        version_ = 4;
+        fx_slot_index_ = 3u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_FXSlot3)
+    {
+        version_ = 5;
+        fx_slot_index_ = 3u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX20_BufferProperties)
+    {
+        version_ = 2;
+        property_set_id_ = EaxEaxCallPropertySetId::source;
+        property_id_ = convert_eax_v2_0_buffer_property_id(property_id_);
+    }
+    else if (property_set_guid == DSPROPSETID_EAX30_BufferProperties)
+    {
+        version_ = 3;
+        property_set_id_ = EaxEaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX40_Source)
+    {
+        version_ = 4;
+        property_set_id_ = EaxEaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == EAXPROPERTYID_EAX50_Source)
+    {
+        version_ = 5;
+        property_set_id_ = EaxEaxCallPropertySetId::source;
+    }
+    else if (property_set_guid == DSPROPSETID_EAX_ReverbProperties)
+    {
+        version_ = 1;
+        fx_slot_index_ = 0u;
+        property_set_id_ = EaxEaxCallPropertySetId::fx_slot_effect;
+    }
+    else if (property_set_guid == DSPROPSETID_EAXBUFFER_ReverbProperties)
+    {
+        version_ = 1;
+        property_set_id_ = EaxEaxCallPropertySetId::source;
+    }
+    else
+    {
+        fail("Unsupported property set id.");
+    }
+
+    if (version_ < 1 || version_ > 5)
+    {
+        fail("EAX version out of range.");
+    }
+
+    if(!(property_id&deferred_flag))
+    {
+        if(property_set_id_ != EaxEaxCallPropertySetId::fx_slot && property_id_ != 0)
+        {
+            if (!property_buffer)
+            {
+                fail("Null property buffer.");
+            }
+
+            if (property_size == 0)
+            {
+                fail("Empty property.");
+            }
+        }
+    }
+
+    if(property_set_id_ == EaxEaxCallPropertySetId::source && property_source_id_ == 0)
+    {
+        fail("Null AL source id.");
+    }
+
+    if (property_set_id_ == EaxEaxCallPropertySetId::fx_slot)
+    {
+        if (property_id_ < EAXFXSLOT_NONE)
+        {
+            property_set_id_ = EaxEaxCallPropertySetId::fx_slot_effect;
+        }
+    }
+}
+
+[[noreturn]]
+void EaxEaxCall::fail(
+    const char* message)
+{
+    throw EaxEaxCallException{message};
+}
+
+ALuint EaxEaxCall::convert_eax_v2_0_listener_property_id(
+    ALuint property_id)
+{
+    switch (property_id)
+    {
+        case DSPROPERTY_EAX20LISTENER_NONE:
+            return EAXREVERB_NONE;
+
+        case DSPROPERTY_EAX20LISTENER_ALLPARAMETERS:
+            return EAXREVERB_ALLPARAMETERS;
+
+        case DSPROPERTY_EAX20LISTENER_ROOM:
+            return EAXREVERB_ROOM;
+
+        case DSPROPERTY_EAX20LISTENER_ROOMHF:
+            return EAXREVERB_ROOMHF;
+
+        case DSPROPERTY_EAX20LISTENER_ROOMROLLOFFFACTOR:
+            return EAXREVERB_ROOMROLLOFFFACTOR;
+
+        case DSPROPERTY_EAX20LISTENER_DECAYTIME:
+            return EAXREVERB_DECAYTIME;
+
+        case DSPROPERTY_EAX20LISTENER_DECAYHFRATIO:
+            return EAXREVERB_DECAYHFRATIO;
+
+        case DSPROPERTY_EAX20LISTENER_REFLECTIONS:
+            return EAXREVERB_REFLECTIONS;
+
+        case DSPROPERTY_EAX20LISTENER_REFLECTIONSDELAY:
+            return EAXREVERB_REFLECTIONSDELAY;
+
+        case DSPROPERTY_EAX20LISTENER_REVERB:
+            return EAXREVERB_REVERB;
+
+        case DSPROPERTY_EAX20LISTENER_REVERBDELAY:
+            return EAXREVERB_REVERBDELAY;
+
+        case DSPROPERTY_EAX20LISTENER_ENVIRONMENT:
+            return EAXREVERB_ENVIRONMENT;
+
+        case DSPROPERTY_EAX20LISTENER_ENVIRONMENTSIZE:
+            return EAXREVERB_ENVIRONMENTSIZE;
+
+        case DSPROPERTY_EAX20LISTENER_ENVIRONMENTDIFFUSION:
+            return EAXREVERB_ENVIRONMENTDIFFUSION;
+
+        case DSPROPERTY_EAX20LISTENER_AIRABSORPTIONHF:
+            return EAXREVERB_AIRABSORPTIONHF;
+
+        case DSPROPERTY_EAX20LISTENER_FLAGS:
+            return EAXREVERB_FLAGS;
+
+        default:
+            fail("Unsupported EAX 2.0 listener property id.");
+    }
+}
+
+ALuint EaxEaxCall::convert_eax_v2_0_buffer_property_id(
+    ALuint property_id)
+{
+    switch (property_id)
+    {
+        case DSPROPERTY_EAX20BUFFER_NONE:
+            return EAXSOURCE_NONE;
+
+        case DSPROPERTY_EAX20BUFFER_ALLPARAMETERS:
+            return EAXSOURCE_ALLPARAMETERS;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECT:
+            return EAXSOURCE_DIRECT;
+
+        case DSPROPERTY_EAX20BUFFER_DIRECTHF:
+            return EAXSOURCE_DIRECTHF;
+
+        case DSPROPERTY_EAX20BUFFER_ROOM:
+            return EAXSOURCE_ROOM;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMHF:
+            return EAXSOURCE_ROOMHF;
+
+        case DSPROPERTY_EAX20BUFFER_ROOMROLLOFFFACTOR:
+            return EAXSOURCE_ROOMROLLOFFFACTOR;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTION:
+            return EAXSOURCE_OBSTRUCTION;
+
+        case DSPROPERTY_EAX20BUFFER_OBSTRUCTIONLFRATIO:
+            return EAXSOURCE_OBSTRUCTIONLFRATIO;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSION:
+            return EAXSOURCE_OCCLUSION;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONLFRATIO:
+            return EAXSOURCE_OCCLUSIONLFRATIO;
+
+        case DSPROPERTY_EAX20BUFFER_OCCLUSIONROOMRATIO:
+            return EAXSOURCE_OCCLUSIONROOMRATIO;
+
+        case DSPROPERTY_EAX20BUFFER_OUTSIDEVOLUMEHF:
+            return EAXSOURCE_OUTSIDEVOLUMEHF;
+
+        case DSPROPERTY_EAX20BUFFER_AIRABSORPTIONFACTOR:
+            return EAXSOURCE_AIRABSORPTIONFACTOR;
+
+        case DSPROPERTY_EAX20BUFFER_FLAGS:
+            return EAXSOURCE_FLAGS;
+
+        default:
+            fail("Unsupported EAX 2.0 buffer property id.");
+    }
+}
+
+
+EaxEaxCall create_eax_call(
+    bool is_get,
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size)
+{
+    if(!property_set_id)
+        throw EaxEaxCallException{"Null property set ID."};
+
+    return EaxEaxCall{
+        is_get,
+        *property_set_id,
+        property_id,
+        property_source_id,
+        property_buffer,
+        property_size
+    };
+}

+ 117 - 0
Engine/lib/openal-soft/al/eax_eax_call.h

@@ -0,0 +1,117 @@
+#ifndef EAX_EAX_CALL_INCLUDED
+#define EAX_EAX_CALL_INCLUDED
+
+
+#include "AL/al.h"
+
+#include "alspan.h"
+
+#include "eax_api.h"
+#include "eax_fx_slot_index.h"
+
+
+enum class EaxEaxCallPropertySetId
+{
+    none,
+
+    context,
+    fx_slot,
+    source,
+    fx_slot_effect,
+}; // EaxEaxCallPropertySetId
+
+
+class EaxEaxCall
+{
+public:
+    EaxEaxCall(
+        bool is_get,
+        const GUID& property_set_guid,
+        ALuint property_id,
+        ALuint property_source_id,
+        ALvoid* property_buffer,
+        ALuint property_size);
+
+    bool is_get() const noexcept { return is_get_; }
+    int get_version() const noexcept { return version_; }
+    EaxEaxCallPropertySetId get_property_set_id() const noexcept { return property_set_id_; }
+    ALuint get_property_id() const noexcept { return property_id_; }
+    ALuint get_property_al_name() const noexcept { return property_source_id_; }
+    EaxFxSlotIndex get_fx_slot_index() const noexcept { return fx_slot_index_; }
+
+    template<
+        typename TException,
+        typename TValue
+    >
+    TValue& get_value() const
+    {
+        if (property_size_ < static_cast<ALuint>(sizeof(TValue)))
+        {
+            throw TException{"Property buffer too small."};
+        }
+
+        return *static_cast<TValue*>(property_buffer_);
+    }
+
+    template<
+        typename TException,
+        typename TValue
+    >
+    al::span<TValue> get_values() const
+    {
+        if (property_size_ < static_cast<ALuint>(sizeof(TValue)))
+        {
+            throw TException{"Property buffer too small."};
+        }
+
+        const auto count = property_size_ / sizeof(TValue);
+
+        return al::span<TValue>{static_cast<TValue*>(property_buffer_), count};
+    }
+
+    template<
+        typename TException,
+        typename TValue
+    >
+    void set_value(
+        const TValue& value) const
+    {
+        get_value<TException, TValue>() = value;
+    }
+
+
+private:
+    const bool is_get_;
+    int version_;
+    EaxFxSlotIndex fx_slot_index_;
+    EaxEaxCallPropertySetId property_set_id_;
+
+    ALuint property_id_;
+    const ALuint property_source_id_;
+    ALvoid*const property_buffer_;
+    const ALuint property_size_;
+
+
+    [[noreturn]]
+    static void fail(
+        const char* message);
+
+
+    static ALuint convert_eax_v2_0_listener_property_id(
+        ALuint property_id);
+
+    static ALuint convert_eax_v2_0_buffer_property_id(
+        ALuint property_id);
+}; // EaxEaxCall
+
+
+EaxEaxCall create_eax_call(
+    bool is_get,
+    const GUID* property_set_id,
+    ALuint property_id,
+    ALuint property_source_id,
+    ALvoid* property_buffer,
+    ALuint property_size);
+
+
+#endif // !EAX_EAX_CALL_INCLUDED

+ 3 - 0
Engine/lib/openal-soft/al/eax_effect.cpp

@@ -0,0 +1,3 @@
+#include "config.h"
+
+#include "eax_effect.h"

+ 44 - 0
Engine/lib/openal-soft/al/eax_effect.h

@@ -0,0 +1,44 @@
+#ifndef EAX_EFFECT_INCLUDED
+#define EAX_EFFECT_INCLUDED
+
+
+#include <memory>
+
+#include "AL/al.h"
+#include "core/effects/base.h"
+#include "eax_eax_call.h"
+
+class EaxEffect
+{
+public:
+    EaxEffect(ALenum type) : al_effect_type_{type} { }
+    virtual ~EaxEffect() = default;
+
+    const ALenum al_effect_type_;
+    EffectProps al_effect_props_{};
+
+    virtual void dispatch(const EaxEaxCall& eax_call) = 0;
+
+    // Returns "true" if any immediated property was changed.
+    // [[nodiscard]]
+    virtual bool apply_deferred() = 0;
+}; // EaxEffect
+
+
+using EaxEffectUPtr = std::unique_ptr<EaxEffect>;
+
+EaxEffectUPtr eax_create_eax_null_effect();
+EaxEffectUPtr eax_create_eax_chorus_effect();
+EaxEffectUPtr eax_create_eax_distortion_effect();
+EaxEffectUPtr eax_create_eax_echo_effect();
+EaxEffectUPtr eax_create_eax_flanger_effect();
+EaxEffectUPtr eax_create_eax_frequency_shifter_effect();
+EaxEffectUPtr eax_create_eax_vocal_morpher_effect();
+EaxEffectUPtr eax_create_eax_pitch_shifter_effect();
+EaxEffectUPtr eax_create_eax_ring_modulator_effect();
+EaxEffectUPtr eax_create_eax_auto_wah_effect();
+EaxEffectUPtr eax_create_eax_compressor_effect();
+EaxEffectUPtr eax_create_eax_equalizer_effect();
+EaxEffectUPtr eax_create_eax_reverb_effect();
+
+#endif // !EAX_EFFECT_INCLUDED

+ 63 - 0
Engine/lib/openal-soft/al/eax_exception.cpp

@@ -0,0 +1,63 @@
+#include "config.h"
+
+#include "eax_exception.h"
+
+#include <cassert>
+
+#include <string>
+
+
+EaxException::EaxException(
+    const char* context,
+    const char* message)
+    :
+    std::runtime_error{make_message(context, message)}
+{
+}
+
+std::string EaxException::make_message(
+    const char* context,
+    const char* message)
+{
+    const auto context_size = (context ? std::string::traits_type::length(context) : 0);
+    const auto has_contex = (context_size > 0);
+
+    const auto message_size = (message ? std::string::traits_type::length(message) : 0);
+    const auto has_message = (message_size > 0);
+
+    if (!has_contex && !has_message)
+    {
+        return std::string{};
+    }
+
+    static constexpr char left_prefix[] = "[";
+    const auto left_prefix_size = std::string::traits_type::length(left_prefix);
+
+    static constexpr char right_prefix[] = "] ";
+    const auto right_prefix_size = std::string::traits_type::length(right_prefix);
+
+    const auto what_size =
+        (
+            has_contex ?
+            left_prefix_size + context_size + right_prefix_size :
+            0) +
+        message_size +
+        1;
+
+    auto what = std::string{};
+    what.reserve(what_size);
+
+    if (has_contex)
+    {
+        what.append(left_prefix, left_prefix_size);
+        what.append(context, context_size);
+        what.append(right_prefix, right_prefix_size);
+    }
+
+    if (has_message)
+    {
+        what.append(message, message_size);
+    }
+
+    return what;
+}

+ 25 - 0
Engine/lib/openal-soft/al/eax_exception.h

@@ -0,0 +1,25 @@
+#ifndef EAX_EXCEPTION_INCLUDED
+#define EAX_EXCEPTION_INCLUDED
+
+
+#include <stdexcept>
+#include <string>
+
+
+class EaxException :
+    public std::runtime_error
+{
+public:
+    EaxException(
+        const char* context,
+        const char* message);
+
+
+private:
+    static std::string make_message(
+        const char* context,
+        const char* message);
+}; // EaxException
+
+
+#endif // !EAX_EXCEPTION_INCLUDED

+ 71 - 0
Engine/lib/openal-soft/al/eax_fx_slot_index.cpp

@@ -0,0 +1,71 @@
+#include "config.h"
+
+#include "eax_fx_slot_index.h"
+
+#include "eax_exception.h"
+
+
+namespace
+{
+
+
+class EaxFxSlotIndexException :
+    public EaxException
+{
+public:
+    explicit EaxFxSlotIndexException(
+        const char* message)
+        :
+        EaxException{"EAX_FX_SLOT_INDEX", message}
+    {
+    }
+}; // EaxFxSlotIndexException
+
+
+} // namespace
+
+
+void EaxFxSlotIndex::set(EaxFxSlotIndexValue index)
+{
+    if(index >= EaxFxSlotIndexValue{EAX_MAX_FXSLOTS})
+        fail("Index out of range.");
+
+    emplace(index);
+}
+
+void EaxFxSlotIndex::set(const GUID &guid)
+{
+    if (false)
+    {
+    }
+    else if (guid == EAX_NULL_GUID)
+    {
+        reset();
+    }
+    else if (guid == EAXPROPERTYID_EAX40_FXSlot0 || guid == EAXPROPERTYID_EAX50_FXSlot0)
+    {
+        emplace(0u);
+    }
+    else if (guid == EAXPROPERTYID_EAX40_FXSlot1 || guid == EAXPROPERTYID_EAX50_FXSlot1)
+    {
+        emplace(1u);
+    }
+    else if (guid == EAXPROPERTYID_EAX40_FXSlot2 || guid == EAXPROPERTYID_EAX50_FXSlot2)
+    {
+        emplace(2u);
+    }
+    else if (guid == EAXPROPERTYID_EAX40_FXSlot3 || guid == EAXPROPERTYID_EAX50_FXSlot3)
+    {
+        emplace(3u);
+    }
+    else
+    {
+        fail("Unsupported GUID.");
+    }
+}
+
+[[noreturn]]
+void EaxFxSlotIndex::fail(const char* message)
+{
+    throw EaxFxSlotIndexException{message};
+}

+ 41 - 0
Engine/lib/openal-soft/al/eax_fx_slot_index.h

@@ -0,0 +1,41 @@
+#ifndef EAX_FX_SLOT_INDEX_INCLUDED
+#define EAX_FX_SLOT_INDEX_INCLUDED
+
+
+#include <cstddef>
+
+#include "aloptional.h"
+#include "eax_api.h"
+
+
+using EaxFxSlotIndexValue = std::size_t;
+
+class EaxFxSlotIndex : public al::optional<EaxFxSlotIndexValue>
+{
+public:
+    using al::optional<EaxFxSlotIndexValue>::optional;
+
+    EaxFxSlotIndex& operator=(const EaxFxSlotIndexValue &value) { set(value); return *this; }
+    EaxFxSlotIndex& operator=(const GUID &guid) { set(guid); return *this; }
+
+    void set(EaxFxSlotIndexValue index);
+    void set(const GUID& guid);
+
+private:
+    [[noreturn]]
+    static void fail(const char *message);
+}; // EaxFxSlotIndex
+
+inline bool operator==(const EaxFxSlotIndex& lhs, const EaxFxSlotIndex& rhs) noexcept
+{
+    if(lhs.has_value() != rhs.has_value())
+        return false;
+    if(lhs.has_value())
+        return *lhs == *rhs;
+    return true;
+}
+
+inline bool operator!=(const EaxFxSlotIndex& lhs, const EaxFxSlotIndex& rhs) noexcept
+{ return !(lhs == rhs); }
+
+#endif // !EAX_FX_SLOT_INDEX_INCLUDED

+ 84 - 0
Engine/lib/openal-soft/al/eax_fx_slots.cpp

@@ -0,0 +1,84 @@
+#include "config.h"
+
+#include "eax_fx_slots.h"
+
+#include <array>
+
+#include "eax_exception.h"
+
+#include "eax_api.h"
+
+
+namespace
+{
+
+
+class EaxFxSlotsException :
+    public EaxException
+{
+public:
+    explicit EaxFxSlotsException(
+        const char* message)
+        :
+        EaxException{"EAX_FX_SLOTS", message}
+    {
+    }
+}; // EaxFxSlotsException
+
+
+} // namespace
+
+
+void EaxFxSlots::initialize(
+    ALCcontext& al_context)
+{
+    initialize_fx_slots(al_context);
+}
+
+void EaxFxSlots::uninitialize() noexcept
+{
+    for (auto& fx_slot : fx_slots_)
+    {
+        fx_slot = nullptr;
+    }
+}
+
+const ALeffectslot& EaxFxSlots::get(EaxFxSlotIndex index) const
+{
+    if(!index.has_value())
+        fail("Empty index.");
+    return *fx_slots_[index.value()];
+}
+
+ALeffectslot& EaxFxSlots::get(EaxFxSlotIndex index)
+{
+    if(!index.has_value())
+        fail("Empty index.");
+    return *fx_slots_[index.value()];
+}
+
+void EaxFxSlots::unlock_legacy() noexcept
+{
+    fx_slots_[0]->eax_unlock_legacy();
+    fx_slots_[1]->eax_unlock_legacy();
+}
+
+[[noreturn]]
+void EaxFxSlots::fail(
+    const char* message)
+{
+    throw EaxFxSlotsException{message};
+}
+
+void EaxFxSlots::initialize_fx_slots(
+    ALCcontext& al_context)
+{
+    auto fx_slot_index = EaxFxSlotIndexValue{};
+
+    for (auto& fx_slot : fx_slots_)
+    {
+        fx_slot = eax_create_al_effect_slot(al_context);
+        fx_slot->eax_initialize(al_context, fx_slot_index);
+        fx_slot_index += 1;
+    }
+}

+ 54 - 0
Engine/lib/openal-soft/al/eax_fx_slots.h

@@ -0,0 +1,54 @@
+#ifndef EAX_FX_SLOTS_INCLUDED
+#define EAX_FX_SLOTS_INCLUDED
+
+
+#include <array>
+
+#include "al/auxeffectslot.h"
+
+#include "eax_api.h"
+
+#include "eax_fx_slot_index.h"
+
+
+class EaxFxSlots
+{
+public:
+    void initialize(
+        ALCcontext& al_context);
+
+    void uninitialize() noexcept;
+
+    void commit()
+    {
+        for(auto& fx_slot : fx_slots_)
+            fx_slot->eax_commit();
+    }
+
+
+    const ALeffectslot& get(
+        EaxFxSlotIndex index) const;
+
+    ALeffectslot& get(
+        EaxFxSlotIndex index);
+
+    void unlock_legacy() noexcept;
+
+
+private:
+    using Items = std::array<EaxAlEffectSlotUPtr, EAX_MAX_FXSLOTS>;
+
+
+    Items fx_slots_{};
+
+
+    [[noreturn]]
+    static void fail(
+        const char* message);
+
+    void initialize_fx_slots(
+        ALCcontext& al_context);
+}; // EaxFxSlots
+
+
+#endif // !EAX_FX_SLOTS_INCLUDED

+ 21 - 0
Engine/lib/openal-soft/al/eax_globals.cpp

@@ -0,0 +1,21 @@
+#include "config.h"
+
+#include "eax_globals.h"
+
+
+bool eax_g_is_enabled = true;
+
+
+const char eax1_ext_name[] = "EAX";
+const char eax2_ext_name[] = "EAX2.0";
+const char eax3_ext_name[] = "EAX3.0";
+const char eax4_ext_name[] = "EAX4.0";
+const char eax5_ext_name[] = "EAX5.0";
+
+const char eax_x_ram_ext_name[] = "EAX-RAM";
+
+const char eax_eax_set_func_name[] = "EAXSet";
+const char eax_eax_get_func_name[] = "EAXGet";
+
+const char eax_eax_set_buffer_mode_func_name[] = "EAXSetBufferMode";
+const char eax_eax_get_buffer_mode_func_name[] = "EAXGetBufferMode";

+ 22 - 0
Engine/lib/openal-soft/al/eax_globals.h

@@ -0,0 +1,22 @@
+#ifndef EAX_GLOBALS_INCLUDED
+#define EAX_GLOBALS_INCLUDED
+
+
+extern bool eax_g_is_enabled;
+
+
+extern const char eax1_ext_name[];
+extern const char eax2_ext_name[];
+extern const char eax3_ext_name[];
+extern const char eax4_ext_name[];
+extern const char eax5_ext_name[];
+
+extern const char eax_x_ram_ext_name[];
+
+extern const char eax_eax_set_func_name[];
+extern const char eax_eax_get_func_name[];
+
+extern const char eax_eax_set_buffer_mode_func_name[];
+extern const char eax_eax_get_buffer_mode_func_name[];
+
+#endif // !EAX_GLOBALS_INCLUDED

+ 36 - 0
Engine/lib/openal-soft/al/eax_utils.cpp

@@ -0,0 +1,36 @@
+#include "config.h"
+
+#include "eax_utils.h"
+
+#include <cassert>
+#include <exception>
+
+#include "core/logging.h"
+
+
+void eax_log_exception(
+    const char* message) noexcept
+{
+    const auto exception_ptr = std::current_exception();
+
+    assert(exception_ptr);
+
+    if (message)
+    {
+        ERR("%s\n", message);
+    }
+
+    try
+    {
+        std::rethrow_exception(exception_ptr);
+    }
+    catch (const std::exception& ex)
+    {
+        const auto ex_message = ex.what();
+        ERR("%s\n", ex_message);
+    }
+    catch (...)
+    {
+        ERR("%s\n", "Generic exception.");
+    }
+}

+ 132 - 0
Engine/lib/openal-soft/al/eax_utils.h

@@ -0,0 +1,132 @@
+#ifndef EAX_UTILS_INCLUDED
+#define EAX_UTILS_INCLUDED
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+#include <type_traits>
+
+
+struct EaxAlLowPassParam
+{
+    float gain;
+    float gain_hf;
+}; // EaxAlLowPassParam
+
+
+void eax_log_exception(
+    const char* message = nullptr) noexcept;
+
+
+template<
+    typename TException,
+    typename TValue
+>
+void eax_validate_range(
+    const char* value_name,
+    const TValue& value,
+    const TValue& min_value,
+    const TValue& max_value)
+{
+    if (value >= min_value && value <= max_value)
+    {
+        return;
+    }
+
+    const auto message =
+        std::string{value_name} +
+        " out of range (value: " +
+        std::to_string(value) + "; min: " +
+        std::to_string(min_value) + "; max: " +
+        std::to_string(max_value) + ").";
+
+    throw TException{message.c_str()};
+}
+
+
+namespace detail
+{
+
+
+template<
+    typename T
+>
+struct EaxIsBitFieldStruct
+{
+private:
+    using yes = std::true_type;
+    using no = std::false_type;
+
+    template<
+        typename U
+    >
+    static auto test(int) -> decltype(std::declval<typename U::EaxIsBitFieldStruct>(), yes{});
+
+    template<
+        typename
+    >
+    static no test(...);
+
+
+public:
+    static constexpr auto value = std::is_same<decltype(test<T>(0)), yes>::value;
+}; // EaxIsBitFieldStruct
+
+
+template<
+    typename T,
+    typename TValue
+>
+inline bool eax_bit_fields_are_equal(
+    const T& lhs,
+    const T& rhs) noexcept
+{
+    static_assert(sizeof(T) == sizeof(TValue), "Invalid type size.");
+
+    return reinterpret_cast<const TValue&>(lhs) == reinterpret_cast<const TValue&>(rhs);
+}
+
+
+} // namespace detail
+
+
+template<
+    typename T,
+    std::enable_if_t<detail::EaxIsBitFieldStruct<T>::value, int> = 0
+>
+inline bool operator==(
+    const T& lhs,
+    const T& rhs) noexcept
+{
+    using Value = std::conditional_t<
+        sizeof(T) == 1,
+        std::uint8_t,
+        std::conditional_t<
+            sizeof(T) == 2,
+            std::uint16_t,
+            std::conditional_t<
+                sizeof(T) == 4,
+                std::uint32_t,
+                void
+            >
+        >
+    >;
+
+    static_assert(!std::is_same<Value, void>::value, "Unsupported type.");
+
+    return detail::eax_bit_fields_are_equal<T, Value>(lhs, rhs);
+}
+
+template<
+    typename T,
+    std::enable_if_t<detail::EaxIsBitFieldStruct<T>::value, int> = 0
+>
+inline bool operator!=(
+    const T& lhs,
+    const T& rhs) noexcept
+{
+    return !(lhs == rhs);
+}
+
+
+#endif // !EAX_UTILS_INCLUDED

+ 3 - 0
Engine/lib/openal-soft/al/eax_x_ram.cpp

@@ -0,0 +1,3 @@
+#include "config.h"
+
+#include "eax_x_ram.h"

+ 38 - 0
Engine/lib/openal-soft/al/eax_x_ram.h

@@ -0,0 +1,38 @@
+#ifndef EAX_X_RAM_INCLUDED
+#define EAX_X_RAM_INCLUDED
+
+
+#include "AL/al.h"
+
+
+constexpr auto eax_x_ram_min_size = ALsizei{};
+constexpr auto eax_x_ram_max_size = ALsizei{64 * 1'024 * 1'024};
+
+
+constexpr auto AL_EAX_RAM_SIZE = ALenum{0x202201};
+constexpr auto AL_EAX_RAM_FREE = ALenum{0x202202};
+
+constexpr auto AL_STORAGE_AUTOMATIC = ALenum{0x202203};
+constexpr auto AL_STORAGE_HARDWARE = ALenum{0x202204};
+constexpr auto AL_STORAGE_ACCESSIBLE = ALenum{0x202205};
+
+
+constexpr auto AL_EAX_RAM_SIZE_NAME = "AL_EAX_RAM_SIZE";
+constexpr auto AL_EAX_RAM_FREE_NAME = "AL_EAX_RAM_FREE";
+
+constexpr auto AL_STORAGE_AUTOMATIC_NAME = "AL_STORAGE_AUTOMATIC";
+constexpr auto AL_STORAGE_HARDWARE_NAME = "AL_STORAGE_HARDWARE";
+constexpr auto AL_STORAGE_ACCESSIBLE_NAME = "AL_STORAGE_ACCESSIBLE";
+
+
+ALboolean AL_APIENTRY EAXSetBufferMode(
+    ALsizei n,
+    const ALuint* buffers,
+    ALint value);
+
+ALenum AL_APIENTRY EAXGetBufferMode(
+    ALuint buffer,
+    ALint* pReserved);
+
+
+#endif // !EAX_X_RAM_INCLUDED

+ 36 - 17
Engine/lib/openal-soft/al/effect.cpp

@@ -39,17 +39,23 @@
 #include "AL/efx.h"
 
 #include "albit.h"
-#include "alcmain.h"
-#include "alcontext.h"
+#include "alc/context.h"
+#include "alc/device.h"
+#include "alc/effects/base.h"
+#include "alc/inprogext.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "alstring.h"
 #include "core/except.h"
 #include "core/logging.h"
-#include "effects/base.h"
 #include "opthelpers.h"
 #include "vector.h"
 
+#ifdef ALSOFT_EAX
+#include <cassert>
+
+#include "eax_exception.h"
+#endif // ALSOFT_EAX
 
 const EffectList gEffectList[16]{
     { "eaxreverb",   EAXREVERB_EFFECT,   AL_EFFECT_EAXREVERB },
@@ -181,12 +187,12 @@ ALeffect *AllocEffect(ALCdevice *device)
 {
     auto sublist = std::find_if(device->EffectList.begin(), device->EffectList.end(),
         [](const EffectSubList &entry) noexcept -> bool
-        { return entry.FreeMask != 0; }
-    );
+        { return entry.FreeMask != 0; });
     auto lidx = static_cast<ALuint>(std::distance(device->EffectList.begin(), sublist));
     auto slidx = static_cast<ALuint>(al::countr_zero(sublist->FreeMask));
+    ASSUME(slidx < 64);
 
-    ALeffect *effect{::new (sublist->Effects + slidx) ALeffect{}};
+    ALeffect *effect{al::construct_at(sublist->Effects + slidx)};
     InitEffectParams(effect, AL_EFFECT_NULL);
 
     /* Add 1 to avoid effect ID 0. */
@@ -233,7 +239,7 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Generating %d effects", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
     if(!EnsureEffects(device, static_cast<ALuint>(n)))
     {
@@ -273,7 +279,7 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Deleting %d effects", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     /* First try to find any effects that are invalid. */
@@ -304,7 +310,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if LIKELY(context)
     {
-        ALCdevice *device{context->mDevice.get()};
+        ALCdevice *device{context->mALDevice.get()};
         std::lock_guard<std::mutex> _{device->EffectLock};
         if(!effect || LookupEffect(device, effect))
             return AL_TRUE;
@@ -319,7 +325,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     ALeffect *aleffect{LookupEffect(device, effect)};
@@ -369,7 +375,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     ALeffect *aleffect{LookupEffect(device, effect)};
@@ -392,7 +398,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     ALeffect *aleffect{LookupEffect(device, effect)};
@@ -415,7 +421,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     ALeffect *aleffect{LookupEffect(device, effect)};
@@ -438,7 +444,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     const ALeffect *aleffect{LookupEffect(device, effect)};
@@ -470,7 +476,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     const ALeffect *aleffect{LookupEffect(device, effect)};
@@ -493,7 +499,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     const ALeffect *aleffect{LookupEffect(device, effect)};
@@ -516,7 +522,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->EffectLock};
 
     const ALeffect *aleffect{LookupEffect(device, effect)};
@@ -744,3 +750,16 @@ void LoadReverbPreset(const char *name, ALeffect *effect)
 
     WARN("Reverb preset '%s' not found\n", name);
 }
+
+bool IsValidEffectType(ALenum type) noexcept
+{
+    if(type == AL_EFFECT_NULL)
+        return true;
+
+    for(const auto &effect_item : gEffectList)
+    {
+        if(type == effect_item.val && !DisabledEffects[effect_item.type])
+            return true;
+    }
+    return false;
+}

+ 2 - 0
Engine/lib/openal-soft/al/effect.h

@@ -57,4 +57,6 @@ void InitEffect(ALeffect *effect);
 
 void LoadReverbPreset(const char *name, ALeffect *effect);
 
+bool IsValidEffectType(ALenum type) noexcept;
+
 #endif

+ 440 - 1
Engine/lib/openal-soft/al/effects/autowah.cpp

@@ -8,9 +8,17 @@
 
 #include "AL/efx.h"
 
-#include "effects/base.h"
+#include "alc/effects/base.h"
 #include "effects.h"
 
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
+
+
 namespace {
 
 void Autowah_setParamf(EffectProps *props, ALenum param, float val)
@@ -107,3 +115,434 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Autowah);
 
 const EffectProps AutowahEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxAutoWahEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxAutoWahEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxAutoWahEffectDirtyFlagsValue flAttackTime : 1;
+    EaxAutoWahEffectDirtyFlagsValue flReleaseTime : 1;
+    EaxAutoWahEffectDirtyFlagsValue lResonance : 1;
+    EaxAutoWahEffectDirtyFlagsValue lPeakLevel : 1;
+}; // EaxAutoWahEffectDirtyFlags
+
+
+class EaxAutoWahEffect final :
+    public EaxEffect
+{
+public:
+    EaxAutoWahEffect();
+
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXAUTOWAHPROPERTIES eax_{};
+    EAXAUTOWAHPROPERTIES eax_d_{};
+    EaxAutoWahEffectDirtyFlags eax_dirty_flags_{};
+
+
+    void set_eax_defaults();
+
+
+    void set_efx_attack_time();
+
+    void set_efx_release_time();
+
+    void set_efx_resonance();
+
+    void set_efx_peak_gain();
+
+    void set_efx_defaults();
+
+
+    void get(const EaxEaxCall& eax_call);
+
+
+    void validate_attack_time(
+        float flAttackTime);
+
+    void validate_release_time(
+        float flReleaseTime);
+
+    void validate_resonance(
+        long lResonance);
+
+    void validate_peak_level(
+        long lPeakLevel);
+
+    void validate_all(
+        const EAXAUTOWAHPROPERTIES& eax_all);
+
+
+    void defer_attack_time(
+        float flAttackTime);
+
+    void defer_release_time(
+        float flReleaseTime);
+
+    void defer_resonance(
+        long lResonance);
+
+    void defer_peak_level(
+        long lPeakLevel);
+
+    void defer_all(
+        const EAXAUTOWAHPROPERTIES& eax_all);
+
+
+    void defer_attack_time(
+        const EaxEaxCall& eax_call);
+
+    void defer_release_time(
+        const EaxEaxCall& eax_call);
+
+    void defer_resonance(
+        const EaxEaxCall& eax_call);
+
+    void defer_peak_level(
+        const EaxEaxCall& eax_call);
+
+    void defer_all(
+        const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxAutoWahEffect
+
+
+class EaxAutoWahEffectException :
+    public EaxException
+{
+public:
+    explicit EaxAutoWahEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_AUTO_WAH_EFFECT", message}
+    {
+    }
+}; // EaxAutoWahEffectException
+
+
+EaxAutoWahEffect::EaxAutoWahEffect()
+    : EaxEffect{AL_EFFECT_AUTOWAH}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxAutoWahEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxAutoWahEffect::set_eax_defaults()
+{
+    eax_.flAttackTime = EAXAUTOWAH_DEFAULTATTACKTIME;
+    eax_.flReleaseTime = EAXAUTOWAH_DEFAULTRELEASETIME;
+    eax_.lResonance = EAXAUTOWAH_DEFAULTRESONANCE;
+    eax_.lPeakLevel = EAXAUTOWAH_DEFAULTPEAKLEVEL;
+
+    eax_d_ = eax_;
+}
+
+void EaxAutoWahEffect::set_efx_attack_time()
+{
+    const auto attack_time = clamp(
+        eax_.flAttackTime,
+        AL_AUTOWAH_MIN_ATTACK_TIME,
+        AL_AUTOWAH_MAX_ATTACK_TIME);
+
+    al_effect_props_.Autowah.AttackTime = attack_time;
+}
+
+void EaxAutoWahEffect::set_efx_release_time()
+{
+    const auto release_time = clamp(
+        eax_.flReleaseTime,
+        AL_AUTOWAH_MIN_RELEASE_TIME,
+        AL_AUTOWAH_MAX_RELEASE_TIME);
+
+    al_effect_props_.Autowah.ReleaseTime = release_time;
+}
+
+void EaxAutoWahEffect::set_efx_resonance()
+{
+    const auto resonance = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lResonance)),
+        AL_AUTOWAH_MIN_RESONANCE,
+        AL_AUTOWAH_MAX_RESONANCE);
+
+    al_effect_props_.Autowah.Resonance = resonance;
+}
+
+void EaxAutoWahEffect::set_efx_peak_gain()
+{
+    const auto peak_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lPeakLevel)),
+        AL_AUTOWAH_MIN_PEAK_GAIN,
+        AL_AUTOWAH_MAX_PEAK_GAIN);
+
+    al_effect_props_.Autowah.PeakGain = peak_gain;
+}
+
+void EaxAutoWahEffect::set_efx_defaults()
+{
+    set_efx_attack_time();
+    set_efx_release_time();
+    set_efx_resonance();
+    set_efx_peak_gain();
+}
+
+void EaxAutoWahEffect::get(const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_id())
+    {
+        case EAXAUTOWAH_NONE:
+            break;
+
+        case EAXAUTOWAH_ALLPARAMETERS:
+            eax_call.set_value<EaxAutoWahEffectException>(eax_);
+            break;
+
+        case EAXAUTOWAH_ATTACKTIME:
+            eax_call.set_value<EaxAutoWahEffectException>(eax_.flAttackTime);
+            break;
+
+        case EAXAUTOWAH_RELEASETIME:
+            eax_call.set_value<EaxAutoWahEffectException>(eax_.flReleaseTime);
+            break;
+
+        case EAXAUTOWAH_RESONANCE:
+            eax_call.set_value<EaxAutoWahEffectException>(eax_.lResonance);
+            break;
+
+        case EAXAUTOWAH_PEAKLEVEL:
+            eax_call.set_value<EaxAutoWahEffectException>(eax_.lPeakLevel);
+            break;
+
+        default:
+            throw EaxAutoWahEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxAutoWahEffect::validate_attack_time(
+    float flAttackTime)
+{
+    eax_validate_range<EaxAutoWahEffectException>(
+        "Attack Time",
+        flAttackTime,
+        EAXAUTOWAH_MINATTACKTIME,
+        EAXAUTOWAH_MAXATTACKTIME);
+}
+
+void EaxAutoWahEffect::validate_release_time(
+    float flReleaseTime)
+{
+    eax_validate_range<EaxAutoWahEffectException>(
+        "Release Time",
+        flReleaseTime,
+        EAXAUTOWAH_MINRELEASETIME,
+        EAXAUTOWAH_MAXRELEASETIME);
+}
+
+void EaxAutoWahEffect::validate_resonance(
+    long lResonance)
+{
+    eax_validate_range<EaxAutoWahEffectException>(
+        "Resonance",
+        lResonance,
+        EAXAUTOWAH_MINRESONANCE,
+        EAXAUTOWAH_MAXRESONANCE);
+}
+
+void EaxAutoWahEffect::validate_peak_level(
+    long lPeakLevel)
+{
+    eax_validate_range<EaxAutoWahEffectException>(
+        "Peak Level",
+        lPeakLevel,
+        EAXAUTOWAH_MINPEAKLEVEL,
+        EAXAUTOWAH_MAXPEAKLEVEL);
+}
+
+void EaxAutoWahEffect::validate_all(
+    const EAXAUTOWAHPROPERTIES& eax_all)
+{
+    validate_attack_time(eax_all.flAttackTime);
+    validate_release_time(eax_all.flReleaseTime);
+    validate_resonance(eax_all.lResonance);
+    validate_peak_level(eax_all.lPeakLevel);
+}
+
+void EaxAutoWahEffect::defer_attack_time(
+    float flAttackTime)
+{
+    eax_d_.flAttackTime = flAttackTime;
+    eax_dirty_flags_.flAttackTime = (eax_.flAttackTime != eax_d_.flAttackTime);
+}
+
+void EaxAutoWahEffect::defer_release_time(
+    float flReleaseTime)
+{
+    eax_d_.flReleaseTime = flReleaseTime;
+    eax_dirty_flags_.flReleaseTime = (eax_.flReleaseTime != eax_d_.flReleaseTime);
+}
+
+void EaxAutoWahEffect::defer_resonance(
+    long lResonance)
+{
+    eax_d_.lResonance = lResonance;
+    eax_dirty_flags_.lResonance = (eax_.lResonance != eax_d_.lResonance);
+}
+
+void EaxAutoWahEffect::defer_peak_level(
+    long lPeakLevel)
+{
+    eax_d_.lPeakLevel = lPeakLevel;
+    eax_dirty_flags_.lPeakLevel = (eax_.lPeakLevel != eax_d_.lPeakLevel);
+}
+
+void EaxAutoWahEffect::defer_all(
+    const EAXAUTOWAHPROPERTIES& eax_all)
+{
+    validate_all(eax_all);
+
+    defer_attack_time(eax_all.flAttackTime);
+    defer_release_time(eax_all.flReleaseTime);
+    defer_resonance(eax_all.lResonance);
+    defer_peak_level(eax_all.lPeakLevel);
+}
+
+void EaxAutoWahEffect::defer_attack_time(
+    const EaxEaxCall& eax_call)
+{
+    const auto& attack_time =
+        eax_call.get_value<EaxAutoWahEffectException, const decltype(EAXAUTOWAHPROPERTIES::flAttackTime)>();
+
+    validate_attack_time(attack_time);
+    defer_attack_time(attack_time);
+}
+
+void EaxAutoWahEffect::defer_release_time(
+    const EaxEaxCall& eax_call)
+{
+    const auto& release_time =
+        eax_call.get_value<EaxAutoWahEffectException, const decltype(EAXAUTOWAHPROPERTIES::flReleaseTime)>();
+
+    validate_release_time(release_time);
+    defer_release_time(release_time);
+}
+
+void EaxAutoWahEffect::defer_resonance(
+    const EaxEaxCall& eax_call)
+{
+    const auto& resonance =
+        eax_call.get_value<EaxAutoWahEffectException, const decltype(EAXAUTOWAHPROPERTIES::lResonance)>();
+
+    validate_resonance(resonance);
+    defer_resonance(resonance);
+}
+
+void EaxAutoWahEffect::defer_peak_level(
+    const EaxEaxCall& eax_call)
+{
+    const auto& peak_level =
+        eax_call.get_value<EaxAutoWahEffectException, const decltype(EAXAUTOWAHPROPERTIES::lPeakLevel)>();
+
+    validate_peak_level(peak_level);
+    defer_peak_level(peak_level);
+}
+
+void EaxAutoWahEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxAutoWahEffectException, const EAXAUTOWAHPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxAutoWahEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxAutoWahEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.flAttackTime)
+    {
+        set_efx_attack_time();
+    }
+
+    if (eax_dirty_flags_.flReleaseTime)
+    {
+        set_efx_release_time();
+    }
+
+    if (eax_dirty_flags_.lResonance)
+    {
+        set_efx_resonance();
+    }
+
+    if (eax_dirty_flags_.lPeakLevel)
+    {
+        set_efx_peak_gain();
+    }
+
+    eax_dirty_flags_ = EaxAutoWahEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxAutoWahEffect::set(const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_id())
+    {
+        case EAXAUTOWAH_NONE:
+            break;
+
+        case EAXAUTOWAH_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXAUTOWAH_ATTACKTIME:
+            defer_attack_time(eax_call);
+            break;
+
+        case EAXAUTOWAH_RELEASETIME:
+            defer_release_time(eax_call);
+            break;
+
+        case EAXAUTOWAH_RESONANCE:
+            defer_resonance(eax_call);
+            break;
+
+        case EAXAUTOWAH_PEAKLEVEL:
+            defer_peak_level(eax_call);
+            break;
+
+        default:
+            throw EaxAutoWahEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_auto_wah_effect()
+{
+    return std::make_unique<::EaxAutoWahEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 1082 - 1
Engine/lib/openal-soft/al/effects/chorus.cpp

@@ -1,17 +1,31 @@
 
 #include "config.h"
 
+#include <stdexcept>
+
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "aloptional.h"
 #include "core/logging.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
 
+static_assert(ChorusMaxDelay >= AL_CHORUS_MAX_DELAY, "Chorus max delay too small");
+static_assert(FlangerMaxDelay >= AL_FLANGER_MAX_DELAY, "Flanger max delay too small");
+
 static_assert(AL_CHORUS_WAVEFORM_SINUSOID == AL_FLANGER_WAVEFORM_SINUSOID, "Chorus/Flanger waveform value mismatch");
 static_assert(AL_CHORUS_WAVEFORM_TRIANGLE == AL_FLANGER_WAVEFORM_TRIANGLE, "Chorus/Flanger waveform value mismatch");
 
@@ -274,3 +288,1070 @@ const EffectProps ChorusEffectProps{genDefaultChorusProps()};
 DEFINE_ALEFFECT_VTABLE(Flanger);
 
 const EffectProps FlangerEffectProps{genDefaultFlangerProps()};
+
+
+#ifdef ALSOFT_EAX
+namespace {
+
+void eax_set_efx_waveform(
+    ALenum waveform,
+    EffectProps& al_effect_props)
+{
+    const auto efx_waveform = WaveformFromEnum(waveform);
+    assert(efx_waveform.has_value());
+    al_effect_props.Chorus.Waveform = *efx_waveform;
+}
+
+void eax_set_efx_phase(
+    ALint phase,
+    EffectProps& al_effect_props)
+{
+    al_effect_props.Chorus.Phase = phase;
+}
+
+void eax_set_efx_rate(
+    ALfloat rate,
+    EffectProps& al_effect_props)
+{
+    al_effect_props.Chorus.Rate = rate;
+}
+
+void eax_set_efx_depth(
+    ALfloat depth,
+    EffectProps& al_effect_props)
+{
+    al_effect_props.Chorus.Depth = depth;
+}
+
+void eax_set_efx_feedback(
+    ALfloat feedback,
+    EffectProps& al_effect_props)
+{
+    al_effect_props.Chorus.Feedback = feedback;
+}
+
+void eax_set_efx_delay(
+    ALfloat delay,
+    EffectProps& al_effect_props)
+{
+    al_effect_props.Chorus.Delay = delay;
+}
+
+
+using EaxChorusEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxChorusEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxChorusEffectDirtyFlagsValue ulWaveform : 1;
+    EaxChorusEffectDirtyFlagsValue lPhase : 1;
+    EaxChorusEffectDirtyFlagsValue flRate : 1;
+    EaxChorusEffectDirtyFlagsValue flDepth : 1;
+    EaxChorusEffectDirtyFlagsValue flFeedback : 1;
+    EaxChorusEffectDirtyFlagsValue flDelay : 1;
+}; // EaxChorusEffectDirtyFlags
+
+
+class EaxChorusEffect final :
+    public EaxEffect
+{
+public:
+    EaxChorusEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXCHORUSPROPERTIES eax_{};
+    EAXCHORUSPROPERTIES eax_d_{};
+    EaxChorusEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults() noexcept;
+
+    void set_efx_waveform();
+    void set_efx_phase();
+    void set_efx_rate();
+    void set_efx_depth();
+    void set_efx_feedback();
+    void set_efx_delay();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_waveform(unsigned long ulWaveform);
+    void validate_phase(long lPhase);
+    void validate_rate(float flRate);
+    void validate_depth(float flDepth);
+    void validate_feedback(float flFeedback);
+    void validate_delay(float flDelay);
+    void validate_all(const EAXCHORUSPROPERTIES& eax_all);
+
+    void defer_waveform(unsigned long ulWaveform);
+    void defer_phase(long lPhase);
+    void defer_rate(float flRate);
+    void defer_depth(float flDepth);
+    void defer_feedback(float flFeedback);
+    void defer_delay(float flDelay);
+    void defer_all(const EAXCHORUSPROPERTIES& eax_all);
+
+    void defer_waveform(const EaxEaxCall& eax_call);
+    void defer_phase(const EaxEaxCall& eax_call);
+    void defer_rate(const EaxEaxCall& eax_call);
+    void defer_depth(const EaxEaxCall& eax_call);
+    void defer_feedback(const EaxEaxCall& eax_call);
+    void defer_delay(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxChorusEffect
+
+
+class EaxChorusEffectException :
+    public EaxException
+{
+public:
+    explicit EaxChorusEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_CHORUS_EFFECT", message}
+    {
+    }
+}; // EaxChorusEffectException
+
+
+EaxChorusEffect::EaxChorusEffect()
+    : EaxEffect{AL_EFFECT_CHORUS}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxChorusEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxChorusEffect::set_eax_defaults() noexcept
+{
+    eax_.ulWaveform = EAXCHORUS_DEFAULTWAVEFORM;
+    eax_.lPhase = EAXCHORUS_DEFAULTPHASE;
+    eax_.flRate = EAXCHORUS_DEFAULTRATE;
+    eax_.flDepth = EAXCHORUS_DEFAULTDEPTH;
+    eax_.flFeedback = EAXCHORUS_DEFAULTFEEDBACK;
+    eax_.flDelay = EAXCHORUS_DEFAULTDELAY;
+
+    eax_d_ = eax_;
+}
+
+void EaxChorusEffect::set_efx_waveform()
+{
+    const auto waveform = clamp(
+        static_cast<ALint>(eax_.ulWaveform),
+        AL_CHORUS_MIN_WAVEFORM,
+        AL_CHORUS_MAX_WAVEFORM);
+
+    eax_set_efx_waveform(waveform, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_phase()
+{
+    const auto phase = clamp(
+        static_cast<ALint>(eax_.lPhase),
+        AL_CHORUS_MIN_PHASE,
+        AL_CHORUS_MAX_PHASE);
+
+    eax_set_efx_phase(phase, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_rate()
+{
+    const auto rate = clamp(
+        eax_.flRate,
+        AL_CHORUS_MIN_RATE,
+        AL_CHORUS_MAX_RATE);
+
+    eax_set_efx_rate(rate, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_depth()
+{
+    const auto depth = clamp(
+        eax_.flDepth,
+        AL_CHORUS_MIN_DEPTH,
+        AL_CHORUS_MAX_DEPTH);
+
+    eax_set_efx_depth(depth, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_feedback()
+{
+    const auto feedback = clamp(
+        eax_.flFeedback,
+        AL_CHORUS_MIN_FEEDBACK,
+        AL_CHORUS_MAX_FEEDBACK);
+
+    eax_set_efx_feedback(feedback, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_delay()
+{
+    const auto delay = clamp(
+        eax_.flDelay,
+        AL_CHORUS_MIN_DELAY,
+        AL_CHORUS_MAX_DELAY);
+
+    eax_set_efx_delay(delay, al_effect_props_);
+}
+
+void EaxChorusEffect::set_efx_defaults()
+{
+    set_efx_waveform();
+    set_efx_phase();
+    set_efx_rate();
+    set_efx_depth();
+    set_efx_feedback();
+    set_efx_delay();
+}
+
+void EaxChorusEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXCHORUS_NONE:
+            break;
+
+        case EAXCHORUS_ALLPARAMETERS:
+            eax_call.set_value<EaxChorusEffectException>(eax_);
+            break;
+
+        case EAXCHORUS_WAVEFORM:
+            eax_call.set_value<EaxChorusEffectException>(eax_.ulWaveform);
+            break;
+
+        case EAXCHORUS_PHASE:
+            eax_call.set_value<EaxChorusEffectException>(eax_.lPhase);
+            break;
+
+        case EAXCHORUS_RATE:
+            eax_call.set_value<EaxChorusEffectException>(eax_.flRate);
+            break;
+
+        case EAXCHORUS_DEPTH:
+            eax_call.set_value<EaxChorusEffectException>(eax_.flDepth);
+            break;
+
+        case EAXCHORUS_FEEDBACK:
+            eax_call.set_value<EaxChorusEffectException>(eax_.flFeedback);
+            break;
+
+        case EAXCHORUS_DELAY:
+            eax_call.set_value<EaxChorusEffectException>(eax_.flDelay);
+            break;
+
+        default:
+            throw EaxChorusEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxChorusEffect::validate_waveform(
+    unsigned long ulWaveform)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Waveform",
+        ulWaveform,
+        EAXCHORUS_MINWAVEFORM,
+        EAXCHORUS_MAXWAVEFORM);
+}
+
+void EaxChorusEffect::validate_phase(
+    long lPhase)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Phase",
+        lPhase,
+        EAXCHORUS_MINPHASE,
+        EAXCHORUS_MAXPHASE);
+}
+
+void EaxChorusEffect::validate_rate(
+    float flRate)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Rate",
+        flRate,
+        EAXCHORUS_MINRATE,
+        EAXCHORUS_MAXRATE);
+}
+
+void EaxChorusEffect::validate_depth(
+    float flDepth)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Depth",
+        flDepth,
+        EAXCHORUS_MINDEPTH,
+        EAXCHORUS_MAXDEPTH);
+}
+
+void EaxChorusEffect::validate_feedback(
+    float flFeedback)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Feedback",
+        flFeedback,
+        EAXCHORUS_MINFEEDBACK,
+        EAXCHORUS_MAXFEEDBACK);
+}
+
+void EaxChorusEffect::validate_delay(
+    float flDelay)
+{
+    eax_validate_range<EaxChorusEffectException>(
+        "Delay",
+        flDelay,
+        EAXCHORUS_MINDELAY,
+        EAXCHORUS_MAXDELAY);
+}
+
+void EaxChorusEffect::validate_all(
+    const EAXCHORUSPROPERTIES& eax_all)
+{
+    validate_waveform(eax_all.ulWaveform);
+    validate_phase(eax_all.lPhase);
+    validate_rate(eax_all.flRate);
+    validate_depth(eax_all.flDepth);
+    validate_feedback(eax_all.flFeedback);
+    validate_delay(eax_all.flDelay);
+}
+
+void EaxChorusEffect::defer_waveform(
+    unsigned long ulWaveform)
+{
+    eax_d_.ulWaveform = ulWaveform;
+    eax_dirty_flags_.ulWaveform = (eax_.ulWaveform != eax_d_.ulWaveform);
+}
+
+void EaxChorusEffect::defer_phase(
+    long lPhase)
+{
+    eax_d_.lPhase = lPhase;
+    eax_dirty_flags_.lPhase = (eax_.lPhase != eax_d_.lPhase);
+}
+
+void EaxChorusEffect::defer_rate(
+    float flRate)
+{
+    eax_d_.flRate = flRate;
+    eax_dirty_flags_.flRate = (eax_.flRate != eax_d_.flRate);
+}
+
+void EaxChorusEffect::defer_depth(
+    float flDepth)
+{
+    eax_d_.flDepth = flDepth;
+    eax_dirty_flags_.flDepth = (eax_.flDepth != eax_d_.flDepth);
+}
+
+void EaxChorusEffect::defer_feedback(
+    float flFeedback)
+{
+    eax_d_.flFeedback = flFeedback;
+    eax_dirty_flags_.flFeedback = (eax_.flFeedback != eax_d_.flFeedback);
+}
+
+void EaxChorusEffect::defer_delay(
+    float flDelay)
+{
+    eax_d_.flDelay = flDelay;
+    eax_dirty_flags_.flDelay = (eax_.flDelay != eax_d_.flDelay);
+}
+
+void EaxChorusEffect::defer_all(
+    const EAXCHORUSPROPERTIES& eax_all)
+{
+    defer_waveform(eax_all.ulWaveform);
+    defer_phase(eax_all.lPhase);
+    defer_rate(eax_all.flRate);
+    defer_depth(eax_all.flDepth);
+    defer_feedback(eax_all.flFeedback);
+    defer_delay(eax_all.flDelay);
+}
+
+void EaxChorusEffect::defer_waveform(
+    const EaxEaxCall& eax_call)
+{
+    const auto& waveform =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::ulWaveform)>();
+
+    validate_waveform(waveform);
+    defer_waveform(waveform);
+}
+
+void EaxChorusEffect::defer_phase(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phase =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::lPhase)>();
+
+    validate_phase(phase);
+    defer_phase(phase);
+}
+
+void EaxChorusEffect::defer_rate(
+    const EaxEaxCall& eax_call)
+{
+    const auto& rate =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::flRate)>();
+
+    validate_rate(rate);
+    defer_rate(rate);
+}
+
+void EaxChorusEffect::defer_depth(
+    const EaxEaxCall& eax_call)
+{
+    const auto& depth =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::flDepth)>();
+
+    validate_depth(depth);
+    defer_depth(depth);
+}
+
+void EaxChorusEffect::defer_feedback(
+    const EaxEaxCall& eax_call)
+{
+    const auto& feedback =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::flFeedback)>();
+
+    validate_feedback(feedback);
+    defer_feedback(feedback);
+}
+
+void EaxChorusEffect::defer_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& delay =
+        eax_call.get_value<EaxChorusEffectException, const decltype(EAXCHORUSPROPERTIES::flDelay)>();
+
+    validate_delay(delay);
+    defer_delay(delay);
+}
+
+void EaxChorusEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxChorusEffectException, const EAXCHORUSPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxChorusEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxChorusEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.ulWaveform)
+    {
+        set_efx_waveform();
+    }
+
+    if (eax_dirty_flags_.lPhase)
+    {
+        set_efx_phase();
+    }
+
+    if (eax_dirty_flags_.flRate)
+    {
+        set_efx_rate();
+    }
+
+    if (eax_dirty_flags_.flDepth)
+    {
+        set_efx_depth();
+    }
+
+    if (eax_dirty_flags_.flFeedback)
+    {
+        set_efx_feedback();
+    }
+
+    if (eax_dirty_flags_.flDelay)
+    {
+        set_efx_delay();
+    }
+
+    eax_dirty_flags_ = EaxChorusEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxChorusEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXCHORUS_NONE:
+            break;
+
+        case EAXCHORUS_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXCHORUS_WAVEFORM:
+            defer_waveform(eax_call);
+            break;
+
+        case EAXCHORUS_PHASE:
+            defer_phase(eax_call);
+            break;
+
+        case EAXCHORUS_RATE:
+            defer_rate(eax_call);
+            break;
+
+        case EAXCHORUS_DEPTH:
+            defer_depth(eax_call);
+            break;
+
+        case EAXCHORUS_FEEDBACK:
+            defer_feedback(eax_call);
+            break;
+
+        case EAXCHORUS_DELAY:
+            defer_delay(eax_call);
+            break;
+
+        default:
+            throw EaxChorusEffectException{"Unsupported property id."};
+    }
+}
+
+
+} // namespace
+
+
+EaxEffectUPtr eax_create_eax_chorus_effect()
+{
+    return std::make_unique<::EaxChorusEffect>();
+}
+
+
+namespace
+{
+
+
+using EaxFlangerEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxFlangerEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxFlangerEffectDirtyFlagsValue ulWaveform : 1;
+    EaxFlangerEffectDirtyFlagsValue lPhase : 1;
+    EaxFlangerEffectDirtyFlagsValue flRate : 1;
+    EaxFlangerEffectDirtyFlagsValue flDepth : 1;
+    EaxFlangerEffectDirtyFlagsValue flFeedback : 1;
+    EaxFlangerEffectDirtyFlagsValue flDelay : 1;
+}; // EaxFlangerEffectDirtyFlags
+
+
+class EaxFlangerEffect final :
+    public EaxEffect
+{
+public:
+    EaxFlangerEffect();
+
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXFLANGERPROPERTIES eax_{};
+    EAXFLANGERPROPERTIES eax_d_{};
+    EaxFlangerEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_waveform();
+    void set_efx_phase();
+    void set_efx_rate();
+    void set_efx_depth();
+    void set_efx_feedback();
+    void set_efx_delay();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_waveform(unsigned long ulWaveform);
+    void validate_phase(long lPhase);
+    void validate_rate(float flRate);
+    void validate_depth(float flDepth);
+    void validate_feedback(float flFeedback);
+    void validate_delay(float flDelay);
+    void validate_all(const EAXFLANGERPROPERTIES& all);
+
+    void defer_waveform(unsigned long ulWaveform);
+    void defer_phase(long lPhase);
+    void defer_rate(float flRate);
+    void defer_depth(float flDepth);
+    void defer_feedback(float flFeedback);
+    void defer_delay(float flDelay);
+    void defer_all(const EAXFLANGERPROPERTIES& all);
+
+    void defer_waveform(const EaxEaxCall& eax_call);
+    void defer_phase(const EaxEaxCall& eax_call);
+    void defer_rate(const EaxEaxCall& eax_call);
+    void defer_depth(const EaxEaxCall& eax_call);
+    void defer_feedback(const EaxEaxCall& eax_call);
+    void defer_delay(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxFlangerEffect
+
+
+class EaxFlangerEffectException :
+    public EaxException
+{
+public:
+    explicit EaxFlangerEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_FLANGER_EFFECT", message}
+    {
+    }
+}; // EaxFlangerEffectException
+
+
+EaxFlangerEffect::EaxFlangerEffect()
+    : EaxEffect{AL_EFFECT_FLANGER}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxFlangerEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxFlangerEffect::set_eax_defaults()
+{
+    eax_.ulWaveform = EAXFLANGER_DEFAULTWAVEFORM;
+    eax_.lPhase = EAXFLANGER_DEFAULTPHASE;
+    eax_.flRate = EAXFLANGER_DEFAULTRATE;
+    eax_.flDepth = EAXFLANGER_DEFAULTDEPTH;
+    eax_.flFeedback = EAXFLANGER_DEFAULTFEEDBACK;
+    eax_.flDelay = EAXFLANGER_DEFAULTDELAY;
+
+    eax_d_ = eax_;
+}
+
+void EaxFlangerEffect::set_efx_waveform()
+{
+    const auto waveform = clamp(
+        static_cast<ALint>(eax_.ulWaveform),
+        AL_FLANGER_MIN_WAVEFORM,
+        AL_FLANGER_MAX_WAVEFORM);
+
+    eax_set_efx_waveform(waveform, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_phase()
+{
+    const auto phase = clamp(
+        static_cast<ALint>(eax_.lPhase),
+        AL_FLANGER_MIN_PHASE,
+        AL_FLANGER_MAX_PHASE);
+
+    eax_set_efx_phase(phase, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_rate()
+{
+    const auto rate = clamp(
+        eax_.flRate,
+        AL_FLANGER_MIN_RATE,
+        AL_FLANGER_MAX_RATE);
+
+    eax_set_efx_rate(rate, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_depth()
+{
+    const auto depth = clamp(
+        eax_.flDepth,
+        AL_FLANGER_MIN_DEPTH,
+        AL_FLANGER_MAX_DEPTH);
+
+    eax_set_efx_depth(depth, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_feedback()
+{
+    const auto feedback = clamp(
+        eax_.flFeedback,
+        AL_FLANGER_MIN_FEEDBACK,
+        AL_FLANGER_MAX_FEEDBACK);
+
+    eax_set_efx_feedback(feedback, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_delay()
+{
+    const auto delay = clamp(
+        eax_.flDelay,
+        AL_FLANGER_MIN_DELAY,
+        AL_FLANGER_MAX_DELAY);
+
+    eax_set_efx_delay(delay, al_effect_props_);
+}
+
+void EaxFlangerEffect::set_efx_defaults()
+{
+    set_efx_waveform();
+    set_efx_phase();
+    set_efx_rate();
+    set_efx_depth();
+    set_efx_feedback();
+    set_efx_delay();
+}
+
+void EaxFlangerEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXFLANGER_NONE:
+            break;
+
+        case EAXFLANGER_ALLPARAMETERS:
+            eax_call.set_value<EaxFlangerEffectException>(eax_);
+            break;
+
+        case EAXFLANGER_WAVEFORM:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.ulWaveform);
+            break;
+
+        case EAXFLANGER_PHASE:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.lPhase);
+            break;
+
+        case EAXFLANGER_RATE:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.flRate);
+            break;
+
+        case EAXFLANGER_DEPTH:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.flDepth);
+            break;
+
+        case EAXFLANGER_FEEDBACK:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.flFeedback);
+            break;
+
+        case EAXFLANGER_DELAY:
+            eax_call.set_value<EaxFlangerEffectException>(eax_.flDelay);
+            break;
+
+        default:
+            throw EaxFlangerEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxFlangerEffect::validate_waveform(
+    unsigned long ulWaveform)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Waveform",
+        ulWaveform,
+        EAXFLANGER_MINWAVEFORM,
+        EAXFLANGER_MAXWAVEFORM);
+}
+
+void EaxFlangerEffect::validate_phase(
+    long lPhase)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Phase",
+        lPhase,
+        EAXFLANGER_MINPHASE,
+        EAXFLANGER_MAXPHASE);
+}
+
+void EaxFlangerEffect::validate_rate(
+    float flRate)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Rate",
+        flRate,
+        EAXFLANGER_MINRATE,
+        EAXFLANGER_MAXRATE);
+}
+
+void EaxFlangerEffect::validate_depth(
+    float flDepth)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Depth",
+        flDepth,
+        EAXFLANGER_MINDEPTH,
+        EAXFLANGER_MAXDEPTH);
+}
+
+void EaxFlangerEffect::validate_feedback(
+    float flFeedback)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Feedback",
+        flFeedback,
+        EAXFLANGER_MINFEEDBACK,
+        EAXFLANGER_MAXFEEDBACK);
+}
+
+void EaxFlangerEffect::validate_delay(
+    float flDelay)
+{
+    eax_validate_range<EaxFlangerEffectException>(
+        "Delay",
+        flDelay,
+        EAXFLANGER_MINDELAY,
+        EAXFLANGER_MAXDELAY);
+}
+
+void EaxFlangerEffect::validate_all(
+    const EAXFLANGERPROPERTIES& all)
+{
+    validate_waveform(all.ulWaveform);
+    validate_phase(all.lPhase);
+    validate_rate(all.flRate);
+    validate_depth(all.flDepth);
+    validate_feedback(all.flDelay);
+    validate_delay(all.flDelay);
+}
+
+void EaxFlangerEffect::defer_waveform(
+    unsigned long ulWaveform)
+{
+    eax_d_.ulWaveform = ulWaveform;
+    eax_dirty_flags_.ulWaveform = (eax_.ulWaveform != eax_d_.ulWaveform);
+}
+
+void EaxFlangerEffect::defer_phase(
+    long lPhase)
+{
+    eax_d_.lPhase = lPhase;
+    eax_dirty_flags_.lPhase = (eax_.lPhase != eax_d_.lPhase);
+}
+
+void EaxFlangerEffect::defer_rate(
+    float flRate)
+{
+    eax_d_.flRate = flRate;
+    eax_dirty_flags_.flRate = (eax_.flRate != eax_d_.flRate);
+}
+
+void EaxFlangerEffect::defer_depth(
+    float flDepth)
+{
+    eax_d_.flDepth = flDepth;
+    eax_dirty_flags_.flDepth = (eax_.flDepth != eax_d_.flDepth);
+}
+
+void EaxFlangerEffect::defer_feedback(
+    float flFeedback)
+{
+    eax_d_.flFeedback = flFeedback;
+    eax_dirty_flags_.flFeedback = (eax_.flFeedback != eax_d_.flFeedback);
+}
+
+void EaxFlangerEffect::defer_delay(
+    float flDelay)
+{
+    eax_d_.flDelay = flDelay;
+    eax_dirty_flags_.flDelay = (eax_.flDelay != eax_d_.flDelay);
+}
+
+void EaxFlangerEffect::defer_all(
+    const EAXFLANGERPROPERTIES& all)
+{
+    defer_waveform(all.ulWaveform);
+    defer_phase(all.lPhase);
+    defer_rate(all.flRate);
+    defer_depth(all.flDepth);
+    defer_feedback(all.flDelay);
+    defer_delay(all.flDelay);
+}
+
+void EaxFlangerEffect::defer_waveform(
+    const EaxEaxCall& eax_call)
+{
+    const auto& waveform =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::ulWaveform)>();
+
+    validate_waveform(waveform);
+    defer_waveform(waveform);
+}
+
+void EaxFlangerEffect::defer_phase(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phase =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::lPhase)>();
+
+    validate_phase(phase);
+    defer_phase(phase);
+}
+
+void EaxFlangerEffect::defer_rate(
+    const EaxEaxCall& eax_call)
+{
+    const auto& rate =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::flRate)>();
+
+    validate_rate(rate);
+    defer_rate(rate);
+}
+
+void EaxFlangerEffect::defer_depth(
+    const EaxEaxCall& eax_call)
+{
+    const auto& depth =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::flDepth)>();
+
+    validate_depth(depth);
+    defer_depth(depth);
+}
+
+void EaxFlangerEffect::defer_feedback(
+    const EaxEaxCall& eax_call)
+{
+    const auto& feedback =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::flFeedback)>();
+
+    validate_feedback(feedback);
+    defer_feedback(feedback);
+}
+
+void EaxFlangerEffect::defer_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& delay =
+        eax_call.get_value<EaxFlangerEffectException, const decltype(EAXFLANGERPROPERTIES::flDelay)>();
+
+    validate_delay(delay);
+    defer_delay(delay);
+}
+
+void EaxFlangerEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxFlangerEffectException, const EAXFLANGERPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxFlangerEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxFlangerEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.ulWaveform)
+    {
+        set_efx_waveform();
+    }
+
+    if (eax_dirty_flags_.lPhase)
+    {
+        set_efx_phase();
+    }
+
+    if (eax_dirty_flags_.flRate)
+    {
+        set_efx_rate();
+    }
+
+    if (eax_dirty_flags_.flDepth)
+    {
+        set_efx_depth();
+    }
+
+    if (eax_dirty_flags_.flFeedback)
+    {
+        set_efx_feedback();
+    }
+
+    if (eax_dirty_flags_.flDelay)
+    {
+        set_efx_delay();
+    }
+
+    eax_dirty_flags_ = EaxFlangerEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxFlangerEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXFLANGER_NONE:
+            break;
+
+        case EAXFLANGER_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXFLANGER_WAVEFORM:
+            defer_waveform(eax_call);
+            break;
+
+        case EAXFLANGER_PHASE:
+            defer_phase(eax_call);
+            break;
+
+        case EAXFLANGER_RATE:
+            defer_rate(eax_call);
+            break;
+
+        case EAXFLANGER_DEPTH:
+            defer_depth(eax_call);
+            break;
+
+        case EAXFLANGER_FEEDBACK:
+            defer_feedback(eax_call);
+            break;
+
+        case EAXFLANGER_DELAY:
+            defer_delay(eax_call);
+            break;
+
+        default:
+            throw EaxFlangerEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_flanger_effect()
+{
+    return std::make_unique<EaxFlangerEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 225 - 1
Engine/lib/openal-soft/al/effects/compressor.cpp

@@ -4,8 +4,15 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -70,3 +77,220 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Compressor);
 
 const EffectProps CompressorEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxCompressorEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxCompressorEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxCompressorEffectDirtyFlagsValue ulOnOff : 1;
+}; // EaxCompressorEffectDirtyFlags
+
+
+class EaxCompressorEffect final :
+    public EaxEffect
+{
+public:
+    EaxCompressorEffect();
+
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXAGCCOMPRESSORPROPERTIES eax_{};
+    EAXAGCCOMPRESSORPROPERTIES eax_d_{};
+    EaxCompressorEffectDirtyFlags eax_dirty_flags_{};
+
+
+    void set_eax_defaults();
+
+    void set_efx_on_off();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_on_off(unsigned long ulOnOff);
+    void validate_all(const EAXAGCCOMPRESSORPROPERTIES& eax_all);
+
+    void defer_on_off(unsigned long ulOnOff);
+    void defer_all(const EAXAGCCOMPRESSORPROPERTIES& eax_all);
+
+    void defer_on_off(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxCompressorEffect
+
+
+class EaxCompressorEffectException :
+    public EaxException
+{
+public:
+    explicit EaxCompressorEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_COMPRESSOR_EFFECT", message}
+    {
+    }
+}; // EaxCompressorEffectException
+
+
+EaxCompressorEffect::EaxCompressorEffect()
+    : EaxEffect{AL_EFFECT_COMPRESSOR}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+// [[nodiscard]]
+void EaxCompressorEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxCompressorEffect::set_eax_defaults()
+{
+    eax_.ulOnOff = EAXAGCCOMPRESSOR_DEFAULTONOFF;
+
+    eax_d_ = eax_;
+}
+
+void EaxCompressorEffect::set_efx_on_off()
+{
+    const auto on_off = clamp(
+        static_cast<ALint>(eax_.ulOnOff),
+        AL_COMPRESSOR_MIN_ONOFF,
+        AL_COMPRESSOR_MAX_ONOFF);
+
+    al_effect_props_.Compressor.OnOff = (on_off != AL_FALSE);
+}
+
+void EaxCompressorEffect::set_efx_defaults()
+{
+    set_efx_on_off();
+}
+
+void EaxCompressorEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXAGCCOMPRESSOR_NONE:
+            break;
+
+        case EAXAGCCOMPRESSOR_ALLPARAMETERS:
+            eax_call.set_value<EaxCompressorEffectException>(eax_);
+            break;
+
+        case EAXAGCCOMPRESSOR_ONOFF:
+            eax_call.set_value<EaxCompressorEffectException>(eax_.ulOnOff);
+            break;
+
+        default:
+            throw EaxCompressorEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxCompressorEffect::validate_on_off(
+    unsigned long ulOnOff)
+{
+    eax_validate_range<EaxCompressorEffectException>(
+        "On-Off",
+        ulOnOff,
+        EAXAGCCOMPRESSOR_MINONOFF,
+        EAXAGCCOMPRESSOR_MAXONOFF);
+}
+
+void EaxCompressorEffect::validate_all(
+    const EAXAGCCOMPRESSORPROPERTIES& eax_all)
+{
+    validate_on_off(eax_all.ulOnOff);
+}
+
+void EaxCompressorEffect::defer_on_off(
+    unsigned long ulOnOff)
+{
+    eax_d_.ulOnOff = ulOnOff;
+    eax_dirty_flags_.ulOnOff = (eax_.ulOnOff != eax_d_.ulOnOff);
+}
+
+void EaxCompressorEffect::defer_all(
+    const EAXAGCCOMPRESSORPROPERTIES& eax_all)
+{
+    defer_on_off(eax_all.ulOnOff);
+}
+
+void EaxCompressorEffect::defer_on_off(
+    const EaxEaxCall& eax_call)
+{
+    const auto& on_off =
+        eax_call.get_value<EaxCompressorEffectException, const decltype(EAXAGCCOMPRESSORPROPERTIES::ulOnOff)>();
+
+    validate_on_off(on_off);
+    defer_on_off(on_off);
+}
+
+void EaxCompressorEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxCompressorEffectException, const EAXAGCCOMPRESSORPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxCompressorEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxCompressorEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.ulOnOff)
+    {
+        set_efx_on_off();
+    }
+
+    eax_dirty_flags_ = EaxCompressorEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxCompressorEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXAGCCOMPRESSOR_NONE:
+            break;
+
+        case EAXAGCCOMPRESSOR_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXAGCCOMPRESSOR_ONOFF:
+            defer_on_off(eax_call);
+            break;
+
+        default:
+            throw EaxCompressorEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_compressor_effect()
+{
+    return std::make_unique<EaxCompressorEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 2 - 2
Engine/lib/openal-soft/al/effects/convolution.cpp

@@ -2,10 +2,10 @@
 #include "config.h"
 
 #include "AL/al.h"
-#include "inprogext.h"
+#include "alc/inprogext.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
 
 
 namespace {

+ 2 - 2
Engine/lib/openal-soft/al/effects/dedicated.cpp

@@ -4,10 +4,10 @@
 #include <cmath>
 
 #include "AL/al.h"
-#include "AL/efx.h"
+#include "AL/alext.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
 
 
 namespace {

+ 458 - 1
Engine/lib/openal-soft/al/effects/distortion.cpp

@@ -4,8 +4,15 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -112,3 +119,453 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Distortion);
 
 const EffectProps DistortionEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxDistortionEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxDistortionEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxDistortionEffectDirtyFlagsValue flEdge : 1;
+    EaxDistortionEffectDirtyFlagsValue lGain : 1;
+    EaxDistortionEffectDirtyFlagsValue flLowPassCutOff : 1;
+    EaxDistortionEffectDirtyFlagsValue flEQCenter : 1;
+    EaxDistortionEffectDirtyFlagsValue flEQBandwidth : 1;
+}; // EaxDistortionEffectDirtyFlags
+
+
+class EaxDistortionEffect final :
+    public EaxEffect
+{
+public:
+    EaxDistortionEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXDISTORTIONPROPERTIES eax_{};
+    EAXDISTORTIONPROPERTIES eax_d_{};
+    EaxDistortionEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_edge();
+    void set_efx_gain();
+    void set_efx_lowpass_cutoff();
+    void set_efx_eq_center();
+    void set_efx_eq_bandwidth();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_edge(float flEdge);
+    void validate_gain(long lGain);
+    void validate_lowpass_cutoff(float flLowPassCutOff);
+    void validate_eq_center(float flEQCenter);
+    void validate_eq_bandwidth(float flEQBandwidth);
+    void validate_all(const EAXDISTORTIONPROPERTIES& eax_all);
+
+    void defer_edge(float flEdge);
+    void defer_gain(long lGain);
+    void defer_low_pass_cutoff(float flLowPassCutOff);
+    void defer_eq_center(float flEQCenter);
+    void defer_eq_bandwidth(float flEQBandwidth);
+    void defer_all(const EAXDISTORTIONPROPERTIES& eax_all);
+
+    void defer_edge(const EaxEaxCall& eax_call);
+    void defer_gain(const EaxEaxCall& eax_call);
+    void defer_low_pass_cutoff(const EaxEaxCall& eax_call);
+    void defer_eq_center(const EaxEaxCall& eax_call);
+    void defer_eq_bandwidth(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxDistortionEffect
+
+
+class EaxDistortionEffectException :
+    public EaxException
+{
+public:
+    explicit EaxDistortionEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_DISTORTION_EFFECT", message}
+    {
+    }
+}; // EaxDistortionEffectException
+
+
+EaxDistortionEffect::EaxDistortionEffect()
+    : EaxEffect{AL_EFFECT_DISTORTION}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxDistortionEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxDistortionEffect::set_eax_defaults()
+{
+    eax_.flEdge = EAXDISTORTION_DEFAULTEDGE;
+    eax_.lGain = EAXDISTORTION_DEFAULTGAIN;
+    eax_.flLowPassCutOff = EAXDISTORTION_DEFAULTLOWPASSCUTOFF;
+    eax_.flEQCenter = EAXDISTORTION_DEFAULTEQCENTER;
+    eax_.flEQBandwidth = EAXDISTORTION_DEFAULTEQBANDWIDTH;
+
+    eax_d_ = eax_;
+}
+
+void EaxDistortionEffect::set_efx_edge()
+{
+    const auto edge = clamp(
+        eax_.flEdge,
+        AL_DISTORTION_MIN_EDGE,
+        AL_DISTORTION_MAX_EDGE);
+
+    al_effect_props_.Distortion.Edge = edge;
+}
+
+void EaxDistortionEffect::set_efx_gain()
+{
+    const auto gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lGain)),
+        AL_DISTORTION_MIN_GAIN,
+        AL_DISTORTION_MAX_GAIN);
+
+    al_effect_props_.Distortion.Gain = gain;
+}
+
+void EaxDistortionEffect::set_efx_lowpass_cutoff()
+{
+    const auto lowpass_cutoff = clamp(
+        eax_.flLowPassCutOff,
+        AL_DISTORTION_MIN_LOWPASS_CUTOFF,
+        AL_DISTORTION_MAX_LOWPASS_CUTOFF);
+
+    al_effect_props_.Distortion.LowpassCutoff = lowpass_cutoff;
+}
+
+void EaxDistortionEffect::set_efx_eq_center()
+{
+    const auto eq_center = clamp(
+        eax_.flEQCenter,
+        AL_DISTORTION_MIN_EQCENTER,
+        AL_DISTORTION_MAX_EQCENTER);
+
+    al_effect_props_.Distortion.EQCenter = eq_center;
+}
+
+void EaxDistortionEffect::set_efx_eq_bandwidth()
+{
+    const auto eq_bandwidth = clamp(
+        eax_.flEdge,
+        AL_DISTORTION_MIN_EQBANDWIDTH,
+        AL_DISTORTION_MAX_EQBANDWIDTH);
+
+    al_effect_props_.Distortion.EQBandwidth = eq_bandwidth;
+}
+
+void EaxDistortionEffect::set_efx_defaults()
+{
+    set_efx_edge();
+    set_efx_gain();
+    set_efx_lowpass_cutoff();
+    set_efx_eq_center();
+    set_efx_eq_bandwidth();
+}
+
+void EaxDistortionEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXDISTORTION_NONE:
+            break;
+
+        case EAXDISTORTION_ALLPARAMETERS:
+            eax_call.set_value<EaxDistortionEffectException>(eax_);
+            break;
+
+        case EAXDISTORTION_EDGE:
+            eax_call.set_value<EaxDistortionEffectException>(eax_.flEdge);
+            break;
+
+        case EAXDISTORTION_GAIN:
+            eax_call.set_value<EaxDistortionEffectException>(eax_.lGain);
+            break;
+
+        case EAXDISTORTION_LOWPASSCUTOFF:
+            eax_call.set_value<EaxDistortionEffectException>(eax_.flLowPassCutOff);
+            break;
+
+        case EAXDISTORTION_EQCENTER:
+            eax_call.set_value<EaxDistortionEffectException>(eax_.flEQCenter);
+            break;
+
+        case EAXDISTORTION_EQBANDWIDTH:
+            eax_call.set_value<EaxDistortionEffectException>(eax_.flEQBandwidth);
+            break;
+
+        default:
+            throw EaxDistortionEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxDistortionEffect::validate_edge(
+    float flEdge)
+{
+    eax_validate_range<EaxDistortionEffectException>(
+        "Edge",
+        flEdge,
+        EAXDISTORTION_MINEDGE,
+        EAXDISTORTION_MAXEDGE);
+}
+
+void EaxDistortionEffect::validate_gain(
+    long lGain)
+{
+    eax_validate_range<EaxDistortionEffectException>(
+        "Gain",
+        lGain,
+        EAXDISTORTION_MINGAIN,
+        EAXDISTORTION_MAXGAIN);
+}
+
+void EaxDistortionEffect::validate_lowpass_cutoff(
+    float flLowPassCutOff)
+{
+    eax_validate_range<EaxDistortionEffectException>(
+        "Low-pass Cut-off",
+        flLowPassCutOff,
+        EAXDISTORTION_MINLOWPASSCUTOFF,
+        EAXDISTORTION_MAXLOWPASSCUTOFF);
+}
+
+void EaxDistortionEffect::validate_eq_center(
+    float flEQCenter)
+{
+    eax_validate_range<EaxDistortionEffectException>(
+        "EQ Center",
+        flEQCenter,
+        EAXDISTORTION_MINEQCENTER,
+        EAXDISTORTION_MAXEQCENTER);
+}
+
+void EaxDistortionEffect::validate_eq_bandwidth(
+    float flEQBandwidth)
+{
+    eax_validate_range<EaxDistortionEffectException>(
+        "EQ Bandwidth",
+        flEQBandwidth,
+        EAXDISTORTION_MINEQBANDWIDTH,
+        EAXDISTORTION_MAXEQBANDWIDTH);
+}
+
+void EaxDistortionEffect::validate_all(
+    const EAXDISTORTIONPROPERTIES& eax_all)
+{
+    validate_edge(eax_all.flEdge);
+    validate_gain(eax_all.lGain);
+    validate_lowpass_cutoff(eax_all.flLowPassCutOff);
+    validate_eq_center(eax_all.flEQCenter);
+    validate_eq_bandwidth(eax_all.flEQBandwidth);
+}
+
+void EaxDistortionEffect::defer_edge(
+    float flEdge)
+{
+    eax_d_.flEdge = flEdge;
+    eax_dirty_flags_.flEdge = (eax_.flEdge != eax_d_.flEdge);
+}
+
+void EaxDistortionEffect::defer_gain(
+    long lGain)
+{
+    eax_d_.lGain = lGain;
+    eax_dirty_flags_.lGain = (eax_.lGain != eax_d_.lGain);
+}
+
+void EaxDistortionEffect::defer_low_pass_cutoff(
+    float flLowPassCutOff)
+{
+    eax_d_.flLowPassCutOff = flLowPassCutOff;
+    eax_dirty_flags_.flLowPassCutOff = (eax_.flLowPassCutOff != eax_d_.flLowPassCutOff);
+}
+
+void EaxDistortionEffect::defer_eq_center(
+    float flEQCenter)
+{
+    eax_d_.flEQCenter = flEQCenter;
+    eax_dirty_flags_.flEQCenter = (eax_.flEQCenter != eax_d_.flEQCenter);
+}
+
+void EaxDistortionEffect::defer_eq_bandwidth(
+    float flEQBandwidth)
+{
+    eax_d_.flEQBandwidth = flEQBandwidth;
+    eax_dirty_flags_.flEQBandwidth = (eax_.flEQBandwidth != eax_d_.flEQBandwidth);
+}
+
+void EaxDistortionEffect::defer_all(
+    const EAXDISTORTIONPROPERTIES& eax_all)
+{
+    defer_edge(eax_all.flEdge);
+    defer_gain(eax_all.lGain);
+    defer_low_pass_cutoff(eax_all.flLowPassCutOff);
+    defer_eq_center(eax_all.flEQCenter);
+    defer_eq_bandwidth(eax_all.flEQBandwidth);
+}
+
+void EaxDistortionEffect::defer_edge(
+    const EaxEaxCall& eax_call)
+{
+    const auto& edge =
+        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEdge)>();
+
+    validate_edge(edge);
+    defer_edge(edge);
+}
+
+void EaxDistortionEffect::defer_gain(
+    const EaxEaxCall& eax_call)
+{
+    const auto& gain =
+        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::lGain)>();
+
+    validate_gain(gain);
+    defer_gain(gain);
+}
+
+void EaxDistortionEffect::defer_low_pass_cutoff(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lowpass_cutoff =
+        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flLowPassCutOff)>();
+
+    validate_lowpass_cutoff(lowpass_cutoff);
+    defer_low_pass_cutoff(lowpass_cutoff);
+}
+
+void EaxDistortionEffect::defer_eq_center(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eq_center =
+        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEQCenter)>();
+
+    validate_eq_center(eq_center);
+    defer_eq_center(eq_center);
+}
+
+void EaxDistortionEffect::defer_eq_bandwidth(
+    const EaxEaxCall& eax_call)
+{
+    const auto& eq_bandwidth =
+        eax_call.get_value<EaxDistortionEffectException, const decltype(EAXDISTORTIONPROPERTIES::flEQBandwidth)>();
+
+    validate_eq_bandwidth(eq_bandwidth);
+    defer_eq_bandwidth(eq_bandwidth);
+}
+
+void EaxDistortionEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxDistortionEffectException, const EAXDISTORTIONPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxDistortionEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxDistortionEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.flEdge)
+    {
+        set_efx_edge();
+    }
+
+    if (eax_dirty_flags_.lGain)
+    {
+        set_efx_gain();
+    }
+
+    if (eax_dirty_flags_.flLowPassCutOff)
+    {
+        set_efx_lowpass_cutoff();
+    }
+
+    if (eax_dirty_flags_.flEQCenter)
+    {
+        set_efx_eq_center();
+    }
+
+    if (eax_dirty_flags_.flEQBandwidth)
+    {
+        set_efx_eq_bandwidth();
+    }
+
+    eax_dirty_flags_ = EaxDistortionEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxDistortionEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXDISTORTION_NONE:
+            break;
+
+        case EAXDISTORTION_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXDISTORTION_EDGE:
+            defer_edge(eax_call);
+            break;
+
+        case EAXDISTORTION_GAIN:
+            defer_gain(eax_call);
+            break;
+
+        case EAXDISTORTION_LOWPASSCUTOFF:
+            defer_low_pass_cutoff(eax_call);
+            break;
+
+        case EAXDISTORTION_EQCENTER:
+            defer_eq_center(eax_call);
+            break;
+
+        case EAXDISTORTION_EQBANDWIDTH:
+            defer_eq_bandwidth(eax_call);
+            break;
+
+        default:
+            throw EaxDistortionEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_distortion_effect()
+{
+    return std::make_unique<EaxDistortionEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 459 - 1
Engine/lib/openal-soft/al/effects/echo.cpp

@@ -4,8 +4,15 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -109,3 +116,454 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Echo);
 
 const EffectProps EchoEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxEchoEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxEchoEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxEchoEffectDirtyFlagsValue flDelay : 1;
+    EaxEchoEffectDirtyFlagsValue flLRDelay : 1;
+    EaxEchoEffectDirtyFlagsValue flDamping : 1;
+    EaxEchoEffectDirtyFlagsValue flFeedback : 1;
+    EaxEchoEffectDirtyFlagsValue flSpread : 1;
+}; // EaxEchoEffectDirtyFlags
+
+
+class EaxEchoEffect final :
+    public EaxEffect
+{
+public:
+    EaxEchoEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXECHOPROPERTIES eax_{};
+    EAXECHOPROPERTIES eax_d_{};
+    EaxEchoEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_delay();
+    void set_efx_lr_delay();
+    void set_efx_damping();
+    void set_efx_feedback();
+    void set_efx_spread();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_delay(float flDelay);
+    void validate_lr_delay(float flLRDelay);
+    void validate_damping(float flDamping);
+    void validate_feedback(float flFeedback);
+    void validate_spread(float flSpread);
+    void validate_all(const EAXECHOPROPERTIES& all);
+
+    void defer_delay(float flDelay);
+    void defer_lr_delay(float flLRDelay);
+    void defer_damping(float flDamping);
+    void defer_feedback(float flFeedback);
+    void defer_spread(float flSpread);
+    void defer_all(const EAXECHOPROPERTIES& all);
+
+    void defer_delay(const EaxEaxCall& eax_call);
+    void defer_lr_delay(const EaxEaxCall& eax_call);
+    void defer_damping(const EaxEaxCall& eax_call);
+    void defer_feedback(const EaxEaxCall& eax_call);
+    void defer_spread(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxEchoEffect
+
+
+class EaxEchoEffectException :
+    public EaxException
+{
+public:
+    explicit EaxEchoEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_ECHO_EFFECT", message}
+    {
+    }
+}; // EaxEchoEffectException
+
+
+EaxEchoEffect::EaxEchoEffect()
+    : EaxEffect{AL_EFFECT_ECHO}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxEchoEffect::dispatch(
+    const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxEchoEffect::set_eax_defaults()
+{
+    eax_.flDelay = EAXECHO_DEFAULTDELAY;
+    eax_.flLRDelay = EAXECHO_DEFAULTLRDELAY;
+    eax_.flDamping = EAXECHO_DEFAULTDAMPING;
+    eax_.flFeedback = EAXECHO_DEFAULTFEEDBACK;
+    eax_.flSpread = EAXECHO_DEFAULTSPREAD;
+
+    eax_d_ = eax_;
+}
+
+void EaxEchoEffect::set_efx_delay()
+{
+    const auto delay = clamp(
+        eax_.flDelay,
+        AL_ECHO_MIN_DELAY,
+        AL_ECHO_MAX_DELAY);
+
+    al_effect_props_.Echo.Delay = delay;
+}
+
+void EaxEchoEffect::set_efx_lr_delay()
+{
+    const auto lr_delay = clamp(
+        eax_.flLRDelay,
+        AL_ECHO_MIN_LRDELAY,
+        AL_ECHO_MAX_LRDELAY);
+
+    al_effect_props_.Echo.LRDelay = lr_delay;
+}
+
+void EaxEchoEffect::set_efx_damping()
+{
+    const auto damping = clamp(
+        eax_.flDamping,
+        AL_ECHO_MIN_DAMPING,
+        AL_ECHO_MAX_DAMPING);
+
+    al_effect_props_.Echo.Damping = damping;
+}
+
+void EaxEchoEffect::set_efx_feedback()
+{
+    const auto feedback = clamp(
+        eax_.flFeedback,
+        AL_ECHO_MIN_FEEDBACK,
+        AL_ECHO_MAX_FEEDBACK);
+
+    al_effect_props_.Echo.Feedback = feedback;
+}
+
+void EaxEchoEffect::set_efx_spread()
+{
+    const auto spread = clamp(
+        eax_.flSpread,
+        AL_ECHO_MIN_SPREAD,
+        AL_ECHO_MAX_SPREAD);
+
+    al_effect_props_.Echo.Spread = spread;
+}
+
+void EaxEchoEffect::set_efx_defaults()
+{
+    set_efx_delay();
+    set_efx_lr_delay();
+    set_efx_damping();
+    set_efx_feedback();
+    set_efx_spread();
+}
+
+void EaxEchoEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXECHO_NONE:
+            break;
+
+        case EAXECHO_ALLPARAMETERS:
+            eax_call.set_value<EaxEchoEffectException>(eax_);
+            break;
+
+        case EAXECHO_DELAY:
+            eax_call.set_value<EaxEchoEffectException>(eax_.flDelay);
+            break;
+
+        case EAXECHO_LRDELAY:
+            eax_call.set_value<EaxEchoEffectException>(eax_.flLRDelay);
+            break;
+
+        case EAXECHO_DAMPING:
+            eax_call.set_value<EaxEchoEffectException>(eax_.flDamping);
+            break;
+
+        case EAXECHO_FEEDBACK:
+            eax_call.set_value<EaxEchoEffectException>(eax_.flFeedback);
+            break;
+
+        case EAXECHO_SPREAD:
+            eax_call.set_value<EaxEchoEffectException>(eax_.flSpread);
+            break;
+
+        default:
+            throw EaxEchoEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxEchoEffect::validate_delay(
+    float flDelay)
+{
+    eax_validate_range<EaxEchoEffectException>(
+        "Delay",
+        flDelay,
+        EAXECHO_MINDELAY,
+        EAXECHO_MAXDELAY);
+}
+
+void EaxEchoEffect::validate_lr_delay(
+    float flLRDelay)
+{
+    eax_validate_range<EaxEchoEffectException>(
+        "LR Delay",
+        flLRDelay,
+        EAXECHO_MINLRDELAY,
+        EAXECHO_MAXLRDELAY);
+}
+
+void EaxEchoEffect::validate_damping(
+    float flDamping)
+{
+    eax_validate_range<EaxEchoEffectException>(
+        "Damping",
+        flDamping,
+        EAXECHO_MINDAMPING,
+        EAXECHO_MAXDAMPING);
+}
+
+void EaxEchoEffect::validate_feedback(
+    float flFeedback)
+{
+    eax_validate_range<EaxEchoEffectException>(
+        "Feedback",
+        flFeedback,
+        EAXECHO_MINFEEDBACK,
+        EAXECHO_MAXFEEDBACK);
+}
+
+void EaxEchoEffect::validate_spread(
+    float flSpread)
+{
+    eax_validate_range<EaxEchoEffectException>(
+        "Spread",
+        flSpread,
+        EAXECHO_MINSPREAD,
+        EAXECHO_MAXSPREAD);
+}
+
+void EaxEchoEffect::validate_all(
+    const EAXECHOPROPERTIES& all)
+{
+    validate_delay(all.flDelay);
+    validate_lr_delay(all.flLRDelay);
+    validate_damping(all.flDamping);
+    validate_feedback(all.flFeedback);
+    validate_spread(all.flSpread);
+}
+
+void EaxEchoEffect::defer_delay(
+    float flDelay)
+{
+    eax_d_.flDelay = flDelay;
+    eax_dirty_flags_.flDelay = (eax_.flDelay != eax_d_.flDelay);
+}
+
+void EaxEchoEffect::defer_lr_delay(
+    float flLRDelay)
+{
+    eax_d_.flLRDelay = flLRDelay;
+    eax_dirty_flags_.flLRDelay = (eax_.flLRDelay != eax_d_.flLRDelay);
+}
+
+void EaxEchoEffect::defer_damping(
+    float flDamping)
+{
+    eax_d_.flDamping = flDamping;
+    eax_dirty_flags_.flDamping = (eax_.flDamping != eax_d_.flDamping);
+}
+
+void EaxEchoEffect::defer_feedback(
+    float flFeedback)
+{
+    eax_d_.flFeedback = flFeedback;
+    eax_dirty_flags_.flFeedback = (eax_.flFeedback != eax_d_.flFeedback);
+}
+
+void EaxEchoEffect::defer_spread(
+    float flSpread)
+{
+    eax_d_.flSpread = flSpread;
+    eax_dirty_flags_.flSpread = (eax_.flSpread != eax_d_.flSpread);
+}
+
+void EaxEchoEffect::defer_all(
+    const EAXECHOPROPERTIES& all)
+{
+    defer_delay(all.flDelay);
+    defer_lr_delay(all.flLRDelay);
+    defer_damping(all.flDamping);
+    defer_feedback(all.flFeedback);
+    defer_spread(all.flSpread);
+}
+
+void EaxEchoEffect::defer_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& delay =
+        eax_call.get_value<EaxEchoEffectException, const decltype(EAXECHOPROPERTIES::flDelay)>();
+
+    validate_delay(delay);
+    defer_delay(delay);
+}
+
+void EaxEchoEffect::defer_lr_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lr_delay =
+        eax_call.get_value<EaxEchoEffectException, const decltype(EAXECHOPROPERTIES::flLRDelay)>();
+
+    validate_lr_delay(lr_delay);
+    defer_lr_delay(lr_delay);
+}
+
+void EaxEchoEffect::defer_damping(
+    const EaxEaxCall& eax_call)
+{
+    const auto& damping =
+        eax_call.get_value<EaxEchoEffectException, const decltype(EAXECHOPROPERTIES::flDamping)>();
+
+    validate_damping(damping);
+    defer_damping(damping);
+}
+
+void EaxEchoEffect::defer_feedback(
+    const EaxEaxCall& eax_call)
+{
+    const auto& feedback =
+        eax_call.get_value<EaxEchoEffectException, const decltype(EAXECHOPROPERTIES::flFeedback)>();
+
+    validate_feedback(feedback);
+    defer_feedback(feedback);
+}
+
+void EaxEchoEffect::defer_spread(
+    const EaxEaxCall& eax_call)
+{
+    const auto& spread =
+        eax_call.get_value<EaxEchoEffectException, const decltype(EAXECHOPROPERTIES::flSpread)>();
+
+    validate_spread(spread);
+    defer_spread(spread);
+}
+
+void EaxEchoEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxEchoEffectException, const EAXECHOPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxEchoEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxEchoEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.flDelay)
+    {
+        set_efx_delay();
+    }
+
+    if (eax_dirty_flags_.flLRDelay)
+    {
+        set_efx_lr_delay();
+    }
+
+    if (eax_dirty_flags_.flDamping)
+    {
+        set_efx_damping();
+    }
+
+    if (eax_dirty_flags_.flFeedback)
+    {
+        set_efx_feedback();
+    }
+
+    if (eax_dirty_flags_.flSpread)
+    {
+        set_efx_spread();
+    }
+
+    eax_dirty_flags_ = EaxEchoEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxEchoEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXECHO_NONE:
+            break;
+
+        case EAXECHO_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXECHO_DELAY:
+            defer_delay(eax_call);
+            break;
+
+        case EAXECHO_LRDELAY:
+            defer_lr_delay(eax_call);
+            break;
+
+        case EAXECHO_DAMPING:
+            defer_damping(eax_call);
+            break;
+
+        case EAXECHO_FEEDBACK:
+            defer_feedback(eax_call);
+            break;
+
+        case EAXECHO_SPREAD:
+            defer_spread(eax_call);
+            break;
+
+        default:
+            throw EaxEchoEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_echo_effect()
+{
+    return std::make_unique<EaxEchoEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 66 - 0
Engine/lib/openal-soft/al/effects/effects.cpp

@@ -0,0 +1,66 @@
+
+#include "config.h"
+
+#ifdef ALSOFT_EAX
+
+#include "effects.h"
+
+#include <cassert>
+
+#include "AL/efx.h"
+
+
+EaxEffectUPtr eax_create_eax_effect(ALenum al_effect_type)
+{
+#define EAX_PREFIX "[EAX_MAKE_EAX_EFFECT] "
+
+    switch (al_effect_type)
+    {
+        case AL_EFFECT_NULL:
+            return eax_create_eax_null_effect();
+
+        case AL_EFFECT_CHORUS:
+            return eax_create_eax_chorus_effect();
+
+        case AL_EFFECT_DISTORTION:
+            return eax_create_eax_distortion_effect();
+
+        case AL_EFFECT_ECHO:
+            return eax_create_eax_echo_effect();
+
+        case AL_EFFECT_FLANGER:
+            return eax_create_eax_flanger_effect();
+
+        case AL_EFFECT_FREQUENCY_SHIFTER:
+            return eax_create_eax_frequency_shifter_effect();
+
+        case AL_EFFECT_VOCAL_MORPHER:
+            return eax_create_eax_vocal_morpher_effect();
+
+        case AL_EFFECT_PITCH_SHIFTER:
+            return eax_create_eax_pitch_shifter_effect();
+
+        case AL_EFFECT_RING_MODULATOR:
+            return eax_create_eax_ring_modulator_effect();
+
+        case AL_EFFECT_AUTOWAH:
+            return eax_create_eax_auto_wah_effect();
+
+        case AL_EFFECT_COMPRESSOR:
+            return eax_create_eax_compressor_effect();
+
+        case AL_EFFECT_EQUALIZER:
+            return eax_create_eax_equalizer_effect();
+
+        case AL_EFFECT_EAXREVERB:
+            return eax_create_eax_reverb_effect();
+
+        default:
+            assert(false && "Unsupported AL effect type.");
+            return nullptr;
+    }
+
+#undef EAX_PREFIX
+}
+
+#endif // ALSOFT_EAX

+ 13 - 0
Engine/lib/openal-soft/al/effects/effects.h

@@ -5,6 +5,10 @@
 
 #include "core/except.h"
 
+#ifdef ALSOFT_EAX
+#include "al/eax_effect.h"
+#endif // ALSOFT_EAX
+
 union EffectProps;
 
 
@@ -12,7 +16,11 @@ class effect_exception final : public al::base_exception {
     ALenum mErrorCode;
 
 public:
+#ifdef __USE_MINGW_ANSI_STDIO
+    [[gnu::format(gnu_printf, 3, 4)]]
+#else
     [[gnu::format(printf, 3, 4)]]
+#endif
     effect_exception(ALenum code, const char *msg, ...);
 
     ALenum errorCode() const noexcept { return mErrorCode; }
@@ -76,4 +84,9 @@ extern const EffectVtable VmorpherEffectVtable;
 extern const EffectVtable DedicatedEffectVtable;
 extern const EffectVtable ConvolutionEffectVtable;
 
+
+#ifdef ALSOFT_EAX
+EaxEffectUPtr eax_create_eax_effect(ALenum al_effect_type);
+#endif // ALSOFT_EAX
+
 #endif /* AL_EFFECTS_EFFECTS_H */

+ 753 - 1
Engine/lib/openal-soft/al/effects/equalizer.cpp

@@ -4,8 +4,15 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -167,3 +174,748 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Equalizer);
 
 const EffectProps EqualizerEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxEqualizerEffectDirtyFlagsValue = std::uint_least16_t;
+
+struct EaxEqualizerEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxEqualizerEffectDirtyFlagsValue lLowGain : 1;
+    EaxEqualizerEffectDirtyFlagsValue flLowCutOff : 1;
+    EaxEqualizerEffectDirtyFlagsValue lMid1Gain : 1;
+    EaxEqualizerEffectDirtyFlagsValue flMid1Center : 1;
+    EaxEqualizerEffectDirtyFlagsValue flMid1Width : 1;
+    EaxEqualizerEffectDirtyFlagsValue lMid2Gain : 1;
+    EaxEqualizerEffectDirtyFlagsValue flMid2Center : 1;
+    EaxEqualizerEffectDirtyFlagsValue flMid2Width : 1;
+    EaxEqualizerEffectDirtyFlagsValue lHighGain : 1;
+    EaxEqualizerEffectDirtyFlagsValue flHighCutOff : 1;
+}; // EaxEqualizerEffectDirtyFlags
+
+
+class EaxEqualizerEffect final :
+    public EaxEffect
+{
+public:
+    EaxEqualizerEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXEQUALIZERPROPERTIES eax_{};
+    EAXEQUALIZERPROPERTIES eax_d_{};
+    EaxEqualizerEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_low_gain();
+    void set_efx_low_cutoff();
+    void set_efx_mid1_gain();
+    void set_efx_mid1_center();
+    void set_efx_mid1_width();
+    void set_efx_mid2_gain();
+    void set_efx_mid2_center();
+    void set_efx_mid2_width();
+    void set_efx_high_gain();
+    void set_efx_high_cutoff();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_low_gain(long lLowGain);
+    void validate_low_cutoff(float flLowCutOff);
+    void validate_mid1_gain(long lMid1Gain);
+    void validate_mid1_center(float flMid1Center);
+    void validate_mid1_width(float flMid1Width);
+    void validate_mid2_gain(long lMid2Gain);
+    void validate_mid2_center(float flMid2Center);
+    void validate_mid2_width(float flMid2Width);
+    void validate_high_gain(long lHighGain);
+    void validate_high_cutoff(float flHighCutOff);
+    void validate_all(const EAXEQUALIZERPROPERTIES& all);
+
+    void defer_low_gain(long lLowGain);
+    void defer_low_cutoff(float flLowCutOff);
+    void defer_mid1_gain(long lMid1Gain);
+    void defer_mid1_center(float flMid1Center);
+    void defer_mid1_width(float flMid1Width);
+    void defer_mid2_gain(long lMid2Gain);
+    void defer_mid2_center(float flMid2Center);
+    void defer_mid2_width(float flMid2Width);
+    void defer_high_gain(long lHighGain);
+    void defer_high_cutoff(float flHighCutOff);
+    void defer_all(const EAXEQUALIZERPROPERTIES& all);
+
+    void defer_low_gain(const EaxEaxCall& eax_call);
+    void defer_low_cutoff(const EaxEaxCall& eax_call);
+    void defer_mid1_gain(const EaxEaxCall& eax_call);
+    void defer_mid1_center(const EaxEaxCall& eax_call);
+    void defer_mid1_width(const EaxEaxCall& eax_call);
+    void defer_mid2_gain(const EaxEaxCall& eax_call);
+    void defer_mid2_center(const EaxEaxCall& eax_call);
+    void defer_mid2_width(const EaxEaxCall& eax_call);
+    void defer_high_gain(const EaxEaxCall& eax_call);
+    void defer_high_cutoff(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxEqualizerEffect
+
+
+class EaxEqualizerEffectException :
+    public EaxException
+{
+public:
+    explicit EaxEqualizerEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_EQUALIZER_EFFECT", message}
+    {
+    }
+}; // EaxEqualizerEffectException
+
+
+EaxEqualizerEffect::EaxEqualizerEffect()
+    : EaxEffect{AL_EFFECT_EQUALIZER}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxEqualizerEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxEqualizerEffect::set_eax_defaults()
+{
+    eax_.lLowGain = EAXEQUALIZER_DEFAULTLOWGAIN;
+    eax_.flLowCutOff = EAXEQUALIZER_DEFAULTLOWCUTOFF;
+    eax_.lMid1Gain = EAXEQUALIZER_DEFAULTMID1GAIN;
+    eax_.flMid1Center = EAXEQUALIZER_DEFAULTMID1CENTER;
+    eax_.flMid1Width = EAXEQUALIZER_DEFAULTMID1WIDTH;
+    eax_.lMid2Gain = EAXEQUALIZER_DEFAULTMID2GAIN;
+    eax_.flMid2Center = EAXEQUALIZER_DEFAULTMID2CENTER;
+    eax_.flMid2Width = EAXEQUALIZER_DEFAULTMID2WIDTH;
+    eax_.lHighGain = EAXEQUALIZER_DEFAULTHIGHGAIN;
+    eax_.flHighCutOff = EAXEQUALIZER_DEFAULTHIGHCUTOFF;
+
+    eax_d_ = eax_;
+}
+
+void EaxEqualizerEffect::set_efx_low_gain()
+{
+    const auto low_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lLowGain)),
+        AL_EQUALIZER_MIN_LOW_GAIN,
+        AL_EQUALIZER_MAX_LOW_GAIN);
+
+    al_effect_props_.Equalizer.LowGain = low_gain;
+}
+
+void EaxEqualizerEffect::set_efx_low_cutoff()
+{
+    const auto low_cutoff = clamp(
+        eax_.flLowCutOff,
+        AL_EQUALIZER_MIN_LOW_CUTOFF,
+        AL_EQUALIZER_MAX_LOW_CUTOFF);
+
+    al_effect_props_.Equalizer.LowCutoff = low_cutoff;
+}
+
+void EaxEqualizerEffect::set_efx_mid1_gain()
+{
+    const auto mid1_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lMid1Gain)),
+        AL_EQUALIZER_MIN_MID1_GAIN,
+        AL_EQUALIZER_MAX_MID1_GAIN);
+
+    al_effect_props_.Equalizer.Mid1Gain = mid1_gain;
+}
+
+void EaxEqualizerEffect::set_efx_mid1_center()
+{
+    const auto mid1_center = clamp(
+        eax_.flMid1Center,
+        AL_EQUALIZER_MIN_MID1_CENTER,
+        AL_EQUALIZER_MAX_MID1_CENTER);
+
+    al_effect_props_.Equalizer.Mid1Center = mid1_center;
+}
+
+void EaxEqualizerEffect::set_efx_mid1_width()
+{
+    const auto mid1_width = clamp(
+        eax_.flMid1Width,
+        AL_EQUALIZER_MIN_MID1_WIDTH,
+        AL_EQUALIZER_MAX_MID1_WIDTH);
+
+    al_effect_props_.Equalizer.Mid1Width = mid1_width;
+}
+
+void EaxEqualizerEffect::set_efx_mid2_gain()
+{
+    const auto mid2_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lMid2Gain)),
+        AL_EQUALIZER_MIN_MID2_GAIN,
+        AL_EQUALIZER_MAX_MID2_GAIN);
+
+    al_effect_props_.Equalizer.Mid2Gain = mid2_gain;
+}
+
+void EaxEqualizerEffect::set_efx_mid2_center()
+{
+    const auto mid2_center = clamp(
+        eax_.flMid2Center,
+        AL_EQUALIZER_MIN_MID2_CENTER,
+        AL_EQUALIZER_MAX_MID2_CENTER);
+
+    al_effect_props_.Equalizer.Mid2Center = mid2_center;
+}
+
+void EaxEqualizerEffect::set_efx_mid2_width()
+{
+    const auto mid2_width = clamp(
+        eax_.flMid2Width,
+        AL_EQUALIZER_MIN_MID2_WIDTH,
+        AL_EQUALIZER_MAX_MID2_WIDTH);
+
+    al_effect_props_.Equalizer.Mid2Width = mid2_width;
+}
+
+void EaxEqualizerEffect::set_efx_high_gain()
+{
+    const auto high_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lHighGain)),
+        AL_EQUALIZER_MIN_HIGH_GAIN,
+        AL_EQUALIZER_MAX_HIGH_GAIN);
+
+    al_effect_props_.Equalizer.HighGain = high_gain;
+}
+
+void EaxEqualizerEffect::set_efx_high_cutoff()
+{
+    const auto high_cutoff = clamp(
+        eax_.flHighCutOff,
+        AL_EQUALIZER_MIN_HIGH_CUTOFF,
+        AL_EQUALIZER_MAX_HIGH_CUTOFF);
+
+    al_effect_props_.Equalizer.HighCutoff = high_cutoff;
+}
+
+void EaxEqualizerEffect::set_efx_defaults()
+{
+    set_efx_low_gain();
+    set_efx_low_cutoff();
+    set_efx_mid1_gain();
+    set_efx_mid1_center();
+    set_efx_mid1_width();
+    set_efx_mid2_gain();
+    set_efx_mid2_center();
+    set_efx_mid2_width();
+    set_efx_high_gain();
+    set_efx_high_cutoff();
+}
+
+void EaxEqualizerEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXEQUALIZER_NONE:
+            break;
+
+        case EAXEQUALIZER_ALLPARAMETERS:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_);
+            break;
+
+        case EAXEQUALIZER_LOWGAIN:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.lLowGain);
+            break;
+
+        case EAXEQUALIZER_LOWCUTOFF:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flLowCutOff);
+            break;
+
+        case EAXEQUALIZER_MID1GAIN:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.lMid1Gain);
+            break;
+
+        case EAXEQUALIZER_MID1CENTER:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flMid1Center);
+            break;
+
+        case EAXEQUALIZER_MID1WIDTH:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flMid1Width);
+            break;
+
+        case EAXEQUALIZER_MID2GAIN:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.lMid2Gain);
+            break;
+
+        case EAXEQUALIZER_MID2CENTER:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flMid2Center);
+            break;
+
+        case EAXEQUALIZER_MID2WIDTH:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flMid2Width);
+            break;
+
+        case EAXEQUALIZER_HIGHGAIN:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.lHighGain);
+            break;
+
+        case EAXEQUALIZER_HIGHCUTOFF:
+            eax_call.set_value<EaxEqualizerEffectException>(eax_.flHighCutOff);
+            break;
+
+        default:
+            throw EaxEqualizerEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxEqualizerEffect::validate_low_gain(
+    long lLowGain)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Low Gain",
+        lLowGain,
+        EAXEQUALIZER_MINLOWGAIN,
+        EAXEQUALIZER_MAXLOWGAIN);
+}
+
+void EaxEqualizerEffect::validate_low_cutoff(
+    float flLowCutOff)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Low Cutoff",
+        flLowCutOff,
+        EAXEQUALIZER_MINLOWCUTOFF,
+        EAXEQUALIZER_MAXLOWCUTOFF);
+}
+
+void EaxEqualizerEffect::validate_mid1_gain(
+    long lMid1Gain)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid1 Gain",
+        lMid1Gain,
+        EAXEQUALIZER_MINMID1GAIN,
+        EAXEQUALIZER_MAXMID1GAIN);
+}
+
+void EaxEqualizerEffect::validate_mid1_center(
+    float flMid1Center)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid1 Center",
+        flMid1Center,
+        EAXEQUALIZER_MINMID1CENTER,
+        EAXEQUALIZER_MAXMID1CENTER);
+}
+
+void EaxEqualizerEffect::validate_mid1_width(
+    float flMid1Width)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid1 Width",
+        flMid1Width,
+        EAXEQUALIZER_MINMID1WIDTH,
+        EAXEQUALIZER_MAXMID1WIDTH);
+}
+
+void EaxEqualizerEffect::validate_mid2_gain(
+    long lMid2Gain)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid2 Gain",
+        lMid2Gain,
+        EAXEQUALIZER_MINMID2GAIN,
+        EAXEQUALIZER_MAXMID2GAIN);
+}
+
+void EaxEqualizerEffect::validate_mid2_center(
+    float flMid2Center)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid2 Center",
+        flMid2Center,
+        EAXEQUALIZER_MINMID2CENTER,
+        EAXEQUALIZER_MAXMID2CENTER);
+}
+
+void EaxEqualizerEffect::validate_mid2_width(
+    float flMid2Width)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "Mid2 Width",
+        flMid2Width,
+        EAXEQUALIZER_MINMID2WIDTH,
+        EAXEQUALIZER_MAXMID2WIDTH);
+}
+
+void EaxEqualizerEffect::validate_high_gain(
+    long lHighGain)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "High Gain",
+        lHighGain,
+        EAXEQUALIZER_MINHIGHGAIN,
+        EAXEQUALIZER_MAXHIGHGAIN);
+}
+
+void EaxEqualizerEffect::validate_high_cutoff(
+    float flHighCutOff)
+{
+    eax_validate_range<EaxEqualizerEffectException>(
+        "High Cutoff",
+        flHighCutOff,
+        EAXEQUALIZER_MINHIGHCUTOFF,
+        EAXEQUALIZER_MAXHIGHCUTOFF);
+}
+
+void EaxEqualizerEffect::validate_all(
+    const EAXEQUALIZERPROPERTIES& all)
+{
+    validate_low_gain(all.lLowGain);
+    validate_low_cutoff(all.flLowCutOff);
+    validate_mid1_gain(all.lMid1Gain);
+    validate_mid1_center(all.flMid1Center);
+    validate_mid1_width(all.flMid1Width);
+    validate_mid2_gain(all.lMid2Gain);
+    validate_mid2_center(all.flMid2Center);
+    validate_mid2_width(all.flMid2Width);
+    validate_high_gain(all.lHighGain);
+    validate_high_cutoff(all.flHighCutOff);
+}
+
+void EaxEqualizerEffect::defer_low_gain(
+    long lLowGain)
+{
+    eax_d_.lLowGain = lLowGain;
+    eax_dirty_flags_.lLowGain = (eax_.lLowGain != eax_d_.lLowGain);
+}
+
+void EaxEqualizerEffect::defer_low_cutoff(
+    float flLowCutOff)
+{
+    eax_d_.flLowCutOff = flLowCutOff;
+    eax_dirty_flags_.flLowCutOff = (eax_.flLowCutOff != eax_d_.flLowCutOff);
+}
+
+void EaxEqualizerEffect::defer_mid1_gain(
+    long lMid1Gain)
+{
+    eax_d_.lMid1Gain = lMid1Gain;
+    eax_dirty_flags_.lMid1Gain = (eax_.lMid1Gain != eax_d_.lMid1Gain);
+}
+
+void EaxEqualizerEffect::defer_mid1_center(
+    float flMid1Center)
+{
+    eax_d_.flMid1Center = flMid1Center;
+    eax_dirty_flags_.flMid1Center = (eax_.flMid1Center != eax_d_.flMid1Center);
+}
+
+void EaxEqualizerEffect::defer_mid1_width(
+    float flMid1Width)
+{
+    eax_d_.flMid1Width = flMid1Width;
+    eax_dirty_flags_.flMid1Width = (eax_.flMid1Width != eax_d_.flMid1Width);
+}
+
+void EaxEqualizerEffect::defer_mid2_gain(
+    long lMid2Gain)
+{
+    eax_d_.lMid2Gain = lMid2Gain;
+    eax_dirty_flags_.lMid2Gain = (eax_.lMid2Gain != eax_d_.lMid2Gain);
+}
+
+void EaxEqualizerEffect::defer_mid2_center(
+    float flMid2Center)
+{
+    eax_d_.flMid2Center = flMid2Center;
+    eax_dirty_flags_.flMid2Center = (eax_.flMid2Center != eax_d_.flMid2Center);
+}
+
+void EaxEqualizerEffect::defer_mid2_width(
+    float flMid2Width)
+{
+    eax_d_.flMid2Width = flMid2Width;
+    eax_dirty_flags_.flMid2Width = (eax_.flMid2Width != eax_d_.flMid2Width);
+}
+
+void EaxEqualizerEffect::defer_high_gain(
+    long lHighGain)
+{
+    eax_d_.lHighGain = lHighGain;
+    eax_dirty_flags_.lHighGain = (eax_.lHighGain != eax_d_.lHighGain);
+}
+
+void EaxEqualizerEffect::defer_high_cutoff(
+    float flHighCutOff)
+{
+    eax_d_.flHighCutOff = flHighCutOff;
+    eax_dirty_flags_.flHighCutOff = (eax_.flHighCutOff != eax_d_.flHighCutOff);
+}
+
+void EaxEqualizerEffect::defer_all(
+    const EAXEQUALIZERPROPERTIES& all)
+{
+    defer_low_gain(all.lLowGain);
+    defer_low_cutoff(all.flLowCutOff);
+    defer_mid1_gain(all.lMid1Gain);
+    defer_mid1_center(all.flMid1Center);
+    defer_mid1_width(all.flMid1Width);
+    defer_mid2_gain(all.lMid2Gain);
+    defer_mid2_center(all.flMid2Center);
+    defer_mid2_width(all.flMid2Width);
+    defer_high_gain(all.lHighGain);
+    defer_high_cutoff(all.flHighCutOff);
+}
+
+void EaxEqualizerEffect::defer_low_gain(
+    const EaxEaxCall& eax_call)
+{
+    const auto& low_gain =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::lLowGain)>();
+
+    validate_low_gain(low_gain);
+    defer_low_gain(low_gain);
+}
+
+void EaxEqualizerEffect::defer_low_cutoff(
+    const EaxEaxCall& eax_call)
+{
+    const auto& low_cutoff =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flLowCutOff)>();
+
+    validate_low_cutoff(low_cutoff);
+    defer_low_cutoff(low_cutoff);
+}
+
+void EaxEqualizerEffect::defer_mid1_gain(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid1_gain =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::lMid1Gain)>();
+
+    validate_mid1_gain(mid1_gain);
+    defer_mid1_gain(mid1_gain);
+}
+
+void EaxEqualizerEffect::defer_mid1_center(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid1_center =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flMid1Center)>();
+
+    validate_mid1_center(mid1_center);
+    defer_mid1_center(mid1_center);
+}
+
+void EaxEqualizerEffect::defer_mid1_width(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid1_width =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flMid1Width)>();
+
+    validate_mid1_width(mid1_width);
+    defer_mid1_width(mid1_width);
+}
+
+void EaxEqualizerEffect::defer_mid2_gain(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid2_gain =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::lMid2Gain)>();
+
+    validate_mid2_gain(mid2_gain);
+    defer_mid2_gain(mid2_gain);
+}
+
+void EaxEqualizerEffect::defer_mid2_center(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid2_center =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flMid2Center)>();
+
+    validate_mid2_center(mid2_center);
+    defer_mid2_center(mid2_center);
+}
+
+void EaxEqualizerEffect::defer_mid2_width(
+    const EaxEaxCall& eax_call)
+{
+    const auto& mid2_width =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flMid2Width)>();
+
+    validate_mid2_width(mid2_width);
+    defer_mid2_width(mid2_width);
+}
+
+void EaxEqualizerEffect::defer_high_gain(
+    const EaxEaxCall& eax_call)
+{
+    const auto& high_gain =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::lHighGain)>();
+
+    validate_high_gain(high_gain);
+    defer_high_gain(high_gain);
+}
+
+void EaxEqualizerEffect::defer_high_cutoff(
+    const EaxEaxCall& eax_call)
+{
+    const auto& high_cutoff =
+        eax_call.get_value<EaxEqualizerEffectException, const decltype(EAXEQUALIZERPROPERTIES::flHighCutOff)>();
+
+    validate_high_cutoff(high_cutoff);
+    defer_high_cutoff(high_cutoff);
+}
+
+void EaxEqualizerEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxEqualizerEffectException, const EAXEQUALIZERPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxEqualizerEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxEqualizerEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.lLowGain)
+    {
+        set_efx_low_gain();
+    }
+
+    if (eax_dirty_flags_.flLowCutOff)
+    {
+        set_efx_low_cutoff();
+    }
+
+    if (eax_dirty_flags_.lMid1Gain)
+    {
+        set_efx_mid1_gain();
+    }
+
+    if (eax_dirty_flags_.flMid1Center)
+    {
+        set_efx_mid1_center();
+    }
+
+    if (eax_dirty_flags_.flMid1Width)
+    {
+        set_efx_mid1_width();
+    }
+
+    if (eax_dirty_flags_.lMid2Gain)
+    {
+        set_efx_mid2_gain();
+    }
+
+    if (eax_dirty_flags_.flMid2Center)
+    {
+        set_efx_mid2_center();
+    }
+
+    if (eax_dirty_flags_.flMid2Width)
+    {
+        set_efx_mid2_width();
+    }
+
+    if (eax_dirty_flags_.lHighGain)
+    {
+        set_efx_high_gain();
+    }
+
+    if (eax_dirty_flags_.flHighCutOff)
+    {
+        set_efx_high_cutoff();
+    }
+
+    eax_dirty_flags_ = EaxEqualizerEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxEqualizerEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXEQUALIZER_NONE:
+            break;
+
+        case EAXEQUALIZER_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXEQUALIZER_LOWGAIN:
+            defer_low_gain(eax_call);
+            break;
+
+        case EAXEQUALIZER_LOWCUTOFF:
+            defer_low_cutoff(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID1GAIN:
+            defer_mid1_gain(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID1CENTER:
+            defer_mid1_center(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID1WIDTH:
+            defer_mid1_width(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID2GAIN:
+            defer_mid2_gain(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID2CENTER:
+            defer_mid2_center(eax_call);
+            break;
+
+        case EAXEQUALIZER_MID2WIDTH:
+            defer_mid2_width(eax_call);
+            break;
+
+        case EAXEQUALIZER_HIGHGAIN:
+            defer_high_gain(eax_call);
+            break;
+
+        case EAXEQUALIZER_HIGHCUTOFF:
+            defer_high_cutoff(eax_call);
+            break;
+
+        default:
+            throw EaxEqualizerEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_equalizer_effect()
+{
+    return std::make_unique<EaxEqualizerEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 352 - 1
Engine/lib/openal-soft/al/effects/fshifter.cpp

@@ -1,12 +1,23 @@
 
 #include "config.h"
 
+#include <stdexcept>
+
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "aloptional.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -126,3 +137,343 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Fshifter);
 
 const EffectProps FshifterEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxFrequencyShifterEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxFrequencyShifterEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxFrequencyShifterEffectDirtyFlagsValue flFrequency : 1;
+    EaxFrequencyShifterEffectDirtyFlagsValue ulLeftDirection : 1;
+    EaxFrequencyShifterEffectDirtyFlagsValue ulRightDirection : 1;
+}; // EaxFrequencyShifterEffectDirtyFlags
+
+
+class EaxFrequencyShifterEffect final :
+    public EaxEffect
+{
+public:
+    EaxFrequencyShifterEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXFREQUENCYSHIFTERPROPERTIES eax_{};
+    EAXFREQUENCYSHIFTERPROPERTIES eax_d_{};
+    EaxFrequencyShifterEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_frequency();
+    void set_efx_left_direction();
+    void set_efx_right_direction();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_frequency(float flFrequency);
+    void validate_left_direction(unsigned long ulLeftDirection);
+    void validate_right_direction(unsigned long ulRightDirection);
+    void validate_all(const EAXFREQUENCYSHIFTERPROPERTIES& all);
+
+    void defer_frequency(float flFrequency);
+    void defer_left_direction(unsigned long ulLeftDirection);
+    void defer_right_direction(unsigned long ulRightDirection);
+    void defer_all(const EAXFREQUENCYSHIFTERPROPERTIES& all);
+
+    void defer_frequency(const EaxEaxCall& eax_call);
+    void defer_left_direction(const EaxEaxCall& eax_call);
+    void defer_right_direction(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxFrequencyShifterEffect
+
+
+class EaxFrequencyShifterEffectException :
+    public EaxException
+{
+public:
+    explicit EaxFrequencyShifterEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_FREQUENCY_SHIFTER_EFFECT", message}
+    {
+    }
+}; // EaxFrequencyShifterEffectException
+
+
+EaxFrequencyShifterEffect::EaxFrequencyShifterEffect()
+    : EaxEffect{AL_EFFECT_FREQUENCY_SHIFTER}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxFrequencyShifterEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxFrequencyShifterEffect::set_eax_defaults()
+{
+    eax_.flFrequency = EAXFREQUENCYSHIFTER_DEFAULTFREQUENCY;
+    eax_.ulLeftDirection = EAXFREQUENCYSHIFTER_DEFAULTLEFTDIRECTION;
+    eax_.ulRightDirection = EAXFREQUENCYSHIFTER_DEFAULTRIGHTDIRECTION;
+
+    eax_d_ = eax_;
+}
+
+void EaxFrequencyShifterEffect::set_efx_frequency()
+{
+    const auto frequency = clamp(
+        eax_.flFrequency,
+        AL_FREQUENCY_SHIFTER_MIN_FREQUENCY,
+        AL_FREQUENCY_SHIFTER_MAX_FREQUENCY);
+
+    al_effect_props_.Fshifter.Frequency = frequency;
+}
+
+void EaxFrequencyShifterEffect::set_efx_left_direction()
+{
+    const auto left_direction = clamp(
+        static_cast<ALint>(eax_.ulLeftDirection),
+        AL_FREQUENCY_SHIFTER_MIN_LEFT_DIRECTION,
+        AL_FREQUENCY_SHIFTER_MAX_LEFT_DIRECTION);
+
+    const auto efx_left_direction = DirectionFromEmum(left_direction);
+    assert(efx_left_direction.has_value());
+    al_effect_props_.Fshifter.LeftDirection = *efx_left_direction;
+}
+
+void EaxFrequencyShifterEffect::set_efx_right_direction()
+{
+    const auto right_direction = clamp(
+        static_cast<ALint>(eax_.ulRightDirection),
+        AL_FREQUENCY_SHIFTER_MIN_RIGHT_DIRECTION,
+        AL_FREQUENCY_SHIFTER_MAX_RIGHT_DIRECTION);
+
+    const auto efx_right_direction = DirectionFromEmum(right_direction);
+    assert(efx_right_direction.has_value());
+    al_effect_props_.Fshifter.RightDirection = *efx_right_direction;
+}
+
+void EaxFrequencyShifterEffect::set_efx_defaults()
+{
+    set_efx_frequency();
+    set_efx_left_direction();
+    set_efx_right_direction();
+}
+
+void EaxFrequencyShifterEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXFREQUENCYSHIFTER_NONE:
+            break;
+
+        case EAXFREQUENCYSHIFTER_ALLPARAMETERS:
+            eax_call.set_value<EaxFrequencyShifterEffectException>(eax_);
+            break;
+
+        case EAXFREQUENCYSHIFTER_FREQUENCY:
+            eax_call.set_value<EaxFrequencyShifterEffectException>(eax_.flFrequency);
+            break;
+
+        case EAXFREQUENCYSHIFTER_LEFTDIRECTION:
+            eax_call.set_value<EaxFrequencyShifterEffectException>(eax_.ulLeftDirection);
+            break;
+
+        case EAXFREQUENCYSHIFTER_RIGHTDIRECTION:
+            eax_call.set_value<EaxFrequencyShifterEffectException>(eax_.ulRightDirection);
+            break;
+
+        default:
+            throw EaxFrequencyShifterEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxFrequencyShifterEffect::validate_frequency(
+    float flFrequency)
+{
+    eax_validate_range<EaxFrequencyShifterEffectException>(
+        "Frequency",
+        flFrequency,
+        EAXFREQUENCYSHIFTER_MINFREQUENCY,
+        EAXFREQUENCYSHIFTER_MAXFREQUENCY);
+}
+
+void EaxFrequencyShifterEffect::validate_left_direction(
+    unsigned long ulLeftDirection)
+{
+    eax_validate_range<EaxFrequencyShifterEffectException>(
+        "Left Direction",
+        ulLeftDirection,
+        EAXFREQUENCYSHIFTER_MINLEFTDIRECTION,
+        EAXFREQUENCYSHIFTER_MAXLEFTDIRECTION);
+}
+
+void EaxFrequencyShifterEffect::validate_right_direction(
+    unsigned long ulRightDirection)
+{
+    eax_validate_range<EaxFrequencyShifterEffectException>(
+        "Right Direction",
+        ulRightDirection,
+        EAXFREQUENCYSHIFTER_MINRIGHTDIRECTION,
+        EAXFREQUENCYSHIFTER_MAXRIGHTDIRECTION);
+}
+
+void EaxFrequencyShifterEffect::validate_all(
+    const EAXFREQUENCYSHIFTERPROPERTIES& all)
+{
+    validate_frequency(all.flFrequency);
+    validate_left_direction(all.ulLeftDirection);
+    validate_right_direction(all.ulRightDirection);
+}
+
+void EaxFrequencyShifterEffect::defer_frequency(
+    float flFrequency)
+{
+    eax_d_.flFrequency = flFrequency;
+    eax_dirty_flags_.flFrequency = (eax_.flFrequency != eax_d_.flFrequency);
+}
+
+void EaxFrequencyShifterEffect::defer_left_direction(
+    unsigned long ulLeftDirection)
+{
+    eax_d_.ulLeftDirection = ulLeftDirection;
+    eax_dirty_flags_.ulLeftDirection = (eax_.ulLeftDirection != eax_d_.ulLeftDirection);
+}
+
+void EaxFrequencyShifterEffect::defer_right_direction(
+    unsigned long ulRightDirection)
+{
+    eax_d_.ulRightDirection = ulRightDirection;
+    eax_dirty_flags_.ulRightDirection = (eax_.ulRightDirection != eax_d_.ulRightDirection);
+}
+
+void EaxFrequencyShifterEffect::defer_all(
+    const EAXFREQUENCYSHIFTERPROPERTIES& all)
+{
+    defer_frequency(all.flFrequency);
+    defer_left_direction(all.ulLeftDirection);
+    defer_right_direction(all.ulRightDirection);
+}
+
+void EaxFrequencyShifterEffect::defer_frequency(
+    const EaxEaxCall& eax_call)
+{
+    const auto& frequency =
+        eax_call.get_value<
+        EaxFrequencyShifterEffectException, const decltype(EAXFREQUENCYSHIFTERPROPERTIES::flFrequency)>();
+
+    validate_frequency(frequency);
+    defer_frequency(frequency);
+}
+
+void EaxFrequencyShifterEffect::defer_left_direction(
+    const EaxEaxCall& eax_call)
+{
+    const auto& left_direction =
+        eax_call.get_value<
+        EaxFrequencyShifterEffectException, const decltype(EAXFREQUENCYSHIFTERPROPERTIES::ulLeftDirection)>();
+
+    validate_left_direction(left_direction);
+    defer_left_direction(left_direction);
+}
+
+void EaxFrequencyShifterEffect::defer_right_direction(
+    const EaxEaxCall& eax_call)
+{
+    const auto& right_direction =
+        eax_call.get_value<
+        EaxFrequencyShifterEffectException, const decltype(EAXFREQUENCYSHIFTERPROPERTIES::ulRightDirection)>();
+
+    validate_right_direction(right_direction);
+    defer_right_direction(right_direction);
+}
+
+void EaxFrequencyShifterEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<
+        EaxFrequencyShifterEffectException, const EAXFREQUENCYSHIFTERPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxFrequencyShifterEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxFrequencyShifterEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.flFrequency)
+    {
+        set_efx_frequency();
+    }
+
+    if (eax_dirty_flags_.ulLeftDirection)
+    {
+        set_efx_left_direction();
+    }
+
+    if (eax_dirty_flags_.ulRightDirection)
+    {
+        set_efx_right_direction();
+    }
+
+    eax_dirty_flags_ = EaxFrequencyShifterEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxFrequencyShifterEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXFREQUENCYSHIFTER_NONE:
+            break;
+
+        case EAXFREQUENCYSHIFTER_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXFREQUENCYSHIFTER_FREQUENCY:
+            defer_frequency(eax_call);
+            break;
+
+        case EAXFREQUENCYSHIFTER_LEFTDIRECTION:
+            defer_left_direction(eax_call);
+            break;
+
+        case EAXFREQUENCYSHIFTER_RIGHTDIRECTION:
+            defer_right_direction(eax_call);
+            break;
+
+        default:
+            throw EaxFrequencyShifterEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_frequency_shifter_effect()
+{
+    return std::make_unique<EaxFrequencyShifterEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 349 - 1
Engine/lib/openal-soft/al/effects/modulator.cpp

@@ -1,12 +1,23 @@
 
 #include "config.h"
 
+#include <stdexcept>
+
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "aloptional.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -132,3 +143,340 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Modulator);
 
 const EffectProps ModulatorEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxRingModulatorEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxRingModulatorEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxRingModulatorEffectDirtyFlagsValue flFrequency : 1;
+    EaxRingModulatorEffectDirtyFlagsValue flHighPassCutOff : 1;
+    EaxRingModulatorEffectDirtyFlagsValue ulWaveform : 1;
+}; // EaxPitchShifterEffectDirtyFlags
+
+
+class EaxRingModulatorEffect final :
+    public EaxEffect
+{
+public:
+    EaxRingModulatorEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXRINGMODULATORPROPERTIES eax_{};
+    EAXRINGMODULATORPROPERTIES eax_d_{};
+    EaxRingModulatorEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_frequency();
+    void set_efx_high_pass_cutoff();
+    void set_efx_waveform();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_frequency(float flFrequency);
+    void validate_high_pass_cutoff(float flHighPassCutOff);
+    void validate_waveform(unsigned long ulWaveform);
+    void validate_all(const EAXRINGMODULATORPROPERTIES& all);
+
+    void defer_frequency(float flFrequency);
+    void defer_high_pass_cutoff(float flHighPassCutOff);
+    void defer_waveform(unsigned long ulWaveform);
+    void defer_all(const EAXRINGMODULATORPROPERTIES& all);
+
+    void defer_frequency(const EaxEaxCall& eax_call);
+    void defer_high_pass_cutoff(const EaxEaxCall& eax_call);
+    void defer_waveform(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxRingModulatorEffect
+
+
+class EaxRingModulatorEffectException :
+    public EaxException
+{
+public:
+    explicit EaxRingModulatorEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_RING_MODULATOR_EFFECT", message}
+    {
+    }
+}; // EaxRingModulatorEffectException
+
+
+EaxRingModulatorEffect::EaxRingModulatorEffect()
+    : EaxEffect{AL_EFFECT_RING_MODULATOR}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxRingModulatorEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxRingModulatorEffect::set_eax_defaults()
+{
+    eax_.flFrequency = EAXRINGMODULATOR_DEFAULTFREQUENCY;
+    eax_.flHighPassCutOff = EAXRINGMODULATOR_DEFAULTHIGHPASSCUTOFF;
+    eax_.ulWaveform = EAXRINGMODULATOR_DEFAULTWAVEFORM;
+
+    eax_d_ = eax_;
+}
+
+void EaxRingModulatorEffect::set_efx_frequency()
+{
+    const auto frequency = clamp(
+        eax_.flFrequency,
+        AL_RING_MODULATOR_MIN_FREQUENCY,
+        AL_RING_MODULATOR_MAX_FREQUENCY);
+
+    al_effect_props_.Modulator.Frequency = frequency;
+}
+
+void EaxRingModulatorEffect::set_efx_high_pass_cutoff()
+{
+    const auto high_pass_cutoff = clamp(
+        eax_.flHighPassCutOff,
+        AL_RING_MODULATOR_MIN_HIGHPASS_CUTOFF,
+        AL_RING_MODULATOR_MAX_HIGHPASS_CUTOFF);
+
+    al_effect_props_.Modulator.HighPassCutoff = high_pass_cutoff;
+}
+
+void EaxRingModulatorEffect::set_efx_waveform()
+{
+    const auto waveform = clamp(
+        static_cast<ALint>(eax_.ulWaveform),
+        AL_RING_MODULATOR_MIN_WAVEFORM,
+        AL_RING_MODULATOR_MAX_WAVEFORM);
+
+    const auto efx_waveform = WaveformFromEmum(waveform);
+    assert(efx_waveform.has_value());
+    al_effect_props_.Modulator.Waveform = *efx_waveform;
+}
+
+void EaxRingModulatorEffect::set_efx_defaults()
+{
+    set_efx_frequency();
+    set_efx_high_pass_cutoff();
+    set_efx_waveform();
+}
+
+void EaxRingModulatorEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXRINGMODULATOR_NONE:
+            break;
+
+        case EAXRINGMODULATOR_ALLPARAMETERS:
+            eax_call.set_value<EaxRingModulatorEffectException>(eax_);
+            break;
+
+        case EAXRINGMODULATOR_FREQUENCY:
+            eax_call.set_value<EaxRingModulatorEffectException>(eax_.flFrequency);
+            break;
+
+        case EAXRINGMODULATOR_HIGHPASSCUTOFF:
+            eax_call.set_value<EaxRingModulatorEffectException>(eax_.flHighPassCutOff);
+            break;
+
+        case EAXRINGMODULATOR_WAVEFORM:
+            eax_call.set_value<EaxRingModulatorEffectException>(eax_.ulWaveform);
+            break;
+
+        default:
+            throw EaxRingModulatorEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxRingModulatorEffect::validate_frequency(
+    float flFrequency)
+{
+    eax_validate_range<EaxRingModulatorEffectException>(
+        "Frequency",
+        flFrequency,
+        EAXRINGMODULATOR_MINFREQUENCY,
+        EAXRINGMODULATOR_MAXFREQUENCY);
+}
+
+void EaxRingModulatorEffect::validate_high_pass_cutoff(
+    float flHighPassCutOff)
+{
+    eax_validate_range<EaxRingModulatorEffectException>(
+        "High-Pass Cutoff",
+        flHighPassCutOff,
+        EAXRINGMODULATOR_MINHIGHPASSCUTOFF,
+        EAXRINGMODULATOR_MAXHIGHPASSCUTOFF);
+}
+
+void EaxRingModulatorEffect::validate_waveform(
+    unsigned long ulWaveform)
+{
+    eax_validate_range<EaxRingModulatorEffectException>(
+        "Waveform",
+        ulWaveform,
+        EAXRINGMODULATOR_MINWAVEFORM,
+        EAXRINGMODULATOR_MAXWAVEFORM);
+}
+
+void EaxRingModulatorEffect::validate_all(
+    const EAXRINGMODULATORPROPERTIES& all)
+{
+    validate_frequency(all.flFrequency);
+    validate_high_pass_cutoff(all.flHighPassCutOff);
+    validate_waveform(all.ulWaveform);
+}
+
+void EaxRingModulatorEffect::defer_frequency(
+    float flFrequency)
+{
+    eax_d_.flFrequency = flFrequency;
+    eax_dirty_flags_.flFrequency = (eax_.flFrequency != eax_d_.flFrequency);
+}
+
+void EaxRingModulatorEffect::defer_high_pass_cutoff(
+    float flHighPassCutOff)
+{
+    eax_d_.flHighPassCutOff = flHighPassCutOff;
+    eax_dirty_flags_.flHighPassCutOff = (eax_.flHighPassCutOff != eax_d_.flHighPassCutOff);
+}
+
+void EaxRingModulatorEffect::defer_waveform(
+    unsigned long ulWaveform)
+{
+    eax_d_.ulWaveform = ulWaveform;
+    eax_dirty_flags_.ulWaveform = (eax_.ulWaveform != eax_d_.ulWaveform);
+}
+
+void EaxRingModulatorEffect::defer_all(
+    const EAXRINGMODULATORPROPERTIES& all)
+{
+    defer_frequency(all.flFrequency);
+    defer_high_pass_cutoff(all.flHighPassCutOff);
+    defer_waveform(all.ulWaveform);
+}
+
+void EaxRingModulatorEffect::defer_frequency(
+    const EaxEaxCall& eax_call)
+{
+    const auto& frequency =
+        eax_call.get_value<
+        EaxRingModulatorEffectException, const decltype(EAXRINGMODULATORPROPERTIES::flFrequency)>();
+
+    validate_frequency(frequency);
+    defer_frequency(frequency);
+}
+
+void EaxRingModulatorEffect::defer_high_pass_cutoff(
+    const EaxEaxCall& eax_call)
+{
+    const auto& high_pass_cutoff =
+        eax_call.get_value<
+        EaxRingModulatorEffectException, const decltype(EAXRINGMODULATORPROPERTIES::flHighPassCutOff)>();
+
+    validate_high_pass_cutoff(high_pass_cutoff);
+    defer_high_pass_cutoff(high_pass_cutoff);
+}
+
+void EaxRingModulatorEffect::defer_waveform(
+    const EaxEaxCall& eax_call)
+{
+    const auto& waveform =
+        eax_call.get_value<
+        EaxRingModulatorEffectException, const decltype(EAXRINGMODULATORPROPERTIES::ulWaveform)>();
+
+    validate_waveform(waveform);
+    defer_waveform(waveform);
+}
+
+void EaxRingModulatorEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxRingModulatorEffectException, const EAXRINGMODULATORPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxRingModulatorEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxRingModulatorEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.flFrequency)
+    {
+        set_efx_frequency();
+    }
+
+    if (eax_dirty_flags_.flHighPassCutOff)
+    {
+        set_efx_high_pass_cutoff();
+    }
+
+    if (eax_dirty_flags_.ulWaveform)
+    {
+        set_efx_waveform();
+    }
+
+    eax_dirty_flags_ = EaxRingModulatorEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxRingModulatorEffect::set(const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_id())
+    {
+        case EAXRINGMODULATOR_NONE:
+            break;
+
+        case EAXRINGMODULATOR_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXRINGMODULATOR_FREQUENCY:
+            defer_frequency(eax_call);
+            break;
+
+        case EAXRINGMODULATOR_HIGHPASSCUTOFF:
+            defer_high_pass_cutoff(eax_call);
+            break;
+
+        case EAXRINGMODULATOR_WAVEFORM:
+            defer_waveform(eax_call);
+            break;
+
+        default:
+            throw EaxRingModulatorEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_ring_modulator_effect()
+{
+    return std::make_unique<EaxRingModulatorEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 60 - 1
Engine/lib/openal-soft/al/effects/null.cpp

@@ -4,8 +4,12 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "al/eax_exception.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -91,3 +95,58 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Null);
 
 const EffectProps NullEffectProps{genDefaultProps()};
+
+
+#ifdef ALSOFT_EAX
+namespace {
+
+class EaxNullEffect final :
+    public EaxEffect
+{
+public:
+    EaxNullEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+}; // EaxNullEffect
+
+
+class EaxNullEffectException :
+    public EaxException
+{
+public:
+    explicit EaxNullEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_NULL_EFFECT", message}
+    {
+    }
+}; // EaxNullEffectException
+
+
+EaxNullEffect::EaxNullEffect()
+    : EaxEffect{AL_EFFECT_NULL}
+{
+}
+
+void EaxNullEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    if(eax_call.get_property_id() != 0)
+        throw EaxNullEffectException{"Unsupported property id."};
+}
+
+bool EaxNullEffect::apply_deferred()
+{
+    return false;
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_null_effect()
+{
+    return std::make_unique<EaxNullEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 281 - 1
Engine/lib/openal-soft/al/effects/pshifter.cpp

@@ -4,8 +4,15 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -82,3 +89,276 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Pshifter);
 
 const EffectProps PshifterEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxPitchShifterEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxPitchShifterEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxPitchShifterEffectDirtyFlagsValue lCoarseTune : 1;
+    EaxPitchShifterEffectDirtyFlagsValue lFineTune : 1;
+}; // EaxPitchShifterEffectDirtyFlags
+
+
+class EaxPitchShifterEffect final :
+    public EaxEffect
+{
+public:
+    EaxPitchShifterEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXPITCHSHIFTERPROPERTIES eax_{};
+    EAXPITCHSHIFTERPROPERTIES eax_d_{};
+    EaxPitchShifterEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_coarse_tune();
+    void set_efx_fine_tune();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_coarse_tune(long lCoarseTune);
+    void validate_fine_tune(long lFineTune);
+    void validate_all(const EAXPITCHSHIFTERPROPERTIES& all);
+
+    void defer_coarse_tune(long lCoarseTune);
+    void defer_fine_tune(long lFineTune);
+    void defer_all(const EAXPITCHSHIFTERPROPERTIES& all);
+
+    void defer_coarse_tune(const EaxEaxCall& eax_call);
+    void defer_fine_tune(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxPitchShifterEffect
+
+
+class EaxPitchShifterEffectException :
+    public EaxException
+{
+public:
+    explicit EaxPitchShifterEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_PITCH_SHIFTER_EFFECT", message}
+    {
+    }
+}; // EaxPitchShifterEffectException
+
+
+EaxPitchShifterEffect::EaxPitchShifterEffect()
+    : EaxEffect{AL_EFFECT_PITCH_SHIFTER}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxPitchShifterEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxPitchShifterEffect::set_eax_defaults()
+{
+    eax_.lCoarseTune = EAXPITCHSHIFTER_DEFAULTCOARSETUNE;
+    eax_.lFineTune = EAXPITCHSHIFTER_DEFAULTFINETUNE;
+
+    eax_d_ = eax_;
+}
+
+void EaxPitchShifterEffect::set_efx_coarse_tune()
+{
+    const auto coarse_tune = clamp(
+        static_cast<ALint>(eax_.lCoarseTune),
+        AL_PITCH_SHIFTER_MIN_COARSE_TUNE,
+        AL_PITCH_SHIFTER_MAX_COARSE_TUNE);
+
+    al_effect_props_.Pshifter.CoarseTune = coarse_tune;
+}
+
+void EaxPitchShifterEffect::set_efx_fine_tune()
+{
+    const auto fine_tune = clamp(
+        static_cast<ALint>(eax_.lFineTune),
+        AL_PITCH_SHIFTER_MIN_FINE_TUNE,
+        AL_PITCH_SHIFTER_MAX_FINE_TUNE);
+
+    al_effect_props_.Pshifter.FineTune = fine_tune;
+}
+
+void EaxPitchShifterEffect::set_efx_defaults()
+{
+    set_efx_coarse_tune();
+    set_efx_fine_tune();
+}
+
+void EaxPitchShifterEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXPITCHSHIFTER_NONE:
+            break;
+
+        case EAXPITCHSHIFTER_ALLPARAMETERS:
+            eax_call.set_value<EaxPitchShifterEffectException>(eax_);
+            break;
+
+        case EAXPITCHSHIFTER_COARSETUNE:
+            eax_call.set_value<EaxPitchShifterEffectException>(eax_.lCoarseTune);
+            break;
+
+        case EAXPITCHSHIFTER_FINETUNE:
+            eax_call.set_value<EaxPitchShifterEffectException>(eax_.lFineTune);
+            break;
+
+        default:
+            throw EaxPitchShifterEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxPitchShifterEffect::validate_coarse_tune(
+    long lCoarseTune)
+{
+    eax_validate_range<EaxPitchShifterEffectException>(
+        "Coarse Tune",
+        lCoarseTune,
+        EAXPITCHSHIFTER_MINCOARSETUNE,
+        EAXPITCHSHIFTER_MAXCOARSETUNE);
+}
+
+void EaxPitchShifterEffect::validate_fine_tune(
+    long lFineTune)
+{
+    eax_validate_range<EaxPitchShifterEffectException>(
+        "Fine Tune",
+        lFineTune,
+        EAXPITCHSHIFTER_MINFINETUNE,
+        EAXPITCHSHIFTER_MAXFINETUNE);
+}
+
+void EaxPitchShifterEffect::validate_all(
+    const EAXPITCHSHIFTERPROPERTIES& all)
+{
+    validate_coarse_tune(all.lCoarseTune);
+    validate_fine_tune(all.lFineTune);
+}
+
+void EaxPitchShifterEffect::defer_coarse_tune(
+    long lCoarseTune)
+{
+    eax_d_.lCoarseTune = lCoarseTune;
+    eax_dirty_flags_.lCoarseTune = (eax_.lCoarseTune != eax_d_.lCoarseTune);
+}
+
+void EaxPitchShifterEffect::defer_fine_tune(
+    long lFineTune)
+{
+    eax_d_.lFineTune = lFineTune;
+    eax_dirty_flags_.lFineTune = (eax_.lFineTune != eax_d_.lFineTune);
+}
+
+void EaxPitchShifterEffect::defer_all(
+    const EAXPITCHSHIFTERPROPERTIES& all)
+{
+    defer_coarse_tune(all.lCoarseTune);
+    defer_fine_tune(all.lFineTune);
+}
+
+void EaxPitchShifterEffect::defer_coarse_tune(
+    const EaxEaxCall& eax_call)
+{
+    const auto& coarse_tune =
+        eax_call.get_value<EaxPitchShifterEffectException, const decltype(EAXPITCHSHIFTERPROPERTIES::lCoarseTune)>();
+
+    validate_coarse_tune(coarse_tune);
+    defer_coarse_tune(coarse_tune);
+}
+
+void EaxPitchShifterEffect::defer_fine_tune(
+    const EaxEaxCall& eax_call)
+{
+    const auto& fine_tune =
+        eax_call.get_value<EaxPitchShifterEffectException, const decltype(EAXPITCHSHIFTERPROPERTIES::lFineTune)>();
+
+    validate_fine_tune(fine_tune);
+    defer_fine_tune(fine_tune);
+}
+
+void EaxPitchShifterEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all =
+        eax_call.get_value<EaxPitchShifterEffectException, const EAXPITCHSHIFTERPROPERTIES>();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxPitchShifterEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxPitchShifterEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.lCoarseTune)
+    {
+        set_efx_coarse_tune();
+    }
+
+    if (eax_dirty_flags_.lFineTune)
+    {
+        set_efx_fine_tune();
+    }
+
+    eax_dirty_flags_ = EaxPitchShifterEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxPitchShifterEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXPITCHSHIFTER_NONE:
+            break;
+
+        case EAXPITCHSHIFTER_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXPITCHSHIFTER_COARSETUNE:
+            defer_coarse_tune(eax_call);
+            break;
+
+        case EAXPITCHSHIFTER_FINETUNE:
+            defer_fine_tune(eax_call);
+            break;
+
+        default:
+            throw EaxPitchShifterEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_pitch_shifter_effect()
+{
+    return std::make_unique<EaxPitchShifterEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 1987 - 1
Engine/lib/openal-soft/al/effects/reverb.cpp

@@ -6,8 +6,16 @@
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include <tuple>
+#include "alnumeric.h"
+#include "AL/efx-presets.h"
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -554,3 +562,1981 @@ const EffectProps ReverbEffectProps{genDefaultProps()};
 DEFINE_ALEFFECT_VTABLE(StdReverb);
 
 const EffectProps StdReverbEffectProps{genDefaultStdProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+extern const EFXEAXREVERBPROPERTIES eax_efx_reverb_presets[];
+
+using EaxReverbEffectDirtyFlagsValue = std::uint_least32_t;
+
+struct EaxReverbEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxReverbEffectDirtyFlagsValue ulEnvironment : 1;
+    EaxReverbEffectDirtyFlagsValue flEnvironmentSize : 1;
+    EaxReverbEffectDirtyFlagsValue flEnvironmentDiffusion : 1;
+    EaxReverbEffectDirtyFlagsValue lRoom : 1;
+    EaxReverbEffectDirtyFlagsValue lRoomHF : 1;
+    EaxReverbEffectDirtyFlagsValue lRoomLF : 1;
+    EaxReverbEffectDirtyFlagsValue flDecayTime : 1;
+    EaxReverbEffectDirtyFlagsValue flDecayHFRatio : 1;
+    EaxReverbEffectDirtyFlagsValue flDecayLFRatio : 1;
+    EaxReverbEffectDirtyFlagsValue lReflections : 1;
+    EaxReverbEffectDirtyFlagsValue flReflectionsDelay : 1;
+    EaxReverbEffectDirtyFlagsValue vReflectionsPan : 1;
+    EaxReverbEffectDirtyFlagsValue lReverb : 1;
+    EaxReverbEffectDirtyFlagsValue flReverbDelay : 1;
+    EaxReverbEffectDirtyFlagsValue vReverbPan : 1;
+    EaxReverbEffectDirtyFlagsValue flEchoTime : 1;
+    EaxReverbEffectDirtyFlagsValue flEchoDepth : 1;
+    EaxReverbEffectDirtyFlagsValue flModulationTime : 1;
+    EaxReverbEffectDirtyFlagsValue flModulationDepth : 1;
+    EaxReverbEffectDirtyFlagsValue flAirAbsorptionHF : 1;
+    EaxReverbEffectDirtyFlagsValue flHFReference : 1;
+    EaxReverbEffectDirtyFlagsValue flLFReference : 1;
+    EaxReverbEffectDirtyFlagsValue flRoomRolloffFactor : 1;
+    EaxReverbEffectDirtyFlagsValue ulFlags : 1;
+}; // EaxReverbEffectDirtyFlags
+
+struct Eax1ReverbEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxReverbEffectDirtyFlagsValue ulEnvironment : 1;
+    EaxReverbEffectDirtyFlagsValue flVolume : 1;
+    EaxReverbEffectDirtyFlagsValue flDecayTime : 1;
+    EaxReverbEffectDirtyFlagsValue flDamping : 1;
+}; // Eax1ReverbEffectDirtyFlags
+
+class EaxReverbEffect final :
+    public EaxEffect
+{
+public:
+    EaxReverbEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAX_REVERBPROPERTIES eax1_{};
+    EAX_REVERBPROPERTIES eax1_d_{};
+    Eax1ReverbEffectDirtyFlags eax1_dirty_flags_{};
+    EAXREVERBPROPERTIES eax_{};
+    EAXREVERBPROPERTIES eax_d_{};
+    EaxReverbEffectDirtyFlags eax_dirty_flags_{};
+
+    [[noreturn]] static void eax_fail(const char* message);
+
+    void set_eax_defaults();
+
+    void set_efx_density_from_environment_size();
+    void set_efx_diffusion();
+    void set_efx_gain();
+    void set_efx_gain_hf();
+    void set_efx_gain_lf();
+    void set_efx_decay_time();
+    void set_efx_decay_hf_ratio();
+    void set_efx_decay_lf_ratio();
+    void set_efx_reflections_gain();
+    void set_efx_reflections_delay();
+    void set_efx_reflections_pan();
+    void set_efx_late_reverb_gain();
+    void set_efx_late_reverb_delay();
+    void set_efx_late_reverb_pan();
+    void set_efx_echo_time();
+    void set_efx_echo_depth();
+    void set_efx_modulation_time();
+    void set_efx_modulation_depth();
+    void set_efx_air_absorption_gain_hf();
+    void set_efx_hf_reference();
+    void set_efx_lf_reference();
+    void set_efx_room_rolloff_factor();
+    void set_efx_flags();
+    void set_efx_defaults();
+
+    void v1_get(const EaxEaxCall& eax_call) const;
+
+    void get_all(const EaxEaxCall& eax_call) const;
+
+    void get(const EaxEaxCall& eax_call) const;
+
+    static void v1_validate_environment(unsigned long environment);
+    static void v1_validate_volume(float volume);
+    static void v1_validate_decay_time(float decay_time);
+    static void v1_validate_damping(float damping);
+    static void v1_validate_all(const EAX_REVERBPROPERTIES& all);
+
+    void v1_defer_environment(unsigned long environment);
+    void v1_defer_volume(float volume);
+    void v1_defer_decay_time(float decay_time);
+    void v1_defer_damping(float damping);
+    void v1_defer_all(const EAX_REVERBPROPERTIES& all);
+
+    void v1_defer_environment(const EaxEaxCall& eax_call);
+    void v1_defer_volume(const EaxEaxCall& eax_call);
+    void v1_defer_decay_time(const EaxEaxCall& eax_call);
+    void v1_defer_damping(const EaxEaxCall& eax_call);
+    void v1_defer_all(const EaxEaxCall& eax_call);
+    void v1_defer(const EaxEaxCall& eax_call);
+
+    void v1_set_efx();
+
+    static void validate_environment(unsigned long ulEnvironment, int version, bool is_standalone);
+    static void validate_environment_size(float flEnvironmentSize);
+    static void validate_environment_diffusion(float flEnvironmentDiffusion);
+    static void validate_room(long lRoom);
+    static void validate_room_hf(long lRoomHF);
+    static void validate_room_lf(long lRoomLF);
+    static void validate_decay_time(float flDecayTime);
+    static void validate_decay_hf_ratio(float flDecayHFRatio);
+    static void validate_decay_lf_ratio(float flDecayLFRatio);
+    static void validate_reflections(long lReflections);
+    static void validate_reflections_delay(float flReflectionsDelay);
+    static void validate_reflections_pan(const EAXVECTOR& vReflectionsPan);
+    static void validate_reverb(long lReverb);
+    static void validate_reverb_delay(float flReverbDelay);
+    static void validate_reverb_pan(const EAXVECTOR& vReverbPan);
+    static void validate_echo_time(float flEchoTime);
+    static void validate_echo_depth(float flEchoDepth);
+    static void validate_modulation_time(float flModulationTime);
+    static void validate_modulation_depth(float flModulationDepth);
+    static void validate_air_absorbtion_hf(float air_absorbtion_hf);
+    static void validate_hf_reference(float flHFReference);
+    static void validate_lf_reference(float flLFReference);
+    static void validate_room_rolloff_factor(float flRoomRolloffFactor);
+    static void validate_flags(unsigned long ulFlags);
+    static void validate_all(const EAX20LISTENERPROPERTIES& all, int version);
+    static void validate_all(const EAXREVERBPROPERTIES& all, int version);
+
+    void defer_environment(unsigned long ulEnvironment);
+    void defer_environment_size(float flEnvironmentSize);
+    void defer_environment_diffusion(float flEnvironmentDiffusion);
+    void defer_room(long lRoom);
+    void defer_room_hf(long lRoomHF);
+    void defer_room_lf(long lRoomLF);
+    void defer_decay_time(float flDecayTime);
+    void defer_decay_hf_ratio(float flDecayHFRatio);
+    void defer_decay_lf_ratio(float flDecayLFRatio);
+    void defer_reflections(long lReflections);
+    void defer_reflections_delay(float flReflectionsDelay);
+    void defer_reflections_pan(const EAXVECTOR& vReflectionsPan);
+    void defer_reverb(long lReverb);
+    void defer_reverb_delay(float flReverbDelay);
+    void defer_reverb_pan(const EAXVECTOR& vReverbPan);
+    void defer_echo_time(float flEchoTime);
+    void defer_echo_depth(float flEchoDepth);
+    void defer_modulation_time(float flModulationTime);
+    void defer_modulation_depth(float flModulationDepth);
+    void defer_air_absorbtion_hf(float flAirAbsorptionHF);
+    void defer_hf_reference(float flHFReference);
+    void defer_lf_reference(float flLFReference);
+    void defer_room_rolloff_factor(float flRoomRolloffFactor);
+    void defer_flags(unsigned long ulFlags);
+    void defer_all(const EAX20LISTENERPROPERTIES& all);
+    void defer_all(const EAXREVERBPROPERTIES& all);
+
+    void defer_environment(const EaxEaxCall& eax_call);
+    void defer_environment_size(const EaxEaxCall& eax_call);
+    void defer_environment_diffusion(const EaxEaxCall& eax_call);
+    void defer_room(const EaxEaxCall& eax_call);
+    void defer_room_hf(const EaxEaxCall& eax_call);
+    void defer_room_lf(const EaxEaxCall& eax_call);
+    void defer_decay_time(const EaxEaxCall& eax_call);
+    void defer_decay_hf_ratio(const EaxEaxCall& eax_call);
+    void defer_decay_lf_ratio(const EaxEaxCall& eax_call);
+    void defer_reflections(const EaxEaxCall& eax_call);
+    void defer_reflections_delay(const EaxEaxCall& eax_call);
+    void defer_reflections_pan(const EaxEaxCall& eax_call);
+    void defer_reverb(const EaxEaxCall& eax_call);
+    void defer_reverb_delay(const EaxEaxCall& eax_call);
+    void defer_reverb_pan(const EaxEaxCall& eax_call);
+    void defer_echo_time(const EaxEaxCall& eax_call);
+    void defer_echo_depth(const EaxEaxCall& eax_call);
+    void defer_modulation_time(const EaxEaxCall& eax_call);
+    void defer_modulation_depth(const EaxEaxCall& eax_call);
+    void defer_air_absorbtion_hf(const EaxEaxCall& eax_call);
+    void defer_hf_reference(const EaxEaxCall& eax_call);
+    void defer_lf_reference(const EaxEaxCall& eax_call);
+    void defer_room_rolloff_factor(const EaxEaxCall& eax_call);
+    void defer_flags(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxReverbEffect
+
+
+class EaxReverbEffectException :
+    public EaxException
+{
+public:
+    explicit EaxReverbEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_REVERB_EFFECT", message}
+    {
+    }
+}; // EaxReverbEffectException
+
+
+EaxReverbEffect::EaxReverbEffect()
+    : EaxEffect{AL_EFFECT_EAXREVERB}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxReverbEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+[[noreturn]] void EaxReverbEffect::eax_fail(const char* message)
+{
+    throw EaxReverbEffectException{message};
+}
+
+void EaxReverbEffect::set_eax_defaults()
+{
+    eax1_ = EAX1REVERB_PRESETS[EAX_ENVIRONMENT_GENERIC];
+    eax1_d_ = eax1_;
+    eax_ = EAXREVERB_PRESETS[EAX_ENVIRONMENT_GENERIC];
+    /* HACK: EAX2 has a default room volume of -10,000dB (silence), although
+     * newer versions use -1,000dB. What should be happening is properties for
+     * each EAX version is tracked separately, with the last version used for
+     * the properties to apply (presumably v2 or nothing being the default).
+     */
+    eax_.lRoom = EAXREVERB_MINROOM;
+    eax_d_ = eax_;
+}
+
+void EaxReverbEffect::set_efx_density_from_environment_size()
+{
+    const auto eax_environment_size = eax_.flEnvironmentSize;
+
+    const auto efx_density = clamp(
+        (eax_environment_size * eax_environment_size * eax_environment_size) / 16.0F,
+        AL_EAXREVERB_MIN_DENSITY,
+        AL_EAXREVERB_MAX_DENSITY);
+
+    al_effect_props_.Reverb.Density = efx_density;
+}
+
+void EaxReverbEffect::set_efx_diffusion()
+{
+    const auto efx_diffusion = clamp(
+        eax_.flEnvironmentDiffusion,
+        AL_EAXREVERB_MIN_DIFFUSION,
+        AL_EAXREVERB_MAX_DIFFUSION);
+
+    al_effect_props_.Reverb.Diffusion = efx_diffusion;
+}
+
+void EaxReverbEffect::set_efx_gain()
+{
+    const auto efx_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lRoom)),
+        AL_EAXREVERB_MIN_GAIN,
+        AL_EAXREVERB_MAX_GAIN);
+
+    al_effect_props_.Reverb.Gain = efx_gain;
+}
+
+void EaxReverbEffect::set_efx_gain_hf()
+{
+    const auto efx_gain_hf = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lRoomHF)),
+        AL_EAXREVERB_MIN_GAINHF,
+        AL_EAXREVERB_MAX_GAINHF);
+
+    al_effect_props_.Reverb.GainHF = efx_gain_hf;
+}
+
+void EaxReverbEffect::set_efx_gain_lf()
+{
+    const auto efx_gain_lf = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lRoomLF)),
+        AL_EAXREVERB_MIN_GAINLF,
+        AL_EAXREVERB_MAX_GAINLF);
+
+    al_effect_props_.Reverb.GainLF = efx_gain_lf;
+}
+
+void EaxReverbEffect::set_efx_decay_time()
+{
+    const auto efx_decay_time = clamp(
+        eax_.flDecayTime,
+        AL_EAXREVERB_MIN_DECAY_TIME,
+        AL_EAXREVERB_MAX_DECAY_TIME);
+
+    al_effect_props_.Reverb.DecayTime = efx_decay_time;
+}
+
+void EaxReverbEffect::set_efx_decay_hf_ratio()
+{
+    const auto efx_decay_hf_ratio = clamp(
+        eax_.flDecayHFRatio,
+        AL_EAXREVERB_MIN_DECAY_HFRATIO,
+        AL_EAXREVERB_MAX_DECAY_HFRATIO);
+
+    al_effect_props_.Reverb.DecayHFRatio = efx_decay_hf_ratio;
+}
+
+void EaxReverbEffect::set_efx_decay_lf_ratio()
+{
+    const auto efx_decay_lf_ratio = clamp(
+        eax_.flDecayLFRatio,
+        AL_EAXREVERB_MIN_DECAY_LFRATIO,
+        AL_EAXREVERB_MAX_DECAY_LFRATIO);
+
+    al_effect_props_.Reverb.DecayLFRatio = efx_decay_lf_ratio;
+}
+
+void EaxReverbEffect::set_efx_reflections_gain()
+{
+    const auto efx_reflections_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lReflections)),
+        AL_EAXREVERB_MIN_REFLECTIONS_GAIN,
+        AL_EAXREVERB_MAX_REFLECTIONS_GAIN);
+
+    al_effect_props_.Reverb.ReflectionsGain = efx_reflections_gain;
+}
+
+void EaxReverbEffect::set_efx_reflections_delay()
+{
+    const auto efx_reflections_delay = clamp(
+        eax_.flReflectionsDelay,
+        AL_EAXREVERB_MIN_REFLECTIONS_DELAY,
+        AL_EAXREVERB_MAX_REFLECTIONS_DELAY);
+
+    al_effect_props_.Reverb.ReflectionsDelay = efx_reflections_delay;
+}
+
+void EaxReverbEffect::set_efx_reflections_pan()
+{
+    al_effect_props_.Reverb.ReflectionsPan[0] = eax_.vReflectionsPan.x;
+    al_effect_props_.Reverb.ReflectionsPan[1] = eax_.vReflectionsPan.y;
+    al_effect_props_.Reverb.ReflectionsPan[2] = eax_.vReflectionsPan.z;
+}
+
+void EaxReverbEffect::set_efx_late_reverb_gain()
+{
+    const auto efx_late_reverb_gain = clamp(
+        level_mb_to_gain(static_cast<float>(eax_.lReverb)),
+        AL_EAXREVERB_MIN_LATE_REVERB_GAIN,
+        AL_EAXREVERB_MAX_LATE_REVERB_GAIN);
+
+    al_effect_props_.Reverb.LateReverbGain = efx_late_reverb_gain;
+}
+
+void EaxReverbEffect::set_efx_late_reverb_delay()
+{
+    const auto efx_late_reverb_delay = clamp(
+        eax_.flReverbDelay,
+        AL_EAXREVERB_MIN_LATE_REVERB_DELAY,
+        AL_EAXREVERB_MAX_LATE_REVERB_DELAY);
+
+    al_effect_props_.Reverb.LateReverbDelay = efx_late_reverb_delay;
+}
+
+void EaxReverbEffect::set_efx_late_reverb_pan()
+{
+    al_effect_props_.Reverb.LateReverbPan[0] = eax_.vReverbPan.x;
+    al_effect_props_.Reverb.LateReverbPan[1] = eax_.vReverbPan.y;
+    al_effect_props_.Reverb.LateReverbPan[2] = eax_.vReverbPan.z;
+}
+
+void EaxReverbEffect::set_efx_echo_time()
+{
+    const auto efx_echo_time = clamp(
+        eax_.flEchoTime,
+        AL_EAXREVERB_MIN_ECHO_TIME,
+        AL_EAXREVERB_MAX_ECHO_TIME);
+
+    al_effect_props_.Reverb.EchoTime = efx_echo_time;
+}
+
+void EaxReverbEffect::set_efx_echo_depth()
+{
+    const auto efx_echo_depth = clamp(
+        eax_.flEchoDepth,
+        AL_EAXREVERB_MIN_ECHO_DEPTH,
+        AL_EAXREVERB_MAX_ECHO_DEPTH);
+
+    al_effect_props_.Reverb.EchoDepth = efx_echo_depth;
+}
+
+void EaxReverbEffect::set_efx_modulation_time()
+{
+    const auto efx_modulation_time = clamp(
+        eax_.flModulationTime,
+        AL_EAXREVERB_MIN_MODULATION_TIME,
+        AL_EAXREVERB_MAX_MODULATION_TIME);
+
+    al_effect_props_.Reverb.ModulationTime = efx_modulation_time;
+}
+
+void EaxReverbEffect::set_efx_modulation_depth()
+{
+    const auto efx_modulation_depth = clamp(
+        eax_.flModulationDepth,
+        AL_EAXREVERB_MIN_MODULATION_DEPTH,
+        AL_EAXREVERB_MAX_MODULATION_DEPTH);
+
+    al_effect_props_.Reverb.ModulationDepth = efx_modulation_depth;
+}
+
+void EaxReverbEffect::set_efx_air_absorption_gain_hf()
+{
+    const auto efx_air_absorption_hf = clamp(
+        level_mb_to_gain(eax_.flAirAbsorptionHF),
+        AL_EAXREVERB_MIN_AIR_ABSORPTION_GAINHF,
+        AL_EAXREVERB_MAX_AIR_ABSORPTION_GAINHF);
+
+    al_effect_props_.Reverb.AirAbsorptionGainHF = efx_air_absorption_hf;
+}
+
+void EaxReverbEffect::set_efx_hf_reference()
+{
+    const auto efx_hf_reference = clamp(
+        eax_.flHFReference,
+        AL_EAXREVERB_MIN_HFREFERENCE,
+        AL_EAXREVERB_MAX_HFREFERENCE);
+
+    al_effect_props_.Reverb.HFReference = efx_hf_reference;
+}
+
+void EaxReverbEffect::set_efx_lf_reference()
+{
+    const auto efx_lf_reference = clamp(
+        eax_.flLFReference,
+        AL_EAXREVERB_MIN_LFREFERENCE,
+        AL_EAXREVERB_MAX_LFREFERENCE);
+
+    al_effect_props_.Reverb.LFReference = efx_lf_reference;
+}
+
+void EaxReverbEffect::set_efx_room_rolloff_factor()
+{
+    const auto efx_room_rolloff_factor = clamp(
+        eax_.flRoomRolloffFactor,
+        AL_EAXREVERB_MIN_ROOM_ROLLOFF_FACTOR,
+        AL_EAXREVERB_MAX_ROOM_ROLLOFF_FACTOR);
+
+    al_effect_props_.Reverb.RoomRolloffFactor = efx_room_rolloff_factor;
+}
+
+void EaxReverbEffect::set_efx_flags()
+{
+    al_effect_props_.Reverb.DecayHFLimit = ((eax_.ulFlags & EAXREVERBFLAGS_DECAYHFLIMIT) != 0);
+}
+
+void EaxReverbEffect::set_efx_defaults()
+{
+    set_efx_density_from_environment_size();
+    set_efx_diffusion();
+    set_efx_gain();
+    set_efx_gain_hf();
+    set_efx_gain_lf();
+    set_efx_decay_time();
+    set_efx_decay_hf_ratio();
+    set_efx_decay_lf_ratio();
+    set_efx_reflections_gain();
+    set_efx_reflections_delay();
+    set_efx_reflections_pan();
+    set_efx_late_reverb_gain();
+    set_efx_late_reverb_delay();
+    set_efx_late_reverb_pan();
+    set_efx_echo_time();
+    set_efx_echo_depth();
+    set_efx_modulation_time();
+    set_efx_modulation_depth();
+    set_efx_air_absorption_gain_hf();
+    set_efx_hf_reference();
+    set_efx_lf_reference();
+    set_efx_room_rolloff_factor();
+    set_efx_flags();
+}
+
+void EaxReverbEffect::v1_get(const EaxEaxCall& eax_call) const
+{
+    switch(eax_call.get_property_id())
+    {
+        case DSPROPERTY_EAX_ALL:
+            eax_call.set_value<EaxReverbEffectException>(eax1_);
+            break;
+
+        case DSPROPERTY_EAX_ENVIRONMENT:
+            eax_call.set_value<EaxReverbEffectException>(eax1_.environment);
+            break;
+
+        case DSPROPERTY_EAX_VOLUME:
+            eax_call.set_value<EaxReverbEffectException>(eax1_.fVolume);
+            break;
+
+        case DSPROPERTY_EAX_DECAYTIME:
+            eax_call.set_value<EaxReverbEffectException>(eax1_.fDecayTime_sec);
+            break;
+
+        case DSPROPERTY_EAX_DAMPING:
+            eax_call.set_value<EaxReverbEffectException>(eax1_.fDamping);
+            break;
+
+        default:
+            eax_fail("Unsupported property id.");
+    }
+}
+
+void EaxReverbEffect::get_all(
+    const EaxEaxCall& eax_call) const
+{
+    if (eax_call.get_version() == 2)
+    {
+        auto& eax_reverb = eax_call.get_value<EaxReverbEffectException, EAX20LISTENERPROPERTIES>();
+        eax_reverb.lRoom = eax_.lRoom;
+        eax_reverb.lRoomHF = eax_.lRoomHF;
+        eax_reverb.flRoomRolloffFactor = eax_.flRoomRolloffFactor;
+        eax_reverb.flDecayTime = eax_.flDecayTime;
+        eax_reverb.flDecayHFRatio = eax_.flDecayHFRatio;
+        eax_reverb.lReflections = eax_.lReflections;
+        eax_reverb.flReflectionsDelay = eax_.flReflectionsDelay;
+        eax_reverb.lReverb = eax_.lReverb;
+        eax_reverb.flReverbDelay = eax_.flReverbDelay;
+        eax_reverb.dwEnvironment = eax_.ulEnvironment;
+        eax_reverb.flEnvironmentSize = eax_.flEnvironmentSize;
+        eax_reverb.flEnvironmentDiffusion = eax_.flEnvironmentDiffusion;
+        eax_reverb.flAirAbsorptionHF = eax_.flAirAbsorptionHF;
+        eax_reverb.dwFlags = eax_.ulFlags;
+    }
+    else
+    {
+        eax_call.set_value<EaxReverbEffectException>(eax_);
+    }
+}
+
+void EaxReverbEffect::get(const EaxEaxCall& eax_call) const
+{
+    if(eax_call.get_version() == 1)
+        v1_get(eax_call);
+    else switch(eax_call.get_property_id())
+    {
+        case EAXREVERB_NONE:
+            break;
+
+        case EAXREVERB_ALLPARAMETERS:
+            get_all(eax_call);
+            break;
+
+        case EAXREVERB_ENVIRONMENT:
+            eax_call.set_value<EaxReverbEffectException>(eax_.ulEnvironment);
+            break;
+
+        case EAXREVERB_ENVIRONMENTSIZE:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flEnvironmentSize);
+            break;
+
+        case EAXREVERB_ENVIRONMENTDIFFUSION:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flEnvironmentDiffusion);
+            break;
+
+        case EAXREVERB_ROOM:
+            eax_call.set_value<EaxReverbEffectException>(eax_.lRoom);
+            break;
+
+        case EAXREVERB_ROOMHF:
+            eax_call.set_value<EaxReverbEffectException>(eax_.lRoomHF);
+            break;
+
+        case EAXREVERB_ROOMLF:
+            eax_call.set_value<EaxReverbEffectException>(eax_.lRoomLF);
+            break;
+
+        case EAXREVERB_DECAYTIME:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flDecayTime);
+            break;
+
+        case EAXREVERB_DECAYHFRATIO:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flDecayHFRatio);
+            break;
+
+        case EAXREVERB_DECAYLFRATIO:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flDecayLFRatio);
+            break;
+
+        case EAXREVERB_REFLECTIONS:
+            eax_call.set_value<EaxReverbEffectException>(eax_.lReflections);
+            break;
+
+        case EAXREVERB_REFLECTIONSDELAY:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flReflectionsDelay);
+            break;
+
+        case EAXREVERB_REFLECTIONSPAN:
+            eax_call.set_value<EaxReverbEffectException>(eax_.vReflectionsPan);
+            break;
+
+        case EAXREVERB_REVERB:
+            eax_call.set_value<EaxReverbEffectException>(eax_.lReverb);
+            break;
+
+        case EAXREVERB_REVERBDELAY:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flReverbDelay);
+            break;
+
+        case EAXREVERB_REVERBPAN:
+            eax_call.set_value<EaxReverbEffectException>(eax_.vReverbPan);
+            break;
+
+        case EAXREVERB_ECHOTIME:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flEchoTime);
+            break;
+
+        case EAXREVERB_ECHODEPTH:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flEchoDepth);
+            break;
+
+        case EAXREVERB_MODULATIONTIME:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flModulationTime);
+            break;
+
+        case EAXREVERB_MODULATIONDEPTH:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flModulationDepth);
+            break;
+
+        case EAXREVERB_AIRABSORPTIONHF:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flAirAbsorptionHF);
+            break;
+
+        case EAXREVERB_HFREFERENCE:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flHFReference);
+            break;
+
+        case EAXREVERB_LFREFERENCE:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flLFReference);
+            break;
+
+        case EAXREVERB_ROOMROLLOFFFACTOR:
+            eax_call.set_value<EaxReverbEffectException>(eax_.flRoomRolloffFactor);
+            break;
+
+        case EAXREVERB_FLAGS:
+            eax_call.set_value<EaxReverbEffectException>(eax_.ulFlags);
+            break;
+
+        default:
+            eax_fail("Unsupported property id.");
+    }
+}
+
+void EaxReverbEffect::v1_validate_environment(unsigned long environment)
+{
+    validate_environment(environment, 1, true);
+}
+
+void EaxReverbEffect::v1_validate_volume(float volume)
+{
+    eax_validate_range<EaxReverbEffectException>("Volume", volume, EAX1REVERB_MINVOLUME, EAX1REVERB_MAXVOLUME);
+}
+
+void EaxReverbEffect::v1_validate_decay_time(float decay_time)
+{
+    validate_decay_time(decay_time);
+}
+
+void EaxReverbEffect::v1_validate_damping(float damping)
+{
+    eax_validate_range<EaxReverbEffectException>("Damping", damping, EAX1REVERB_MINDAMPING, EAX1REVERB_MAXDAMPING);
+}
+
+void EaxReverbEffect::v1_validate_all(const EAX_REVERBPROPERTIES& all)
+{
+    v1_validate_environment(all.environment);
+    v1_validate_volume(all.fVolume);
+    v1_validate_decay_time(all.fDecayTime_sec);
+    v1_validate_damping(all.fDamping);
+}
+
+void EaxReverbEffect::validate_environment(
+    unsigned long ulEnvironment,
+    int version,
+    bool is_standalone)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Environment",
+        ulEnvironment,
+        EAXREVERB_MINENVIRONMENT,
+        (version <= 2 || is_standalone) ? EAX1REVERB_MAXENVIRONMENT : EAX30REVERB_MAXENVIRONMENT);
+}
+
+void EaxReverbEffect::validate_environment_size(
+    float flEnvironmentSize)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Environment Size",
+        flEnvironmentSize,
+        EAXREVERB_MINENVIRONMENTSIZE,
+        EAXREVERB_MAXENVIRONMENTSIZE);
+}
+
+void EaxReverbEffect::validate_environment_diffusion(
+    float flEnvironmentDiffusion)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Environment Diffusion",
+        flEnvironmentDiffusion,
+        EAXREVERB_MINENVIRONMENTDIFFUSION,
+        EAXREVERB_MAXENVIRONMENTDIFFUSION);
+}
+
+void EaxReverbEffect::validate_room(
+    long lRoom)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Room",
+        lRoom,
+        EAXREVERB_MINROOM,
+        EAXREVERB_MAXROOM);
+}
+
+void EaxReverbEffect::validate_room_hf(
+    long lRoomHF)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Room HF",
+        lRoomHF,
+        EAXREVERB_MINROOMHF,
+        EAXREVERB_MAXROOMHF);
+}
+
+void EaxReverbEffect::validate_room_lf(
+    long lRoomLF)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Room LF",
+        lRoomLF,
+        EAXREVERB_MINROOMLF,
+        EAXREVERB_MAXROOMLF);
+}
+
+void EaxReverbEffect::validate_decay_time(
+    float flDecayTime)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Decay Time",
+        flDecayTime,
+        EAXREVERB_MINDECAYTIME,
+        EAXREVERB_MAXDECAYTIME);
+}
+
+void EaxReverbEffect::validate_decay_hf_ratio(
+    float flDecayHFRatio)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Decay HF Ratio",
+        flDecayHFRatio,
+        EAXREVERB_MINDECAYHFRATIO,
+        EAXREVERB_MAXDECAYHFRATIO);
+}
+
+void EaxReverbEffect::validate_decay_lf_ratio(
+    float flDecayLFRatio)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Decay LF Ratio",
+        flDecayLFRatio,
+        EAXREVERB_MINDECAYLFRATIO,
+        EAXREVERB_MAXDECAYLFRATIO);
+}
+
+void EaxReverbEffect::validate_reflections(
+    long lReflections)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Reflections",
+        lReflections,
+        EAXREVERB_MINREFLECTIONS,
+        EAXREVERB_MAXREFLECTIONS);
+}
+
+void EaxReverbEffect::validate_reflections_delay(
+    float flReflectionsDelay)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Reflections Delay",
+        flReflectionsDelay,
+        EAXREVERB_MINREFLECTIONSDELAY,
+        EAXREVERB_MAXREFLECTIONSDELAY);
+}
+
+void EaxReverbEffect::validate_reflections_pan(
+    const EAXVECTOR& vReflectionsPan)
+{
+    std::ignore = vReflectionsPan;
+}
+
+void EaxReverbEffect::validate_reverb(
+    long lReverb)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Reverb",
+        lReverb,
+        EAXREVERB_MINREVERB,
+        EAXREVERB_MAXREVERB);
+}
+
+void EaxReverbEffect::validate_reverb_delay(
+    float flReverbDelay)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Reverb Delay",
+        flReverbDelay,
+        EAXREVERB_MINREVERBDELAY,
+        EAXREVERB_MAXREVERBDELAY);
+}
+
+void EaxReverbEffect::validate_reverb_pan(
+    const EAXVECTOR& vReverbPan)
+{
+    std::ignore = vReverbPan;
+}
+
+void EaxReverbEffect::validate_echo_time(
+    float flEchoTime)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Echo Time",
+        flEchoTime,
+        EAXREVERB_MINECHOTIME,
+        EAXREVERB_MAXECHOTIME);
+}
+
+void EaxReverbEffect::validate_echo_depth(
+    float flEchoDepth)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Echo Depth",
+        flEchoDepth,
+        EAXREVERB_MINECHODEPTH,
+        EAXREVERB_MAXECHODEPTH);
+}
+
+void EaxReverbEffect::validate_modulation_time(
+    float flModulationTime)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Modulation Time",
+        flModulationTime,
+        EAXREVERB_MINMODULATIONTIME,
+        EAXREVERB_MAXMODULATIONTIME);
+}
+
+void EaxReverbEffect::validate_modulation_depth(
+    float flModulationDepth)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Modulation Depth",
+        flModulationDepth,
+        EAXREVERB_MINMODULATIONDEPTH,
+        EAXREVERB_MAXMODULATIONDEPTH);
+}
+
+void EaxReverbEffect::validate_air_absorbtion_hf(
+    float air_absorbtion_hf)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Air Absorbtion HF",
+        air_absorbtion_hf,
+        EAXREVERB_MINAIRABSORPTIONHF,
+        EAXREVERB_MAXAIRABSORPTIONHF);
+}
+
+void EaxReverbEffect::validate_hf_reference(
+    float flHFReference)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "HF Reference",
+        flHFReference,
+        EAXREVERB_MINHFREFERENCE,
+        EAXREVERB_MAXHFREFERENCE);
+}
+
+void EaxReverbEffect::validate_lf_reference(
+    float flLFReference)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "LF Reference",
+        flLFReference,
+        EAXREVERB_MINLFREFERENCE,
+        EAXREVERB_MAXLFREFERENCE);
+}
+
+void EaxReverbEffect::validate_room_rolloff_factor(
+    float flRoomRolloffFactor)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Room Rolloff Factor",
+        flRoomRolloffFactor,
+        EAXREVERB_MINROOMROLLOFFFACTOR,
+        EAXREVERB_MAXROOMROLLOFFFACTOR);
+}
+
+void EaxReverbEffect::validate_flags(
+    unsigned long ulFlags)
+{
+    eax_validate_range<EaxReverbEffectException>(
+        "Flags",
+        ulFlags,
+        0UL,
+        ~EAXREVERBFLAGS_RESERVED);
+}
+
+void EaxReverbEffect::validate_all(
+    const EAX20LISTENERPROPERTIES& listener,
+    int version)
+{
+    validate_room(listener.lRoom);
+    validate_room_hf(listener.lRoomHF);
+    validate_room_rolloff_factor(listener.flRoomRolloffFactor);
+    validate_decay_time(listener.flDecayTime);
+    validate_decay_hf_ratio(listener.flDecayHFRatio);
+    validate_reflections(listener.lReflections);
+    validate_reflections_delay(listener.flReflectionsDelay);
+    validate_reverb(listener.lReverb);
+    validate_reverb_delay(listener.flReverbDelay);
+    validate_environment(listener.dwEnvironment, version, false);
+    validate_environment_size(listener.flEnvironmentSize);
+    validate_environment_diffusion(listener.flEnvironmentDiffusion);
+    validate_air_absorbtion_hf(listener.flAirAbsorptionHF);
+    validate_flags(listener.dwFlags);
+}
+
+void EaxReverbEffect::validate_all(
+    const EAXREVERBPROPERTIES& lReverb,
+    int version)
+{
+    validate_environment(lReverb.ulEnvironment, version, false);
+    validate_environment_size(lReverb.flEnvironmentSize);
+    validate_environment_diffusion(lReverb.flEnvironmentDiffusion);
+    validate_room(lReverb.lRoom);
+    validate_room_hf(lReverb.lRoomHF);
+    validate_room_lf(lReverb.lRoomLF);
+    validate_decay_time(lReverb.flDecayTime);
+    validate_decay_hf_ratio(lReverb.flDecayHFRatio);
+    validate_decay_lf_ratio(lReverb.flDecayLFRatio);
+    validate_reflections(lReverb.lReflections);
+    validate_reflections_delay(lReverb.flReflectionsDelay);
+    validate_reverb(lReverb.lReverb);
+    validate_reverb_delay(lReverb.flReverbDelay);
+    validate_echo_time(lReverb.flEchoTime);
+    validate_echo_depth(lReverb.flEchoDepth);
+    validate_modulation_time(lReverb.flModulationTime);
+    validate_modulation_depth(lReverb.flModulationDepth);
+    validate_air_absorbtion_hf(lReverb.flAirAbsorptionHF);
+    validate_hf_reference(lReverb.flHFReference);
+    validate_lf_reference(lReverb.flLFReference);
+    validate_room_rolloff_factor(lReverb.flRoomRolloffFactor);
+    validate_flags(lReverb.ulFlags);
+}
+
+void EaxReverbEffect::v1_defer_environment(unsigned long environment)
+{
+    eax1_d_ = EAX1REVERB_PRESETS[environment];
+    eax1_dirty_flags_.ulEnvironment = true;
+}
+
+void EaxReverbEffect::v1_defer_volume(float volume)
+{
+    eax1_d_.fVolume = volume;
+    eax1_dirty_flags_.flVolume = (eax1_.fVolume != eax1_d_.fVolume);
+}
+
+void EaxReverbEffect::v1_defer_decay_time(float decay_time)
+{
+    eax1_d_.fDecayTime_sec = decay_time;
+    eax1_dirty_flags_.flDecayTime = (eax1_.fDecayTime_sec != eax1_d_.fDecayTime_sec);
+}
+
+void EaxReverbEffect::v1_defer_damping(float damping)
+{
+    eax1_d_.fDamping = damping;
+    eax1_dirty_flags_.flDamping = (eax1_.fDamping != eax1_d_.fDamping);
+}
+
+void EaxReverbEffect::v1_defer_all(const EAX_REVERBPROPERTIES& lReverb)
+{
+    v1_defer_environment(lReverb.environment);
+    v1_defer_volume(lReverb.fVolume);
+    v1_defer_decay_time(lReverb.fDecayTime_sec);
+    v1_defer_damping(lReverb.fDamping);
+}
+
+
+void EaxReverbEffect::v1_set_efx()
+{
+    auto efx_props = eax_efx_reverb_presets[eax1_.environment];
+    efx_props.flGain = eax1_.fVolume;
+    efx_props.flDecayTime = eax1_.fDecayTime_sec;
+    efx_props.flDecayHFRatio = clamp(eax1_.fDamping, AL_EAXREVERB_MIN_DECAY_HFRATIO, AL_EAXREVERB_MAX_DECAY_HFRATIO);
+
+    al_effect_props_.Reverb.Density = efx_props.flDensity;
+    al_effect_props_.Reverb.Diffusion = efx_props.flDiffusion;
+    al_effect_props_.Reverb.Gain = efx_props.flGain;
+    al_effect_props_.Reverb.GainHF = efx_props.flGainHF;
+    al_effect_props_.Reverb.GainLF = efx_props.flGainLF;
+    al_effect_props_.Reverb.DecayTime = efx_props.flDecayTime;
+    al_effect_props_.Reverb.DecayHFRatio = efx_props.flDecayHFRatio;
+    al_effect_props_.Reverb.DecayLFRatio = efx_props.flDecayLFRatio;
+    al_effect_props_.Reverb.ReflectionsGain = efx_props.flReflectionsGain;
+    al_effect_props_.Reverb.ReflectionsDelay = efx_props.flReflectionsDelay;
+    al_effect_props_.Reverb.ReflectionsPan[0] = efx_props.flReflectionsPan[0];
+    al_effect_props_.Reverb.ReflectionsPan[1] = efx_props.flReflectionsPan[1];
+    al_effect_props_.Reverb.ReflectionsPan[2] = efx_props.flReflectionsPan[2];
+    al_effect_props_.Reverb.LateReverbGain = efx_props.flLateReverbGain;
+    al_effect_props_.Reverb.LateReverbDelay = efx_props.flLateReverbDelay;
+    al_effect_props_.Reverb.LateReverbPan[0] = efx_props.flLateReverbPan[0];
+    al_effect_props_.Reverb.LateReverbPan[1] = efx_props.flLateReverbPan[1];
+    al_effect_props_.Reverb.LateReverbPan[2] = efx_props.flLateReverbPan[2];
+    al_effect_props_.Reverb.EchoTime = efx_props.flEchoTime;
+    al_effect_props_.Reverb.EchoDepth = efx_props.flEchoDepth;
+    al_effect_props_.Reverb.ModulationTime = efx_props.flModulationTime;
+    al_effect_props_.Reverb.ModulationDepth = efx_props.flModulationDepth;
+    al_effect_props_.Reverb.HFReference = efx_props.flHFReference;
+    al_effect_props_.Reverb.LFReference = efx_props.flLFReference;
+    al_effect_props_.Reverb.RoomRolloffFactor = efx_props.flRoomRolloffFactor;
+    al_effect_props_.Reverb.AirAbsorptionGainHF = efx_props.flAirAbsorptionGainHF;
+    al_effect_props_.Reverb.DecayHFLimit = false;
+}
+
+void EaxReverbEffect::defer_environment(
+    unsigned long ulEnvironment)
+{
+    eax_d_.ulEnvironment = ulEnvironment;
+    eax_dirty_flags_.ulEnvironment = (eax_.ulEnvironment != eax_d_.ulEnvironment);
+}
+
+void EaxReverbEffect::defer_environment_size(
+    float flEnvironmentSize)
+{
+    eax_d_.flEnvironmentSize = flEnvironmentSize;
+    eax_dirty_flags_.flEnvironmentSize = (eax_.flEnvironmentSize != eax_d_.flEnvironmentSize);
+}
+
+void EaxReverbEffect::defer_environment_diffusion(
+    float flEnvironmentDiffusion)
+{
+    eax_d_.flEnvironmentDiffusion = flEnvironmentDiffusion;
+    eax_dirty_flags_.flEnvironmentDiffusion = (eax_.flEnvironmentDiffusion != eax_d_.flEnvironmentDiffusion);
+}
+
+void EaxReverbEffect::defer_room(
+    long lRoom)
+{
+    eax_d_.lRoom = lRoom;
+    eax_dirty_flags_.lRoom = (eax_.lRoom != eax_d_.lRoom);
+}
+
+void EaxReverbEffect::defer_room_hf(
+    long lRoomHF)
+{
+    eax_d_.lRoomHF = lRoomHF;
+    eax_dirty_flags_.lRoomHF = (eax_.lRoomHF != eax_d_.lRoomHF);
+}
+
+void EaxReverbEffect::defer_room_lf(
+    long lRoomLF)
+{
+    eax_d_.lRoomLF = lRoomLF;
+    eax_dirty_flags_.lRoomLF = (eax_.lRoomLF != eax_d_.lRoomLF);
+}
+
+void EaxReverbEffect::defer_decay_time(
+    float flDecayTime)
+{
+    eax_d_.flDecayTime = flDecayTime;
+    eax_dirty_flags_.flDecayTime = (eax_.flDecayTime != eax_d_.flDecayTime);
+}
+
+void EaxReverbEffect::defer_decay_hf_ratio(
+    float flDecayHFRatio)
+{
+    eax_d_.flDecayHFRatio = flDecayHFRatio;
+    eax_dirty_flags_.flDecayHFRatio = (eax_.flDecayHFRatio != eax_d_.flDecayHFRatio);
+}
+
+void EaxReverbEffect::defer_decay_lf_ratio(
+    float flDecayLFRatio)
+{
+    eax_d_.flDecayLFRatio = flDecayLFRatio;
+    eax_dirty_flags_.flDecayLFRatio = (eax_.flDecayLFRatio != eax_d_.flDecayLFRatio);
+}
+
+void EaxReverbEffect::defer_reflections(
+    long lReflections)
+{
+    eax_d_.lReflections = lReflections;
+    eax_dirty_flags_.lReflections = (eax_.lReflections != eax_d_.lReflections);
+}
+
+void EaxReverbEffect::defer_reflections_delay(
+    float flReflectionsDelay)
+{
+    eax_d_.flReflectionsDelay = flReflectionsDelay;
+    eax_dirty_flags_.flReflectionsDelay = (eax_.flReflectionsDelay != eax_d_.flReflectionsDelay);
+}
+
+void EaxReverbEffect::defer_reflections_pan(
+    const EAXVECTOR& vReflectionsPan)
+{
+    eax_d_.vReflectionsPan = vReflectionsPan;
+    eax_dirty_flags_.vReflectionsPan = (eax_.vReflectionsPan != eax_d_.vReflectionsPan);
+}
+
+void EaxReverbEffect::defer_reverb(
+    long lReverb)
+{
+    eax_d_.lReverb = lReverb;
+    eax_dirty_flags_.lReverb = (eax_.lReverb != eax_d_.lReverb);
+}
+
+void EaxReverbEffect::defer_reverb_delay(
+    float flReverbDelay)
+{
+    eax_d_.flReverbDelay = flReverbDelay;
+    eax_dirty_flags_.flReverbDelay = (eax_.flReverbDelay != eax_d_.flReverbDelay);
+}
+
+void EaxReverbEffect::defer_reverb_pan(
+    const EAXVECTOR& vReverbPan)
+{
+    eax_d_.vReverbPan = vReverbPan;
+    eax_dirty_flags_.vReverbPan = (eax_.vReverbPan != eax_d_.vReverbPan);
+}
+
+void EaxReverbEffect::defer_echo_time(
+    float flEchoTime)
+{
+    eax_d_.flEchoTime = flEchoTime;
+    eax_dirty_flags_.flEchoTime = (eax_.flEchoTime != eax_d_.flEchoTime);
+}
+
+void EaxReverbEffect::defer_echo_depth(
+    float flEchoDepth)
+{
+    eax_d_.flEchoDepth = flEchoDepth;
+    eax_dirty_flags_.flEchoDepth = (eax_.flEchoDepth != eax_d_.flEchoDepth);
+}
+
+void EaxReverbEffect::defer_modulation_time(
+    float flModulationTime)
+{
+    eax_d_.flModulationTime = flModulationTime;
+    eax_dirty_flags_.flModulationTime = (eax_.flModulationTime != eax_d_.flModulationTime);
+}
+
+void EaxReverbEffect::defer_modulation_depth(
+    float flModulationDepth)
+{
+    eax_d_.flModulationDepth = flModulationDepth;
+    eax_dirty_flags_.flModulationDepth = (eax_.flModulationDepth != eax_d_.flModulationDepth);
+}
+
+void EaxReverbEffect::defer_air_absorbtion_hf(
+    float flAirAbsorptionHF)
+{
+    eax_d_.flAirAbsorptionHF = flAirAbsorptionHF;
+    eax_dirty_flags_.flAirAbsorptionHF = (eax_.flAirAbsorptionHF != eax_d_.flAirAbsorptionHF);
+}
+
+void EaxReverbEffect::defer_hf_reference(
+    float flHFReference)
+{
+    eax_d_.flHFReference = flHFReference;
+    eax_dirty_flags_.flHFReference = (eax_.flHFReference != eax_d_.flHFReference);
+}
+
+void EaxReverbEffect::defer_lf_reference(
+    float flLFReference)
+{
+    eax_d_.flLFReference = flLFReference;
+    eax_dirty_flags_.flLFReference = (eax_.flLFReference != eax_d_.flLFReference);
+}
+
+void EaxReverbEffect::defer_room_rolloff_factor(
+    float flRoomRolloffFactor)
+{
+    eax_d_.flRoomRolloffFactor = flRoomRolloffFactor;
+    eax_dirty_flags_.flRoomRolloffFactor = (eax_.flRoomRolloffFactor != eax_d_.flRoomRolloffFactor);
+}
+
+void EaxReverbEffect::defer_flags(
+    unsigned long ulFlags)
+{
+    eax_d_.ulFlags = ulFlags;
+    eax_dirty_flags_.ulFlags = (eax_.ulFlags != eax_d_.ulFlags);
+}
+
+void EaxReverbEffect::defer_all(
+    const EAX20LISTENERPROPERTIES& listener)
+{
+    defer_room(listener.lRoom);
+    defer_room_hf(listener.lRoomHF);
+    defer_room_rolloff_factor(listener.flRoomRolloffFactor);
+    defer_decay_time(listener.flDecayTime);
+    defer_decay_hf_ratio(listener.flDecayHFRatio);
+    defer_reflections(listener.lReflections);
+    defer_reflections_delay(listener.flReflectionsDelay);
+    defer_reverb(listener.lReverb);
+    defer_reverb_delay(listener.flReverbDelay);
+    defer_environment(listener.dwEnvironment);
+    defer_environment_size(listener.flEnvironmentSize);
+    defer_environment_diffusion(listener.flEnvironmentDiffusion);
+    defer_air_absorbtion_hf(listener.flAirAbsorptionHF);
+    defer_flags(listener.dwFlags);
+}
+
+void EaxReverbEffect::defer_all(
+    const EAXREVERBPROPERTIES& lReverb)
+{
+    defer_environment(lReverb.ulEnvironment);
+    defer_environment_size(lReverb.flEnvironmentSize);
+    defer_environment_diffusion(lReverb.flEnvironmentDiffusion);
+    defer_room(lReverb.lRoom);
+    defer_room_hf(lReverb.lRoomHF);
+    defer_room_lf(lReverb.lRoomLF);
+    defer_decay_time(lReverb.flDecayTime);
+    defer_decay_hf_ratio(lReverb.flDecayHFRatio);
+    defer_decay_lf_ratio(lReverb.flDecayLFRatio);
+    defer_reflections(lReverb.lReflections);
+    defer_reflections_delay(lReverb.flReflectionsDelay);
+    defer_reflections_pan(lReverb.vReflectionsPan);
+    defer_reverb(lReverb.lReverb);
+    defer_reverb_delay(lReverb.flReverbDelay);
+    defer_reverb_pan(lReverb.vReverbPan);
+    defer_echo_time(lReverb.flEchoTime);
+    defer_echo_depth(lReverb.flEchoDepth);
+    defer_modulation_time(lReverb.flModulationTime);
+    defer_modulation_depth(lReverb.flModulationDepth);
+    defer_air_absorbtion_hf(lReverb.flAirAbsorptionHF);
+    defer_hf_reference(lReverb.flHFReference);
+    defer_lf_reference(lReverb.flLFReference);
+    defer_room_rolloff_factor(lReverb.flRoomRolloffFactor);
+    defer_flags(lReverb.ulFlags);
+}
+
+
+void EaxReverbEffect::v1_defer_environment(const EaxEaxCall& eax_call)
+{
+    const auto& environment = eax_call.get_value<EaxReverbEffectException,
+        const decltype(EAX_REVERBPROPERTIES::environment)>();
+
+    validate_environment(environment, 1, true);
+
+    const auto& reverb_preset = EAX1REVERB_PRESETS[environment];
+    v1_defer_all(reverb_preset);
+}
+
+void EaxReverbEffect::v1_defer_volume(const EaxEaxCall& eax_call)
+{
+    const auto& volume = eax_call.get_value<EaxReverbEffectException,
+        const decltype(EAX_REVERBPROPERTIES::fVolume)>();
+
+    v1_validate_volume(volume);
+    v1_defer_volume(volume);
+}
+
+void EaxReverbEffect::v1_defer_decay_time(const EaxEaxCall& eax_call)
+{
+    const auto& decay_time = eax_call.get_value<EaxReverbEffectException,
+        const decltype(EAX_REVERBPROPERTIES::fDecayTime_sec)>();
+
+    v1_validate_decay_time(decay_time);
+    v1_defer_decay_time(decay_time);
+}
+
+void EaxReverbEffect::v1_defer_damping(const EaxEaxCall& eax_call)
+{
+    const auto& damping = eax_call.get_value<EaxReverbEffectException,
+        const decltype(EAX_REVERBPROPERTIES::fDamping)>();
+
+    v1_validate_damping(damping);
+    v1_defer_damping(damping);
+}
+
+void EaxReverbEffect::v1_defer_all(const EaxEaxCall& eax_call)
+{
+    const auto& reverb_all = eax_call.get_value<EaxReverbEffectException,
+        const EAX_REVERBPROPERTIES>();
+
+    v1_validate_all(reverb_all);
+    v1_defer_all(reverb_all);
+}
+
+
+void EaxReverbEffect::defer_environment(
+    const EaxEaxCall& eax_call)
+{
+    const auto& ulEnvironment =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::ulEnvironment)>();
+
+    validate_environment(ulEnvironment, eax_call.get_version(), true);
+
+    if (eax_d_.ulEnvironment == ulEnvironment)
+    {
+        return;
+    }
+
+    const auto& reverb_preset = EAXREVERB_PRESETS[ulEnvironment];
+
+    defer_all(reverb_preset);
+}
+
+void EaxReverbEffect::defer_environment_size(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flEnvironmentSize =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flEnvironmentSize)>();
+
+    validate_environment_size(flEnvironmentSize);
+
+    if (eax_d_.flEnvironmentSize == flEnvironmentSize)
+    {
+        return;
+    }
+
+    const auto scale = flEnvironmentSize / eax_d_.flEnvironmentSize;
+
+    defer_environment_size(flEnvironmentSize);
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_DECAYTIMESCALE) != 0)
+    {
+        const auto flDecayTime = clamp(
+            scale * eax_d_.flDecayTime,
+            EAXREVERB_MINDECAYTIME,
+            EAXREVERB_MAXDECAYTIME);
+
+        defer_decay_time(flDecayTime);
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_REFLECTIONSSCALE) != 0)
+    {
+        if ((eax_d_.ulFlags & EAXREVERBFLAGS_REFLECTIONSDELAYSCALE) != 0)
+        {
+            const auto lReflections = clamp(
+                eax_d_.lReflections - static_cast<long>(gain_to_level_mb(scale)),
+                EAXREVERB_MINREFLECTIONS,
+                EAXREVERB_MAXREFLECTIONS);
+
+            defer_reflections(lReflections);
+        }
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_REFLECTIONSDELAYSCALE) != 0)
+    {
+        const auto flReflectionsDelay = clamp(
+            eax_d_.flReflectionsDelay * scale,
+            EAXREVERB_MINREFLECTIONSDELAY,
+            EAXREVERB_MAXREFLECTIONSDELAY);
+
+        defer_reflections_delay(flReflectionsDelay);
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_REVERBSCALE) != 0)
+    {
+        const auto log_scalar = ((eax_d_.ulFlags & EAXREVERBFLAGS_DECAYTIMESCALE) != 0) ? 2'000.0F : 3'000.0F;
+
+        const auto lReverb = clamp(
+            eax_d_.lReverb - static_cast<long>(std::log10(scale) * log_scalar),
+            EAXREVERB_MINREVERB,
+            EAXREVERB_MAXREVERB);
+
+        defer_reverb(lReverb);
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_REVERBDELAYSCALE) != 0)
+    {
+        const auto flReverbDelay = clamp(
+            scale * eax_d_.flReverbDelay,
+            EAXREVERB_MINREVERBDELAY,
+            EAXREVERB_MAXREVERBDELAY);
+
+        defer_reverb_delay(flReverbDelay);
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_ECHOTIMESCALE) != 0)
+    {
+        const auto flEchoTime = clamp(
+            eax_d_.flEchoTime * scale,
+            EAXREVERB_MINECHOTIME,
+            EAXREVERB_MAXECHOTIME);
+
+        defer_echo_time(flEchoTime);
+    }
+
+    if ((eax_d_.ulFlags & EAXREVERBFLAGS_MODULATIONTIMESCALE) != 0)
+    {
+        const auto flModulationTime = clamp(
+            scale * eax_d_.flModulationTime,
+            EAXREVERB_MINMODULATIONTIME,
+            EAXREVERB_MAXMODULATIONTIME);
+
+        defer_modulation_time(flModulationTime);
+    }
+}
+
+void EaxReverbEffect::defer_environment_diffusion(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flEnvironmentDiffusion =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flEnvironmentDiffusion)>();
+
+    validate_environment_diffusion(flEnvironmentDiffusion);
+    defer_environment_diffusion(flEnvironmentDiffusion);
+}
+
+void EaxReverbEffect::defer_room(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lRoom =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::lRoom)>();
+
+    validate_room(lRoom);
+    defer_room(lRoom);
+}
+
+void EaxReverbEffect::defer_room_hf(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lRoomHF =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::lRoomHF)>();
+
+    validate_room_hf(lRoomHF);
+    defer_room_hf(lRoomHF);
+}
+
+void EaxReverbEffect::defer_room_lf(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lRoomLF =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::lRoomLF)>();
+
+    validate_room_lf(lRoomLF);
+    defer_room_lf(lRoomLF);
+}
+
+void EaxReverbEffect::defer_decay_time(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flDecayTime =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flDecayTime)>();
+
+    validate_decay_time(flDecayTime);
+    defer_decay_time(flDecayTime);
+}
+
+void EaxReverbEffect::defer_decay_hf_ratio(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flDecayHFRatio =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flDecayHFRatio)>();
+
+    validate_decay_hf_ratio(flDecayHFRatio);
+    defer_decay_hf_ratio(flDecayHFRatio);
+}
+
+void EaxReverbEffect::defer_decay_lf_ratio(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flDecayLFRatio =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flDecayLFRatio)>();
+
+    validate_decay_lf_ratio(flDecayLFRatio);
+    defer_decay_lf_ratio(flDecayLFRatio);
+}
+
+void EaxReverbEffect::defer_reflections(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lReflections =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::lReflections)>();
+
+    validate_reflections(lReflections);
+    defer_reflections(lReflections);
+}
+
+void EaxReverbEffect::defer_reflections_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flReflectionsDelay =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flReflectionsDelay)>();
+
+    validate_reflections_delay(flReflectionsDelay);
+    defer_reflections_delay(flReflectionsDelay);
+}
+
+void EaxReverbEffect::defer_reflections_pan(
+    const EaxEaxCall& eax_call)
+{
+    const auto& vReflectionsPan =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::vReflectionsPan)>();
+
+    validate_reflections_pan(vReflectionsPan);
+    defer_reflections_pan(vReflectionsPan);
+}
+
+void EaxReverbEffect::defer_reverb(
+    const EaxEaxCall& eax_call)
+{
+    const auto& lReverb =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::lReverb)>();
+
+    validate_reverb(lReverb);
+    defer_reverb(lReverb);
+}
+
+void EaxReverbEffect::defer_reverb_delay(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flReverbDelay =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flReverbDelay)>();
+
+    validate_reverb_delay(flReverbDelay);
+    defer_reverb_delay(flReverbDelay);
+}
+
+void EaxReverbEffect::defer_reverb_pan(
+    const EaxEaxCall& eax_call)
+{
+    const auto& vReverbPan =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::vReverbPan)>();
+
+    validate_reverb_pan(vReverbPan);
+    defer_reverb_pan(vReverbPan);
+}
+
+void EaxReverbEffect::defer_echo_time(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flEchoTime =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flEchoTime)>();
+
+    validate_echo_time(flEchoTime);
+    defer_echo_time(flEchoTime);
+}
+
+void EaxReverbEffect::defer_echo_depth(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flEchoDepth =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flEchoDepth)>();
+
+    validate_echo_depth(flEchoDepth);
+    defer_echo_depth(flEchoDepth);
+}
+
+void EaxReverbEffect::defer_modulation_time(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flModulationTime =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flModulationTime)>();
+
+    validate_modulation_time(flModulationTime);
+    defer_modulation_time(flModulationTime);
+}
+
+void EaxReverbEffect::defer_modulation_depth(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flModulationDepth =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flModulationDepth)>();
+
+    validate_modulation_depth(flModulationDepth);
+    defer_modulation_depth(flModulationDepth);
+}
+
+void EaxReverbEffect::defer_air_absorbtion_hf(
+    const EaxEaxCall& eax_call)
+{
+    const auto& air_absorbtion_hf =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flAirAbsorptionHF)>();
+
+    validate_air_absorbtion_hf(air_absorbtion_hf);
+    defer_air_absorbtion_hf(air_absorbtion_hf);
+}
+
+void EaxReverbEffect::defer_hf_reference(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flHFReference =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flHFReference)>();
+
+    validate_hf_reference(flHFReference);
+    defer_hf_reference(flHFReference);
+}
+
+void EaxReverbEffect::defer_lf_reference(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flLFReference =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flLFReference)>();
+
+    validate_lf_reference(flLFReference);
+    defer_lf_reference(flLFReference);
+}
+
+void EaxReverbEffect::defer_room_rolloff_factor(
+    const EaxEaxCall& eax_call)
+{
+    const auto& flRoomRolloffFactor =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::flRoomRolloffFactor)>();
+
+    validate_room_rolloff_factor(flRoomRolloffFactor);
+    defer_room_rolloff_factor(flRoomRolloffFactor);
+}
+
+void EaxReverbEffect::defer_flags(
+    const EaxEaxCall& eax_call)
+{
+    const auto& ulFlags =
+        eax_call.get_value<EaxReverbEffectException, const decltype(EAXREVERBPROPERTIES::ulFlags)>();
+
+    validate_flags(ulFlags);
+    defer_flags(ulFlags);
+}
+
+void EaxReverbEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto eax_version = eax_call.get_version();
+
+    if (eax_version == 2)
+    {
+        const auto& listener =
+            eax_call.get_value<EaxReverbEffectException, const EAX20LISTENERPROPERTIES>();
+
+        validate_all(listener, eax_version);
+        defer_all(listener);
+    }
+    else
+    {
+        const auto& reverb_all =
+            eax_call.get_value<EaxReverbEffectException, const EAXREVERBPROPERTIES>();
+
+        validate_all(reverb_all, eax_version);
+        defer_all(reverb_all);
+    }
+}
+
+
+void EaxReverbEffect::v1_defer(const EaxEaxCall& eax_call)
+{
+    switch (eax_call.get_property_id())
+    {
+        case DSPROPERTY_EAX_ALL: return v1_defer_all(eax_call);
+        case DSPROPERTY_EAX_ENVIRONMENT: return v1_defer_environment(eax_call);
+        case DSPROPERTY_EAX_VOLUME: return v1_defer_volume(eax_call);
+        case DSPROPERTY_EAX_DECAYTIME: return v1_defer_decay_time(eax_call);
+        case DSPROPERTY_EAX_DAMPING: return v1_defer_damping(eax_call);
+        default: eax_fail("Unsupported property id.");
+    }
+}
+
+// [[nodiscard]]
+bool EaxReverbEffect::apply_deferred()
+{
+    bool ret{false};
+
+    if(unlikely(eax1_dirty_flags_ != Eax1ReverbEffectDirtyFlags{}))
+    {
+        eax1_ = eax1_d_;
+
+        v1_set_efx();
+
+        eax1_dirty_flags_ = Eax1ReverbEffectDirtyFlags{};
+
+        ret = true;
+    }
+
+    if(eax_dirty_flags_ == EaxReverbEffectDirtyFlags{})
+        return ret;
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.ulEnvironment)
+    {
+    }
+
+    if (eax_dirty_flags_.flEnvironmentSize)
+    {
+        set_efx_density_from_environment_size();
+    }
+
+    if (eax_dirty_flags_.flEnvironmentDiffusion)
+    {
+        set_efx_diffusion();
+    }
+
+    if (eax_dirty_flags_.lRoom)
+    {
+        set_efx_gain();
+    }
+
+    if (eax_dirty_flags_.lRoomHF)
+    {
+        set_efx_gain_hf();
+    }
+
+    if (eax_dirty_flags_.lRoomLF)
+    {
+        set_efx_gain_lf();
+    }
+
+    if (eax_dirty_flags_.flDecayTime)
+    {
+        set_efx_decay_time();
+    }
+
+    if (eax_dirty_flags_.flDecayHFRatio)
+    {
+        set_efx_decay_hf_ratio();
+    }
+
+    if (eax_dirty_flags_.flDecayLFRatio)
+    {
+        set_efx_decay_lf_ratio();
+    }
+
+    if (eax_dirty_flags_.lReflections)
+    {
+        set_efx_reflections_gain();
+    }
+
+    if (eax_dirty_flags_.flReflectionsDelay)
+    {
+        set_efx_reflections_delay();
+    }
+
+    if (eax_dirty_flags_.vReflectionsPan)
+    {
+        set_efx_reflections_pan();
+    }
+
+    if (eax_dirty_flags_.lReverb)
+    {
+        set_efx_late_reverb_gain();
+    }
+
+    if (eax_dirty_flags_.flReverbDelay)
+    {
+        set_efx_late_reverb_delay();
+    }
+
+    if (eax_dirty_flags_.vReverbPan)
+    {
+        set_efx_late_reverb_pan();
+    }
+
+    if (eax_dirty_flags_.flEchoTime)
+    {
+        set_efx_echo_time();
+    }
+
+    if (eax_dirty_flags_.flEchoDepth)
+    {
+        set_efx_echo_depth();
+    }
+
+    if (eax_dirty_flags_.flModulationTime)
+    {
+        set_efx_modulation_time();
+    }
+
+    if (eax_dirty_flags_.flModulationDepth)
+    {
+        set_efx_modulation_depth();
+    }
+
+    if (eax_dirty_flags_.flAirAbsorptionHF)
+    {
+        set_efx_air_absorption_gain_hf();
+    }
+
+    if (eax_dirty_flags_.flHFReference)
+    {
+        set_efx_hf_reference();
+    }
+
+    if (eax_dirty_flags_.flLFReference)
+    {
+        set_efx_lf_reference();
+    }
+
+    if (eax_dirty_flags_.flRoomRolloffFactor)
+    {
+        set_efx_room_rolloff_factor();
+    }
+
+    if (eax_dirty_flags_.ulFlags)
+    {
+        set_efx_flags();
+    }
+
+    eax_dirty_flags_ = EaxReverbEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxReverbEffect::set(const EaxEaxCall& eax_call)
+{
+    if(eax_call.get_version() == 1)
+        v1_defer(eax_call);
+    else switch(eax_call.get_property_id())
+    {
+        case EAXREVERB_NONE:
+            break;
+
+        case EAXREVERB_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXREVERB_ENVIRONMENT:
+            defer_environment(eax_call);
+            break;
+
+        case EAXREVERB_ENVIRONMENTSIZE:
+            defer_environment_size(eax_call);
+            break;
+
+        case EAXREVERB_ENVIRONMENTDIFFUSION:
+            defer_environment_diffusion(eax_call);
+            break;
+
+        case EAXREVERB_ROOM:
+            defer_room(eax_call);
+            break;
+
+        case EAXREVERB_ROOMHF:
+            defer_room_hf(eax_call);
+            break;
+
+        case EAXREVERB_ROOMLF:
+            defer_room_lf(eax_call);
+            break;
+
+        case EAXREVERB_DECAYTIME:
+            defer_decay_time(eax_call);
+            break;
+
+        case EAXREVERB_DECAYHFRATIO:
+            defer_decay_hf_ratio(eax_call);
+            break;
+
+        case EAXREVERB_DECAYLFRATIO:
+            defer_decay_lf_ratio(eax_call);
+            break;
+
+        case EAXREVERB_REFLECTIONS:
+            defer_reflections(eax_call);
+            break;
+
+        case EAXREVERB_REFLECTIONSDELAY:
+            defer_reflections_delay(eax_call);
+            break;
+
+        case EAXREVERB_REFLECTIONSPAN:
+            defer_reflections_pan(eax_call);
+            break;
+
+        case EAXREVERB_REVERB:
+            defer_reverb(eax_call);
+            break;
+
+        case EAXREVERB_REVERBDELAY:
+            defer_reverb_delay(eax_call);
+            break;
+
+        case EAXREVERB_REVERBPAN:
+            defer_reverb_pan(eax_call);
+            break;
+
+        case EAXREVERB_ECHOTIME:
+            defer_echo_time(eax_call);
+            break;
+
+        case EAXREVERB_ECHODEPTH:
+            defer_echo_depth(eax_call);
+            break;
+
+        case EAXREVERB_MODULATIONTIME:
+            defer_modulation_time(eax_call);
+            break;
+
+        case EAXREVERB_MODULATIONDEPTH:
+            defer_modulation_depth(eax_call);
+            break;
+
+        case EAXREVERB_AIRABSORPTIONHF:
+            defer_air_absorbtion_hf(eax_call);
+            break;
+
+        case EAXREVERB_HFREFERENCE:
+            defer_hf_reference(eax_call);
+            break;
+
+        case EAXREVERB_LFREFERENCE:
+            defer_lf_reference(eax_call);
+            break;
+
+        case EAXREVERB_ROOMROLLOFFFACTOR:
+            defer_room_rolloff_factor(eax_call);
+            break;
+
+        case EAXREVERB_FLAGS:
+            defer_flags(eax_call);
+            break;
+
+        default:
+            eax_fail("Unsupported property id.");
+    }
+}
+
+const EFXEAXREVERBPROPERTIES eax_efx_reverb_presets[EAX1_ENVIRONMENT_COUNT] =
+{
+    EFX_REVERB_PRESET_GENERIC,
+    EFX_REVERB_PRESET_PADDEDCELL,
+    EFX_REVERB_PRESET_ROOM,
+    EFX_REVERB_PRESET_BATHROOM,
+    EFX_REVERB_PRESET_LIVINGROOM,
+    EFX_REVERB_PRESET_STONEROOM,
+    EFX_REVERB_PRESET_AUDITORIUM,
+    EFX_REVERB_PRESET_CONCERTHALL,
+    EFX_REVERB_PRESET_CAVE,
+    EFX_REVERB_PRESET_ARENA,
+    EFX_REVERB_PRESET_HANGAR,
+    EFX_REVERB_PRESET_CARPETEDHALLWAY,
+    EFX_REVERB_PRESET_HALLWAY,
+    EFX_REVERB_PRESET_STONECORRIDOR,
+    EFX_REVERB_PRESET_ALLEY,
+    EFX_REVERB_PRESET_FOREST,
+    EFX_REVERB_PRESET_CITY,
+    EFX_REVERB_PRESET_MOUNTAINS,
+    EFX_REVERB_PRESET_QUARRY,
+    EFX_REVERB_PRESET_PLAIN,
+    EFX_REVERB_PRESET_PARKINGLOT,
+    EFX_REVERB_PRESET_SEWERPIPE,
+    EFX_REVERB_PRESET_UNDERWATER,
+    EFX_REVERB_PRESET_DRUGGED,
+    EFX_REVERB_PRESET_DIZZY,
+    EFX_REVERB_PRESET_PSYCHOTIC,
+}; // EFXEAXREVERBPROPERTIES
+
+} // namespace
+
+EaxEffectUPtr eax_create_eax_reverb_effect()
+{
+    return std::make_unique<EaxReverbEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 540 - 1
Engine/lib/openal-soft/al/effects/vmorpher.cpp

@@ -1,12 +1,23 @@
 
 #include "config.h"
 
+#include <stdexcept>
+
 #include "AL/al.h"
 #include "AL/efx.h"
 
+#include "alc/effects/base.h"
 #include "aloptional.h"
 #include "effects.h"
-#include "effects/base.h"
+
+#ifdef ALSOFT_EAX
+#include <cassert>
+
+#include "alnumeric.h"
+
+#include "al/eax_exception.h"
+#include "al/eax_utils.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -245,3 +256,531 @@ EffectProps genDefaultProps() noexcept
 DEFINE_ALEFFECT_VTABLE(Vmorpher);
 
 const EffectProps VmorpherEffectProps{genDefaultProps()};
+
+#ifdef ALSOFT_EAX
+namespace {
+
+using EaxVocalMorpherEffectDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxVocalMorpherEffectDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+    EaxVocalMorpherEffectDirtyFlagsValue ulPhonemeA : 1;
+    EaxVocalMorpherEffectDirtyFlagsValue lPhonemeACoarseTuning : 1;
+    EaxVocalMorpherEffectDirtyFlagsValue ulPhonemeB : 1;
+    EaxVocalMorpherEffectDirtyFlagsValue lPhonemeBCoarseTuning : 1;
+    EaxVocalMorpherEffectDirtyFlagsValue ulWaveform : 1;
+    EaxVocalMorpherEffectDirtyFlagsValue flRate : 1;
+}; // EaxPitchShifterEffectDirtyFlags
+
+
+class EaxVocalMorpherEffect final :
+    public EaxEffect
+{
+public:
+    EaxVocalMorpherEffect();
+
+    void dispatch(const EaxEaxCall& eax_call) override;
+
+    // [[nodiscard]]
+    bool apply_deferred() override;
+
+private:
+    EAXVOCALMORPHERPROPERTIES eax_{};
+    EAXVOCALMORPHERPROPERTIES eax_d_{};
+    EaxVocalMorpherEffectDirtyFlags eax_dirty_flags_{};
+
+    void set_eax_defaults();
+
+    void set_efx_phoneme_a();
+    void set_efx_phoneme_a_coarse_tuning();
+    void set_efx_phoneme_b();
+    void set_efx_phoneme_b_coarse_tuning();
+    void set_efx_waveform();
+    void set_efx_rate();
+    void set_efx_defaults();
+
+    void get(const EaxEaxCall& eax_call);
+
+    void validate_phoneme_a(unsigned long ulPhonemeA);
+    void validate_phoneme_a_coarse_tuning(long lPhonemeACoarseTuning);
+    void validate_phoneme_b(unsigned long ulPhonemeB);
+    void validate_phoneme_b_coarse_tuning(long lPhonemeBCoarseTuning);
+    void validate_waveform(unsigned long ulWaveform);
+    void validate_rate(float flRate);
+    void validate_all(const EAXVOCALMORPHERPROPERTIES& all);
+
+    void defer_phoneme_a(unsigned long ulPhonemeA);
+    void defer_phoneme_a_coarse_tuning(long lPhonemeACoarseTuning);
+    void defer_phoneme_b(unsigned long ulPhonemeB);
+    void defer_phoneme_b_coarse_tuning(long lPhonemeBCoarseTuning);
+    void defer_waveform(unsigned long ulWaveform);
+    void defer_rate(float flRate);
+    void defer_all(const EAXVOCALMORPHERPROPERTIES& all);
+
+    void defer_phoneme_a(const EaxEaxCall& eax_call);
+    void defer_phoneme_a_coarse_tuning(const EaxEaxCall& eax_call);
+    void defer_phoneme_b(const EaxEaxCall& eax_call);
+    void defer_phoneme_b_coarse_tuning(const EaxEaxCall& eax_call);
+    void defer_waveform(const EaxEaxCall& eax_call);
+    void defer_rate(const EaxEaxCall& eax_call);
+    void defer_all(const EaxEaxCall& eax_call);
+
+    void set(const EaxEaxCall& eax_call);
+}; // EaxVocalMorpherEffect
+
+
+class EaxVocalMorpherEffectException :
+    public EaxException
+{
+public:
+    explicit EaxVocalMorpherEffectException(
+        const char* message)
+        :
+        EaxException{"EAX_VOCAL_MORPHER_EFFECT", message}
+    {
+    }
+}; // EaxVocalMorpherEffectException
+
+
+EaxVocalMorpherEffect::EaxVocalMorpherEffect()
+    : EaxEffect{AL_EFFECT_VOCAL_MORPHER}
+{
+    set_eax_defaults();
+    set_efx_defaults();
+}
+
+void EaxVocalMorpherEffect::dispatch(const EaxEaxCall& eax_call)
+{
+    eax_call.is_get() ? get(eax_call) : set(eax_call);
+}
+
+void EaxVocalMorpherEffect::set_eax_defaults()
+{
+    eax_.ulPhonemeA = EAXVOCALMORPHER_DEFAULTPHONEMEA;
+    eax_.lPhonemeACoarseTuning = EAXVOCALMORPHER_DEFAULTPHONEMEACOARSETUNING;
+    eax_.ulPhonemeB = EAXVOCALMORPHER_DEFAULTPHONEMEB;
+    eax_.lPhonemeBCoarseTuning = EAXVOCALMORPHER_DEFAULTPHONEMEBCOARSETUNING;
+    eax_.ulWaveform = EAXVOCALMORPHER_DEFAULTWAVEFORM;
+    eax_.flRate = EAXVOCALMORPHER_DEFAULTRATE;
+
+    eax_d_ = eax_;
+}
+
+void EaxVocalMorpherEffect::set_efx_phoneme_a()
+{
+    const auto phoneme_a = clamp(
+        static_cast<ALint>(eax_.ulPhonemeA),
+        AL_VOCAL_MORPHER_MIN_PHONEMEA,
+        AL_VOCAL_MORPHER_MAX_PHONEMEA);
+
+    const auto efx_phoneme_a = PhenomeFromEnum(phoneme_a);
+    assert(efx_phoneme_a.has_value());
+    al_effect_props_.Vmorpher.PhonemeA = *efx_phoneme_a;
+}
+
+void EaxVocalMorpherEffect::set_efx_phoneme_a_coarse_tuning()
+{
+    const auto phoneme_a_coarse_tuning = clamp(
+        static_cast<ALint>(eax_.lPhonemeACoarseTuning),
+        AL_VOCAL_MORPHER_MIN_PHONEMEA_COARSE_TUNING,
+        AL_VOCAL_MORPHER_MAX_PHONEMEA_COARSE_TUNING);
+
+    al_effect_props_.Vmorpher.PhonemeACoarseTuning = phoneme_a_coarse_tuning;
+}
+
+void EaxVocalMorpherEffect::set_efx_phoneme_b()
+{
+    const auto phoneme_b = clamp(
+        static_cast<ALint>(eax_.ulPhonemeB),
+        AL_VOCAL_MORPHER_MIN_PHONEMEB,
+        AL_VOCAL_MORPHER_MAX_PHONEMEB);
+
+    const auto efx_phoneme_b = PhenomeFromEnum(phoneme_b);
+    assert(efx_phoneme_b.has_value());
+    al_effect_props_.Vmorpher.PhonemeB = *efx_phoneme_b;
+}
+
+void EaxVocalMorpherEffect::set_efx_phoneme_b_coarse_tuning()
+{
+    const auto phoneme_b_coarse_tuning = clamp(
+        static_cast<ALint>(eax_.lPhonemeBCoarseTuning),
+        AL_VOCAL_MORPHER_MIN_PHONEMEB_COARSE_TUNING,
+        AL_VOCAL_MORPHER_MAX_PHONEMEB_COARSE_TUNING);
+
+    al_effect_props_.Vmorpher.PhonemeBCoarseTuning = phoneme_b_coarse_tuning;
+}
+
+void EaxVocalMorpherEffect::set_efx_waveform()
+{
+    const auto waveform = clamp(
+        static_cast<ALint>(eax_.ulWaveform),
+        AL_VOCAL_MORPHER_MIN_WAVEFORM,
+        AL_VOCAL_MORPHER_MAX_WAVEFORM);
+
+    const auto wfx_waveform = WaveformFromEmum(waveform);
+    assert(wfx_waveform.has_value());
+    al_effect_props_.Vmorpher.Waveform = *wfx_waveform;
+}
+
+void EaxVocalMorpherEffect::set_efx_rate()
+{
+    const auto rate = clamp(
+        eax_.flRate,
+        AL_VOCAL_MORPHER_MIN_RATE,
+        AL_VOCAL_MORPHER_MAX_RATE);
+
+    al_effect_props_.Vmorpher.Rate = rate;
+}
+
+void EaxVocalMorpherEffect::set_efx_defaults()
+{
+    set_efx_phoneme_a();
+    set_efx_phoneme_a_coarse_tuning();
+    set_efx_phoneme_b();
+    set_efx_phoneme_b_coarse_tuning();
+    set_efx_waveform();
+    set_efx_rate();
+}
+
+void EaxVocalMorpherEffect::get(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXVOCALMORPHER_NONE:
+            break;
+
+        case EAXVOCALMORPHER_ALLPARAMETERS:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEA:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.ulPhonemeA);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEACOARSETUNING:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.lPhonemeACoarseTuning);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEB:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.ulPhonemeB);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEBCOARSETUNING:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.lPhonemeBCoarseTuning);
+            break;
+
+        case EAXVOCALMORPHER_WAVEFORM:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.ulWaveform);
+            break;
+
+        case EAXVOCALMORPHER_RATE:
+            eax_call.set_value<EaxVocalMorpherEffectException>(eax_.flRate);
+            break;
+
+        default:
+            throw EaxVocalMorpherEffectException{"Unsupported property id."};
+    }
+}
+
+void EaxVocalMorpherEffect::validate_phoneme_a(
+    unsigned long ulPhonemeA)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Phoneme A",
+        ulPhonemeA,
+        EAXVOCALMORPHER_MINPHONEMEA,
+        EAXVOCALMORPHER_MAXPHONEMEA);
+}
+
+void EaxVocalMorpherEffect::validate_phoneme_a_coarse_tuning(
+    long lPhonemeACoarseTuning)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Phoneme A Coarse Tuning",
+        lPhonemeACoarseTuning,
+        EAXVOCALMORPHER_MINPHONEMEACOARSETUNING,
+        EAXVOCALMORPHER_MAXPHONEMEACOARSETUNING);
+}
+
+void EaxVocalMorpherEffect::validate_phoneme_b(
+    unsigned long ulPhonemeB)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Phoneme B",
+        ulPhonemeB,
+        EAXVOCALMORPHER_MINPHONEMEB,
+        EAXVOCALMORPHER_MAXPHONEMEB);
+}
+
+void EaxVocalMorpherEffect::validate_phoneme_b_coarse_tuning(
+    long lPhonemeBCoarseTuning)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Phoneme B Coarse Tuning",
+        lPhonemeBCoarseTuning,
+        EAXVOCALMORPHER_MINPHONEMEBCOARSETUNING,
+        EAXVOCALMORPHER_MAXPHONEMEBCOARSETUNING);
+}
+
+void EaxVocalMorpherEffect::validate_waveform(
+    unsigned long ulWaveform)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Waveform",
+        ulWaveform,
+        EAXVOCALMORPHER_MINWAVEFORM,
+        EAXVOCALMORPHER_MAXWAVEFORM);
+}
+
+void EaxVocalMorpherEffect::validate_rate(
+    float flRate)
+{
+    eax_validate_range<EaxVocalMorpherEffectException>(
+        "Rate",
+        flRate,
+        EAXVOCALMORPHER_MINRATE,
+        EAXVOCALMORPHER_MAXRATE);
+}
+
+void EaxVocalMorpherEffect::validate_all(
+    const EAXVOCALMORPHERPROPERTIES& all)
+{
+    validate_phoneme_a(all.ulPhonemeA);
+    validate_phoneme_a_coarse_tuning(all.lPhonemeACoarseTuning);
+    validate_phoneme_b(all.ulPhonemeB);
+    validate_phoneme_b_coarse_tuning(all.lPhonemeBCoarseTuning);
+    validate_waveform(all.ulWaveform);
+    validate_rate(all.flRate);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_a(
+    unsigned long ulPhonemeA)
+{
+    eax_d_.ulPhonemeA = ulPhonemeA;
+    eax_dirty_flags_.ulPhonemeA = (eax_.ulPhonemeA != eax_d_.ulPhonemeA);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_a_coarse_tuning(
+    long lPhonemeACoarseTuning)
+{
+    eax_d_.lPhonemeACoarseTuning = lPhonemeACoarseTuning;
+    eax_dirty_flags_.lPhonemeACoarseTuning = (eax_.lPhonemeACoarseTuning != eax_d_.lPhonemeACoarseTuning);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_b(
+    unsigned long ulPhonemeB)
+{
+    eax_d_.ulPhonemeB = ulPhonemeB;
+    eax_dirty_flags_.ulPhonemeB = (eax_.ulPhonemeB != eax_d_.ulPhonemeB);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_b_coarse_tuning(
+    long lPhonemeBCoarseTuning)
+{
+    eax_d_.lPhonemeBCoarseTuning = lPhonemeBCoarseTuning;
+    eax_dirty_flags_.lPhonemeBCoarseTuning = (eax_.lPhonemeBCoarseTuning != eax_d_.lPhonemeBCoarseTuning);
+}
+
+void EaxVocalMorpherEffect::defer_waveform(
+    unsigned long ulWaveform)
+{
+    eax_d_.ulWaveform = ulWaveform;
+    eax_dirty_flags_.ulWaveform = (eax_.ulWaveform != eax_d_.ulWaveform);
+}
+
+void EaxVocalMorpherEffect::defer_rate(
+    float flRate)
+{
+    eax_d_.flRate = flRate;
+    eax_dirty_flags_.flRate = (eax_.flRate != eax_d_.flRate);
+}
+
+void EaxVocalMorpherEffect::defer_all(
+    const EAXVOCALMORPHERPROPERTIES& all)
+{
+    defer_phoneme_a(all.ulPhonemeA);
+    defer_phoneme_a_coarse_tuning(all.lPhonemeACoarseTuning);
+    defer_phoneme_b(all.ulPhonemeB);
+    defer_phoneme_b_coarse_tuning(all.lPhonemeBCoarseTuning);
+    defer_waveform(all.ulWaveform);
+    defer_rate(all.flRate);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_a(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phoneme_a = eax_call.get_value<EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::ulPhonemeA)>();
+
+    validate_phoneme_a(phoneme_a);
+    defer_phoneme_a(phoneme_a);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_a_coarse_tuning(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phoneme_a_coarse_tuning = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::lPhonemeACoarseTuning)
+    >();
+
+    validate_phoneme_a_coarse_tuning(phoneme_a_coarse_tuning);
+    defer_phoneme_a_coarse_tuning(phoneme_a_coarse_tuning);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_b(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phoneme_b = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::ulPhonemeB)
+    >();
+
+    validate_phoneme_b(phoneme_b);
+    defer_phoneme_b(phoneme_b);
+}
+
+void EaxVocalMorpherEffect::defer_phoneme_b_coarse_tuning(
+    const EaxEaxCall& eax_call)
+{
+    const auto& phoneme_b_coarse_tuning = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::lPhonemeBCoarseTuning)
+    >();
+
+    validate_phoneme_b_coarse_tuning(phoneme_b_coarse_tuning);
+    defer_phoneme_b_coarse_tuning(phoneme_b_coarse_tuning);
+}
+
+void EaxVocalMorpherEffect::defer_waveform(
+    const EaxEaxCall& eax_call)
+{
+    const auto& waveform = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::ulWaveform)
+    >();
+
+    validate_waveform(waveform);
+    defer_waveform(waveform);
+}
+
+void EaxVocalMorpherEffect::defer_rate(
+    const EaxEaxCall& eax_call)
+{
+    const auto& rate = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const decltype(EAXVOCALMORPHERPROPERTIES::flRate)
+    >();
+
+    validate_rate(rate);
+    defer_rate(rate);
+}
+
+void EaxVocalMorpherEffect::defer_all(
+    const EaxEaxCall& eax_call)
+{
+    const auto& all = eax_call.get_value<
+        EaxVocalMorpherEffectException,
+        const EAXVOCALMORPHERPROPERTIES
+    >();
+
+    validate_all(all);
+    defer_all(all);
+}
+
+// [[nodiscard]]
+bool EaxVocalMorpherEffect::apply_deferred()
+{
+    if (eax_dirty_flags_ == EaxVocalMorpherEffectDirtyFlags{})
+    {
+        return false;
+    }
+
+    eax_ = eax_d_;
+
+    if (eax_dirty_flags_.ulPhonemeA)
+    {
+        set_efx_phoneme_a();
+    }
+
+    if (eax_dirty_flags_.lPhonemeACoarseTuning)
+    {
+        set_efx_phoneme_a_coarse_tuning();
+    }
+
+    if (eax_dirty_flags_.ulPhonemeB)
+    {
+        set_efx_phoneme_b();
+    }
+
+    if (eax_dirty_flags_.lPhonemeBCoarseTuning)
+    {
+        set_efx_phoneme_b_coarse_tuning();
+    }
+
+    if (eax_dirty_flags_.ulWaveform)
+    {
+        set_efx_waveform();
+    }
+
+    if (eax_dirty_flags_.flRate)
+    {
+        set_efx_rate();
+    }
+
+    eax_dirty_flags_ = EaxVocalMorpherEffectDirtyFlags{};
+
+    return true;
+}
+
+void EaxVocalMorpherEffect::set(const EaxEaxCall& eax_call)
+{
+    switch(eax_call.get_property_id())
+    {
+        case EAXVOCALMORPHER_NONE:
+            break;
+
+        case EAXVOCALMORPHER_ALLPARAMETERS:
+            defer_all(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEA:
+            defer_phoneme_a(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEACOARSETUNING:
+            defer_phoneme_a_coarse_tuning(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEB:
+            defer_phoneme_b(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_PHONEMEBCOARSETUNING:
+            defer_phoneme_b_coarse_tuning(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_WAVEFORM:
+            defer_waveform(eax_call);
+            break;
+
+        case EAXVOCALMORPHER_RATE:
+            defer_rate(eax_call);
+            break;
+
+        default:
+            throw EaxVocalMorpherEffectException{"Unsupported property id."};
+    }
+}
+
+} // namespace
+
+
+EaxEffectUPtr eax_create_eax_vocal_morpher_effect()
+{
+    return std::make_unique<EaxVocalMorpherEffect>();
+}
+
+#endif // ALSOFT_EAX

+ 3 - 3
Engine/lib/openal-soft/al/error.cpp

@@ -35,7 +35,7 @@
 #include "AL/al.h"
 #include "AL/alc.h"
 
-#include "alcontext.h"
+#include "alc/context.h"
 #include "almalloc.h"
 #include "core/except.h"
 #include "core/logging.h"
@@ -85,9 +85,9 @@ AL_API ALenum AL_APIENTRY alGetError(void)
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
-    if UNLIKELY(!context)
+    if(unlikely(!context))
     {
-        constexpr ALenum deferror{AL_INVALID_OPERATION};
+        static constexpr ALenum deferror{AL_INVALID_OPERATION};
         WARN("Querying error state on null context (implicitly 0x%04x)\n", deferror);
         if(TrapALError)
         {

+ 26 - 29
Engine/lib/openal-soft/al/event.cpp

@@ -18,24 +18,24 @@
 #include "AL/alc.h"
 
 #include "albyte.h"
-#include "alcontext.h"
+#include "alc/context.h"
+#include "alc/effects/base.h"
+#include "alc/inprogext.h"
 #include "almalloc.h"
-#include "async_event.h"
+#include "core/async_event.h"
 #include "core/except.h"
 #include "core/logging.h"
-#include "effects/base.h"
-#include "inprogext.h"
+#include "core/voice_change.h"
 #include "opthelpers.h"
 #include "ringbuffer.h"
 #include "threads.h"
-#include "voice_change.h"
 
 
 static int EventThread(ALCcontext *context)
 {
     RingBuffer *ring{context->mAsyncEvents.get()};
     bool quitnow{false};
-    while LIKELY(!quitnow)
+    while(likely(!quitnow))
     {
         auto evt_data = ring->getReadVector().first;
         if(evt_data.len == 0)
@@ -54,10 +54,10 @@ static int EventThread(ALCcontext *context)
             al::destroy_at(evt_ptr);
             ring->readAdvance(1);
 
-            quitnow = evt.EnumType == EventType_KillThread;
-            if UNLIKELY(quitnow) break;
+            quitnow = evt.EnumType == AsyncEvent::KillThread;
+            if(unlikely(quitnow)) break;
 
-            if(evt.EnumType == EventType_ReleaseEffectState)
+            if(evt.EnumType == AsyncEvent::ReleaseEffectState)
             {
                 evt.u.mEffectState->release();
                 continue;
@@ -66,41 +66,38 @@ static int EventThread(ALCcontext *context)
             uint enabledevts{context->mEnabledEvts.load(std::memory_order_acquire)};
             if(!context->mEventCb) continue;
 
-            if(evt.EnumType == EventType_SourceStateChange)
+            if(evt.EnumType == AsyncEvent::SourceStateChange)
             {
-                if(!(enabledevts&EventType_SourceStateChange))
+                if(!(enabledevts&AsyncEvent::SourceStateChange))
                     continue;
                 ALuint state{};
                 std::string msg{"Source ID " + std::to_string(evt.u.srcstate.id)};
                 msg += " state has changed to ";
                 switch(evt.u.srcstate.state)
                 {
-                case VChangeState::Reset:
+                case AsyncEvent::SrcState::Reset:
                     msg += "AL_INITIAL";
                     state = AL_INITIAL;
                     break;
-                case VChangeState::Stop:
+                case AsyncEvent::SrcState::Stop:
                     msg += "AL_STOPPED";
                     state = AL_STOPPED;
                     break;
-                case VChangeState::Play:
+                case AsyncEvent::SrcState::Play:
                     msg += "AL_PLAYING";
                     state = AL_PLAYING;
                     break;
-                case VChangeState::Pause:
+                case AsyncEvent::SrcState::Pause:
                     msg += "AL_PAUSED";
                     state = AL_PAUSED;
                     break;
-                /* Shouldn't happen */
-                case VChangeState::Restart:
-                    break;
                 }
                 context->mEventCb(AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT, evt.u.srcstate.id,
                     state, static_cast<ALsizei>(msg.length()), msg.c_str(), context->mEventParam);
             }
-            else if(evt.EnumType == EventType_BufferCompleted)
+            else if(evt.EnumType == AsyncEvent::BufferCompleted)
             {
-                if(!(enabledevts&EventType_BufferCompleted))
+                if(!(enabledevts&AsyncEvent::BufferCompleted))
                     continue;
                 std::string msg{std::to_string(evt.u.bufcomp.count)};
                 if(evt.u.bufcomp.count == 1) msg += " buffer completed";
@@ -109,9 +106,9 @@ static int EventThread(ALCcontext *context)
                     evt.u.bufcomp.count, static_cast<ALsizei>(msg.length()), msg.c_str(),
                     context->mEventParam);
             }
-            else if(evt.EnumType == EventType_Disconnected)
+            else if(evt.EnumType == AsyncEvent::Disconnected)
             {
-                if(!(enabledevts&EventType_Disconnected))
+                if(!(enabledevts&AsyncEvent::Disconnected))
                     continue;
                 context->mEventCb(AL_EVENT_TYPE_DISCONNECTED_SOFT, 0, 0,
                     static_cast<ALsizei>(strlen(evt.u.disconnect.msg)), evt.u.disconnect.msg,
@@ -146,7 +143,7 @@ void StopEventThrd(ALCcontext *ctx)
             evt_data = ring->getWriteVector().first;
         } while(evt_data.len == 0);
     }
-    ::new(evt_data.buf) AsyncEvent{EventType_KillThread};
+    al::construct_at(reinterpret_cast<AsyncEvent*>(evt_data.buf), AsyncEvent::KillThread);
     ring->writeAdvance(1);
 
     ctx->mEventSem.post();
@@ -158,7 +155,7 @@ AL_API void AL_APIENTRY alEventControlSOFT(ALsizei count, const ALenum *types, A
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
-    if UNLIKELY(!context) return;
+    if(unlikely(!context)) return;
 
     if(count < 0) context->setError(AL_INVALID_VALUE, "Controlling %d events", count);
     if(count <= 0) return;
@@ -170,11 +167,11 @@ START_API_FUNC
         [&flags](ALenum type) noexcept -> bool
         {
             if(type == AL_EVENT_TYPE_BUFFER_COMPLETED_SOFT)
-                flags |= EventType_BufferCompleted;
+                flags |= AsyncEvent::BufferCompleted;
             else if(type == AL_EVENT_TYPE_SOURCE_STATE_CHANGED_SOFT)
-                flags |= EventType_SourceStateChange;
+                flags |= AsyncEvent::SourceStateChange;
             else if(type == AL_EVENT_TYPE_DISCONNECTED_SOFT)
-                flags |= EventType_Disconnected;
+                flags |= AsyncEvent::Disconnected;
             else
                 return false;
             return true;
@@ -204,7 +201,7 @@ START_API_FUNC
         /* Wait to ensure the event handler sees the changed flags before
          * returning.
          */
-        std::lock_guard<std::mutex>{context->mEventCbLock};
+        std::lock_guard<std::mutex> _{context->mEventCbLock};
     }
 }
 END_API_FUNC
@@ -213,7 +210,7 @@ AL_API void AL_APIENTRY alEventCallbackSOFT(ALEVENTPROCSOFT callback, void *user
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
-    if UNLIKELY(!context) return;
+    if(unlikely(!context)) return;
 
     std::lock_guard<std::mutex> _{context->mPropLock};
     std::lock_guard<std::mutex> __{context->mEventCbLock};

+ 2 - 2
Engine/lib/openal-soft/al/extension.cpp

@@ -27,7 +27,7 @@
 #include "AL/al.h"
 #include "AL/alc.h"
 
-#include "alcontext.h"
+#include "alc/context.h"
 #include "alstring.h"
 #include "core/except.h"
 #include "opthelpers.h"
@@ -37,7 +37,7 @@ AL_API ALboolean AL_APIENTRY alIsExtensionPresent(const ALchar *extName)
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
-    if UNLIKELY(!context) return AL_FALSE;
+    if(unlikely(!context)) return AL_FALSE;
 
     if(!extName)
         SETERR_RETURN(context, AL_INVALID_VALUE, AL_FALSE, "NULL pointer");

+ 24 - 22
Engine/lib/openal-soft/al/filter.cpp

@@ -37,8 +37,8 @@
 #include "AL/efx.h"
 
 #include "albit.h"
-#include "alcmain.h"
-#include "alcontext.h"
+#include "alc/context.h"
+#include "alc/device.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "core/except.h"
@@ -52,7 +52,11 @@ class filter_exception final : public al::base_exception {
     ALenum mErrorCode;
 
 public:
+#ifdef __USE_MINGW_ANSI_STDIO
+    [[gnu::format(gnu_printf, 3, 4)]]
+#else
     [[gnu::format(printf, 3, 4)]]
+#endif
     filter_exception(ALenum code, const char *msg, ...) : mErrorCode{code}
     {
         std::va_list args;
@@ -63,8 +67,6 @@ public:
     ALenum errorCode() const noexcept { return mErrorCode; }
 };
 
-#define FILTER_MIN_GAIN 0.0f
-#define FILTER_MAX_GAIN 4.0f /* +12dB */
 
 #define DEFINE_ALFILTER_VTABLE(T)                                  \
 const ALfilter::Vtable T##_vtable = {                              \
@@ -84,7 +86,7 @@ void ALlowpass_setParamf(ALfilter *filter, ALenum param, float val)
     switch(param)
     {
     case AL_LOWPASS_GAIN:
-        if(!(val >= FILTER_MIN_GAIN && val <= FILTER_MAX_GAIN))
+        if(!(val >= AL_LOWPASS_MIN_GAIN && val <= AL_LOWPASS_MAX_GAIN))
             throw filter_exception{AL_INVALID_VALUE, "Low-pass gain %f out of range", val};
         filter->Gain = val;
         break;
@@ -143,7 +145,7 @@ void ALhighpass_setParamf(ALfilter *filter, ALenum param, float val)
     switch(param)
     {
     case AL_HIGHPASS_GAIN:
-        if(!(val >= FILTER_MIN_GAIN && val <= FILTER_MAX_GAIN))
+        if(!(val >= AL_HIGHPASS_MIN_GAIN && val <= AL_HIGHPASS_MAX_GAIN))
             throw filter_exception{AL_INVALID_VALUE, "High-pass gain %f out of range", val};
         filter->Gain = val;
         break;
@@ -202,7 +204,7 @@ void ALbandpass_setParamf(ALfilter *filter, ALenum param, float val)
     switch(param)
     {
     case AL_BANDPASS_GAIN:
-        if(!(val >= FILTER_MIN_GAIN && val <= FILTER_MAX_GAIN))
+        if(!(val >= AL_BANDPASS_MIN_GAIN && val <= AL_BANDPASS_MAX_GAIN))
             throw filter_exception{AL_INVALID_VALUE, "Band-pass gain %f out of range", val};
         filter->Gain = val;
         break;
@@ -351,12 +353,12 @@ ALfilter *AllocFilter(ALCdevice *device)
 {
     auto sublist = std::find_if(device->FilterList.begin(), device->FilterList.end(),
         [](const FilterSubList &entry) noexcept -> bool
-        { return entry.FreeMask != 0; }
-    );
+        { return entry.FreeMask != 0; });
     auto lidx = static_cast<ALuint>(std::distance(device->FilterList.begin(), sublist));
     auto slidx = static_cast<ALuint>(al::countr_zero(sublist->FreeMask));
+    ASSUME(slidx < 64);
 
-    ALfilter *filter{::new(sublist->Filters + slidx) ALfilter{}};
+    ALfilter *filter{al::construct_at(sublist->Filters + slidx)};
     InitFilterParams(filter, AL_FILTER_NULL);
 
     /* Add 1 to avoid filter ID 0. */
@@ -404,8 +406,8 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Generating %d filters", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
-    std::lock_guard<std::mutex> _{device->EffectLock};
+    ALCdevice *device{context->mALDevice.get()};
+    std::lock_guard<std::mutex> _{device->FilterLock};
     if(!EnsureFilters(device, static_cast<ALuint>(n)))
     {
         context->setError(AL_OUT_OF_MEMORY, "Failed to allocate %d filter%s", n, (n==1)?"":"s");
@@ -444,7 +446,7 @@ START_API_FUNC
         context->setError(AL_INVALID_VALUE, "Deleting %d filters", n);
     if UNLIKELY(n <= 0) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     /* First try to find any filters that are invalid. */
@@ -475,7 +477,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if LIKELY(context)
     {
-        ALCdevice *device{context->mDevice.get()};
+        ALCdevice *device{context->mALDevice.get()};
         std::lock_guard<std::mutex> _{device->FilterLock};
         if(!filter || LookupFilter(device, filter))
             return AL_TRUE;
@@ -491,7 +493,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     ALfilter *alfilt{LookupFilter(device, filter)};
@@ -532,7 +534,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     ALfilter *alfilt{LookupFilter(device, filter)};
@@ -555,7 +557,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     ALfilter *alfilt{LookupFilter(device, filter)};
@@ -578,7 +580,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     ALfilter *alfilt{LookupFilter(device, filter)};
@@ -601,7 +603,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     const ALfilter *alfilt{LookupFilter(device, filter)};
@@ -636,7 +638,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     const ALfilter *alfilt{LookupFilter(device, filter)};
@@ -659,7 +661,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     const ALfilter *alfilt{LookupFilter(device, filter)};
@@ -682,7 +684,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    ALCdevice *device{context->mDevice.get()};
+    ALCdevice *device{context->mALDevice.get()};
     std::lock_guard<std::mutex> _{device->FilterLock};
 
     const ALfilter *alfilt{LookupFilter(device, filter)};

+ 1 - 1
Engine/lib/openal-soft/al/filter.h

@@ -1,13 +1,13 @@
 #ifndef AL_FILTER_H
 #define AL_FILTER_H
 
+
 #include "AL/al.h"
 #include "AL/alc.h"
 #include "AL/alext.h"
 
 #include "almalloc.h"
 
-
 #define LOWPASSFREQREF  5000.0f
 #define HIGHPASSFREQREF  250.0f
 

+ 45 - 48
Engine/lib/openal-soft/al/listener.cpp

@@ -29,20 +29,53 @@
 #include "AL/alc.h"
 #include "AL/efx.h"
 
-#include "alcontext.h"
+#include "alc/context.h"
 #include "almalloc.h"
 #include "atomic.h"
 #include "core/except.h"
 #include "opthelpers.h"
 
 
-#define DO_UPDATEPROPS() do {                                                 \
-    if(!context->mDeferUpdates.load(std::memory_order_acquire))               \
-        UpdateListenerProps(context.get());                                   \
-    else                                                                      \
-        listener.PropsClean.clear(std::memory_order_release);                 \
-} while(0)
+namespace {
 
+inline void UpdateProps(ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+        UpdateContextProps(context);
+        return;
+    }
+    context->mPropsDirty = true;
+}
+
+#ifdef ALSOFT_EAX
+inline void CommitAndUpdateProps(ALCcontext *context)
+{
+    if(!context->mDeferUpdates)
+    {
+        if(context->has_eax())
+        {
+            context->mHoldUpdates.store(true, std::memory_order_release);
+            while((context->mUpdateCount.load(std::memory_order_acquire)&1) != 0) {
+                /* busy-wait */
+            }
+
+            context->eax_commit_and_update_sources();
+        }
+        UpdateContextProps(context);
+        context->mHoldUpdates.store(false, std::memory_order_release);
+        return;
+    }
+    context->mPropsDirty = true;
+}
+
+#else
+
+inline void CommitAndUpdateProps(ALCcontext *context)
+{ UpdateProps(context); }
+#endif
+
+} // namespace
 
 AL_API void AL_APIENTRY alListenerf(ALenum param, ALfloat value)
 START_API_FUNC
@@ -58,14 +91,14 @@ START_API_FUNC
         if(!(value >= 0.0f && std::isfinite(value)))
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Listener gain out of range");
         listener.Gain = value;
-        DO_UPDATEPROPS();
+        UpdateProps(context.get());
         break;
 
     case AL_METERS_PER_UNIT:
         if(!(value >= AL_MIN_METERS_PER_UNIT && value <= AL_MAX_METERS_PER_UNIT))
             SETERR_RETURN(context, AL_INVALID_VALUE,, "Listener meters per unit out of range");
         listener.mMetersPerUnit = value;
-        DO_UPDATEPROPS();
+        UpdateProps(context.get());
         break;
 
     default:
@@ -90,7 +123,7 @@ START_API_FUNC
         listener.Position[0] = value1;
         listener.Position[1] = value2;
         listener.Position[2] = value3;
-        DO_UPDATEPROPS();
+        CommitAndUpdateProps(context.get());
         break;
 
     case AL_VELOCITY:
@@ -99,7 +132,7 @@ START_API_FUNC
         listener.Velocity[0] = value1;
         listener.Velocity[1] = value2;
         listener.Velocity[2] = value3;
-        DO_UPDATEPROPS();
+        CommitAndUpdateProps(context.get());
         break;
 
     default:
@@ -146,7 +179,7 @@ START_API_FUNC
         listener.OrientUp[0] = values[3];
         listener.OrientUp[1] = values[4];
         listener.OrientUp[2] = values[5];
-        DO_UPDATEPROPS();
+        CommitAndUpdateProps(context.get());
         break;
 
     default:
@@ -414,39 +447,3 @@ START_API_FUNC
     }
 }
 END_API_FUNC
-
-
-void UpdateListenerProps(ALCcontext *context)
-{
-    /* Get an unused proprty container, or allocate a new one as needed. */
-    ListenerProps *props{context->mFreeListenerProps.load(std::memory_order_acquire)};
-    if(!props)
-        props = new ListenerProps{};
-    else
-    {
-        ListenerProps *next;
-        do {
-            next = props->next.load(std::memory_order_relaxed);
-        } while(context->mFreeListenerProps.compare_exchange_weak(props, next,
-                std::memory_order_seq_cst, std::memory_order_acquire) == 0);
-    }
-
-    /* Copy in current property values. */
-    ALlistener &listener = context->mListener;
-    props->Position = listener.Position;
-    props->Velocity = listener.Velocity;
-    props->OrientAt = listener.OrientAt;
-    props->OrientUp = listener.OrientUp;
-    props->Gain = listener.Gain;
-    props->MetersPerUnit = listener.mMetersPerUnit;
-
-    /* Set the new container for updating internal parameters. */
-    props = context->mParams.ListenerUpdate.exchange(props, std::memory_order_acq_rel);
-    if(props)
-    {
-        /* If there was an unused update container, put it back in the
-         * freelist.
-         */
-        AtomicReplaceHead(context->mFreeListenerProps, props);
-    }
-}

+ 0 - 7
Engine/lib/openal-soft/al/listener.h

@@ -2,7 +2,6 @@
 #define AL_LISTENER_H
 
 #include <array>
-#include <atomic>
 
 #include "AL/al.h"
 #include "AL/alc.h"
@@ -19,13 +18,7 @@ struct ALlistener {
     float Gain{1.0f};
     float mMetersPerUnit{AL_DEFAULT_METERS_PER_UNIT};
 
-    std::atomic_flag PropsClean;
-
-    ALlistener() { PropsClean.test_and_set(std::memory_order_relaxed); }
-
     DISABLE_ALLOC()
 };
 
-void UpdateListenerProps(ALCcontext *context);
-
 #endif

File diff suppressed because it is too large
+ 302 - 196
Engine/lib/openal-soft/al/source.cpp


+ 681 - 6
Engine/lib/openal-soft/al/source.h

@@ -11,19 +11,31 @@
 #include "AL/al.h"
 #include "AL/alc.h"
 
-#include "alcontext.h"
+#include "alc/alu.h"
+#include "alc/context.h"
+#include "alc/inprogext.h"
 #include "aldeque.h"
 #include "almalloc.h"
 #include "alnumeric.h"
-#include "alu.h"
-#include "math_defs.h"
+#include "atomic.h"
+#include "core/voice.h"
 #include "vector.h"
-#include "voice.h"
+
+#ifdef ALSOFT_EAX
+#include "eax_eax_call.h"
+#include "eax_fx_slot_index.h"
+#include "eax_utils.h"
+#endif // ALSOFT_EAX
 
 struct ALbuffer;
 struct ALeffectslot;
 
 
+enum class SourceStereo : bool {
+    Normal = AL_NORMAL_SOFT,
+    Enhanced = AL_SUPER_STEREO_SOFT
+};
+
 #define DEFAULT_SENDS  2
 
 #define INVALID_VOICE_IDX static_cast<ALuint>(-1)
@@ -35,6 +47,70 @@ struct ALbufferQueueItem : public VoiceBufferItem {
 };
 
 
+#ifdef ALSOFT_EAX
+using EaxSourceSourceFilterDirtyFlagsValue = std::uint_least16_t;
+
+struct EaxSourceSourceFilterDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+	EaxSourceSourceFilterDirtyFlagsValue lDirect : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lDirectHF : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lRoom : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lRoomHF : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lObstruction : 1;
+	EaxSourceSourceFilterDirtyFlagsValue flObstructionLFRatio : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lOcclusion : 1;
+	EaxSourceSourceFilterDirtyFlagsValue flOcclusionLFRatio : 1;
+	EaxSourceSourceFilterDirtyFlagsValue flOcclusionRoomRatio : 1;
+	EaxSourceSourceFilterDirtyFlagsValue flOcclusionDirectRatio : 1;
+	EaxSourceSourceFilterDirtyFlagsValue lExclusion : 1;
+	EaxSourceSourceFilterDirtyFlagsValue flExclusionLFRatio : 1;
+}; // EaxSourceSourceFilterDirtyFlags
+
+
+using EaxSourceSourceMiscDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxSourceSourceMiscDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+	EaxSourceSourceMiscDirtyFlagsValue lOutsideVolumeHF : 1;
+	EaxSourceSourceMiscDirtyFlagsValue flDopplerFactor : 1;
+	EaxSourceSourceMiscDirtyFlagsValue flRolloffFactor : 1;
+	EaxSourceSourceMiscDirtyFlagsValue flRoomRolloffFactor : 1;
+	EaxSourceSourceMiscDirtyFlagsValue flAirAbsorptionFactor : 1;
+	EaxSourceSourceMiscDirtyFlagsValue ulFlags : 1;
+	EaxSourceSourceMiscDirtyFlagsValue flMacroFXFactor : 1;
+	EaxSourceSourceMiscDirtyFlagsValue speaker_levels : 1;
+}; // EaxSourceSourceMiscDirtyFlags
+
+
+using EaxSourceSendDirtyFlagsValue = std::uint_least8_t;
+
+struct EaxSourceSendDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+	EaxSourceSendDirtyFlagsValue lSend : 1;
+	EaxSourceSendDirtyFlagsValue lSendHF : 1;
+	EaxSourceSendDirtyFlagsValue lOcclusion : 1;
+	EaxSourceSendDirtyFlagsValue flOcclusionLFRatio : 1;
+	EaxSourceSendDirtyFlagsValue flOcclusionRoomRatio : 1;
+	EaxSourceSendDirtyFlagsValue flOcclusionDirectRatio : 1;
+	EaxSourceSendDirtyFlagsValue lExclusion : 1;
+	EaxSourceSendDirtyFlagsValue flExclusionLFRatio : 1;
+}; // EaxSourceSendDirtyFlags
+
+
+struct EaxSourceSendsDirtyFlags
+{
+    using EaxIsBitFieldStruct = bool;
+
+	EaxSourceSendDirtyFlags sends[EAX_MAX_FXSLOTS];
+}; // EaxSourceSendsDirtyFlags
+#endif // ALSOFT_EAX
+
 struct ALsource {
     /** Source properties. */
     float Pitch{1.0f};
@@ -47,6 +123,11 @@ struct ALsource {
     float RefDistance{1.0f};
     float MaxDistance{std::numeric_limits<float>::max()};
     float RolloffFactor{1.0f};
+#ifdef ALSOFT_EAX
+    // For EAXSOURCE_ROLLOFFFACTOR, which is distinct from and added to
+    // AL_ROLLOFF_FACTOR
+    float RolloffFactor2{0.0f};
+#endif
     std::array<float,3> Position{{0.0f, 0.0f, 0.0f}};
     std::array<float,3> Velocity{{0.0f, 0.0f, 0.0f}};
     std::array<float,3> Direction{{0.0f, 0.0f, 0.0f}};
@@ -58,6 +139,7 @@ struct ALsource {
     Resampler mResampler{ResamplerDefault};
     DirectMode DirectChannels{DirectMode::Off};
     SpatializeMode mSpatialize{SpatializeMode::Auto};
+    SourceStereo mStereoMode{SourceStereo::Normal};
 
     bool DryGainHFAuto{true};
     bool WetGainAuto{true};
@@ -71,9 +153,10 @@ struct ALsource {
     /* NOTE: Stereo pan angles are specified in radians, counter-clockwise
      * rather than clockwise.
      */
-    std::array<float,2> StereoPan{{Deg2Rad( 30.0f), Deg2Rad(-30.0f)}};
+    std::array<float,2> StereoPan{{al::numbers::pi_v<float>/6.0f, -al::numbers::pi_v<float>/6.0f}};
 
     float Radius{0.0f};
+    float EnhWidth{0.593f};
 
     /** Direct filter and auxiliary send info. */
     struct {
@@ -109,7 +192,7 @@ struct ALsource {
     /** Source Buffer Queue head. */
     al::deque<ALbufferQueueItem> mQueue;
 
-    std::atomic_flag PropsClean;
+    bool mPropsDirty{true};
 
     /* Index into the context's Voices array. Lazily updated, only checked and
      * reset when looking up the voice.
@@ -127,6 +210,598 @@ struct ALsource {
     ALsource& operator=(const ALsource&) = delete;
 
     DISABLE_ALLOC()
+
+#ifdef ALSOFT_EAX
+public:
+    void eax_initialize(ALCcontext *context) noexcept;
+
+
+    void eax_dispatch(const EaxEaxCall& eax_call)
+    { eax_call.is_get() ? eax_get(eax_call) : eax_set(eax_call); }
+
+
+    void eax_update_filters();
+
+    void eax_update(
+        EaxContextSharedDirtyFlags dirty_flags);
+
+    void eax_commit() { eax_apply_deferred(); }
+    void eax_commit_and_update();
+
+    bool eax_is_initialized() const noexcept { return eax_al_context_; }
+
+
+    static ALsource* eax_lookup_source(
+        ALCcontext& al_context,
+        ALuint source_id) noexcept;
+
+
+private:
+    static constexpr auto eax_max_speakers = 9;
+
+
+    using EaxActiveFxSlots = std::array<bool, EAX_MAX_FXSLOTS>;
+    using EaxSpeakerLevels = std::array<long, eax_max_speakers>;
+
+    struct Eax
+    {
+        using Sends = std::array<EAXSOURCEALLSENDPROPERTIES, EAX_MAX_FXSLOTS>;
+
+        EAX50ACTIVEFXSLOTS active_fx_slots{};
+        EAX50SOURCEPROPERTIES source{};
+        Sends sends{};
+        EaxSpeakerLevels speaker_levels{};
+    }; // Eax
+
+
+    bool eax_uses_primary_id_{};
+    bool eax_has_active_fx_slots_{};
+    bool eax_are_active_fx_slots_dirty_{};
+
+    ALCcontext* eax_al_context_{};
+
+    EAXBUFFER_REVERBPROPERTIES eax1_{};
+    Eax eax_{};
+    Eax eax_d_{};
+    EaxActiveFxSlots eax_active_fx_slots_{};
+
+    EaxSourceSendsDirtyFlags eax_sends_dirty_flags_{};
+    EaxSourceSourceFilterDirtyFlags eax_source_dirty_filter_flags_{};
+    EaxSourceSourceMiscDirtyFlags eax_source_dirty_misc_flags_{};
+
+
+    [[noreturn]]
+    static void eax_fail(
+        const char* message);
+
+
+    void eax_set_source_defaults() noexcept;
+    void eax_set_active_fx_slots_defaults() noexcept;
+    void eax_set_send_defaults(EAXSOURCEALLSENDPROPERTIES& eax_send) noexcept;
+    void eax_set_sends_defaults() noexcept;
+    void eax_set_speaker_levels_defaults() noexcept;
+    void eax_set_defaults() noexcept;
+
+
+    static float eax_calculate_dst_occlusion_mb(
+        long src_occlusion_mb,
+        float path_ratio,
+        float lf_ratio) noexcept;
+
+    EaxAlLowPassParam eax_create_direct_filter_param() const noexcept;
+
+    EaxAlLowPassParam eax_create_room_filter_param(
+        const ALeffectslot& fx_slot,
+        const EAXSOURCEALLSENDPROPERTIES& send) const noexcept;
+
+    void eax_set_fx_slots();
+
+    void eax_initialize_fx_slots();
+
+    void eax_update_direct_filter_internal();
+
+    void eax_update_room_filters_internal();
+
+    void eax_update_filters_internal();
+
+    void eax_update_primary_fx_slot_id();
+
+
+    void eax_defer_active_fx_slots(
+        const EaxEaxCall& eax_call);
+
+
+    static const char* eax_get_exclusion_name() noexcept;
+
+    static const char* eax_get_exclusion_lf_ratio_name() noexcept;
+
+
+    static const char* eax_get_occlusion_name() noexcept;
+
+    static const char* eax_get_occlusion_lf_ratio_name() noexcept;
+
+    static const char* eax_get_occlusion_direct_ratio_name() noexcept;
+
+    static const char* eax_get_occlusion_room_ratio_name() noexcept;
+
+
+    static void eax1_validate_reverb_mix(float reverb_mix);
+
+    static void eax_validate_send_receiving_fx_slot_guid(
+        const GUID& guidReceivingFXSlotID);
+
+    static void eax_validate_send_send(
+        long lSend);
+
+    static void eax_validate_send_send_hf(
+        long lSendHF);
+
+    static void eax_validate_send_occlusion(
+        long lOcclusion);
+
+    static void eax_validate_send_occlusion_lf_ratio(
+        float flOcclusionLFRatio);
+
+    static void eax_validate_send_occlusion_room_ratio(
+        float flOcclusionRoomRatio);
+
+    static void eax_validate_send_occlusion_direct_ratio(
+        float flOcclusionDirectRatio);
+
+    static void eax_validate_send_exclusion(
+        long lExclusion);
+
+    static void eax_validate_send_exclusion_lf_ratio(
+        float flExclusionLFRatio);
+
+    static void eax_validate_send(
+        const EAXSOURCESENDPROPERTIES& all);
+
+    static void eax_validate_send_exclusion_all(
+        const EAXSOURCEEXCLUSIONSENDPROPERTIES& all);
+
+    static void eax_validate_send_occlusion_all(
+        const EAXSOURCEOCCLUSIONSENDPROPERTIES& all);
+
+    static void eax_validate_send_all(
+        const EAXSOURCEALLSENDPROPERTIES& all);
+
+
+    static EaxFxSlotIndexValue eax_get_send_index(
+        const GUID& send_guid);
+
+
+    void eax_defer_send_send(
+        long lSend,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_send_hf(
+        long lSendHF,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_occlusion(
+        long lOcclusion,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_occlusion_lf_ratio(
+        float flOcclusionLFRatio,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_occlusion_room_ratio(
+        float flOcclusionRoomRatio,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_occlusion_direct_ratio(
+        float flOcclusionDirectRatio,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_exclusion(
+        long lExclusion,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_exclusion_lf_ratio(
+        float flExclusionLFRatio,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send(
+        const EAXSOURCESENDPROPERTIES& all,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_exclusion_all(
+        const EAXSOURCEEXCLUSIONSENDPROPERTIES& all,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_occlusion_all(
+        const EAXSOURCEOCCLUSIONSENDPROPERTIES& all,
+        EaxFxSlotIndexValue index);
+
+    void eax_defer_send_all(
+        const EAXSOURCEALLSENDPROPERTIES& all,
+        EaxFxSlotIndexValue index);
+
+
+    void eax_defer_send(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_send_exclusion_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_send_occlusion_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_send_all(
+        const EaxEaxCall& eax_call);
+
+
+    static void eax_validate_source_direct(
+        long direct);
+
+    static void eax_validate_source_direct_hf(
+        long direct_hf);
+
+    static void eax_validate_source_room(
+        long room);
+
+    static void eax_validate_source_room_hf(
+        long room_hf);
+
+    static void eax_validate_source_obstruction(
+        long obstruction);
+
+    static void eax_validate_source_obstruction_lf_ratio(
+        float obstruction_lf_ratio);
+
+    static void eax_validate_source_occlusion(
+        long occlusion);
+
+    static void eax_validate_source_occlusion_lf_ratio(
+        float occlusion_lf_ratio);
+
+    static void eax_validate_source_occlusion_room_ratio(
+        float occlusion_room_ratio);
+
+    static void eax_validate_source_occlusion_direct_ratio(
+        float occlusion_direct_ratio);
+
+    static void eax_validate_source_exclusion(
+        long exclusion);
+
+    static void eax_validate_source_exclusion_lf_ratio(
+        float exclusion_lf_ratio);
+
+    static void eax_validate_source_outside_volume_hf(
+        long outside_volume_hf);
+
+    static void eax_validate_source_doppler_factor(
+        float doppler_factor);
+
+    static void eax_validate_source_rolloff_factor(
+        float rolloff_factor);
+
+    static void eax_validate_source_room_rolloff_factor(
+        float room_rolloff_factor);
+
+    static void eax_validate_source_air_absorption_factor(
+        float air_absorption_factor);
+
+    static void eax_validate_source_flags(
+        unsigned long flags,
+        int eax_version);
+
+    static void eax_validate_source_macro_fx_factor(
+        float macro_fx_factor);
+
+    static void eax_validate_source_2d_all(
+        const EAXSOURCE2DPROPERTIES& all,
+        int eax_version);
+
+    static void eax_validate_source_obstruction_all(
+        const EAXOBSTRUCTIONPROPERTIES& all);
+
+    static void eax_validate_source_exclusion_all(
+        const EAXEXCLUSIONPROPERTIES& all);
+
+    static void eax_validate_source_occlusion_all(
+        const EAXOCCLUSIONPROPERTIES& all);
+
+    static void eax_validate_source_all(
+        const EAX20BUFFERPROPERTIES& all,
+        int eax_version);
+
+    static void eax_validate_source_all(
+        const EAX30SOURCEPROPERTIES& all,
+        int eax_version);
+
+    static void eax_validate_source_all(
+        const EAX50SOURCEPROPERTIES& all,
+        int eax_version);
+
+    static void eax_validate_source_speaker_id(
+        long speaker_id);
+
+    static void eax_validate_source_speaker_level(
+        long speaker_level);
+
+    static void eax_validate_source_speaker_level_all(
+        const EAXSPEAKERLEVELPROPERTIES& all);
+
+
+    void eax_defer_source_direct(
+        long lDirect);
+
+    void eax_defer_source_direct_hf(
+        long lDirectHF);
+
+    void eax_defer_source_room(
+        long lRoom);
+
+    void eax_defer_source_room_hf(
+        long lRoomHF);
+
+    void eax_defer_source_obstruction(
+        long lObstruction);
+
+    void eax_defer_source_obstruction_lf_ratio(
+        float flObstructionLFRatio);
+
+    void eax_defer_source_occlusion(
+        long lOcclusion);
+
+    void eax_defer_source_occlusion_lf_ratio(
+        float flOcclusionLFRatio);
+
+    void eax_defer_source_occlusion_room_ratio(
+        float flOcclusionRoomRatio);
+
+    void eax_defer_source_occlusion_direct_ratio(
+        float flOcclusionDirectRatio);
+
+    void eax_defer_source_exclusion(
+        long lExclusion);
+
+    void eax_defer_source_exclusion_lf_ratio(
+        float flExclusionLFRatio);
+
+    void eax_defer_source_outside_volume_hf(
+        long lOutsideVolumeHF);
+
+    void eax_defer_source_doppler_factor(
+        float flDopplerFactor);
+
+    void eax_defer_source_rolloff_factor(
+        float flRolloffFactor);
+
+    void eax_defer_source_room_rolloff_factor(
+        float flRoomRolloffFactor);
+
+    void eax_defer_source_air_absorption_factor(
+        float flAirAbsorptionFactor);
+
+    void eax_defer_source_flags(
+        unsigned long ulFlags);
+
+    void eax_defer_source_macro_fx_factor(
+        float flMacroFXFactor);
+
+    void eax_defer_source_2d_all(
+        const EAXSOURCE2DPROPERTIES& all);
+
+    void eax_defer_source_obstruction_all(
+        const EAXOBSTRUCTIONPROPERTIES& all);
+
+    void eax_defer_source_exclusion_all(
+        const EAXEXCLUSIONPROPERTIES& all);
+
+    void eax_defer_source_occlusion_all(
+        const EAXOCCLUSIONPROPERTIES& all);
+
+    void eax_defer_source_all(
+        const EAX20BUFFERPROPERTIES& all);
+
+    void eax_defer_source_all(
+        const EAX30SOURCEPROPERTIES& all);
+
+    void eax_defer_source_all(
+        const EAX50SOURCEPROPERTIES& all);
+
+    void eax_defer_source_speaker_level_all(
+        const EAXSPEAKERLEVELPROPERTIES& all);
+
+
+    void eax_defer_source_direct(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_direct_hf(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_room(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_room_hf(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_obstruction(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_obstruction_lf_ratio(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_occlusion(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_occlusion_lf_ratio(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_occlusion_room_ratio(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_occlusion_direct_ratio(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_exclusion(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_exclusion_lf_ratio(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_outside_volume_hf(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_doppler_factor(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_rolloff_factor(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_room_rolloff_factor(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_air_absorption_factor(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_flags(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_macro_fx_factor(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_2d_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_obstruction_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_exclusion_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_occlusion_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_defer_source_speaker_level_all(
+        const EaxEaxCall& eax_call);
+
+
+    void eax_set_outside_volume_hf();
+
+    void eax_set_doppler_factor();
+
+    void eax_set_rolloff_factor();
+
+    void eax_set_room_rolloff_factor();
+
+    void eax_set_air_absorption_factor();
+
+
+    void eax_set_direct_hf_auto_flag();
+
+    void eax_set_room_auto_flag();
+
+    void eax_set_room_hf_auto_flag();
+
+    void eax_set_flags();
+
+
+    void eax_set_macro_fx_factor();
+
+    void eax_set_speaker_levels();
+
+
+    void eax1_set_efx();
+    void eax1_set_reverb_mix(const EaxEaxCall& eax_call);
+    void eax1_set(const EaxEaxCall& eax_call);
+
+    void eax_apply_deferred();
+
+    void eax_set(
+        const EaxEaxCall& eax_call);
+
+
+    static const GUID& eax_get_send_fx_slot_guid(
+        int eax_version,
+        EaxFxSlotIndexValue fx_slot_index);
+
+    static void eax_copy_send(
+        const EAXSOURCEALLSENDPROPERTIES& src_send,
+        EAXSOURCESENDPROPERTIES& dst_send);
+
+    static void eax_copy_send(
+        const EAXSOURCEALLSENDPROPERTIES& src_send,
+        EAXSOURCEALLSENDPROPERTIES& dst_send);
+
+    static void eax_copy_send(
+        const EAXSOURCEALLSENDPROPERTIES& src_send,
+        EAXSOURCEOCCLUSIONSENDPROPERTIES& dst_send);
+
+    static void eax_copy_send(
+        const EAXSOURCEALLSENDPROPERTIES& src_send,
+        EAXSOURCEEXCLUSIONSENDPROPERTIES& dst_send);
+
+    template<
+        typename TException,
+        typename TSrcSend
+    >
+    void eax_api_get_send_properties(
+        const EaxEaxCall& eax_call) const
+    {
+        const auto eax_version = eax_call.get_version();
+        const auto dst_sends = eax_call.get_values<TException, TSrcSend>();
+        const auto send_count = dst_sends.size();
+
+        for (auto fx_slot_index = EaxFxSlotIndexValue{}; fx_slot_index < send_count; ++fx_slot_index)
+        {
+            auto& dst_send = dst_sends[fx_slot_index];
+            const auto& src_send = eax_.sends[fx_slot_index];
+
+            eax_copy_send(src_send, dst_send);
+
+            dst_send.guidReceivingFXSlotID = eax_get_send_fx_slot_guid(eax_version, fx_slot_index);
+        }
+    }
+
+
+    void eax1_get(const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_v2(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_v3(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_v5(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_obstruction(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_occlusion(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_exclusion(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_active_fx_slot_id(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_all_2d(
+        const EaxEaxCall& eax_call);
+
+    void eax_api_get_source_speaker_level_all(
+        const EaxEaxCall& eax_call);
+
+    void eax_get(
+        const EaxEaxCall& eax_call);
+
+
+    // `alSource3i(source, AL_AUXILIARY_SEND_FILTER, ...)`
+    void eax_set_al_source_send(ALeffectslot *slot, size_t sendidx,
+        const EaxAlLowPassParam &filter);
+#endif // ALSOFT_EAX
 };
 
 void UpdateAllSourceProps(ALCcontext *context);

+ 94 - 25
Engine/lib/openal-soft/al/state.cpp

@@ -24,26 +24,34 @@
 
 #include <atomic>
 #include <cmath>
-#include <cstdlib>
-#include <cstring>
 #include <mutex>
+#include <stdexcept>
+#include <string>
 
 #include "AL/al.h"
 #include "AL/alc.h"
 #include "AL/alext.h"
 
-#include "alcontext.h"
-#include "almalloc.h"
+#include "alc/alu.h"
+#include "alc/context.h"
+#include "alc/inprogext.h"
 #include "alnumeric.h"
-#include "alspan.h"
-#include "alu.h"
+#include "aloptional.h"
 #include "atomic.h"
+#include "core/context.h"
 #include "core/except.h"
-#include "event.h"
-#include "inprogext.h"
+#include "core/mixer/defs.h"
+#include "core/voice.h"
+#include "intrusive_ptr.h"
 #include "opthelpers.h"
 #include "strutils.h"
-#include "voice.h"
+
+#ifdef ALSOFT_EAX
+#include "alc/device.h"
+
+#include "eax_globals.h"
+#include "eax_x_ram.h"
+#endif // ALSOFT_EAX
 
 
 namespace {
@@ -129,7 +137,7 @@ ALenum ALenumFromDistanceModel(DistanceModel model)
 /* WARNING: Non-standard export! Not part of any extension, or exposed in the
  * alcFunctions list.
  */
-extern "C" AL_API const ALchar* AL_APIENTRY alsoft_get_version(void)
+AL_API const ALchar* AL_APIENTRY alsoft_get_version(void)
 START_API_FUNC
 {
     static const auto spoof = al::getenv("ALSOFT_SPOOF_VERSION");
@@ -139,10 +147,10 @@ START_API_FUNC
 END_API_FUNC
 
 #define DO_UPDATEPROPS() do {                                                 \
-    if(!context->mDeferUpdates.load(std::memory_order_acquire))               \
+    if(!context->mDeferUpdates)                                               \
         UpdateContextProps(context.get());                                    \
     else                                                                      \
-        context->mPropsClean.clear(std::memory_order_release);                \
+        context->mPropsDirty = true;                                          \
 } while(0)
 
 
@@ -152,12 +160,18 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    std::lock_guard<std::mutex> _{context->mPropLock};
     switch(capability)
     {
     case AL_SOURCE_DISTANCE_MODEL:
-        context->mSourceDistanceModel = true;
-        DO_UPDATEPROPS();
+        {
+            std::lock_guard<std::mutex> _{context->mPropLock};
+            context->mSourceDistanceModel = true;
+            DO_UPDATEPROPS();
+        }
+        break;
+
+    case AL_STOP_SOURCES_ON_DISCONNECT_SOFT:
+        context->setError(AL_INVALID_OPERATION, "Re-enabling AL_STOP_SOURCES_ON_DISCONNECT_SOFT not yet supported");
         break;
 
     default:
@@ -172,12 +186,18 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
-    std::lock_guard<std::mutex> _{context->mPropLock};
     switch(capability)
     {
     case AL_SOURCE_DISTANCE_MODEL:
-        context->mSourceDistanceModel = false;
-        DO_UPDATEPROPS();
+        {
+            std::lock_guard<std::mutex> _{context->mPropLock};
+            context->mSourceDistanceModel = false;
+            DO_UPDATEPROPS();
+        }
+        break;
+
+    case AL_STOP_SOURCES_ON_DISCONNECT_SOFT:
+        context->mStopVoicesOnDisconnect = false;
         break;
 
     default:
@@ -200,6 +220,10 @@ START_API_FUNC
         value = context->mSourceDistanceModel ? AL_TRUE : AL_FALSE;
         break;
 
+    case AL_STOP_SOURCES_ON_DISCONNECT_SOFT:
+        value = context->mStopVoicesOnDisconnect ? AL_TRUE : AL_FALSE;
+        break;
+
     default:
         context->setError(AL_INVALID_VALUE, "Invalid is enabled property 0x%04x", capability);
     }
@@ -239,7 +263,7 @@ START_API_FUNC
         break;
 
     case AL_DEFERRED_UPDATES_SOFT:
-        if(context->mDeferUpdates.load(std::memory_order_acquire))
+        if(context->mDeferUpdates)
             value = AL_TRUE;
         break;
 
@@ -292,7 +316,7 @@ START_API_FUNC
         break;
 
     case AL_DEFERRED_UPDATES_SOFT:
-        if(context->mDeferUpdates.load(std::memory_order_acquire))
+        if(context->mDeferUpdates)
             value = static_cast<ALdouble>(AL_TRUE);
         break;
 
@@ -343,7 +367,7 @@ START_API_FUNC
         break;
 
     case AL_DEFERRED_UPDATES_SOFT:
-        if(context->mDeferUpdates.load(std::memory_order_acquire))
+        if(context->mDeferUpdates)
             value = static_cast<ALfloat>(AL_TRUE);
         break;
 
@@ -394,7 +418,7 @@ START_API_FUNC
         break;
 
     case AL_DEFERRED_UPDATES_SOFT:
-        if(context->mDeferUpdates.load(std::memory_order_acquire))
+        if(context->mDeferUpdates)
             value = AL_TRUE;
         break;
 
@@ -410,6 +434,41 @@ START_API_FUNC
         value = static_cast<int>(ResamplerDefault);
         break;
 
+#ifdef ALSOFT_EAX
+
+#define EAX_ERROR "[alGetInteger] EAX not enabled."
+
+    case AL_EAX_RAM_SIZE:
+        if (eax_g_is_enabled)
+        {
+            value = eax_x_ram_max_size;
+        }
+        else
+        {
+            context->setError(AL_INVALID_VALUE, EAX_ERROR);
+        }
+
+        break;
+
+    case AL_EAX_RAM_FREE:
+        if (eax_g_is_enabled)
+        {
+            auto device = context->mALDevice.get();
+            std::lock_guard<std::mutex> device_lock{device->BufferLock};
+
+            value = static_cast<ALint>(device->eax_x_ram_free_size);
+        }
+        else
+        {
+            context->setError(AL_INVALID_VALUE, EAX_ERROR);
+        }
+
+        break;
+
+#undef EAX_ERROR
+
+#endif // ALSOFT_EAX
+
     default:
         context->setError(AL_INVALID_VALUE, "Invalid integer property 0x%04x", pname);
     }
@@ -418,7 +477,7 @@ START_API_FUNC
 }
 END_API_FUNC
 
-extern "C" AL_API ALint64SOFT AL_APIENTRY alGetInteger64SOFT(ALenum pname)
+AL_API ALint64SOFT AL_APIENTRY alGetInteger64SOFT(ALenum pname)
 START_API_FUNC
 {
     ContextRef context{GetContextRef()};
@@ -445,7 +504,7 @@ START_API_FUNC
         break;
 
     case AL_DEFERRED_UPDATES_SOFT:
-        if(context->mDeferUpdates.load(std::memory_order_acquire))
+        if(context->mDeferUpdates)
             value = AL_TRUE;
         break;
 
@@ -627,7 +686,7 @@ START_API_FUNC
 }
 END_API_FUNC
 
-extern "C" AL_API void AL_APIENTRY alGetInteger64vSOFT(ALenum pname, ALint64SOFT *values)
+AL_API void AL_APIENTRY alGetInteger64vSOFT(ALenum pname, ALint64SOFT *values)
 START_API_FUNC
 {
     if(values)
@@ -819,6 +878,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
+    std::lock_guard<std::mutex> _{context->mPropLock};
     context->deferUpdates();
 }
 END_API_FUNC
@@ -829,6 +889,7 @@ START_API_FUNC
     ContextRef context{GetContextRef()};
     if UNLIKELY(!context) return;
 
+    std::lock_guard<std::mutex> _{context->mPropLock};
     context->processUpdates();
 }
 END_API_FUNC
@@ -874,6 +935,14 @@ void UpdateContextProps(ALCcontext *context)
     }
 
     /* Copy in current property values. */
+    ALlistener &listener = context->mListener;
+    props->Position = listener.Position;
+    props->Velocity = listener.Velocity;
+    props->OrientAt = listener.OrientAt;
+    props->OrientUp = listener.OrientUp;
+    props->Gain = listener.Gain;
+    props->MetersPerUnit = listener.mMetersPerUnit;
+
     props->DopplerFactor = context->mDopplerFactor;
     props->DopplerVelocity = context->mDopplerVelocity;
     props->SpeedOfSound = context->mSpeedOfSound;

File diff suppressed because it is too large
+ 418 - 346
Engine/lib/openal-soft/alc/alc.cpp


+ 68 - 82
Engine/lib/openal-soft/Alc/alconfig.cpp → Engine/lib/openal-soft/alc/alconfig.cpp

@@ -40,7 +40,7 @@
 
 #include "alfstream.h"
 #include "alstring.h"
-#include "compat.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "strutils.h"
 #include "vector.h"
@@ -277,6 +277,52 @@ void LoadConfigFromFile(std::istream &f)
     ConfOpts.shrink_to_fit();
 }
 
+const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName)
+{
+    if(!keyName)
+        return nullptr;
+
+    std::string key;
+    if(blockName && al::strcasecmp(blockName, "general") != 0)
+    {
+        key = blockName;
+        if(devName)
+        {
+            key += '/';
+            key += devName;
+        }
+        key += '/';
+        key += keyName;
+    }
+    else
+    {
+        if(devName)
+        {
+            key = devName;
+            key += '/';
+        }
+        key += keyName;
+    }
+
+    auto iter = std::find_if(ConfOpts.cbegin(), ConfOpts.cend(),
+        [&key](const ConfigEntry &entry) -> bool
+        { return entry.key == key; });
+    if(iter != ConfOpts.cend())
+    {
+        TRACE("Found %s = \"%s\"\n", key.c_str(), iter->value.c_str());
+        if(!iter->value.empty())
+            return iter->value.c_str();
+        return nullptr;
+    }
+
+    if(!devName)
+    {
+        TRACE("Key %s not found\n", key.c_str());
+        return nullptr;
+    }
+    return GetConfigValue(nullptr, blockName, keyName);
+}
+
 } // namespace
 
 
@@ -437,106 +483,46 @@ void ReadALConfig()
 }
 #endif
 
-const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName, const char *def)
-{
-    if(!keyName)
-        return def;
-
-    std::string key;
-    if(blockName && al::strcasecmp(blockName, "general") != 0)
-    {
-        key = blockName;
-        if(devName)
-        {
-            key += '/';
-            key += devName;
-        }
-        key += '/';
-        key += keyName;
-    }
-    else
-    {
-        if(devName)
-        {
-            key = devName;
-            key += '/';
-        }
-        key += keyName;
-    }
-
-    auto iter = std::find_if(ConfOpts.cbegin(), ConfOpts.cend(),
-        [&key](const ConfigEntry &entry) -> bool
-        { return entry.key == key; }
-    );
-    if(iter != ConfOpts.cend())
-    {
-        TRACE("Found %s = \"%s\"\n", key.c_str(), iter->value.c_str());
-        if(!iter->value.empty())
-            return iter->value.c_str();
-        return def;
-    }
-
-    if(!devName)
-    {
-        TRACE("Key %s not found\n", key.c_str());
-        return def;
-    }
-    return GetConfigValue(nullptr, blockName, keyName, def);
-}
-
-int ConfigValueExists(const char *devName, const char *blockName, const char *keyName)
-{
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    return val[0] != 0;
-}
-
 al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    if(!val[0]) return al::nullopt;
-
-    return al::make_optional<std::string>(val);
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return al::make_optional<std::string>(val);
+    return al::nullopt;
 }
 
 al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    if(!val[0]) return al::nullopt;
-
-    return al::make_optional(static_cast<int>(std::strtol(val, nullptr, 0)));
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return al::make_optional(static_cast<int>(std::strtol(val, nullptr, 0)));
+    return al::nullopt;
 }
 
 al::optional<unsigned int> ConfigValueUInt(const char *devName, const char *blockName, const char *keyName)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    if(!val[0]) return al::nullopt;
-
-    return al::make_optional(static_cast<unsigned int>(std::strtoul(val, nullptr, 0)));
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return al::make_optional(static_cast<unsigned int>(std::strtoul(val, nullptr, 0)));
+    return al::nullopt;
 }
 
 al::optional<float> ConfigValueFloat(const char *devName, const char *blockName, const char *keyName)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    if(!val[0]) return al::nullopt;
-
-    return al::make_optional(std::strtof(val, nullptr));
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return al::make_optional(std::strtof(val, nullptr));
+    return al::nullopt;
 }
 
 al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-    if(!val[0]) return al::nullopt;
-
-    return al::make_optional(
-        al::strcasecmp(val, "true") == 0 || al::strcasecmp(val, "yes") == 0 ||
-        al::strcasecmp(val, "on") == 0 || atoi(val) != 0);
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return al::make_optional(al::strcasecmp(val, "on") == 0 || al::strcasecmp(val, "yes") == 0
+            || al::strcasecmp(val, "true")==0 || atoi(val) != 0);
+    return al::nullopt;
 }
 
-int GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, int def)
+bool GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, bool def)
 {
-    const char *val = GetConfigValue(devName, blockName, keyName, "");
-
-    if(!val[0]) return def != 0;
-    return (al::strcasecmp(val, "true") == 0 || al::strcasecmp(val, "yes") == 0 ||
-            al::strcasecmp(val, "on") == 0 || atoi(val) != 0);
+    if(const char *val{GetConfigValue(devName, blockName, keyName)})
+        return (al::strcasecmp(val, "on") == 0 || al::strcasecmp(val, "yes") == 0
+            || al::strcasecmp(val, "true") == 0 || atoi(val) != 0);
+    return def;
 }

+ 1 - 3
Engine/lib/openal-soft/Alc/alconfig.h → Engine/lib/openal-soft/alc/alconfig.h

@@ -7,9 +7,7 @@
 
 void ReadALConfig();
 
-int ConfigValueExists(const char *devName, const char *blockName, const char *keyName);
-const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName, const char *def);
-int GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, int def);
+bool GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, bool def);
 
 al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName);
 al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName);

File diff suppressed because it is too large
+ 278 - 283
Engine/lib/openal-soft/alc/alu.cpp


+ 38 - 0
Engine/lib/openal-soft/alc/alu.h

@@ -0,0 +1,38 @@
+#ifndef ALU_H
+#define ALU_H
+
+#include <bitset>
+
+#include "aloptional.h"
+
+struct ALCcontext;
+struct ALCdevice;
+struct EffectSlot;
+
+enum class StereoEncoding : unsigned char;
+
+
+constexpr float GainMixMax{1000.0f}; /* +60dB */
+
+
+enum CompatFlags : uint8_t {
+    ReverseX,
+    ReverseY,
+    ReverseZ,
+
+    Count
+};
+using CompatFlagBitset = std::bitset<CompatFlags::Count>;
+
+void aluInit(CompatFlagBitset flags);
+
+/* aluInitRenderer
+ *
+ * Set up the appropriate panning method and mixing method given the device
+ * properties.
+ */
+void aluInitRenderer(ALCdevice *device, int hrtf_id, al::optional<StereoEncoding> stereomode);
+
+void aluInitEffectPanning(EffectSlot *slot, ALCcontext *context);
+
+#endif

+ 86 - 85
Engine/lib/openal-soft/Alc/backends/alsa.cpp → Engine/lib/openal-soft/alc/backends/alsa.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/alsa.h"
+#include "alsa.h"
 
 #include <algorithm>
 #include <atomic>
@@ -36,12 +36,12 @@
 #include <utility>
 
 #include "albyte.h"
-#include "alcmain.h"
-#include "alconfig.h"
+#include "alc/alconfig.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "aloptional.h"
-#include "alu.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "dynload.h"
 #include "ringbuffer.h"
@@ -68,35 +68,37 @@ constexpr char alsaDevice[] = "ALSA Default";
     MAGIC(snd_pcm_hw_params_free);                                            \
     MAGIC(snd_pcm_hw_params_any);                                             \
     MAGIC(snd_pcm_hw_params_current);                                         \
+    MAGIC(snd_pcm_hw_params_get_access);                                      \
+    MAGIC(snd_pcm_hw_params_get_buffer_size);                                 \
+    MAGIC(snd_pcm_hw_params_get_buffer_time_min);                             \
+    MAGIC(snd_pcm_hw_params_get_buffer_time_max);                             \
+    MAGIC(snd_pcm_hw_params_get_channels);                                    \
+    MAGIC(snd_pcm_hw_params_get_period_size);                                 \
+    MAGIC(snd_pcm_hw_params_get_period_time_max);                             \
+    MAGIC(snd_pcm_hw_params_get_period_time_min);                             \
+    MAGIC(snd_pcm_hw_params_get_periods);                                     \
     MAGIC(snd_pcm_hw_params_set_access);                                      \
-    MAGIC(snd_pcm_hw_params_set_format);                                      \
+    MAGIC(snd_pcm_hw_params_set_buffer_size_min);                             \
+    MAGIC(snd_pcm_hw_params_set_buffer_size_near);                            \
+    MAGIC(snd_pcm_hw_params_set_buffer_time_near);                            \
     MAGIC(snd_pcm_hw_params_set_channels);                                    \
+    MAGIC(snd_pcm_hw_params_set_channels_near);                               \
+    MAGIC(snd_pcm_hw_params_set_format);                                      \
+    MAGIC(snd_pcm_hw_params_set_period_time_near);                            \
+    MAGIC(snd_pcm_hw_params_set_period_size_near);                            \
     MAGIC(snd_pcm_hw_params_set_periods_near);                                \
     MAGIC(snd_pcm_hw_params_set_rate_near);                                   \
     MAGIC(snd_pcm_hw_params_set_rate);                                        \
     MAGIC(snd_pcm_hw_params_set_rate_resample);                               \
-    MAGIC(snd_pcm_hw_params_set_buffer_time_near);                            \
-    MAGIC(snd_pcm_hw_params_set_period_time_near);                            \
-    MAGIC(snd_pcm_hw_params_set_buffer_size_near);                            \
-    MAGIC(snd_pcm_hw_params_set_period_size_near);                            \
-    MAGIC(snd_pcm_hw_params_set_buffer_size_min);                             \
-    MAGIC(snd_pcm_hw_params_get_buffer_time_min);                             \
-    MAGIC(snd_pcm_hw_params_get_buffer_time_max);                             \
-    MAGIC(snd_pcm_hw_params_get_period_time_min);                             \
-    MAGIC(snd_pcm_hw_params_get_period_time_max);                             \
-    MAGIC(snd_pcm_hw_params_get_buffer_size);                                 \
-    MAGIC(snd_pcm_hw_params_get_period_size);                                 \
-    MAGIC(snd_pcm_hw_params_get_access);                                      \
-    MAGIC(snd_pcm_hw_params_get_periods);                                     \
     MAGIC(snd_pcm_hw_params_test_format);                                     \
     MAGIC(snd_pcm_hw_params_test_channels);                                   \
     MAGIC(snd_pcm_hw_params);                                                 \
-    MAGIC(snd_pcm_sw_params_malloc);                                          \
+    MAGIC(snd_pcm_sw_params);                                                 \
     MAGIC(snd_pcm_sw_params_current);                                         \
+    MAGIC(snd_pcm_sw_params_free);                                            \
+    MAGIC(snd_pcm_sw_params_malloc);                                          \
     MAGIC(snd_pcm_sw_params_set_avail_min);                                   \
     MAGIC(snd_pcm_sw_params_set_stop_threshold);                              \
-    MAGIC(snd_pcm_sw_params);                                                 \
-    MAGIC(snd_pcm_sw_params_free);                                            \
     MAGIC(snd_pcm_prepare);                                                   \
     MAGIC(snd_pcm_start);                                                     \
     MAGIC(snd_pcm_resume);                                                    \
@@ -105,7 +107,6 @@ constexpr char alsaDevice[] = "ALSA Default";
     MAGIC(snd_pcm_delay);                                                     \
     MAGIC(snd_pcm_state);                                                     \
     MAGIC(snd_pcm_avail_update);                                              \
-    MAGIC(snd_pcm_areas_silence);                                             \
     MAGIC(snd_pcm_mmap_begin);                                                \
     MAGIC(snd_pcm_mmap_commit);                                               \
     MAGIC(snd_pcm_readi);                                                     \
@@ -150,6 +151,7 @@ ALSA_FUNCS(MAKE_FUNC);
 #define snd_pcm_hw_params_set_access psnd_pcm_hw_params_set_access
 #define snd_pcm_hw_params_set_format psnd_pcm_hw_params_set_format
 #define snd_pcm_hw_params_set_channels psnd_pcm_hw_params_set_channels
+#define snd_pcm_hw_params_set_channels_near psnd_pcm_hw_params_set_channels_near
 #define snd_pcm_hw_params_set_periods_near psnd_pcm_hw_params_set_periods_near
 #define snd_pcm_hw_params_set_rate_near psnd_pcm_hw_params_set_rate_near
 #define snd_pcm_hw_params_set_rate psnd_pcm_hw_params_set_rate
@@ -167,6 +169,7 @@ ALSA_FUNCS(MAKE_FUNC);
 #define snd_pcm_hw_params_get_period_size psnd_pcm_hw_params_get_period_size
 #define snd_pcm_hw_params_get_access psnd_pcm_hw_params_get_access
 #define snd_pcm_hw_params_get_periods psnd_pcm_hw_params_get_periods
+#define snd_pcm_hw_params_get_channels psnd_pcm_hw_params_get_channels
 #define snd_pcm_hw_params_test_format psnd_pcm_hw_params_test_format
 #define snd_pcm_hw_params_test_channels psnd_pcm_hw_params_test_channels
 #define snd_pcm_hw_params psnd_pcm_hw_params
@@ -184,7 +187,6 @@ ALSA_FUNCS(MAKE_FUNC);
 #define snd_pcm_delay psnd_pcm_delay
 #define snd_pcm_state psnd_pcm_state
 #define snd_pcm_avail_update psnd_pcm_avail_update
-#define snd_pcm_areas_silence psnd_pcm_areas_silence
 #define snd_pcm_mmap_begin psnd_pcm_mmap_begin
 #define snd_pcm_mmap_commit psnd_pcm_mmap_commit
 #define snd_pcm_readi psnd_pcm_readi
@@ -260,29 +262,36 @@ al::vector<DevMap> probe_devices(snd_pcm_stream_t stream)
     snd_pcm_info_t *pcminfo;
     snd_pcm_info_malloc(&pcminfo);
 
-    devlist.emplace_back(DevMap{alsaDevice,
-        GetConfigValue(nullptr, "alsa", (stream==SND_PCM_STREAM_PLAYBACK) ? "device" : "capture",
-            "default")});
+    auto defname = ConfigValueStr(nullptr, "alsa",
+        (stream == SND_PCM_STREAM_PLAYBACK) ? "device" : "capture");
+    devlist.emplace_back(DevMap{alsaDevice, defname ? defname->c_str() : "default"});
 
-    const char *customdevs{GetConfigValue(nullptr, "alsa",
-        (stream == SND_PCM_STREAM_PLAYBACK) ? "custom-devices" : "custom-captures", "")};
-    while(const char *curdev{customdevs})
+    if(auto customdevs = ConfigValueStr(nullptr, "alsa",
+        (stream == SND_PCM_STREAM_PLAYBACK) ? "custom-devices" : "custom-captures"))
     {
-        if(!curdev[0]) break;
-        customdevs = strchr(curdev, ';');
-        const char *sep{strchr(curdev, '=')};
-        if(!sep)
+        size_t nextpos{customdevs->find_first_not_of(';')};
+        size_t curpos;
+        while((curpos=nextpos) < customdevs->length())
         {
-            std::string spec{customdevs ? std::string(curdev, customdevs++) : std::string(curdev)};
-            ERR("Invalid ALSA device specification \"%s\"\n", spec.c_str());
-            continue;
-        }
+            nextpos = customdevs->find_first_of(';', curpos+1);
 
-        const char *oldsep{sep++};
-        devlist.emplace_back(DevMap{std::string(curdev, oldsep),
-            customdevs ? std::string(sep, customdevs++) : std::string(sep)});
-        const auto &entry = devlist.back();
-        TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+            size_t seppos{customdevs->find_first_of('=', curpos)};
+            if(seppos == curpos || seppos >= nextpos)
+            {
+                std::string spec{customdevs->substr(curpos, nextpos-curpos)};
+                ERR("Invalid ALSA device specification \"%s\"\n", spec.c_str());
+            }
+            else
+            {
+                devlist.emplace_back(DevMap{customdevs->substr(curpos, seppos-curpos),
+                    customdevs->substr(seppos+1, nextpos-seppos-1)});
+                const auto &entry = devlist.back();
+                TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+            }
+
+            if(nextpos < customdevs->length())
+                nextpos = customdevs->find_first_not_of(';', nextpos+1);
+        }
     }
 
     const std::string main_prefix{
@@ -407,7 +416,7 @@ int verify_state(snd_pcm_t *handle)
 
 
 struct AlsaPlayback final : public BackendBase {
-    AlsaPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    AlsaPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~AlsaPlayback() override;
 
     int mixerProc();
@@ -424,6 +433,7 @@ struct AlsaPlayback final : public BackendBase {
 
     std::mutex mMutex;
 
+    uint mFrameStep{};
     al::vector<al::byte> mBuffer;
 
     std::atomic<bool> mKillNow{true};
@@ -445,7 +455,6 @@ int AlsaPlayback::mixerProc()
     SetRTPriority();
     althrd_setname(MIXER_THREAD_NAME);
 
-    const size_t samplebits{mDevice->bytesFromFmt() * 8};
     const snd_pcm_uframes_t update_size{mDevice->UpdateSize};
     const snd_pcm_uframes_t buffer_size{mDevice->BufferSize};
     while(!mKillNow.load(std::memory_order_acquire))
@@ -507,7 +516,7 @@ int AlsaPlayback::mixerProc()
             }
 
             char *WritePtr{static_cast<char*>(areas->addr) + (offset * areas->step / 8)};
-            mDevice->renderSamples(WritePtr, static_cast<uint>(frames), areas->step/samplebits);
+            mDevice->renderSamples(WritePtr, static_cast<uint>(frames), mFrameStep);
 
             snd_pcm_sframes_t commitres{snd_pcm_mmap_commit(mPcmHandle, offset, frames)};
             if(commitres < 0 || static_cast<snd_pcm_uframes_t>(commitres) != frames)
@@ -529,7 +538,6 @@ int AlsaPlayback::mixerNoMMapProc()
     SetRTPriority();
     althrd_setname(MIXER_THREAD_NAME);
 
-    const size_t frame_step{mDevice->channelsFromFmt()};
     const snd_pcm_uframes_t update_size{mDevice->UpdateSize};
     const snd_pcm_uframes_t buffer_size{mDevice->BufferSize};
     while(!mKillNow.load(std::memory_order_acquire))
@@ -575,7 +583,7 @@ int AlsaPlayback::mixerNoMMapProc()
         al::byte *WritePtr{mBuffer.data()};
         avail = snd_pcm_bytes_to_frames(mPcmHandle, static_cast<ssize_t>(mBuffer.size()));
         std::lock_guard<std::mutex> _{mMutex};
-        mDevice->renderSamples(WritePtr, static_cast<uint>(avail), frame_step);
+        mDevice->renderSamples(WritePtr, static_cast<uint>(avail), mFrameStep);
         while(avail > 0)
         {
             snd_pcm_sframes_t ret{snd_pcm_writei(mPcmHandle, WritePtr,
@@ -615,16 +623,15 @@ int AlsaPlayback::mixerNoMMapProc()
 
 void AlsaPlayback::open(const char *name)
 {
-    const char *driver{};
+    al::optional<std::string> driveropt;
+    const char *driver{"default"};
     if(name)
     {
         if(PlaybackDevices.empty())
             PlaybackDevices = probe_devices(SND_PCM_STREAM_PLAYBACK);
 
         auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
-            [name](const DevMap &entry) -> bool
-            { return entry.name == name; }
-        );
+            [name](const DevMap &entry) -> bool { return entry.name == name; });
         if(iter == PlaybackDevices.cend())
             throw al::backend_exception{al::backend_error::NoDevice,
                 "Device name \"%s\" not found", name};
@@ -633,14 +640,19 @@ void AlsaPlayback::open(const char *name)
     else
     {
         name = alsaDevice;
-        driver = GetConfigValue(nullptr, "alsa", "device", "default");
+        if(bool{driveropt = ConfigValueStr(nullptr, "alsa", "device")})
+            driver = driveropt->c_str();
     }
-
     TRACE("Opening device \"%s\"\n", driver);
-    int err{snd_pcm_open(&mPcmHandle, driver, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)};
+
+    snd_pcm_t *pcmHandle{};
+    int err{snd_pcm_open(&pcmHandle, driver, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)};
     if(err < 0)
         throw al::backend_exception{al::backend_error::NoDevice,
             "Could not open ALSA device \"%s\"", driver};
+    if(mPcmHandle)
+        snd_pcm_close(mPcmHandle);
+    mPcmHandle = pcmHandle;
 
     /* Free alsa's global config tree. Otherwise valgrind reports a ton of leaks. */
     snd_config_update_free_global();
@@ -723,37 +735,25 @@ bool AlsaPlayback::reset()
         }
     }
     CHECK(snd_pcm_hw_params_set_format(mPcmHandle, hp.get(), format));
-    /* test and set channels (implicitly sets frame bits) */
-    if(snd_pcm_hw_params_test_channels(mPcmHandle, hp.get(), mDevice->channelsFromFmt()) < 0)
+    /* set channels (implicitly sets frame bits) */
+    if(snd_pcm_hw_params_set_channels(mPcmHandle, hp.get(), mDevice->channelsFromFmt()) < 0)
     {
-        static const DevFmtChannels channellist[] = {
-            DevFmtStereo,
-            DevFmtQuad,
-            DevFmtX51,
-            DevFmtX71,
-            DevFmtMono,
-        };
-
-        for(const auto &chan : channellist)
-        {
-            if(snd_pcm_hw_params_test_channels(mPcmHandle, hp.get(), ChannelsFromDevFmt(chan, 0)) >= 0)
-            {
-                mDevice->FmtChans = chan;
-                mDevice->mAmbiOrder = 0;
-                break;
-            }
-        }
+        uint numchans{2u};
+        CHECK(snd_pcm_hw_params_set_channels_near(mPcmHandle, hp.get(), &numchans));
+        if(numchans < 1)
+            throw al::backend_exception{al::backend_error::DeviceError, "Got 0 device channels"};
+        if(numchans == 1) mDevice->FmtChans = DevFmtMono;
+        else mDevice->FmtChans = DevFmtStereo;
     }
-    CHECK(snd_pcm_hw_params_set_channels(mPcmHandle, hp.get(), mDevice->channelsFromFmt()));
     /* set rate (implicitly constrains period/buffer parameters) */
     if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "alsa", "allow-resampler", 0)
         || !mDevice->Flags.test(FrequencyRequest))
     {
         if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp.get(), 0) < 0)
-            ERR("Failed to disable ALSA resampler\n");
+            WARN("Failed to disable ALSA resampler\n");
     }
     else if(snd_pcm_hw_params_set_rate_resample(mPcmHandle, hp.get(), 1) < 0)
-        ERR("Failed to enable ALSA resampler\n");
+        WARN("Failed to enable ALSA resampler\n");
     CHECK(snd_pcm_hw_params_set_rate_near(mPcmHandle, hp.get(), &rate, nullptr));
     /* set period time (implicitly constrains period/buffer parameters) */
     if((err=snd_pcm_hw_params_set_period_time_near(mPcmHandle, hp.get(), &periodLen, nullptr)) < 0)
@@ -772,6 +772,7 @@ bool AlsaPlayback::reset()
     CHECK(snd_pcm_hw_params_get_access(hp.get(), &access));
     CHECK(snd_pcm_hw_params_get_period_size(hp.get(), &periodSizeInFrames, nullptr));
     CHECK(snd_pcm_hw_params_get_buffer_size(hp.get(), &bufferSizeInFrames));
+    CHECK(snd_pcm_hw_params_get_channels(hp.get(), &mFrameStep));
     hp = nullptr;
 
     SwParamsPtr sp{CreateSwParams()};
@@ -809,8 +810,8 @@ void AlsaPlayback::start()
     int (AlsaPlayback::*thread_func)(){};
     if(access == SND_PCM_ACCESS_RW_INTERLEAVED)
     {
-        mBuffer.resize(
-            static_cast<size_t>(snd_pcm_frames_to_bytes(mPcmHandle, mDevice->UpdateSize)));
+        auto datalen = snd_pcm_frames_to_bytes(mPcmHandle, mDevice->UpdateSize);
+        mBuffer.resize(static_cast<size_t>(datalen));
         thread_func = &AlsaPlayback::mixerNoMMapProc;
     }
     else
@@ -863,7 +864,7 @@ ClockLatency AlsaPlayback::getClockLatency()
 
 
 struct AlsaCapture final : public BackendBase {
-    AlsaCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    AlsaCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~AlsaCapture() override;
 
     void open(const char *name) override;
@@ -895,16 +896,15 @@ AlsaCapture::~AlsaCapture()
 
 void AlsaCapture::open(const char *name)
 {
-    const char *driver{};
+    al::optional<std::string> driveropt;
+    const char *driver{"default"};
     if(name)
     {
         if(CaptureDevices.empty())
             CaptureDevices = probe_devices(SND_PCM_STREAM_CAPTURE);
 
         auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
-            [name](const DevMap &entry) -> bool
-            { return entry.name == name; }
-        );
+            [name](const DevMap &entry) -> bool { return entry.name == name; });
         if(iter == CaptureDevices.cend())
             throw al::backend_exception{al::backend_error::NoDevice,
                 "Device name \"%s\" not found", name};
@@ -913,7 +913,8 @@ void AlsaCapture::open(const char *name)
     else
     {
         name = alsaDevice;
-        driver = GetConfigValue(nullptr, "alsa", "capture", "default");
+        if(bool{driveropt = ConfigValueStr(nullptr, "alsa", "capture")})
+            driver = driveropt->c_str();
     }
 
     TRACE("Opening device \"%s\"\n", driver);
@@ -1252,7 +1253,7 @@ std::string AlsaBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr AlsaBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr AlsaBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new AlsaPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/alsa.h → Engine/lib/openal-soft/alc/backends/alsa.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_ALSA_H
 #define BACKENDS_ALSA_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct AlsaBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 16 - 17
Engine/lib/openal-soft/Alc/backends/base.cpp → Engine/lib/openal-soft/alc/backends/base.cpp

@@ -3,21 +3,22 @@
 
 #include "base.h"
 
+#include <algorithm>
+#include <array>
 #include <atomic>
-#include <thread>
 
 #ifdef _WIN32
 #define WIN32_LEAN_AND_MEAN
 #include <windows.h>
 #include <mmreg.h>
-#endif
 
 #include "albit.h"
-#include "alcmain.h"
-#include "alnumeric.h"
+#include "core/logging.h"
 #include "aloptional.h"
+#endif
+
 #include "atomic.h"
-#include "core/logging.h"
+#include "core/devformat.h"
 
 
 bool BackendBase::reset()
@@ -78,14 +79,6 @@ void BackendBase::setDefaultWFXChannelOrder()
         mDevice->RealOut.ChannelIndex[SideLeft]    = 4;
         mDevice->RealOut.ChannelIndex[SideRight]   = 5;
         break;
-    case DevFmtX51Rear:
-        mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
-        mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
-        mDevice->RealOut.ChannelIndex[FrontCenter] = 2;
-        mDevice->RealOut.ChannelIndex[LFE]         = 3;
-        mDevice->RealOut.ChannelIndex[BackLeft]    = 4;
-        mDevice->RealOut.ChannelIndex[BackRight]   = 5;
-        break;
     case DevFmtX61:
         mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
         mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
@@ -116,11 +109,11 @@ void BackendBase::setDefaultChannelOrder()
 
     switch(mDevice->FmtChans)
     {
-    case DevFmtX51Rear:
+    case DevFmtX51:
         mDevice->RealOut.ChannelIndex[FrontLeft]   = 0;
         mDevice->RealOut.ChannelIndex[FrontRight]  = 1;
-        mDevice->RealOut.ChannelIndex[BackLeft]    = 2;
-        mDevice->RealOut.ChannelIndex[BackRight]   = 3;
+        mDevice->RealOut.ChannelIndex[SideLeft]    = 2;
+        mDevice->RealOut.ChannelIndex[SideRight]   = 3;
         mDevice->RealOut.ChannelIndex[FrontCenter] = 4;
         mDevice->RealOut.ChannelIndex[LFE]         = 5;
         return;
@@ -139,7 +132,6 @@ void BackendBase::setDefaultChannelOrder()
     case DevFmtMono:
     case DevFmtStereo:
     case DevFmtQuad:
-    case DevFmtX51:
     case DevFmtX61:
     case DevFmtAmbi3D:
         setDefaultWFXChannelOrder();
@@ -150,6 +142,13 @@ void BackendBase::setDefaultChannelOrder()
 #ifdef _WIN32
 void BackendBase::setChannelOrderFromWFXMask(uint chanmask)
 {
+    static constexpr uint x51{SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER
+        | SPEAKER_LOW_FREQUENCY | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT};
+    static constexpr uint x51rear{SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER
+        | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT};
+    /* Swap a 5.1 mask using the back channels for one with the sides. */
+    if(chanmask == x51rear) chanmask = x51;
+
     auto get_channel = [](const DWORD chanbit) noexcept -> al::optional<Channel>
     {
         switch(chanbit)

+ 12 - 8
Engine/lib/openal-soft/Alc/backends/base.h → Engine/lib/openal-soft/alc/backends/base.h

@@ -2,12 +2,13 @@
 #define ALC_BACKENDS_BASE_H
 
 #include <chrono>
+#include <cstdarg>
 #include <memory>
-#include <mutex>
+#include <ratio>
 #include <string>
 
 #include "albyte.h"
-#include "alcmain.h"
+#include "core/device.h"
 #include "core/except.h"
 
 
@@ -30,9 +31,9 @@ struct BackendBase {
 
     virtual ClockLatency getClockLatency();
 
-    ALCdevice *const mDevice;
+    DeviceBase *const mDevice;
 
-    BackendBase(ALCdevice *device) noexcept : mDevice{device} { }
+    BackendBase(DeviceBase *device) noexcept : mDevice{device} { }
     virtual ~BackendBase() = default;
 
 protected:
@@ -57,7 +58,7 @@ enum class BackendType {
 /* Helper to get the current clock time from the device's ClockBase, and
  * SamplesDone converted from the sample rate.
  */
-inline std::chrono::nanoseconds GetDeviceClockTime(ALCdevice *device)
+inline std::chrono::nanoseconds GetDeviceClockTime(DeviceBase *device)
 {
     using std::chrono::seconds;
     using std::chrono::nanoseconds;
@@ -69,9 +70,8 @@ inline std::chrono::nanoseconds GetDeviceClockTime(ALCdevice *device)
 /* Helper to get the device latency from the backend, including any fixed
  * latency from post-processing.
  */
-inline ClockLatency GetClockLatency(ALCdevice *device)
+inline ClockLatency GetClockLatency(DeviceBase *device, BackendBase *backend)
 {
-    BackendBase *backend{device->Backend.get()};
     ClockLatency ret{backend->getClockLatency()};
     ret.Latency += device->FixedLatency;
     return ret;
@@ -85,7 +85,7 @@ struct BackendFactory {
 
     virtual std::string probe(BackendType type) = 0;
 
-    virtual BackendPtr createBackend(ALCdevice *device, BackendType type) = 0;
+    virtual BackendPtr createBackend(DeviceBase *device, BackendType type) = 0;
 
 protected:
     virtual ~BackendFactory() = default;
@@ -103,7 +103,11 @@ class backend_exception final : public base_exception {
     backend_error mErrorCode;
 
 public:
+#ifdef __USE_MINGW_ANSI_STDIO
+    [[gnu::format(gnu_printf, 3, 4)]]
+#else
     [[gnu::format(printf, 3, 4)]]
+#endif
     backend_exception(backend_error code, const char *msg, ...) : mErrorCode{code}
     {
         std::va_list args;

+ 378 - 120
Engine/lib/openal-soft/Alc/backends/coreaudio.cpp → Engine/lib/openal-soft/alc/backends/coreaudio.cpp

@@ -20,34 +20,239 @@
 
 #include "config.h"
 
-#include "backends/coreaudio.h"
+#include "coreaudio.h"
 
 #include <inttypes.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include <cmath>
+#include <memory>
+#include <string>
 
-#include "alcmain.h"
-#include "alu.h"
-#include "ringbuffer.h"
-#include "converter.h"
+#include "alnumeric.h"
+#include "core/converter.h"
+#include "core/device.h"
 #include "core/logging.h"
-#include "backends/base.h"
+#include "ringbuffer.h"
 
-#include <unistd.h>
 #include <AudioUnit/AudioUnit.h>
 #include <AudioToolbox/AudioToolbox.h>
 
 
 namespace {
 
-static const char ca_device[] = "CoreAudio Default";
+#if TARGET_OS_IOS || TARGET_OS_TV
+#define CAN_ENUMERATE 0
+#else
+#define CAN_ENUMERATE 1
+#endif
+
+
+#if CAN_ENUMERATE
+struct DeviceEntry {
+    AudioDeviceID mId;
+    std::string mName;
+};
+
+std::vector<DeviceEntry> PlaybackList;
+std::vector<DeviceEntry> CaptureList;
+
+
+OSStatus GetHwProperty(AudioHardwarePropertyID propId, UInt32 dataSize, void *propData)
+{
+    const AudioObjectPropertyAddress addr{propId, kAudioObjectPropertyScopeGlobal,
+        kAudioObjectPropertyElementMaster};
+    return AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, nullptr, &dataSize,
+        propData);
+}
+
+OSStatus GetHwPropertySize(AudioHardwarePropertyID propId, UInt32 *outSize)
+{
+    const AudioObjectPropertyAddress addr{propId, kAudioObjectPropertyScopeGlobal,
+        kAudioObjectPropertyElementMaster};
+    return AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, 0, nullptr, outSize);
+}
+
+OSStatus GetDevProperty(AudioDeviceID devId, AudioDevicePropertyID propId, bool isCapture,
+    UInt32 elem, UInt32 dataSize, void *propData)
+{
+    static const AudioObjectPropertyScope scopes[2]{kAudioDevicePropertyScopeOutput,
+        kAudioDevicePropertyScopeInput};
+    const AudioObjectPropertyAddress addr{propId, scopes[isCapture], elem};
+    return AudioObjectGetPropertyData(devId, &addr, 0, nullptr, &dataSize, propData);
+}
+
+OSStatus GetDevPropertySize(AudioDeviceID devId, AudioDevicePropertyID inPropertyID,
+    bool isCapture, UInt32 elem, UInt32 *outSize)
+{
+    static const AudioObjectPropertyScope scopes[2]{kAudioDevicePropertyScopeOutput,
+        kAudioDevicePropertyScopeInput};
+    const AudioObjectPropertyAddress addr{inPropertyID, scopes[isCapture], elem};
+    return AudioObjectGetPropertyDataSize(devId, &addr, 0, nullptr, outSize);
+}
+
+
+std::string GetDeviceName(AudioDeviceID devId)
+{
+    std::string devname;
+    CFStringRef nameRef;
+
+    /* Try to get the device name as a CFString, for Unicode name support. */
+    OSStatus err{GetDevProperty(devId, kAudioDevicePropertyDeviceNameCFString, false, 0,
+        sizeof(nameRef), &nameRef)};
+    if(err == noErr)
+    {
+        const CFIndex propSize{CFStringGetMaximumSizeForEncoding(CFStringGetLength(nameRef),
+            kCFStringEncodingUTF8)};
+        devname.resize(static_cast<size_t>(propSize)+1, '\0');
+
+        CFStringGetCString(nameRef, &devname[0], propSize+1, kCFStringEncodingUTF8);
+        CFRelease(nameRef);
+    }
+    else
+    {
+        /* If that failed, just get the C string. Hopefully there's nothing bad
+         * with this.
+         */
+        UInt32 propSize{};
+        if(GetDevPropertySize(devId, kAudioDevicePropertyDeviceName, false, 0, &propSize))
+            return devname;
+
+        devname.resize(propSize+1, '\0');
+        if(GetDevProperty(devId, kAudioDevicePropertyDeviceName, false, 0, propSize, &devname[0]))
+        {
+            devname.clear();
+            return devname;
+        }
+    }
+
+    /* Clear extraneous nul chars that may have been written with the name
+     * string, and return it.
+     */
+    while(!devname.back())
+        devname.pop_back();
+    return devname;
+}
+
+UInt32 GetDeviceChannelCount(AudioDeviceID devId, bool isCapture)
+{
+    UInt32 propSize{};
+    auto err = GetDevPropertySize(devId, kAudioDevicePropertyStreamConfiguration, isCapture, 0,
+        &propSize);
+    if(err)
+    {
+        ERR("kAudioDevicePropertyStreamConfiguration size query failed: %u\n", err);
+        return 0;
+    }
+
+    auto buflist_data = std::make_unique<char[]>(propSize);
+    auto *buflist = reinterpret_cast<AudioBufferList*>(buflist_data.get());
+
+    err = GetDevProperty(devId, kAudioDevicePropertyStreamConfiguration, isCapture, 0, propSize,
+        buflist);
+    if(err)
+    {
+        ERR("kAudioDevicePropertyStreamConfiguration query failed: %u\n", err);
+        return 0;
+    }
+
+    UInt32 numChannels{0};
+    for(size_t i{0};i < buflist->mNumberBuffers;++i)
+        numChannels += buflist->mBuffers[i].mNumberChannels;
+
+    return numChannels;
+}
+
+
+void EnumerateDevices(std::vector<DeviceEntry> &list, bool isCapture)
+{
+    UInt32 propSize{};
+    if(auto err = GetHwPropertySize(kAudioHardwarePropertyDevices, &propSize))
+    {
+        ERR("Failed to get device list size: %u\n", err);
+        return;
+    }
+
+    auto devIds = std::vector<AudioDeviceID>(propSize/sizeof(AudioDeviceID), kAudioDeviceUnknown);
+    if(auto err = GetHwProperty(kAudioHardwarePropertyDevices, propSize, devIds.data()))
+    {
+        ERR("Failed to get device list: %u\n", err);
+        return;
+    }
+
+    std::vector<DeviceEntry> newdevs;
+    newdevs.reserve(devIds.size());
+
+    AudioDeviceID defaultId{kAudioDeviceUnknown};
+    GetHwProperty(isCapture ? kAudioHardwarePropertyDefaultInputDevice :
+        kAudioHardwarePropertyDefaultOutputDevice, sizeof(defaultId), &defaultId);
+
+    if(defaultId != kAudioDeviceUnknown)
+    {
+        newdevs.emplace_back(DeviceEntry{defaultId, GetDeviceName(defaultId)});
+        const auto &entry = newdevs.back();
+        TRACE("Got device: %s = ID %u\n", entry.mName.c_str(), entry.mId);
+    }
+    for(const AudioDeviceID devId : devIds)
+    {
+        if(devId == kAudioDeviceUnknown)
+            continue;
+
+        auto match_devid = [devId](const DeviceEntry &entry) noexcept -> bool
+        { return entry.mId == devId; };
+        auto match = std::find_if(newdevs.cbegin(), newdevs.cend(), match_devid);
+        if(match != newdevs.cend()) continue;
+
+        auto numChannels = GetDeviceChannelCount(devId, isCapture);
+        if(numChannels > 0)
+        {
+            newdevs.emplace_back(DeviceEntry{devId, GetDeviceName(devId)});
+            const auto &entry = newdevs.back();
+            TRACE("Got device: %s = ID %u\n", entry.mName.c_str(), entry.mId);
+        }
+    }
+
+    if(newdevs.size() > 1)
+    {
+        /* Rename entries that have matching names, by appending '#2', '#3',
+         * etc, as needed.
+         */
+        for(auto curitem = newdevs.begin()+1;curitem != newdevs.end();++curitem)
+        {
+            auto check_match = [curitem](const DeviceEntry &entry) -> bool
+            { return entry.mName == curitem->mName; };
+            if(std::find_if(newdevs.begin(), curitem, check_match) != curitem)
+            {
+                std::string name{curitem->mName};
+                size_t count{1};
+                auto check_name = [&name](const DeviceEntry &entry) -> bool
+                { return entry.mName == name; };
+                do {
+                    name = curitem->mName;
+                    name += " #";
+                    name += std::to_string(++count);
+                } while(std::find_if(newdevs.begin(), curitem, check_name) != curitem);
+                curitem->mName = std::move(name);
+            }
+        }
+    }
+
+    newdevs.shrink_to_fit();
+    newdevs.swap(list);
+}
+
+#else
+
+static constexpr char ca_device[] = "CoreAudio Default";
+#endif
 
 
 struct CoreAudioPlayback final : public BackendBase {
-    CoreAudioPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    CoreAudioPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~CoreAudioPlayback() override;
 
     OSStatus MixerProc(AudioUnitRenderActionFlags *ioActionFlags,
@@ -96,19 +301,41 @@ OSStatus CoreAudioPlayback::MixerProc(AudioUnitRenderActionFlags*, const AudioTi
 
 void CoreAudioPlayback::open(const char *name)
 {
+#if CAN_ENUMERATE
+    AudioDeviceID audioDevice{kAudioDeviceUnknown};
+    if(!name)
+        GetHwProperty(kAudioHardwarePropertyDefaultOutputDevice, sizeof(audioDevice),
+            &audioDevice);
+    else
+    {
+        if(PlaybackList.empty())
+            EnumerateDevices(PlaybackList, false);
+
+        auto find_name = [name](const DeviceEntry &entry) -> bool
+        { return entry.mName == name; };
+        auto devmatch = std::find_if(PlaybackList.cbegin(), PlaybackList.cend(), find_name);
+        if(devmatch == PlaybackList.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
+
+        audioDevice = devmatch->mId;
+    }
+#else
     if(!name)
         name = ca_device;
     else if(strcmp(name, ca_device) != 0)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
+#endif
 
     /* open the default output unit */
     AudioComponentDescription desc{};
     desc.componentType = kAudioUnitType_Output;
-#if TARGET_OS_IOS
-    desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#if CAN_ENUMERATE
+    desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ?
+        kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput;
 #else
-    desc.componentSubType = kAudioUnitSubType_DefaultOutput;
+    desc.componentSubType = kAudioUnitSubType_RemoteIO;
 #endif
     desc.componentManufacturer = kAudioUnitManufacturer_Apple;
     desc.componentFlags = 0;
@@ -118,18 +345,50 @@ void CoreAudioPlayback::open(const char *name)
     if(comp == nullptr)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"};
 
-    OSStatus err{AudioComponentInstanceNew(comp, &mAudioUnit)};
+    AudioUnit audioUnit{};
+    OSStatus err{AudioComponentInstanceNew(comp, &audioUnit)};
     if(err != noErr)
         throw al::backend_exception{al::backend_error::NoDevice,
             "Could not create component instance: %u", err};
 
-    /* init and start the default audio unit... */
-    err = AudioUnitInitialize(mAudioUnit);
+#if CAN_ENUMERATE
+    if(audioDevice != kAudioDeviceUnknown)
+        AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, 0, &audioDevice, sizeof(AudioDeviceID));
+#endif
+
+    err = AudioUnitInitialize(audioUnit);
     if(err != noErr)
         throw al::backend_exception{al::backend_error::DeviceError,
             "Could not initialize audio unit: %u", err};
 
+    /* WARNING: I don't know if "valid" audio unit values are guaranteed to be
+     * non-0. If not, this logic is broken.
+     */
+    if(mAudioUnit)
+    {
+        AudioUnitUninitialize(mAudioUnit);
+        AudioComponentInstanceDispose(mAudioUnit);
+    }
+    mAudioUnit = audioUnit;
+
+#if CAN_ENUMERATE
+    if(name)
+        mDevice->DeviceName = name;
+    else
+    {
+        UInt32 propSize{sizeof(audioDevice)};
+        audioDevice = kAudioDeviceUnknown;
+        AudioUnitGetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, 0, &audioDevice, &propSize);
+
+        std::string devname{GetDeviceName(audioDevice)};
+        if(!devname.empty()) mDevice->DeviceName = std::move(devname);
+        else mDevice->DeviceName = "Unknown Device Name";
+    }
+#else
     mDevice->DeviceName = name;
+#endif
 }
 
 bool CoreAudioPlayback::reset()
@@ -140,10 +399,10 @@ bool CoreAudioPlayback::reset()
 
     /* retrieve default output unit's properties (output side) */
     AudioStreamBasicDescription streamFormat{};
-    auto size = static_cast<UInt32>(sizeof(AudioStreamBasicDescription));
+    UInt32 size{sizeof(streamFormat)};
     err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
         0, &streamFormat, &size);
-    if(err != noErr || size != sizeof(AudioStreamBasicDescription))
+    if(err != noErr || size != sizeof(streamFormat))
     {
         ERR("AudioUnitGetProperty failed\n");
         return false;
@@ -159,15 +418,9 @@ bool CoreAudioPlayback::reset()
     TRACE("  streamFormat.mSampleRate = %5.0f\n", streamFormat.mSampleRate);
 #endif
 
-    /* set default output unit's input side to match output side */
-    err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
-        0, &streamFormat, size);
-    if(err != noErr)
-    {
-        ERR("AudioUnitSetProperty failed\n");
-        return false;
-    }
-
+    /* Use the sample rate from the output unit's current parameters, but reset
+     * everything else.
+     */
     if(mDevice->Frequency != streamFormat.mSampleRate)
     {
         mDevice->BufferSize = static_cast<uint>(uint64_t{mDevice->BufferSize} *
@@ -176,82 +429,54 @@ bool CoreAudioPlayback::reset()
     }
 
     /* FIXME: How to tell what channels are what in the output device, and how
-     * to specify what we're giving?  eg, 6.0 vs 5.1 */
-    switch(streamFormat.mChannelsPerFrame)
-    {
-        case 1:
-            mDevice->FmtChans = DevFmtMono;
-            break;
-        case 2:
-            mDevice->FmtChans = DevFmtStereo;
-            break;
-        case 4:
-            mDevice->FmtChans = DevFmtQuad;
-            break;
-        case 6:
-            mDevice->FmtChans = DevFmtX51;
-            break;
-        case 7:
-            mDevice->FmtChans = DevFmtX61;
-            break;
-        case 8:
-            mDevice->FmtChans = DevFmtX71;
-            break;
-        default:
-            ERR("Unhandled channel count (%d), using Stereo\n", streamFormat.mChannelsPerFrame);
-            mDevice->FmtChans = DevFmtStereo;
-            streamFormat.mChannelsPerFrame = 2;
-            break;
-    }
-    setDefaultWFXChannelOrder();
+     * to specify what we're giving? e.g. 6.0 vs 5.1
+     */
+    streamFormat.mChannelsPerFrame = mDevice->channelsFromFmt();
 
-    /* use channel count and sample rate from the default output unit's current
-     * parameters, but reset everything else */
     streamFormat.mFramesPerPacket = 1;
-    streamFormat.mFormatFlags = 0;
+    streamFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kLinearPCMFormatFlagIsPacked;
+    streamFormat.mFormatID = kAudioFormatLinearPCM;
     switch(mDevice->FmtType)
     {
         case DevFmtUByte:
             mDevice->FmtType = DevFmtByte;
             /* fall-through */
         case DevFmtByte:
-            streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
             streamFormat.mBitsPerChannel = 8;
             break;
         case DevFmtUShort:
             mDevice->FmtType = DevFmtShort;
             /* fall-through */
         case DevFmtShort:
-            streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
             streamFormat.mBitsPerChannel = 16;
             break;
         case DevFmtUInt:
             mDevice->FmtType = DevFmtInt;
             /* fall-through */
         case DevFmtInt:
-            streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
             streamFormat.mBitsPerChannel = 32;
             break;
         case DevFmtFloat:
-            streamFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat;
+            streamFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat;
             streamFormat.mBitsPerChannel = 32;
             break;
     }
-    streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame *
-                                  streamFormat.mBitsPerChannel / 8;
-    streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame;
-    streamFormat.mFormatID = kAudioFormatLinearPCM;
-    streamFormat.mFormatFlags |= kAudioFormatFlagsNativeEndian |
-                                 kLinearPCMFormatFlagIsPacked;
+    streamFormat.mBytesPerFrame = streamFormat.mChannelsPerFrame*streamFormat.mBitsPerChannel/8;
+    streamFormat.mBytesPerPacket = streamFormat.mBytesPerFrame*streamFormat.mFramesPerPacket;
 
     err = AudioUnitSetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
-        0, &streamFormat, sizeof(AudioStreamBasicDescription));
+        0, &streamFormat, sizeof(streamFormat));
     if(err != noErr)
     {
         ERR("AudioUnitSetProperty failed\n");
         return false;
     }
 
+    setDefaultWFXChannelOrder();
+
     /* setup callback */
     mFrameSize = mDevice->frameSizeFromFmt();
     AURenderCallbackStruct input{};
@@ -294,7 +519,7 @@ void CoreAudioPlayback::stop()
 
 
 struct CoreAudioCapture final : public BackendBase {
-    CoreAudioCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    CoreAudioCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~CoreAudioCapture() override;
 
     OSStatus RecordProc(AudioUnitRenderActionFlags *ioActionFlags,
@@ -383,45 +608,64 @@ OSStatus CoreAudioCapture::RecordProc(AudioUnitRenderActionFlags*,
 
 void CoreAudioCapture::open(const char *name)
 {
-    AudioStreamBasicDescription requestedFormat;  // The application requested format
-    AudioStreamBasicDescription hardwareFormat;   // The hardware format
-    AudioStreamBasicDescription outputFormat;     // The AudioUnit output format
-    AURenderCallbackStruct input;
-    AudioComponentDescription desc;
-    UInt32 propertySize;
-    UInt32 enableIO;
-    AudioComponent comp;
-    OSStatus err;
+#if CAN_ENUMERATE
+    AudioDeviceID audioDevice{kAudioDeviceUnknown};
+    if(!name)
+        GetHwProperty(kAudioHardwarePropertyDefaultInputDevice, sizeof(audioDevice),
+            &audioDevice);
+    else
+    {
+        if(CaptureList.empty())
+            EnumerateDevices(CaptureList, true);
+
+        auto find_name = [name](const DeviceEntry &entry) -> bool
+        { return entry.mName == name; };
+        auto devmatch = std::find_if(CaptureList.cbegin(), CaptureList.cend(), find_name);
+        if(devmatch == CaptureList.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
 
+        audioDevice = devmatch->mId;
+    }
+#else
     if(!name)
         name = ca_device;
     else if(strcmp(name, ca_device) != 0)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
+#endif
 
+    AudioComponentDescription desc{};
     desc.componentType = kAudioUnitType_Output;
-#if TARGET_OS_IOS
-    desc.componentSubType = kAudioUnitSubType_RemoteIO;
+#if CAN_ENUMERATE
+    desc.componentSubType = (audioDevice == kAudioDeviceUnknown) ?
+        kAudioUnitSubType_DefaultOutput : kAudioUnitSubType_HALOutput;
 #else
-    desc.componentSubType = kAudioUnitSubType_HALOutput;
+    desc.componentSubType = kAudioUnitSubType_RemoteIO;
 #endif
     desc.componentManufacturer = kAudioUnitManufacturer_Apple;
     desc.componentFlags = 0;
     desc.componentFlagsMask = 0;
 
     // Search for component with given description
-    comp = AudioComponentFindNext(NULL, &desc);
+    AudioComponent comp{AudioComponentFindNext(NULL, &desc)};
     if(comp == NULL)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not find audio component"};
 
     // Open the component
-    err = AudioComponentInstanceNew(comp, &mAudioUnit);
+    OSStatus err{AudioComponentInstanceNew(comp, &mAudioUnit)};
     if(err != noErr)
         throw al::backend_exception{al::backend_error::NoDevice,
             "Could not create component instance: %u", err};
 
+#if CAN_ENUMERATE
+    if(audioDevice != kAudioDeviceUnknown)
+        AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, 0, &audioDevice, sizeof(AudioDeviceID));
+#endif
+
     // Turn off AudioUnit output
-    enableIO = 0;
+    UInt32 enableIO{0};
     err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_EnableIO,
         kAudioUnitScope_Output, 0, &enableIO, sizeof(enableIO));
     if(err != noErr)
@@ -436,35 +680,8 @@ void CoreAudioCapture::open(const char *name)
         throw al::backend_exception{al::backend_error::DeviceError,
             "Could not enable audio unit input property: %u", err};
 
-#if !TARGET_OS_IOS
-    {
-        // Get the default input device
-        AudioDeviceID inputDevice = kAudioDeviceUnknown;
-
-        propertySize = sizeof(AudioDeviceID);
-        AudioObjectPropertyAddress propertyAddress{};
-        propertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice;
-        propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
-        propertyAddress.mElement = kAudioObjectPropertyElementMaster;
-
-        err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, nullptr,
-            &propertySize, &inputDevice);
-        if(err != noErr)
-            throw al::backend_exception{al::backend_error::NoDevice,
-                "Could not get input device: %u", err};
-        if(inputDevice == kAudioDeviceUnknown)
-            throw al::backend_exception{al::backend_error::NoDevice, "Unknown input device"};
-
-        // Track the input device
-        err = AudioUnitSetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
-            kAudioUnitScope_Global, 0, &inputDevice, sizeof(AudioDeviceID));
-        if(err != noErr)
-            throw al::backend_exception{al::backend_error::NoDevice,
-                "Could not set input device: %u", err};
-    }
-#endif
-
     // set capture callback
+    AURenderCallbackStruct input{};
     input.inputProc = CoreAudioCapture::RecordProcC;
     input.inputProcRefCon = this;
 
@@ -489,14 +706,16 @@ void CoreAudioCapture::open(const char *name)
             "Could not initialize audio unit: %u", err};
 
     // Get the hardware format
-    propertySize = sizeof(AudioStreamBasicDescription);
+    AudioStreamBasicDescription hardwareFormat{};
+    UInt32 propertySize{sizeof(hardwareFormat)};
     err = AudioUnitGetProperty(mAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
         1, &hardwareFormat, &propertySize);
-    if(err != noErr || propertySize != sizeof(AudioStreamBasicDescription))
+    if(err != noErr || propertySize != sizeof(hardwareFormat))
         throw al::backend_exception{al::backend_error::DeviceError,
             "Could not get input format: %u", err};
 
     // Set up the requested format description
+    AudioStreamBasicDescription requestedFormat{};
     switch(mDevice->FmtType)
     {
     case DevFmtByte:
@@ -509,7 +728,8 @@ void CoreAudioCapture::open(const char *name)
         break;
     case DevFmtShort:
         requestedFormat.mBitsPerChannel = 16;
-        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger
+            | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
         break;
     case DevFmtUShort:
         requestedFormat.mBitsPerChannel = 16;
@@ -517,7 +737,8 @@ void CoreAudioCapture::open(const char *name)
         break;
     case DevFmtInt:
         requestedFormat.mBitsPerChannel = 32;
-        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        requestedFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger
+            | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
         break;
     case DevFmtUInt:
         requestedFormat.mBitsPerChannel = 32;
@@ -525,7 +746,8 @@ void CoreAudioCapture::open(const char *name)
         break;
     case DevFmtFloat:
         requestedFormat.mBitsPerChannel = 32;
-        requestedFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
+        requestedFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian
+            | kAudioFormatFlagIsPacked;
         break;
     }
 
@@ -540,7 +762,6 @@ void CoreAudioCapture::open(const char *name)
 
     case DevFmtQuad:
     case DevFmtX51:
-    case DevFmtX51Rear:
     case DevFmtX61:
     case DevFmtX71:
     case DevFmtAmbi3D:
@@ -561,7 +782,7 @@ void CoreAudioCapture::open(const char *name)
 
     // Use intermediate format for sample rate conversion (outputFormat)
     // Set sample rate to the same as hardware for resampling later
-    outputFormat = requestedFormat;
+    AudioStreamBasicDescription outputFormat{requestedFormat};
     outputFormat.mSampleRate = hardwareFormat.mSampleRate;
 
     // The output format should be the requested format, but using the hardware sample rate
@@ -600,7 +821,23 @@ void CoreAudioCapture::open(const char *name)
             mFormat.mChannelsPerFrame, static_cast<uint>(hardwareFormat.mSampleRate),
             mDevice->Frequency, Resampler::FastBSinc24);
 
+#if CAN_ENUMERATE
+    if(name)
+        mDevice->DeviceName = name;
+    else
+    {
+        UInt32 propSize{sizeof(audioDevice)};
+        audioDevice = kAudioDeviceUnknown;
+        AudioUnitGetProperty(mAudioUnit, kAudioOutputUnitProperty_CurrentDevice,
+            kAudioUnitScope_Global, 0, &audioDevice, &propSize);
+
+        std::string devname{GetDeviceName(audioDevice)};
+        if(!devname.empty()) mDevice->DeviceName = std::move(devname);
+        else mDevice->DeviceName = "Unknown Device Name";
+    }
+#else
     mDevice->DeviceName = name;
+#endif
 }
 
 
@@ -665,6 +902,26 @@ bool CoreAudioBackendFactory::querySupport(BackendType type)
 std::string CoreAudioBackendFactory::probe(BackendType type)
 {
     std::string outnames;
+#if CAN_ENUMERATE
+    auto append_name = [&outnames](const DeviceEntry &entry) -> void
+    {
+        /* Includes null char. */
+        outnames.append(entry.mName.c_str(), entry.mName.length()+1);
+    };
+    switch(type)
+    {
+    case BackendType::Playback:
+        EnumerateDevices(PlaybackList, false);
+        std::for_each(PlaybackList.cbegin(), PlaybackList.cend(), append_name);
+        break;
+    case BackendType::Capture:
+        EnumerateDevices(CaptureList, true);
+        std::for_each(CaptureList.cbegin(), CaptureList.cend(), append_name);
+        break;
+    }
+
+#else
+
     switch(type)
     {
     case BackendType::Playback:
@@ -673,10 +930,11 @@ std::string CoreAudioBackendFactory::probe(BackendType type)
         outnames.append(ca_device, sizeof(ca_device));
         break;
     }
+#endif
     return outnames;
 }
 
-BackendPtr CoreAudioBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr CoreAudioBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new CoreAudioPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/coreaudio.h → Engine/lib/openal-soft/alc/backends/coreaudio.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_COREAUDIO_H
 #define BACKENDS_COREAUDIO_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct CoreAudioBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 39 - 64
Engine/lib/openal-soft/Alc/backends/dsound.cpp → Engine/lib/openal-soft/alc/backends/dsound.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/dsound.h"
+#include "dsound.h"
 
 #define WIN32_LEAN_AND_MEAN
 #include <windows.h>
@@ -44,9 +44,10 @@
 #include <algorithm>
 #include <functional>
 
-#include "alcmain.h"
-#include "alu.h"
-#include "compat.h"
+#include "alnumeric.h"
+#include "comptr.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "dynload.h"
 #include "ringbuffer.h"
@@ -169,21 +170,21 @@ BOOL CALLBACK DSoundEnumDevices(GUID *guid, const WCHAR *desc, const WCHAR*, voi
 
 
 struct DSoundPlayback final : public BackendBase {
-    DSoundPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    DSoundPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~DSoundPlayback() override;
 
     int mixerProc();
 
-    void open(const ALCchar *name) override;
+    void open(const char *name) override;
     bool reset() override;
     void start() override;
     void stop() override;
 
-    IDirectSound       *mDS{nullptr};
-    IDirectSoundBuffer *mPrimaryBuffer{nullptr};
-    IDirectSoundBuffer *mBuffer{nullptr};
-    IDirectSoundNotify *mNotifies{nullptr};
-    HANDLE             mNotifyEvent{nullptr};
+    ComPtr<IDirectSound>       mDS;
+    ComPtr<IDirectSoundBuffer> mPrimaryBuffer;
+    ComPtr<IDirectSoundBuffer> mBuffer;
+    ComPtr<IDirectSoundNotify> mNotifies;
+    HANDLE mNotifyEvent{nullptr};
 
     std::atomic<bool> mKillNow{true};
     std::thread mThread;
@@ -193,19 +194,11 @@ struct DSoundPlayback final : public BackendBase {
 
 DSoundPlayback::~DSoundPlayback()
 {
-    if(mNotifies)
-        mNotifies->Release();
     mNotifies = nullptr;
-    if(mBuffer)
-        mBuffer->Release();
     mBuffer = nullptr;
-    if(mPrimaryBuffer)
-        mPrimaryBuffer->Release();
     mPrimaryBuffer = nullptr;
-
-    if(mDS)
-        mDS->Release();
     mDS = nullptr;
+
     if(mNotifyEvent)
         CloseHandle(mNotifyEvent);
     mNotifyEvent = nullptr;
@@ -234,8 +227,8 @@ FORCE_ALIGN int DSoundPlayback::mixerProc()
     bool Playing{false};
     DWORD LastCursor{0u};
     mBuffer->GetCurrentPosition(&LastCursor, nullptr);
-    while(!mKillNow.load(std::memory_order_acquire) &&
-          mDevice->Connected.load(std::memory_order_acquire))
+    while(!mKillNow.load(std::memory_order_acquire)
+        && mDevice->Connected.load(std::memory_order_acquire))
     {
         // Get current play cursor
         DWORD PlayCursor;
@@ -344,31 +337,34 @@ void DSoundPlayback::open(const char *name)
     }
 
     hr = DS_OK;
-    mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
-    if(!mNotifyEvent) hr = E_FAIL;
+    if(!mNotifyEvent)
+    {
+        mNotifyEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
+        if(!mNotifyEvent) hr = E_FAIL;
+    }
 
     //DirectSound Init code
+    ComPtr<IDirectSound> ds;
     if(SUCCEEDED(hr))
-        hr = DirectSoundCreate(guid, &mDS, nullptr);
+        hr = DirectSoundCreate(guid, ds.getPtr(), nullptr);
     if(SUCCEEDED(hr))
-        hr = mDS->SetCooperativeLevel(GetForegroundWindow(), DSSCL_PRIORITY);
+        hr = ds->SetCooperativeLevel(GetForegroundWindow(), DSSCL_PRIORITY);
     if(FAILED(hr))
         throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
             hr};
 
+    mNotifies = nullptr;
+    mBuffer = nullptr;
+    mPrimaryBuffer = nullptr;
+    mDS = std::move(ds);
+
     mDevice->DeviceName = name;
 }
 
 bool DSoundPlayback::reset()
 {
-    if(mNotifies)
-        mNotifies->Release();
     mNotifies = nullptr;
-    if(mBuffer)
-        mBuffer->Release();
     mBuffer = nullptr;
-    if(mPrimaryBuffer)
-        mPrimaryBuffer->Release();
     mPrimaryBuffer = nullptr;
 
     switch(mDevice->FmtType)
@@ -406,17 +402,14 @@ bool DSoundPlayback::reset()
                 mDevice->FmtChans = DevFmtStereo;
             else if(speakers == DSSPEAKER_QUAD)
                 mDevice->FmtChans = DevFmtQuad;
-            else if(speakers == DSSPEAKER_5POINT1_SURROUND)
+            else if(speakers == DSSPEAKER_5POINT1_SURROUND || speakers == DSSPEAKER_5POINT1_BACK)
                 mDevice->FmtChans = DevFmtX51;
-            else if(speakers == DSSPEAKER_5POINT1_BACK)
-                mDevice->FmtChans = DevFmtX51Rear;
             else if(speakers == DSSPEAKER_7POINT1 || speakers == DSSPEAKER_7POINT1_SURROUND)
                 mDevice->FmtChans = DevFmtX71;
             else
                 ERR("Unknown system speaker config: 0x%lx\n", speakers);
         }
-        mDevice->IsHeadphones = mDevice->FmtChans == DevFmtStereo
-            && speakers == DSSPEAKER_HEADPHONE;
+        mDevice->Flags.set(DirectEar, (speakers == DSSPEAKER_HEADPHONE));
 
         switch(mDevice->FmtChans)
         {
@@ -426,7 +419,6 @@ bool DSoundPlayback::reset()
         case DevFmtStereo: OutputType.dwChannelMask = STEREO; break;
         case DevFmtQuad: OutputType.dwChannelMask = QUAD; break;
         case DevFmtX51: OutputType.dwChannelMask = X5DOT1; break;
-        case DevFmtX51Rear: OutputType.dwChannelMask = X5DOT1REAR; break;
         case DevFmtX61: OutputType.dwChannelMask = X6DOT1; break;
         case DevFmtX71: OutputType.dwChannelMask = X7DOT1; break;
         }
@@ -454,8 +446,6 @@ retry_open:
         else
             OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
 
-        if(mPrimaryBuffer)
-            mPrimaryBuffer->Release();
         mPrimaryBuffer = nullptr;
     }
     else
@@ -465,7 +455,7 @@ retry_open:
             DSBUFFERDESC DSBDescription{};
             DSBDescription.dwSize = sizeof(DSBDescription);
             DSBDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;
-            hr = mDS->CreateSoundBuffer(&DSBDescription, &mPrimaryBuffer, nullptr);
+            hr = mDS->CreateSoundBuffer(&DSBDescription, mPrimaryBuffer.getPtr(), nullptr);
         }
         if(SUCCEEDED(hr))
             hr = mPrimaryBuffer->SetFormat(&OutputType.Format);
@@ -485,7 +475,7 @@ retry_open:
         DSBDescription.dwBufferBytes = mDevice->BufferSize * OutputType.Format.nBlockAlign;
         DSBDescription.lpwfxFormat = &OutputType.Format;
 
-        hr = mDS->CreateSoundBuffer(&DSBDescription, &mBuffer, nullptr);
+        hr = mDS->CreateSoundBuffer(&DSBDescription, mBuffer.getPtr(), nullptr);
         if(FAILED(hr) && mDevice->FmtType == DevFmtFloat)
         {
             mDevice->FmtType = DevFmtShort;
@@ -499,7 +489,7 @@ retry_open:
         hr = mBuffer->QueryInterface(IID_IDirectSoundNotify, &ptr);
         if(SUCCEEDED(hr))
         {
-            mNotifies = static_cast<IDirectSoundNotify*>(ptr);
+            mNotifies = ComPtr<IDirectSoundNotify>{static_cast<IDirectSoundNotify*>(ptr)};
 
             uint num_updates{mDevice->BufferSize / mDevice->UpdateSize};
             assert(num_updates <= MAX_UPDATES);
@@ -517,14 +507,8 @@ retry_open:
 
     if(FAILED(hr))
     {
-        if(mNotifies)
-            mNotifies->Release();
         mNotifies = nullptr;
-        if(mBuffer)
-            mBuffer->Release();
         mBuffer = nullptr;
-        if(mPrimaryBuffer)
-            mPrimaryBuffer->Release();
         mPrimaryBuffer = nullptr;
         return false;
     }
@@ -558,7 +542,7 @@ void DSoundPlayback::stop()
 
 
 struct DSoundCapture final : public BackendBase {
-    DSoundCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    DSoundCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~DSoundCapture() override;
 
     void open(const char *name) override;
@@ -567,8 +551,8 @@ struct DSoundCapture final : public BackendBase {
     void captureSamples(al::byte *buffer, uint samples) override;
     uint availableSamples() override;
 
-    IDirectSoundCapture *mDSC{nullptr};
-    IDirectSoundCaptureBuffer *mDSCbuffer{nullptr};
+    ComPtr<IDirectSoundCapture> mDSC;
+    ComPtr<IDirectSoundCaptureBuffer> mDSCbuffer;
     DWORD mBufferBytes{0u};
     DWORD mCursor{0u};
 
@@ -582,12 +566,8 @@ DSoundCapture::~DSoundCapture()
     if(mDSCbuffer)
     {
         mDSCbuffer->Stop();
-        mDSCbuffer->Release();
         mDSCbuffer = nullptr;
     }
-
-    if(mDSC)
-        mDSC->Release();
     mDSC = nullptr;
 }
 
@@ -653,7 +633,6 @@ void DSoundCapture::open(const char *name)
     case DevFmtStereo: InputType.dwChannelMask = STEREO; break;
     case DevFmtQuad: InputType.dwChannelMask = QUAD; break;
     case DevFmtX51: InputType.dwChannelMask = X5DOT1; break;
-    case DevFmtX51Rear: InputType.dwChannelMask = X5DOT1REAR; break;
     case DevFmtX61: InputType.dwChannelMask = X6DOT1; break;
     case DevFmtX71: InputType.dwChannelMask = X7DOT1; break;
     case DevFmtAmbi3D:
@@ -693,20 +672,16 @@ void DSoundCapture::open(const char *name)
     DSCBDescription.lpwfxFormat = &InputType.Format;
 
     //DirectSoundCapture Init code
-    hr = DirectSoundCaptureCreate(guid, &mDSC, nullptr);
+    hr = DirectSoundCaptureCreate(guid, mDSC.getPtr(), nullptr);
     if(SUCCEEDED(hr))
-        mDSC->CreateCaptureBuffer(&DSCBDescription, &mDSCbuffer, nullptr);
+        mDSC->CreateCaptureBuffer(&DSCBDescription, mDSCbuffer.getPtr(), nullptr);
     if(SUCCEEDED(hr))
          mRing = RingBuffer::Create(mDevice->BufferSize, InputType.Format.nBlockAlign, false);
 
     if(FAILED(hr))
     {
         mRing = nullptr;
-        if(mDSCbuffer)
-            mDSCbuffer->Release();
         mDSCbuffer = nullptr;
-        if(mDSC)
-            mDSC->Release();
         mDSC = nullptr;
 
         throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx",
@@ -858,7 +833,7 @@ std::string DSoundBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr DSoundBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr DSoundBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new DSoundPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/dsound.h → Engine/lib/openal-soft/alc/backends/dsound.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_DSOUND_H
 #define BACKENDS_DSOUND_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct DSoundBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 263 - 125
Engine/lib/openal-soft/Alc/backends/jack.cpp → Engine/lib/openal-soft/alc/backends/jack.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/jack.h"
+#include "jack.h"
 
 #include <cstdlib>
 #include <cstdio>
@@ -31,9 +31,10 @@
 #include <thread>
 #include <functional>
 
-#include "alcmain.h"
-#include "alu.h"
-#include "alconfig.h"
+#include "alc/alconfig.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "dynload.h"
 #include "ringbuffer.h"
@@ -45,9 +46,6 @@
 
 namespace {
 
-constexpr char jackDevice[] = "JACK Default";
-
-
 #ifdef HAVE_DYNLOAD
 #define JACK_FUNCS(MAGIC)          \
     MAGIC(jack_client_open);       \
@@ -101,6 +99,8 @@ decltype(jack_error_callback) * pjack_error_callback;
 #endif
 
 
+constexpr char JackDefaultAudioType[] = JACK_DEFAULT_AUDIO_TYPE;
+
 jack_options_t ClientOptions = JackNullOption;
 
 bool jack_load()
@@ -152,6 +152,11 @@ bool jack_load()
 }
 
 
+struct JackDeleter {
+    void operator()(void *ptr) { jack_free(ptr); }
+};
+using JackPortsPtr = std::unique_ptr<const char*[],JackDeleter>;
+
 struct DeviceEntry {
     std::string mName;
     std::string mPattern;
@@ -160,53 +165,125 @@ struct DeviceEntry {
 al::vector<DeviceEntry> PlaybackList;
 
 
-void EnumerateDevices(al::vector<DeviceEntry> &list)
+void EnumerateDevices(jack_client_t *client, al::vector<DeviceEntry> &list)
 {
-    al::vector<DeviceEntry>{}.swap(list);
+    std::remove_reference_t<decltype(list)>{}.swap(list);
+
+    if(JackPortsPtr ports{jack_get_ports(client, nullptr, JackDefaultAudioType, JackPortIsInput)})
+    {
+        for(size_t i{0};ports[i];++i)
+        {
+            const char *sep{std::strchr(ports[i], ':')};
+            if(!sep || ports[i] == sep) continue;
 
-    list.emplace_back(DeviceEntry{jackDevice, ""});
+            const al::span<const char> portdev{ports[i], sep};
+            auto check_name = [portdev](const DeviceEntry &entry) -> bool
+            {
+                const size_t len{portdev.size()};
+                return entry.mName.length() == len
+                    && entry.mName.compare(0, len, portdev.data(), len) == 0;
+            };
+            if(std::find_if(list.cbegin(), list.cend(), check_name) != list.cend())
+                continue;
 
-    std::string customList{ConfigValueStr(nullptr, "jack", "custom-devices").value_or("")};
-    size_t strpos{0};
-    while(strpos < customList.size())
+            std::string name{portdev.data(), portdev.size()};
+            list.emplace_back(DeviceEntry{name, name+":"});
+            const auto &entry = list.back();
+            TRACE("Got device: %s = %s\n", entry.mName.c_str(), entry.mPattern.c_str());
+        }
+        /* There are ports but couldn't get device names from them. Add a
+         * generic entry.
+         */
+        if(ports[0] && list.empty())
+        {
+            WARN("No device names found in available ports, adding a generic name.\n");
+            list.emplace_back(DeviceEntry{"JACK", ""});
+        }
+    }
+
+    if(auto listopt = ConfigValueStr(nullptr, "jack", "custom-devices"))
     {
-        size_t nextpos{customList.find(';', strpos)};
-        size_t seppos{customList.find('=', strpos)};
-        if(seppos >= nextpos || seppos == strpos)
+        for(size_t strpos{0};strpos < listopt->size();)
         {
-            const std::string entry{customList.substr(strpos, nextpos-strpos)};
-            ERR("Invalid device entry: \"%s\"\n", entry.c_str());
+            size_t nextpos{listopt->find(';', strpos)};
+            size_t seppos{listopt->find('=', strpos)};
+            if(seppos >= nextpos || seppos == strpos)
+            {
+                const std::string entry{listopt->substr(strpos, nextpos-strpos)};
+                ERR("Invalid device entry: \"%s\"\n", entry.c_str());
+                if(nextpos != std::string::npos) ++nextpos;
+                strpos = nextpos;
+                continue;
+            }
+
+            const al::span<const char> name{listopt->data()+strpos, seppos-strpos};
+            const al::span<const char> pattern{listopt->data()+(seppos+1),
+                std::min(nextpos, listopt->size())-(seppos+1)};
+
+            /* Check if this custom pattern already exists in the list. */
+            auto check_pattern = [pattern](const DeviceEntry &entry) -> bool
+            {
+                const size_t len{pattern.size()};
+                return entry.mPattern.length() == len
+                    && entry.mPattern.compare(0, len, pattern.data(), len) == 0;
+            };
+            auto itemmatch = std::find_if(list.begin(), list.end(), check_pattern);
+            if(itemmatch != list.end())
+            {
+                /* If so, replace the name with this custom one. */
+                itemmatch->mName.assign(name.data(), name.size());
+                TRACE("Customized device name: %s = %s\n", itemmatch->mName.c_str(),
+                    itemmatch->mPattern.c_str());
+            }
+            else
+            {
+                /* Otherwise, add a new device entry. */
+                list.emplace_back(DeviceEntry{std::string{name.data(), name.size()},
+                    std::string{pattern.data(), pattern.size()}});
+                const auto &entry = list.back();
+                TRACE("Got custom device: %s = %s\n", entry.mName.c_str(), entry.mPattern.c_str());
+            }
+
             if(nextpos != std::string::npos) ++nextpos;
             strpos = nextpos;
-            continue;
         }
+    }
 
-        size_t count{1};
-        std::string name{customList.substr(strpos, seppos-strpos)};
-        auto check_name = [&name](const DeviceEntry &entry) -> bool
-        { return entry.mName == name; };
-        while(std::find_if(list.cbegin(), list.cend(), check_name) != list.cend())
+    if(list.size() > 1)
+    {
+        /* Rename entries that have matching names, by appending '#2', '#3',
+         * etc, as needed.
+         */
+        for(auto curitem = list.begin()+1;curitem != list.end();++curitem)
         {
-            name = customList.substr(strpos, seppos-strpos);
-            name += " #";
-            name += std::to_string(++count);
+            auto check_match = [curitem](const DeviceEntry &entry) -> bool
+            { return entry.mName == curitem->mName; };
+            if(std::find_if(list.begin(), curitem, check_match) != curitem)
+            {
+                std::string name{curitem->mName};
+                size_t count{1};
+                auto check_name = [&name](const DeviceEntry &entry) -> bool
+                { return entry.mName == name; };
+                do {
+                    name = curitem->mName;
+                    name += " #";
+                    name += std::to_string(++count);
+                } while(std::find_if(list.begin(), curitem, check_name) != curitem);
+                curitem->mName = std::move(name);
+            }
         }
-
-        ++seppos;
-        list.emplace_back(DeviceEntry{std::move(name), customList.substr(seppos, nextpos-seppos)});
-        const auto &entry = list.back();
-        TRACE("Got custom device: %s = %s\n", entry.mName.c_str(), entry.mPattern.c_str());
-
-        if(nextpos != std::string::npos) ++nextpos;
-        strpos = nextpos;
     }
 }
 
 
 struct JackPlayback final : public BackendBase {
-    JackPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    JackPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~JackPlayback() override;
 
+    int processRt(jack_nframes_t numframes) noexcept;
+    static int processRtC(jack_nframes_t numframes, void *arg) noexcept
+    { return static_cast<JackPlayback*>(arg)->processRt(numframes); }
+
     int process(jack_nframes_t numframes) noexcept;
     static int processC(jack_nframes_t numframes, void *arg) noexcept
     { return static_cast<JackPlayback*>(arg)->process(numframes); }
@@ -227,6 +304,7 @@ struct JackPlayback final : public BackendBase {
     std::mutex mMutex;
 
     std::atomic<bool> mPlaying{false};
+    bool mRTMixing{false};
     RingBufferPtr mRing;
     al::semaphore mSem;
 
@@ -241,16 +319,40 @@ JackPlayback::~JackPlayback()
     if(!mClient)
         return;
 
-    std::for_each(mPort.begin(), mPort.end(),
-        [this](jack_port_t *port) -> void
-        { if(port) jack_port_unregister(mClient, port); }
-    );
+    auto unregister_port = [this](jack_port_t *port) -> void
+    { if(port) jack_port_unregister(mClient, port); };
+    std::for_each(mPort.begin(), mPort.end(), unregister_port);
     mPort.fill(nullptr);
+
     jack_client_close(mClient);
     mClient = nullptr;
 }
 
 
+int JackPlayback::processRt(jack_nframes_t numframes) noexcept
+{
+    std::array<jack_default_audio_sample_t*,MAX_OUTPUT_CHANNELS> out;
+    size_t numchans{0};
+    for(auto port : mPort)
+    {
+        if(!port || numchans == mDevice->RealOut.Buffer.size())
+            break;
+        out[numchans++] = static_cast<float*>(jack_port_get_buffer(port, numframes));
+    }
+
+    if LIKELY(mPlaying.load(std::memory_order_acquire))
+        mDevice->renderSamples({out.data(), numchans}, static_cast<uint>(numframes));
+    else
+    {
+        auto clear_buf = [numframes](float *outbuf) -> void
+        { std::fill_n(outbuf, numframes, 0.0f); };
+        std::for_each(out.begin(), out.begin()+numchans, clear_buf);
+    }
+
+    return 0;
+}
+
+
 int JackPlayback::process(jack_nframes_t numframes) noexcept
 {
     std::array<jack_default_audio_sample_t*,MAX_OUTPUT_CHANNELS> out;
@@ -352,49 +454,56 @@ int JackPlayback::mixerProc()
 
 void JackPlayback::open(const char *name)
 {
-    mPortPattern.clear();
-
-    if(!name)
-        name = jackDevice;
-    else if(strcmp(name, jackDevice) != 0)
+    if(!mClient)
     {
-        if(PlaybackList.empty())
-            EnumerateDevices(PlaybackList);
+        const PathNamePair &binname = GetProcBinary();
+        const char *client_name{binname.fname.empty() ? "alsoft" : binname.fname.c_str()};
+
+        jack_status_t status;
+        mClient = jack_client_open(client_name, ClientOptions, &status, nullptr);
+        if(mClient == nullptr)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to open client connection: 0x%02x", status};
+        if((status&JackServerStarted))
+            TRACE("JACK server started\n");
+        if((status&JackNameNotUnique))
+        {
+            client_name = jack_get_client_name(mClient);
+            TRACE("Client name not unique, got '%s' instead\n", client_name);
+        }
+    }
+
+    if(PlaybackList.empty())
+        EnumerateDevices(mClient, PlaybackList);
 
+    if(!name && !PlaybackList.empty())
+    {
+        name = PlaybackList[0].mName.c_str();
+        mPortPattern = PlaybackList[0].mPattern;
+    }
+    else
+    {
         auto check_name = [name](const DeviceEntry &entry) -> bool
         { return entry.mName == name; };
         auto iter = std::find_if(PlaybackList.cbegin(), PlaybackList.cend(), check_name);
         if(iter == PlaybackList.cend())
             throw al::backend_exception{al::backend_error::NoDevice,
-                "Device name \"%s\" not found", name};
+                "Device name \"%s\" not found", name?name:""};
         mPortPattern = iter->mPattern;
     }
 
-    const char *client_name{"alsoft"};
-    jack_status_t status;
-    mClient = jack_client_open(client_name, ClientOptions, &status, nullptr);
-    if(mClient == nullptr)
-        throw al::backend_exception{al::backend_error::DeviceError,
-            "Failed to open client connection: 0x%02x", status};
-
-    if((status&JackServerStarted))
-        TRACE("JACK server started\n");
-    if((status&JackNameNotUnique))
-    {
-        client_name = jack_get_client_name(mClient);
-        TRACE("Client name not unique, got '%s' instead\n", client_name);
-    }
-
-    jack_set_process_callback(mClient, &JackPlayback::processC, this);
+    mRTMixing = GetConfigValueBool(name, "jack", "rt-mix", 1);
+    jack_set_process_callback(mClient,
+        mRTMixing ? &JackPlayback::processRtC : &JackPlayback::processC, this);
 
     mDevice->DeviceName = name;
 }
 
 bool JackPlayback::reset()
 {
-    std::for_each(mPort.begin(), mPort.end(),
-        [this](jack_port_t *port) -> void
-        { if(port) jack_port_unregister(mClient, port); });
+    auto unregister_port = [this](jack_port_t *port) -> void
+    { if(port) jack_port_unregister(mClient, port); };
+    std::for_each(mPort.begin(), mPort.end(), unregister_port);
     mPort.fill(nullptr);
 
     /* Ignore the requested buffer metrics and just keep one JACK-sized buffer
@@ -402,28 +511,39 @@ bool JackPlayback::reset()
      */
     mDevice->Frequency = jack_get_sample_rate(mClient);
     mDevice->UpdateSize = jack_get_buffer_size(mClient);
-    mDevice->BufferSize = mDevice->UpdateSize * 2;
-
-    const char *devname{mDevice->DeviceName.c_str()};
-    uint bufsize{ConfigValueUInt(devname, "jack", "buffer-size").value_or(mDevice->UpdateSize)};
-    bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
-    mDevice->BufferSize = bufsize + mDevice->UpdateSize;
+    if(mRTMixing)
+    {
+        /* Assume only two periods when directly mixing. Should try to query
+         * the total port latency when connected.
+         */
+        mDevice->BufferSize = mDevice->UpdateSize * 2;
+    }
+    else
+    {
+        const char *devname{mDevice->DeviceName.c_str()};
+        uint bufsize{ConfigValueUInt(devname, "jack", "buffer-size").value_or(mDevice->UpdateSize)};
+        bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
+        mDevice->BufferSize = bufsize + mDevice->UpdateSize;
+    }
 
     /* Force 32-bit float output. */
     mDevice->FmtType = DevFmtFloat;
 
+    int port_num{0};
     auto ports_end = mPort.begin() + mDevice->channelsFromFmt();
-    auto bad_port = std::find_if_not(mPort.begin(), ports_end,
-        [this](jack_port_t *&port) -> bool
-        {
-            std::string name{"channel_" + std::to_string(&port - &mPort[0] + 1)};
-            port = jack_port_register(mClient, name.c_str(), JACK_DEFAULT_AUDIO_TYPE,
-                JackPortIsOutput, 0);
-            return port != nullptr;
-        });
+    auto bad_port = mPort.begin();
+    while(bad_port != ports_end)
+    {
+        std::string name{"channel_" + std::to_string(++port_num)};
+        *bad_port = jack_port_register(mClient, name.c_str(), JackDefaultAudioType,
+            JackPortIsOutput | JackPortIsTerminal, 0);
+        if(!*bad_port) break;
+        ++bad_port;
+    }
     if(bad_port != ports_end)
     {
-        ERR("Not enough JACK ports available for %s output\n", DevFmtChannelsString(mDevice->FmtChans));
+        ERR("Failed to register enough JACK ports for %s output\n",
+            DevFmtChannelsString(mDevice->FmtChans));
         if(bad_port == mPort.begin()) return false;
 
         if(bad_port == mPort.begin()+1)
@@ -453,29 +573,25 @@ void JackPlayback::start()
     const char *devname{mDevice->DeviceName.c_str()};
     if(ConfigValueBool(devname, "jack", "connect-ports").value_or(true))
     {
-        const char **ports{jack_get_ports(mClient, mPortPattern.c_str(), nullptr,
-            JackPortIsPhysical|JackPortIsInput)};
-        if(ports == nullptr)
+        JackPortsPtr pnames{jack_get_ports(mClient, mPortPattern.c_str(), JackDefaultAudioType,
+            JackPortIsInput)};
+        if(!pnames)
         {
             jack_deactivate(mClient);
-            throw al::backend_exception{al::backend_error::DeviceError,
-                "No physical playback ports found"};
+            throw al::backend_exception{al::backend_error::DeviceError, "No playback ports found"};
         }
-        auto connect_port = [this](const jack_port_t *port, const char *pname) -> bool
+
+        for(size_t i{0};i < al::size(mPort) && mPort[i];++i)
         {
-            if(!port) return false;
-            if(!pname)
+            if(!pnames[i])
             {
-                ERR("No physical playback port for \"%s\"\n", jack_port_name(port));
-                return false;
+                ERR("No physical playback port for \"%s\"\n", jack_port_name(mPort[i]));
+                break;
             }
-            if(jack_connect(mClient, jack_port_name(port), pname))
-                ERR("Failed to connect output port \"%s\" to \"%s\"\n", jack_port_name(port),
-                    pname);
-            return true;
-        };
-        std::mismatch(mPort.begin(), mPort.end(), ports, connect_port);
-        jack_free(ports);
+            if(jack_connect(mClient, jack_port_name(mPort[i]), pnames[i]))
+                ERR("Failed to connect output port \"%s\" to \"%s\"\n", jack_port_name(mPort[i]),
+                    pnames[i]);
+        }
     }
 
     /* Reconfigure buffer metrics in case the server changed it since the reset
@@ -486,36 +602,45 @@ void JackPlayback::start()
     mDevice->UpdateSize = jack_get_buffer_size(mClient);
     mDevice->BufferSize = mDevice->UpdateSize * 2;
 
-    uint bufsize{ConfigValueUInt(devname, "jack", "buffer-size").value_or(mDevice->UpdateSize)};
-    bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
-    mDevice->BufferSize = bufsize + mDevice->UpdateSize;
-
     mRing = nullptr;
-    mRing = RingBuffer::Create(bufsize, mDevice->frameSizeFromFmt(), true);
-
-    try {
+    if(mRTMixing)
         mPlaying.store(true, std::memory_order_release);
-        mKillNow.store(false, std::memory_order_release);
-        mThread = std::thread{std::mem_fn(&JackPlayback::mixerProc), this};
-    }
-    catch(std::exception& e) {
-        jack_deactivate(mClient);
-        mPlaying.store(false, std::memory_order_release);
-        throw al::backend_exception{al::backend_error::DeviceError,
-            "Failed to start mixing thread: %s", e.what()};
+    else
+    {
+        uint bufsize{ConfigValueUInt(devname, "jack", "buffer-size").value_or(mDevice->UpdateSize)};
+        bufsize = maxu(NextPowerOf2(bufsize), mDevice->UpdateSize);
+        mDevice->BufferSize = bufsize + mDevice->UpdateSize;
+
+        mRing = RingBuffer::Create(bufsize, mDevice->frameSizeFromFmt(), true);
+
+        try {
+            mPlaying.store(true, std::memory_order_release);
+            mKillNow.store(false, std::memory_order_release);
+            mThread = std::thread{std::mem_fn(&JackPlayback::mixerProc), this};
+        }
+        catch(std::exception& e) {
+            jack_deactivate(mClient);
+            mPlaying.store(false, std::memory_order_release);
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to start mixing thread: %s", e.what()};
+        }
     }
 }
 
 void JackPlayback::stop()
 {
-    if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
-        return;
-
-    mSem.post();
-    mThread.join();
+    if(mPlaying.load(std::memory_order_acquire))
+    {
+        mKillNow.store(true, std::memory_order_release);
+        if(mThread.joinable())
+        {
+            mSem.post();
+            mThread.join();
+        }
 
-    jack_deactivate(mClient);
-    mPlaying.store(false, std::memory_order_release);
+        jack_deactivate(mClient);
+        mPlaying.store(false, std::memory_order_release);
+    }
 }
 
 
@@ -525,7 +650,7 @@ ClockLatency JackPlayback::getClockLatency()
 
     std::lock_guard<std::mutex> _{mMutex};
     ret.ClockTime = GetDeviceClockTime(mDevice);
-    ret.Latency  = std::chrono::seconds{mRing->readSpace()};
+    ret.Latency  = std::chrono::seconds{mRing ? mRing->readSpace() : mDevice->UpdateSize};
     ret.Latency /= mDevice->Frequency;
 
     return ret;
@@ -547,10 +672,13 @@ bool JackBackendFactory::init()
     if(!GetConfigValueBool(nullptr, "jack", "spawn-server", 0))
         ClientOptions = static_cast<jack_options_t>(ClientOptions | JackNoStartServer);
 
+    const PathNamePair &binname = GetProcBinary();
+    const char *client_name{binname.fname.empty() ? "alsoft" : binname.fname.c_str()};
+
     void (*old_error_cb)(const char*){&jack_error_callback ? jack_error_callback : nullptr};
     jack_set_error_function(jack_msg_handler);
     jack_status_t status;
-    jack_client_t *client{jack_client_open("alsoft", ClientOptions, &status, nullptr)};
+    jack_client_t *client{jack_client_open(client_name, ClientOptions, &status, nullptr)};
     jack_set_error_function(old_error_cb);
     if(!client)
     {
@@ -575,10 +703,20 @@ std::string JackBackendFactory::probe(BackendType type)
         /* Includes null char. */
         outnames.append(entry.mName.c_str(), entry.mName.length()+1);
     };
+
+    const PathNamePair &binname = GetProcBinary();
+    const char *client_name{binname.fname.empty() ? "alsoft" : binname.fname.c_str()};
+    jack_status_t status;
     switch(type)
     {
     case BackendType::Playback:
-        EnumerateDevices(PlaybackList);
+        if(jack_client_t *client{jack_client_open(client_name, ClientOptions, &status, nullptr)})
+        {
+            EnumerateDevices(client, PlaybackList);
+            jack_client_close(client);
+        }
+        else
+            WARN("jack_client_open() failed, 0x%02x\n", status);
         std::for_each(PlaybackList.cbegin(), PlaybackList.cend(), append_name);
         break;
     case BackendType::Capture:
@@ -587,7 +725,7 @@ std::string JackBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr JackBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr JackBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new JackPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/jack.h → Engine/lib/openal-soft/alc/backends/jack.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_JACK_H
 #define BACKENDS_JACK_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct JackBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 6 - 7
Engine/lib/openal-soft/Alc/backends/loopback.cpp → Engine/lib/openal-soft/alc/backends/loopback.cpp

@@ -20,18 +20,17 @@
 
 #include "config.h"
 
-#include "backends/loopback.h"
+#include "loopback.h"
 
-#include "alcmain.h"
-#include "alu.h"
+#include "core/device.h"
 
 
 namespace {
 
 struct LoopbackBackend final : public BackendBase {
-    LoopbackBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+    LoopbackBackend(DeviceBase *device) noexcept : BackendBase{device} { }
 
-    void open(const ALCchar *name) override;
+    void open(const char *name) override;
     bool reset() override;
     void start() override;
     void stop() override;
@@ -40,7 +39,7 @@ struct LoopbackBackend final : public BackendBase {
 };
 
 
-void LoopbackBackend::open(const ALCchar *name)
+void LoopbackBackend::open(const char *name)
 {
     mDevice->DeviceName = name;
 }
@@ -69,7 +68,7 @@ bool LoopbackBackendFactory::querySupport(BackendType)
 std::string LoopbackBackendFactory::probe(BackendType)
 { return std::string{}; }
 
-BackendPtr LoopbackBackendFactory::createBackend(ALCdevice *device, BackendType)
+BackendPtr LoopbackBackendFactory::createBackend(DeviceBase *device, BackendType)
 { return BackendPtr{new LoopbackBackend{device}}; }
 
 BackendFactory &LoopbackBackendFactory::getFactory()

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/loopback.h → Engine/lib/openal-soft/alc/backends/loopback.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_LOOPBACK_H
 #define BACKENDS_LOOPBACK_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct LoopbackBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 5 - 5
Engine/lib/openal-soft/Alc/backends/null.cpp → Engine/lib/openal-soft/alc/backends/null.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/null.h"
+#include "null.h"
 
 #include <exception>
 #include <atomic>
@@ -30,9 +30,9 @@
 #include <functional>
 #include <thread>
 
-#include "alcmain.h"
+#include "core/device.h"
 #include "almalloc.h"
-#include "alu.h"
+#include "core/helpers.h"
 #include "threads.h"
 
 
@@ -46,7 +46,7 @@ constexpr char nullDevice[] = "No Output";
 
 
 struct NullBackend final : public BackendBase {
-    NullBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+    NullBackend(DeviceBase *device) noexcept : BackendBase{device} { }
 
     int mixerProc();
 
@@ -165,7 +165,7 @@ std::string NullBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr NullBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr NullBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new NullBackend{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/null.h → Engine/lib/openal-soft/alc/backends/null.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_NULL_H
 #define BACKENDS_NULL_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct NullBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 384 - 0
Engine/lib/openal-soft/alc/backends/oboe.cpp

@@ -0,0 +1,384 @@
+
+#include "config.h"
+
+#include "oboe.h"
+
+#include <cassert>
+#include <cstring>
+#include <stdint.h>
+
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/logging.h"
+
+#include "oboe/Oboe.h"
+
+
+namespace {
+
+constexpr char device_name[] = "Oboe Default";
+
+
+struct OboePlayback final : public BackendBase, public oboe::AudioStreamCallback {
+    OboePlayback(DeviceBase *device) : BackendBase{device} { }
+
+    oboe::ManagedStream mStream;
+
+    oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
+        int32_t numFrames) override;
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+};
+
+
+oboe::DataCallbackResult OboePlayback::onAudioReady(oboe::AudioStream *oboeStream, void *audioData,
+    int32_t numFrames)
+{
+    assert(numFrames > 0);
+    const int32_t numChannels{oboeStream->getChannelCount()};
+
+    mDevice->renderSamples(audioData, static_cast<uint32_t>(numFrames),
+        static_cast<uint32_t>(numChannels));
+    return oboe::DataCallbackResult::Continue;
+}
+
+
+void OboePlayback::open(const char *name)
+{
+    if(!name)
+        name = device_name;
+    else if(std::strcmp(name, device_name) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    /* Open a basic output stream, just to ensure it can work. */
+    oboe::ManagedStream stream;
+    oboe::Result result{oboe::AudioStreamBuilder{}.setDirection(oboe::Direction::Output)
+        ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+        ->openManagedStream(stream)};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s",
+            oboe::convertToText(result)};
+
+    mDevice->DeviceName = name;
+}
+
+bool OboePlayback::reset()
+{
+    oboe::AudioStreamBuilder builder;
+    builder.setDirection(oboe::Direction::Output);
+    builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
+    /* Don't let Oboe convert. We should be able to handle anything it gives
+     * back.
+     */
+    builder.setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None);
+    builder.setChannelConversionAllowed(false);
+    builder.setFormatConversionAllowed(false);
+    builder.setCallback(this);
+
+    if(mDevice->Flags.test(FrequencyRequest))
+        builder.setSampleRate(static_cast<int32_t>(mDevice->Frequency));
+    if(mDevice->Flags.test(ChannelsRequest))
+    {
+        /* Only use mono or stereo at user request. There's no telling what
+         * other counts may be inferred as.
+         */
+        builder.setChannelCount((mDevice->FmtChans==DevFmtMono) ? oboe::ChannelCount::Mono
+            : (mDevice->FmtChans==DevFmtStereo) ? oboe::ChannelCount::Stereo
+            : oboe::ChannelCount::Unspecified);
+    }
+    if(mDevice->Flags.test(SampleTypeRequest))
+    {
+        oboe::AudioFormat format{oboe::AudioFormat::Unspecified};
+        switch(mDevice->FmtType)
+        {
+        case DevFmtByte:
+        case DevFmtUByte:
+        case DevFmtShort:
+        case DevFmtUShort:
+            format = oboe::AudioFormat::I16;
+            break;
+        case DevFmtInt:
+        case DevFmtUInt:
+        case DevFmtFloat:
+            format = oboe::AudioFormat::Float;
+            break;
+        }
+        builder.setFormat(format);
+    }
+
+    oboe::Result result{builder.openManagedStream(mStream)};
+    /* If the format failed, try asking for the defaults. */
+    while(result == oboe::Result::ErrorInvalidFormat)
+    {
+        if(builder.getFormat() != oboe::AudioFormat::Unspecified)
+            builder.setFormat(oboe::AudioFormat::Unspecified);
+        else if(builder.getSampleRate() != oboe::kUnspecified)
+            builder.setSampleRate(oboe::kUnspecified);
+        else if(builder.getChannelCount() != oboe::ChannelCount::Unspecified)
+            builder.setChannelCount(oboe::ChannelCount::Unspecified);
+        else
+            break;
+        result = builder.openManagedStream(mStream);
+    }
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s",
+            oboe::convertToText(result)};
+    mStream->setBufferSizeInFrames(mini(static_cast<int32_t>(mDevice->BufferSize),
+        mStream->getBufferCapacityInFrames()));
+    TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get()));
+
+    if(static_cast<uint>(mStream->getChannelCount()) != mDevice->channelsFromFmt())
+    {
+        if(mStream->getChannelCount() >= 2)
+            mDevice->FmtChans = DevFmtStereo;
+        else if(mStream->getChannelCount() == 1)
+            mDevice->FmtChans = DevFmtMono;
+        else
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Got unhandled channel count: %d", mStream->getChannelCount()};
+    }
+    setDefaultWFXChannelOrder();
+
+    switch(mStream->getFormat())
+    {
+    case oboe::AudioFormat::I16:
+        mDevice->FmtType = DevFmtShort;
+        break;
+    case oboe::AudioFormat::Float:
+        mDevice->FmtType = DevFmtFloat;
+        break;
+    case oboe::AudioFormat::Unspecified:
+    case oboe::AudioFormat::Invalid:
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Got unhandled sample type: %s", oboe::convertToText(mStream->getFormat())};
+    }
+    mDevice->Frequency = static_cast<uint32_t>(mStream->getSampleRate());
+
+    /* Ensure the period size is no less than 10ms. It's possible for FramesPerCallback to be 0
+     * indicating variable updates, but OpenAL should have a reasonable minimum update size set.
+     * FramesPerBurst may not necessarily be correct, but hopefully it can act as a minimum
+     * update size.
+     */
+    mDevice->UpdateSize = maxu(mDevice->Frequency / 100,
+        static_cast<uint32_t>(mStream->getFramesPerBurst()));
+    mDevice->BufferSize = maxu(mDevice->UpdateSize * 2,
+        static_cast<uint32_t>(mStream->getBufferSizeInFrames()));
+
+    return true;
+}
+
+void OboePlayback::start()
+{
+    const oboe::Result result{mStream->start()};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to start stream: %s",
+            oboe::convertToText(result)};
+}
+
+void OboePlayback::stop()
+{
+    oboe::Result result{mStream->stop()};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to stop stream: %s",
+            oboe::convertToText(result)};
+}
+
+
+struct OboeCapture final : public BackendBase {
+    OboeCapture(DeviceBase *device) : BackendBase{device} { }
+
+    oboe::ManagedStream mStream;
+
+    std::vector<al::byte> mSamples;
+    uint mLastAvail{0u};
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+};
+
+void OboeCapture::open(const char *name)
+{
+    if(!name)
+        name = device_name;
+    else if(std::strcmp(name, device_name) != 0)
+        throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
+            name};
+
+    oboe::AudioStreamBuilder builder;
+    builder.setDirection(oboe::Direction::Input)
+        ->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+        ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::High)
+        ->setChannelConversionAllowed(true)
+        ->setFormatConversionAllowed(true)
+        ->setBufferCapacityInFrames(static_cast<int32_t>(mDevice->BufferSize))
+        ->setSampleRate(static_cast<int32_t>(mDevice->Frequency));
+    /* Only use mono or stereo at user request. There's no telling what
+     * other counts may be inferred as.
+     */
+    switch(mDevice->FmtChans)
+    {
+    case DevFmtMono:
+        builder.setChannelCount(oboe::ChannelCount::Mono);
+        break;
+    case DevFmtStereo:
+        builder.setChannelCount(oboe::ChannelCount::Stereo);
+        break;
+    case DevFmtQuad:
+    case DevFmtX51:
+    case DevFmtX61:
+    case DevFmtX71:
+    case DevFmtAmbi3D:
+        throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported",
+            DevFmtChannelsString(mDevice->FmtChans)};
+    }
+
+    /* FIXME: This really should support UByte, but Oboe doesn't. We'll need to
+     * use a temp buffer and convert.
+     */
+    switch(mDevice->FmtType)
+    {
+    case DevFmtShort:
+        builder.setFormat(oboe::AudioFormat::I16);
+        break;
+    case DevFmtFloat:
+        builder.setFormat(oboe::AudioFormat::Float);
+        break;
+    case DevFmtByte:
+    case DevFmtUByte:
+    case DevFmtUShort:
+    case DevFmtInt:
+    case DevFmtUInt:
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)};
+    }
+
+    oboe::Result result{builder.openManagedStream(mStream)};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s",
+            oboe::convertToText(result)};
+    if(static_cast<int32_t>(mDevice->BufferSize) > mStream->getBufferCapacityInFrames())
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Buffer size too large (%u > %d)", mDevice->BufferSize,
+            mStream->getBufferCapacityInFrames()};
+    auto buffer_result = mStream->setBufferSizeInFrames(static_cast<int32_t>(mDevice->BufferSize));
+    if(!buffer_result)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set buffer size: %s", oboe::convertToText(buffer_result.error())};
+    else if(buffer_result.value() < static_cast<int32_t>(mDevice->BufferSize))
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set large enough buffer size (%u > %d)", mDevice->BufferSize,
+            buffer_result.value()};
+    mDevice->BufferSize = static_cast<uint>(buffer_result.value());
+
+    TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get()));
+
+    mDevice->DeviceName = name;
+}
+
+void OboeCapture::start()
+{
+    const oboe::Result result{mStream->start()};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to start stream: %s",
+            oboe::convertToText(result)};
+}
+
+void OboeCapture::stop()
+{
+    /* Capture any unread samples before stopping. Oboe drops whatever's left
+     * in the stream.
+     */
+    if(auto availres = mStream->getAvailableFrames())
+    {
+        const auto avail = std::max(static_cast<uint>(availres.value()), mLastAvail);
+        const size_t frame_size{static_cast<uint32_t>(mStream->getBytesPerFrame())};
+        const size_t pos{mSamples.size()};
+        mSamples.resize(pos + avail*frame_size);
+
+        auto result = mStream->read(&mSamples[pos], availres.value(), 0);
+        uint got{bool{result} ? static_cast<uint>(result.value()) : 0u};
+        if(got < avail)
+            std::fill_n(&mSamples[pos + got*frame_size], (avail-got)*frame_size, al::byte{});
+        mLastAvail = 0;
+    }
+
+    const oboe::Result result{mStream->stop()};
+    if(result != oboe::Result::OK)
+        throw al::backend_exception{al::backend_error::DeviceError, "Failed to stop stream: %s",
+            oboe::convertToText(result)};
+}
+
+uint OboeCapture::availableSamples()
+{
+    /* Keep track of the max available frame count, to ensure it doesn't go
+     * backwards.
+     */
+    if(auto result = mStream->getAvailableFrames())
+        mLastAvail = std::max(static_cast<uint>(result.value()), mLastAvail);
+
+    const auto frame_size = static_cast<uint32_t>(mStream->getBytesPerFrame());
+    return static_cast<uint>(mSamples.size()/frame_size) + mLastAvail;
+}
+
+void OboeCapture::captureSamples(al::byte *buffer, uint samples)
+{
+    const auto frame_size = static_cast<uint>(mStream->getBytesPerFrame());
+    if(const size_t storelen{mSamples.size()})
+    {
+        const auto instore = static_cast<uint>(storelen / frame_size);
+        const uint tocopy{std::min(samples, instore) * frame_size};
+        std::copy_n(mSamples.begin(), tocopy, buffer);
+        mSamples.erase(mSamples.begin(), mSamples.begin() + tocopy);
+
+        buffer += tocopy;
+        samples -= tocopy/frame_size;
+        if(!samples) return;
+    }
+
+    auto result = mStream->read(buffer, static_cast<int32_t>(samples), 0);
+    uint got{bool{result} ? static_cast<uint>(result.value()) : 0u};
+    if(got < samples)
+        std::fill_n(buffer + got*frame_size, (samples-got)*frame_size, al::byte{});
+    mLastAvail = std::max(mLastAvail, samples) - samples;
+}
+
+} // namespace
+
+bool OboeBackendFactory::init() { return true; }
+
+bool OboeBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+std::string OboeBackendFactory::probe(BackendType type)
+{
+    switch(type)
+    {
+    case BackendType::Playback:
+    case BackendType::Capture:
+        /* Includes null char. */
+        return std::string{device_name, sizeof(device_name)};
+    }
+    return std::string{};
+}
+
+BackendPtr OboeBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new OboePlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new OboeCapture{device}};
+    return BackendPtr{};
+}
+
+BackendFactory &OboeBackendFactory::getFactory()
+{
+    static OboeBackendFactory factory{};
+    return factory;
+}

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/oboe.h → Engine/lib/openal-soft/alc/backends/oboe.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_OBOE_H
 #define BACKENDS_OBOE_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct OboeBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 23 - 14
Engine/lib/openal-soft/Alc/backends/opensl.cpp → Engine/lib/openal-soft/alc/backends/opensl.cpp

@@ -21,7 +21,7 @@
 
 #include "config.h"
 
-#include "backends/opensl.h"
+#include "opensl.h"
 
 #include <stdlib.h>
 #include <jni.h>
@@ -33,9 +33,9 @@
 #include <functional>
 
 #include "albit.h"
-#include "alcmain.h"
-#include "alu.h"
-#include "compat.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "opthelpers.h"
 #include "ringbuffer.h"
@@ -68,9 +68,6 @@ constexpr SLuint32 GetChannelMask(DevFmtChannels chans) noexcept
     case DevFmtX51: return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT |
         SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY | SL_SPEAKER_SIDE_LEFT |
         SL_SPEAKER_SIDE_RIGHT;
-    case DevFmtX51Rear: return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT |
-        SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY | SL_SPEAKER_BACK_LEFT |
-        SL_SPEAKER_BACK_RIGHT;
     case DevFmtX61: return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT |
         SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY | SL_SPEAKER_BACK_CENTER |
         SL_SPEAKER_SIDE_LEFT | SL_SPEAKER_SIDE_RIGHT;
@@ -105,7 +102,7 @@ constexpr SLuint32 GetTypeRepresentation(DevFmtType type) noexcept
 
 constexpr SLuint32 GetByteOrderEndianness() noexcept
 {
-    if_constexpr(al::endian::native == al::endian::little)
+    if(al::endian::native == al::endian::little)
         return SL_BYTEORDER_LITTLEENDIAN;
     return SL_BYTEORDER_BIGENDIAN;
 }
@@ -151,7 +148,7 @@ const char *res_str(SLresult result) noexcept
 
 
 struct OpenSLPlayback final : public BackendBase {
-    OpenSLPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    OpenSLPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~OpenSLPlayback() override;
 
     void process(SLAndroidSimpleBufferQueueItf bq) noexcept;
@@ -316,6 +313,9 @@ void OpenSLPlayback::open(const char *name)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
 
+    /* There's only one device, so if it's already open, there's nothing to do. */
+    if(mEngineObj) return;
+
     // create engine
     SLresult result{slCreateEngine(&mEngineObj, 0, nullptr, 0, nullptr, nullptr)};
     PRINTERR(result, "slCreateEngine");
@@ -629,7 +629,7 @@ ClockLatency OpenSLPlayback::getClockLatency()
 
 
 struct OpenSLCapture final : public BackendBase {
-    OpenSLCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    OpenSLCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~OpenSLCapture() override;
 
     void process(SLAndroidSimpleBufferQueueItf bq) noexcept;
@@ -810,8 +810,11 @@ void OpenSLCapture::open(const char* name)
     if(SL_RESULT_SUCCESS == result)
     {
         const uint chunk_size{mDevice->UpdateSize * mFrameSize};
+        const auto silence = (mDevice->FmtType == DevFmtUByte) ? al::byte{0x80} : al::byte{0};
 
         auto data = mRing->getWriteVector();
+        std::fill_n(data.first.buf, data.first.len*chunk_size, silence);
+        std::fill_n(data.second.buf, data.second.len*chunk_size, silence);
         for(size_t i{0u};i < data.first.len && SL_RESULT_SUCCESS == result;i++)
         {
             result = VCALL(bufferQueue,Enqueue)(data.first.buf + chunk_size*i, chunk_size);
@@ -875,6 +878,7 @@ void OpenSLCapture::captureSamples(al::byte *buffer, uint samples)
 {
     const uint update_size{mDevice->UpdateSize};
     const uint chunk_size{update_size * mFrameSize};
+    const auto silence = (mDevice->FmtType == DevFmtUByte) ? al::byte{0x80} : al::byte{0};
 
     /* Read the desired samples from the ring buffer then advance its read
      * pointer.
@@ -922,15 +926,20 @@ void OpenSLCapture::captureSamples(al::byte *buffer, uint samples)
     {
         SLresult result{SL_RESULT_SUCCESS};
         auto wdata = mRing->getWriteVector();
+        std::fill_n(wdata.first.buf, wdata.first.len*chunk_size, silence);
         for(size_t i{0u};i < wdata.first.len && SL_RESULT_SUCCESS == result;i++)
         {
             result = VCALL(bufferQueue,Enqueue)(wdata.first.buf + chunk_size*i, chunk_size);
             PRINTERR(result, "bufferQueue->Enqueue");
         }
-        for(size_t i{0u};i < wdata.second.len && SL_RESULT_SUCCESS == result;i++)
+        if(wdata.second.len > 0)
         {
-            result = VCALL(bufferQueue,Enqueue)(wdata.second.buf + chunk_size*i, chunk_size);
-            PRINTERR(result, "bufferQueue->Enqueue");
+            std::fill_n(wdata.second.buf, wdata.second.len*chunk_size, silence);
+            for(size_t i{0u};i < wdata.second.len && SL_RESULT_SUCCESS == result;i++)
+            {
+                result = VCALL(bufferQueue,Enqueue)(wdata.second.buf + chunk_size*i, chunk_size);
+                PRINTERR(result, "bufferQueue->Enqueue");
+            }
         }
     }
 }
@@ -959,7 +968,7 @@ std::string OSLBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr OSLBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr OSLBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new OpenSLPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/opensl.h → Engine/lib/openal-soft/alc/backends/opensl.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_OSL_H
 #define BACKENDS_OSL_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct OSLBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 14 - 10
Engine/lib/openal-soft/Alc/backends/oss.cpp → Engine/lib/openal-soft/alc/backends/oss.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/oss.h"
+#include "oss.h"
 
 #include <fcntl.h>
 #include <poll.h>
@@ -41,13 +41,13 @@
 #include <thread>
 #include <utility>
 
-#include "alcmain.h"
-#include "alconfig.h"
 #include "albyte.h"
+#include "alc/alconfig.h"
 #include "almalloc.h"
 #include "alnumeric.h"
 #include "aloptional.h"
-#include "alu.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "ringbuffer.h"
 #include "threads.h"
@@ -226,7 +226,7 @@ uint log2i(uint x)
 
 
 struct OSSPlayback final : public BackendBase {
-    OSSPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    OSSPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~OSSPlayback() override;
 
     int mixerProc();
@@ -249,7 +249,7 @@ struct OSSPlayback final : public BackendBase {
 OSSPlayback::~OSSPlayback()
 {
     if(mFd != -1)
-        close(mFd);
+        ::close(mFd);
     mFd = -1;
 }
 
@@ -328,11 +328,15 @@ void OSSPlayback::open(const char *name)
         devname = iter->device_name.c_str();
     }
 
-    mFd = ::open(devname, O_WRONLY);
-    if(mFd == -1)
+    int fd{::open(devname, O_WRONLY)};
+    if(fd == -1)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not open %s: %s", devname,
             strerror(errno)};
 
+    if(mFd != -1)
+        ::close(mFd);
+    mFd = fd;
+
     mDevice->DeviceName = name;
 }
 
@@ -438,7 +442,7 @@ void OSSPlayback::stop()
 
 
 struct OSScapture final : public BackendBase {
-    OSScapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    OSScapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~OSScapture() override;
 
     int recordProc();
@@ -676,7 +680,7 @@ std::string OSSBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr OSSBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr OSSBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new OSSPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/oss.h → Engine/lib/openal-soft/alc/backends/oss.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_OSS_H
 #define BACKENDS_OSS_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct OSSBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 2008 - 0
Engine/lib/openal-soft/alc/backends/pipewire.cpp

@@ -0,0 +1,2008 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 2010 by Chris Robinson
+ * This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ *  License along with this library; if not, write to the
+ *  Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * Or go to http://www.gnu.org/copyleft/lgpl.html
+ */
+
+#include "config.h"
+
+#include "pipewire.h"
+
+#include <algorithm>
+#include <atomic>
+#include <cstring>
+#include <cerrno>
+#include <chrono>
+#include <ctime>
+#include <list>
+#include <memory>
+#include <mutex>
+#include <stdint.h>
+#include <type_traits>
+#include <utility>
+
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "alstring.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/helpers.h"
+#include "core/logging.h"
+#include "dynload.h"
+#include "opthelpers.h"
+#include "ringbuffer.h"
+
+/* Ignore warnings caused by PipeWire headers (lots in standard C++ mode). */
+_Pragma("GCC diagnostic push")
+_Pragma("GCC diagnostic ignored \"-Weverything\"")
+#include "pipewire/pipewire.h"
+#include "pipewire/extensions/metadata.h"
+#include "spa/buffer/buffer.h"
+#include "spa/param/audio/format-utils.h"
+#include "spa/param/audio/raw.h"
+#include "spa/param/param.h"
+#include "spa/pod/builder.h"
+#include "spa/utils/json.h"
+
+namespace {
+/* Wrap some nasty macros here too... */
+template<typename ...Args>
+auto ppw_core_add_listener(pw_core *core, Args&& ...args)
+{ return pw_core_add_listener(core, std::forward<Args>(args)...); }
+template<typename ...Args>
+auto ppw_core_sync(pw_core *core, Args&& ...args)
+{ return pw_core_sync(core, std::forward<Args>(args)...); }
+template<typename ...Args>
+auto ppw_registry_add_listener(pw_registry *reg, Args&& ...args)
+{ return pw_registry_add_listener(reg, std::forward<Args>(args)...); }
+template<typename ...Args>
+auto ppw_node_add_listener(pw_node *node, Args&& ...args)
+{ return pw_node_add_listener(node, std::forward<Args>(args)...); }
+template<typename ...Args>
+auto ppw_node_subscribe_params(pw_node *node, Args&& ...args)
+{ return pw_node_subscribe_params(node, std::forward<Args>(args)...); }
+template<typename ...Args>
+auto ppw_metadata_add_listener(pw_metadata *mdata, Args&& ...args)
+{ return pw_metadata_add_listener(mdata, std::forward<Args>(args)...); }
+
+
+constexpr auto get_pod_type(const spa_pod *pod) noexcept
+{ return SPA_POD_TYPE(pod); }
+
+template<typename T>
+constexpr auto get_pod_body(const spa_pod *pod, size_t count) noexcept
+{ return al::span<T>{static_cast<T*>(SPA_POD_BODY(pod)), count}; }
+template<typename T, size_t N>
+constexpr auto get_pod_body(const spa_pod *pod) noexcept
+{ return al::span<T,N>{static_cast<T*>(SPA_POD_BODY(pod)), N}; }
+
+constexpr auto make_pod_builder(void *data, uint32_t size) noexcept
+{ return SPA_POD_BUILDER_INIT(data, size); }
+
+constexpr auto get_array_value_type(const spa_pod *pod) noexcept
+{ return SPA_POD_ARRAY_VALUE_TYPE(pod); }
+
+constexpr auto PwIdAny = PW_ID_ANY;
+
+} // namespace
+_Pragma("GCC diagnostic pop")
+
+namespace {
+
+using std::chrono::seconds;
+using std::chrono::nanoseconds;
+using uint = unsigned int;
+
+constexpr char pwireDevice[] = "PipeWire Output";
+constexpr char pwireInput[] = "PipeWire Input";
+
+
+#ifdef HAVE_DYNLOAD
+#define PWIRE_FUNCS(MAGIC)                                                    \
+    MAGIC(pw_context_connect)                                                 \
+    MAGIC(pw_context_destroy)                                                 \
+    MAGIC(pw_context_new)                                                     \
+    MAGIC(pw_core_disconnect)                                                 \
+    MAGIC(pw_init)                                                            \
+    MAGIC(pw_properties_free)                                                 \
+    MAGIC(pw_properties_new)                                                  \
+    MAGIC(pw_properties_set)                                                  \
+    MAGIC(pw_properties_setf)                                                 \
+    MAGIC(pw_proxy_add_object_listener)                                       \
+    MAGIC(pw_proxy_destroy)                                                   \
+    MAGIC(pw_proxy_get_user_data)                                             \
+    MAGIC(pw_stream_add_listener)                                             \
+    MAGIC(pw_stream_connect)                                                  \
+    MAGIC(pw_stream_dequeue_buffer)                                           \
+    MAGIC(pw_stream_destroy)                                                  \
+    MAGIC(pw_stream_get_state)                                                \
+    MAGIC(pw_stream_get_time)                                                 \
+    MAGIC(pw_stream_new)                                                      \
+    MAGIC(pw_stream_queue_buffer)                                             \
+    MAGIC(pw_stream_set_active)                                               \
+    MAGIC(pw_thread_loop_new)                                                 \
+    MAGIC(pw_thread_loop_destroy)                                             \
+    MAGIC(pw_thread_loop_get_loop)                                            \
+    MAGIC(pw_thread_loop_start)                                               \
+    MAGIC(pw_thread_loop_stop)                                                \
+    MAGIC(pw_thread_loop_lock)                                                \
+    MAGIC(pw_thread_loop_wait)                                                \
+    MAGIC(pw_thread_loop_signal)                                              \
+    MAGIC(pw_thread_loop_unlock)                                              \
+
+void *pwire_handle;
+#define MAKE_FUNC(f) decltype(f) * p##f;
+PWIRE_FUNCS(MAKE_FUNC)
+#undef MAKE_FUNC
+
+bool pwire_load()
+{
+    if(pwire_handle)
+        return true;
+
+    static constexpr char pwire_library[] = "libpipewire-0.3.so.0";
+    std::string missing_funcs;
+
+    pwire_handle = LoadLib(pwire_library);
+    if(!pwire_handle)
+    {
+        WARN("Failed to load %s\n", pwire_library);
+        return false;
+    }
+
+#define LOAD_FUNC(f) do {                                                     \
+    p##f = reinterpret_cast<decltype(p##f)>(GetSymbol(pwire_handle, #f));     \
+    if(p##f == nullptr) missing_funcs += "\n" #f;                             \
+} while(0);
+    PWIRE_FUNCS(LOAD_FUNC)
+#undef LOAD_FUNC
+
+    if(!missing_funcs.empty())
+    {
+        WARN("Missing expected functions:%s\n", missing_funcs.c_str());
+        CloseLib(pwire_handle);
+        pwire_handle = nullptr;
+        return false;
+    }
+
+    return true;
+}
+
+#ifndef IN_IDE_PARSER
+#define pw_context_connect ppw_context_connect
+#define pw_context_destroy ppw_context_destroy
+#define pw_context_new ppw_context_new
+#define pw_core_disconnect ppw_core_disconnect
+#define pw_init ppw_init
+#define pw_properties_free ppw_properties_free
+#define pw_properties_new ppw_properties_new
+#define pw_properties_set ppw_properties_set
+#define pw_properties_setf ppw_properties_setf
+#define pw_proxy_add_object_listener ppw_proxy_add_object_listener
+#define pw_proxy_destroy ppw_proxy_destroy
+#define pw_proxy_get_user_data ppw_proxy_get_user_data
+#define pw_stream_add_listener ppw_stream_add_listener
+#define pw_stream_connect ppw_stream_connect
+#define pw_stream_dequeue_buffer ppw_stream_dequeue_buffer
+#define pw_stream_destroy ppw_stream_destroy
+#define pw_stream_get_state ppw_stream_get_state
+#define pw_stream_get_time ppw_stream_get_time
+#define pw_stream_new ppw_stream_new
+#define pw_stream_queue_buffer ppw_stream_queue_buffer
+#define pw_stream_set_active ppw_stream_set_active
+#define pw_thread_loop_destroy ppw_thread_loop_destroy
+#define pw_thread_loop_get_loop ppw_thread_loop_get_loop
+#define pw_thread_loop_lock ppw_thread_loop_lock
+#define pw_thread_loop_new ppw_thread_loop_new
+#define pw_thread_loop_signal ppw_thread_loop_signal
+#define pw_thread_loop_start ppw_thread_loop_start
+#define pw_thread_loop_stop ppw_thread_loop_stop
+#define pw_thread_loop_unlock ppw_thread_loop_unlock
+#define pw_thread_loop_wait ppw_thread_loop_wait
+#endif
+
+#else
+
+constexpr bool pwire_load() { return true; }
+#endif
+
+/* Helpers for retrieving values from params */
+template<uint32_t T> struct PodInfo { };
+
+template<>
+struct PodInfo<SPA_TYPE_Int> {
+    using Type = int32_t;
+    static auto get_value(const spa_pod *pod, int32_t *val)
+    { return spa_pod_get_int(pod, val); }
+};
+template<>
+struct PodInfo<SPA_TYPE_Id> {
+    using Type = uint32_t;
+    static auto get_value(const spa_pod *pod, uint32_t *val)
+    { return spa_pod_get_id(pod, val); }
+};
+
+template<uint32_t T>
+using Pod_t = typename PodInfo<T>::Type;
+
+template<uint32_t T>
+al::span<const Pod_t<T>> get_array_span(const spa_pod *pod)
+{
+    uint32_t nvals;
+    if(void *v{spa_pod_get_array(pod, &nvals)})
+    {
+        if(get_array_value_type(pod) == T)
+            return {static_cast<const Pod_t<T>*>(v), nvals};
+    }
+    return {};
+}
+
+template<uint32_t T>
+al::optional<Pod_t<T>> get_value(const spa_pod *value)
+{
+    Pod_t<T> val{};
+    if(PodInfo<T>::get_value(value, &val) == 0)
+        return val;
+    return al::nullopt;
+}
+
+/* Internally, PipeWire types "inherit" from each other, but this is hidden
+ * from the API and the caller is expected to C-style cast to inherited types
+ * as needed. It's also not made very clear what types a given type can be
+ * casted to. To make it a bit safer, this as() method allows casting pw_*
+ * types to known inherited types, generating a compile-time error for
+ * unexpected/invalid casts.
+ */
+template<typename To, typename From>
+To as(From) noexcept = delete;
+
+/* pw_proxy
+ * - pw_registry
+ * - pw_node
+ * - pw_metadata
+ */
+template<>
+pw_proxy* as(pw_registry *reg) noexcept { return reinterpret_cast<pw_proxy*>(reg); }
+template<>
+pw_proxy* as(pw_node *node) noexcept { return reinterpret_cast<pw_proxy*>(node); }
+template<>
+pw_proxy* as(pw_metadata *mdata) noexcept { return reinterpret_cast<pw_proxy*>(mdata); }
+
+
+struct PwContextDeleter {
+    void operator()(pw_context *context) const { pw_context_destroy(context); }
+};
+using PwContextPtr = std::unique_ptr<pw_context,PwContextDeleter>;
+
+struct PwCoreDeleter {
+    void operator()(pw_core *core) const { pw_core_disconnect(core); }
+};
+using PwCorePtr = std::unique_ptr<pw_core,PwCoreDeleter>;
+
+struct PwRegistryDeleter {
+    void operator()(pw_registry *reg) const { pw_proxy_destroy(as<pw_proxy*>(reg)); }
+};
+using PwRegistryPtr = std::unique_ptr<pw_registry,PwRegistryDeleter>;
+
+struct PwNodeDeleter {
+    void operator()(pw_node *node) const { pw_proxy_destroy(as<pw_proxy*>(node)); }
+};
+using PwNodePtr = std::unique_ptr<pw_node,PwNodeDeleter>;
+
+struct PwMetadataDeleter {
+    void operator()(pw_metadata *mdata) const { pw_proxy_destroy(as<pw_proxy*>(mdata)); }
+};
+using PwMetadataPtr = std::unique_ptr<pw_metadata,PwMetadataDeleter>;
+
+struct PwStreamDeleter {
+    void operator()(pw_stream *stream) const { pw_stream_destroy(stream); }
+};
+using PwStreamPtr = std::unique_ptr<pw_stream,PwStreamDeleter>;
+
+/* Enums for bitflags... again... *sigh* */
+constexpr pw_stream_flags operator|(pw_stream_flags lhs, pw_stream_flags rhs) noexcept
+{ return static_cast<pw_stream_flags>(lhs | std::underlying_type_t<pw_stream_flags>{rhs}); }
+
+class ThreadMainloop {
+    pw_thread_loop *mLoop{};
+
+public:
+    ThreadMainloop() = default;
+    ThreadMainloop(const ThreadMainloop&) = delete;
+    ThreadMainloop(ThreadMainloop&& rhs) noexcept : mLoop{rhs.mLoop} { rhs.mLoop = nullptr; }
+    explicit ThreadMainloop(pw_thread_loop *loop) noexcept : mLoop{loop} { }
+    ~ThreadMainloop() { if(mLoop) pw_thread_loop_destroy(mLoop); }
+
+    ThreadMainloop& operator=(const ThreadMainloop&) = delete;
+    ThreadMainloop& operator=(ThreadMainloop&& rhs) noexcept
+    { std::swap(mLoop, rhs.mLoop); return *this; }
+    ThreadMainloop& operator=(std::nullptr_t) noexcept
+    {
+        if(mLoop)
+            pw_thread_loop_destroy(mLoop);
+        mLoop = nullptr;
+        return *this;
+    }
+
+    explicit operator bool() const noexcept { return mLoop != nullptr; }
+
+    auto start() const { return pw_thread_loop_start(mLoop); }
+    auto stop() const { return pw_thread_loop_stop(mLoop); }
+
+    auto getLoop() const { return pw_thread_loop_get_loop(mLoop); }
+
+    auto lock() const { return pw_thread_loop_lock(mLoop); }
+    auto unlock() const { return pw_thread_loop_unlock(mLoop); }
+
+    auto signal(bool wait) const { return pw_thread_loop_signal(mLoop, wait); }
+
+    auto newContext(pw_properties *props=nullptr, size_t user_data_size=0)
+    { return PwContextPtr{pw_context_new(getLoop(), props, user_data_size)}; }
+
+    static auto Create(const char *name, spa_dict *props=nullptr)
+    { return ThreadMainloop{pw_thread_loop_new(name, props)}; }
+
+    friend struct MainloopUniqueLock;
+};
+struct MainloopUniqueLock : public std::unique_lock<ThreadMainloop> {
+    using std::unique_lock<ThreadMainloop>::unique_lock;
+    MainloopUniqueLock& operator=(MainloopUniqueLock&&) = default;
+
+    auto wait() const -> void
+    { pw_thread_loop_wait(mutex()->mLoop); }
+
+    template<typename Predicate>
+    auto wait(Predicate done_waiting) const -> void
+    { while(!done_waiting()) wait(); }
+};
+using MainloopLockGuard = std::lock_guard<ThreadMainloop>;
+
+
+/* There's quite a mess here, but the purpose is to track active devices and
+ * their default formats, so playback devices can be configured to match. The
+ * device list is updated asynchronously, so it will have the latest list of
+ * devices provided by the server.
+ */
+
+struct NodeProxy;
+struct MetadataProxy;
+
+/* The global thread watching for global events. This particular class responds
+ * to objects being added to or removed from the registry.
+ */
+struct EventManager {
+    ThreadMainloop mLoop{};
+    PwContextPtr mContext{};
+    PwCorePtr mCore{};
+    PwRegistryPtr mRegistry{};
+    spa_hook mRegistryListener{};
+    spa_hook mCoreListener{};
+
+    /* A list of proxy objects watching for events about changes to objects in
+     * the registry.
+     */
+    std::vector<NodeProxy*> mNodeList;
+    MetadataProxy *mDefaultMetadata{nullptr};
+
+    /* Initialization handling. When init() is called, mInitSeq is set to a
+     * SequenceID that marks the end of populating the registry. As objects of
+     * interest are found, events to parse them are generated and mInitSeq is
+     * updated with a newer ID. When mInitSeq stops being updated and the event
+     * corresponding to it is reached, mInitDone will be set to true.
+     */
+    std::atomic<bool> mInitDone{false};
+    std::atomic<bool> mHasAudio{false};
+    int mInitSeq{};
+
+    bool init();
+    ~EventManager();
+
+    void kill();
+
+    auto lock() const { return mLoop.lock(); }
+    auto unlock() const { return mLoop.unlock(); }
+
+    /**
+     * Waits for initialization to finish. The event manager must *NOT* be
+     * locked when calling this.
+     */
+    void waitForInit()
+    {
+        if(unlikely(!mInitDone.load(std::memory_order_acquire)))
+        {
+            MainloopUniqueLock plock{mLoop};
+            plock.wait([this](){ return mInitDone.load(std::memory_order_acquire); });
+        }
+    }
+
+    /**
+     * Waits for audio support to be detected, or initialization to finish,
+     * whichever is first. Returns true if audio support was detected. The
+     * event manager must *NOT* be locked when calling this.
+     */
+    bool waitForAudio()
+    {
+        MainloopUniqueLock plock{mLoop};
+        bool has_audio{};
+        plock.wait([this,&has_audio]()
+        {
+            has_audio = mHasAudio.load(std::memory_order_acquire);
+            return has_audio || mInitDone.load(std::memory_order_acquire);
+        });
+        return has_audio;
+    }
+
+    void syncInit()
+    {
+        /* If initialization isn't done, update the sequence ID so it won't
+         * complete until after currently scheduled events.
+         */
+        if(!mInitDone.load(std::memory_order_relaxed))
+            mInitSeq = ppw_core_sync(mCore.get(), PW_ID_CORE, mInitSeq);
+    }
+
+    void addCallback(uint32_t id, uint32_t permissions, const char *type, uint32_t version,
+        const spa_dict *props);
+    static void addCallbackC(void *object, uint32_t id, uint32_t permissions, const char *type,
+        uint32_t version, const spa_dict *props)
+    { static_cast<EventManager*>(object)->addCallback(id, permissions, type, version, props); }
+
+    void removeCallback(uint32_t id);
+    static void removeCallbackC(void *object, uint32_t id)
+    { static_cast<EventManager*>(object)->removeCallback(id); }
+
+    static constexpr pw_registry_events CreateRegistryEvents()
+    {
+        pw_registry_events ret{};
+        ret.version = PW_VERSION_REGISTRY_EVENTS;
+        ret.global = &EventManager::addCallbackC;
+        ret.global_remove = &EventManager::removeCallbackC;
+        return ret;
+    }
+
+    void coreCallback(uint32_t id, int seq);
+    static void coreCallbackC(void *object, uint32_t id, int seq)
+    { static_cast<EventManager*>(object)->coreCallback(id, seq); }
+
+    static constexpr pw_core_events CreateCoreEvents()
+    {
+        pw_core_events ret{};
+        ret.version = PW_VERSION_CORE_EVENTS;
+        ret.done = &EventManager::coreCallbackC;
+        return ret;
+    }
+};
+using EventWatcherUniqueLock = std::unique_lock<EventManager>;
+using EventWatcherLockGuard = std::lock_guard<EventManager>;
+
+EventManager gEventHandler;
+
+/* Enumerated devices. This is updated asynchronously as the app runs, and the
+ * gEventHandler thread loop must be locked when accessing the list.
+ */
+enum class NodeType : unsigned char {
+    Sink, Source, Duplex
+};
+constexpr auto InvalidChannelConfig = DevFmtChannels(255);
+struct DeviceNode {
+    std::string mName;
+    std::string mDevName;
+
+    uint32_t mId{};
+    NodeType mType{};
+    bool mIsHeadphones{};
+    bool mIs51Rear{};
+
+    uint mSampleRate{};
+    DevFmtChannels mChannels{InvalidChannelConfig};
+
+    static std::vector<DeviceNode> sList;
+    static DeviceNode &Add(uint32_t id);
+    static DeviceNode *Find(uint32_t id);
+    static void Remove(uint32_t id);
+    static std::vector<DeviceNode> &GetList() noexcept { return sList; }
+
+    void parseSampleRate(const spa_pod *value) noexcept;
+    void parsePositions(const spa_pod *value) noexcept;
+    void parseChannelCount(const spa_pod *value) noexcept;
+};
+std::vector<DeviceNode> DeviceNode::sList;
+std::string DefaultSinkDevice;
+std::string DefaultSourceDevice;
+
+const char *AsString(NodeType type) noexcept
+{
+    switch(type)
+    {
+    case NodeType::Sink: return "sink";
+    case NodeType::Source: return "source";
+    case NodeType::Duplex: return "duplex";
+    }
+    return "<unknown>";
+}
+
+DeviceNode &DeviceNode::Add(uint32_t id)
+{
+    auto match_id = [id](DeviceNode &n) noexcept -> bool
+    { return n.mId == id; };
+
+    /* If the node is already in the list, return the existing entry. */
+    auto match = std::find_if(sList.begin(), sList.end(), match_id);
+    if(match != sList.end()) return *match;
+
+    sList.emplace_back();
+    auto &n = sList.back();
+    n.mId = id;
+    return n;
+}
+
+DeviceNode *DeviceNode::Find(uint32_t id)
+{
+    auto match_id = [id](DeviceNode &n) noexcept -> bool
+    { return n.mId == id; };
+
+    auto match = std::find_if(sList.begin(), sList.end(), match_id);
+    if(match != sList.end()) return std::addressof(*match);
+
+    return nullptr;
+}
+
+void DeviceNode::Remove(uint32_t id)
+{
+    auto match_id = [id](DeviceNode &n) noexcept -> bool
+    {
+        if(n.mId != id)
+            return false;
+        TRACE("Removing device \"%s\"\n", n.mDevName.c_str());
+        return true;
+    };
+
+    auto end = std::remove_if(sList.begin(), sList.end(), match_id);
+    sList.erase(end, sList.end());
+}
+
+
+const spa_audio_channel MonoMap[]{
+    SPA_AUDIO_CHANNEL_MONO
+}, StereoMap[] {
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR
+}, QuadMap[]{
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR
+}, X51Map[]{
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
+    SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
+}, X51RearMap[]{
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
+    SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR
+}, X61Map[]{
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
+    SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
+}, X71Map[]{
+    SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
+    SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR
+};
+
+/**
+ * Checks if every channel in 'map1' exists in 'map0' (that is, map0 is equal
+ * to or a superset of map1).
+ */
+template<size_t N>
+bool MatchChannelMap(const al::span<const uint32_t> map0, const spa_audio_channel (&map1)[N])
+{
+    if(map0.size() < N)
+        return false;
+    for(const spa_audio_channel chid : map1)
+    {
+        if(std::find(map0.begin(), map0.end(), chid) == map0.end())
+            return false;
+    }
+    return true;
+}
+
+void DeviceNode::parseSampleRate(const spa_pod *value) noexcept
+{
+    /* TODO: Can this be anything else? Long, Float, Double? */
+    uint32_t nvals{}, choiceType{};
+    value = spa_pod_get_values(value, &nvals, &choiceType);
+
+    const uint podType{get_pod_type(value)};
+    if(podType != SPA_TYPE_Int)
+    {
+        WARN("Unhandled sample rate POD type: %u\n", podType);
+        return;
+    }
+
+    if(choiceType == SPA_CHOICE_Range)
+    {
+        if(nvals != 3)
+        {
+            WARN("Unexpected SPA_CHOICE_Range count: %u\n", nvals);
+            return;
+        }
+        auto srates = get_pod_body<int32_t,3>(value);
+
+        /* [0] is the default, [1] is the min, and [2] is the max. */
+        TRACE("Device ID %u sample rate: %d (range: %d -> %d)\n", mId, srates[0], srates[1],
+            srates[2]);
+        mSampleRate = static_cast<uint>(clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE));
+        return;
+    }
+
+    if(choiceType == SPA_CHOICE_Enum)
+    {
+        if(nvals == 0)
+        {
+            WARN("Unexpected SPA_CHOICE_Enum count: %u\n", nvals);
+            return;
+        }
+        auto srates = get_pod_body<int32_t>(value, nvals);
+
+        /* [0] is the default, [1...size()-1] are available selections. */
+        std::string others{(srates.size() > 1) ? std::to_string(srates[1]) : std::string{}};
+        for(size_t i{2};i < srates.size();++i)
+        {
+            others += ", ";
+            others += std::to_string(srates[i]);
+        }
+        TRACE("Device ID %u sample rate: %d (%s)\n", mId, srates[0], others.c_str());
+        /* Pick the first rate listed that's within the allowed range (default
+         * rate if possible).
+         */
+        for(const auto &rate : srates)
+        {
+            if(rate >= MIN_OUTPUT_RATE && rate <= MAX_OUTPUT_RATE)
+            {
+                mSampleRate = static_cast<uint>(rate);
+                break;
+            }
+        }
+        return;
+    }
+
+    if(choiceType == SPA_CHOICE_None)
+    {
+        if(nvals != 1)
+        {
+            WARN("Unexpected SPA_CHOICE_None count: %u\n", nvals);
+            return;
+        }
+        auto srates = get_pod_body<int32_t,1>(value);
+
+        TRACE("Device ID %u sample rate: %d\n", mId, srates[0]);
+        mSampleRate = static_cast<uint>(clampi(srates[0], MIN_OUTPUT_RATE, MAX_OUTPUT_RATE));
+        return;
+    }
+
+    WARN("Unhandled sample rate choice type: %u\n", choiceType);
+}
+
+void DeviceNode::parsePositions(const spa_pod *value) noexcept
+{
+    const auto chanmap = get_array_span<SPA_TYPE_Id>(value);
+    if(chanmap.empty()) return;
+
+    mIs51Rear = false;
+
+    if(MatchChannelMap(chanmap, X71Map))
+        mChannels = DevFmtX71;
+    else if(MatchChannelMap(chanmap, X61Map))
+        mChannels = DevFmtX61;
+    else if(MatchChannelMap(chanmap, X51Map))
+        mChannels = DevFmtX51;
+    else if(MatchChannelMap(chanmap, X51RearMap))
+    {
+        mChannels = DevFmtX51;
+        mIs51Rear = true;
+    }
+    else if(MatchChannelMap(chanmap, QuadMap))
+        mChannels = DevFmtQuad;
+    else if(MatchChannelMap(chanmap, StereoMap))
+        mChannels = DevFmtStereo;
+    else
+        mChannels = DevFmtMono;
+    TRACE("Device ID %u got %zu position%s for %s%s\n", mId, chanmap.size(),
+        (chanmap.size()==1)?"":"s", DevFmtChannelsString(mChannels), mIs51Rear?"(rear)":"");
+}
+
+void DeviceNode::parseChannelCount(const spa_pod *value) noexcept
+{
+    /* As a fallback with just a channel count, just assume mono or stereo. */
+    const auto chancount = get_value<SPA_TYPE_Int>(value);
+    if(!chancount) return;
+
+    mIs51Rear = false;
+
+    if(*chancount >= 2)
+        mChannels = DevFmtStereo;
+    else if(*chancount >= 1)
+        mChannels = DevFmtMono;
+    TRACE("Device ID %u got %d channel%s for %s\n", mId, *chancount, (*chancount==1)?"":"s",
+        DevFmtChannelsString(mChannels));
+}
+
+
+constexpr char MonitorPrefix[]{"Monitor of "};
+constexpr auto MonitorPrefixLen = al::size(MonitorPrefix) - 1;
+constexpr char AudioSinkClass[]{"Audio/Sink"};
+constexpr char AudioSourceClass[]{"Audio/Source"};
+constexpr char AudioDuplexClass[]{"Audio/Duplex"};
+constexpr char StreamClass[]{"Stream/"};
+
+/* A generic PipeWire node proxy object used to track changes to sink and
+ * source nodes.
+ */
+struct NodeProxy {
+    static constexpr pw_node_events CreateNodeEvents()
+    {
+        pw_node_events ret{};
+        ret.version = PW_VERSION_NODE_EVENTS;
+        ret.info = &NodeProxy::infoCallbackC;
+        ret.param = &NodeProxy::paramCallbackC;
+        return ret;
+    }
+
+    uint32_t mId{};
+
+    PwNodePtr mNode{};
+    spa_hook mListener{};
+
+    NodeProxy(uint32_t id, PwNodePtr node)
+      : mId{id}, mNode{std::move(node)}
+    {
+        static constexpr pw_node_events nodeEvents{CreateNodeEvents()};
+        ppw_node_add_listener(mNode.get(), &mListener, &nodeEvents, this);
+
+        /* Track changes to the enumerable formats (indicates the default
+         * format, which is what we're interested in).
+         */
+        uint32_t fmtids[]{SPA_PARAM_EnumFormat};
+        ppw_node_subscribe_params(mNode.get(), al::data(fmtids), al::size(fmtids));
+    }
+    ~NodeProxy()
+    { spa_hook_remove(&mListener); }
+
+
+    void infoCallback(const pw_node_info *info);
+    static void infoCallbackC(void *object, const pw_node_info *info)
+    { static_cast<NodeProxy*>(object)->infoCallback(info); }
+
+    void paramCallback(int seq, uint32_t id, uint32_t index, uint32_t next, const spa_pod *param);
+    static void paramCallbackC(void *object, int seq, uint32_t id, uint32_t index, uint32_t next,
+        const spa_pod *param)
+    { static_cast<NodeProxy*>(object)->paramCallback(seq, id, index, next, param); }
+};
+
+void NodeProxy::infoCallback(const pw_node_info *info)
+{
+    /* We only care about property changes here (media class, name/desc).
+     * Format changes will automatically invoke the param callback.
+     *
+     * TODO: Can the media class or name/desc change without being removed and
+     * readded?
+     */
+    if((info->change_mask&PW_NODE_CHANGE_MASK_PROPS))
+    {
+        /* Can this actually change? */
+        const char *media_class{spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS)};
+        if(unlikely(!media_class)) return;
+
+        NodeType ntype{};
+        if(al::strcasecmp(media_class, AudioSinkClass) == 0)
+            ntype = NodeType::Sink;
+        else if(al::strcasecmp(media_class, AudioSourceClass) == 0)
+            ntype = NodeType::Source;
+        else if(al::strcasecmp(media_class, AudioDuplexClass) == 0)
+            ntype = NodeType::Duplex;
+        else
+        {
+            TRACE("Dropping device node %u which became type \"%s\"\n", info->id, media_class);
+            DeviceNode::Remove(info->id);
+            return;
+        }
+
+        const char *devName{spa_dict_lookup(info->props, PW_KEY_NODE_NAME)};
+        const char *nodeName{spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION)};
+        if(!nodeName || !*nodeName) nodeName = spa_dict_lookup(info->props, PW_KEY_NODE_NICK);
+        if(!nodeName || !*nodeName) nodeName = devName;
+
+        const char *form_factor{spa_dict_lookup(info->props, PW_KEY_DEVICE_FORM_FACTOR)};
+        TRACE("Got %s device \"%s\"%s%s%s\n", AsString(ntype), devName ? devName : "(nil)",
+            form_factor?" (":"", form_factor?form_factor:"", form_factor?")":"");
+        TRACE("  \"%s\" = ID %u\n", nodeName ? nodeName : "(nil)", info->id);
+
+        DeviceNode &node = DeviceNode::Add(info->id);
+        if(nodeName && *nodeName) node.mName = nodeName;
+        else node.mName = "PipeWire node #"+std::to_string(info->id);
+        node.mDevName = devName ? devName : "";
+        node.mType = ntype;
+        node.mIsHeadphones = form_factor && (al::strcasecmp(form_factor, "headphones") == 0
+            || al::strcasecmp(form_factor, "headset") == 0);
+    }
+}
+
+void NodeProxy::paramCallback(int, uint32_t id, uint32_t, uint32_t, const spa_pod *param)
+{
+    if(id == SPA_PARAM_EnumFormat)
+    {
+        DeviceNode *node{DeviceNode::Find(mId)};
+        if(unlikely(!node)) return;
+
+        if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_rate)})
+            node->parseSampleRate(&prop->value);
+
+        if(const spa_pod_prop *prop{spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_position)})
+            node->parsePositions(&prop->value);
+        else if((prop=spa_pod_find_prop(param, nullptr, SPA_FORMAT_AUDIO_channels)) != nullptr)
+            node->parseChannelCount(&prop->value);
+    }
+}
+
+
+/* A metadata proxy object used to query the default sink and source. */
+struct MetadataProxy {
+    static constexpr pw_metadata_events CreateMetadataEvents()
+    {
+        pw_metadata_events ret{};
+        ret.version = PW_VERSION_METADATA_EVENTS;
+        ret.property = &MetadataProxy::propertyCallbackC;
+        return ret;
+    }
+
+    uint32_t mId{};
+
+    PwMetadataPtr mMetadata{};
+    spa_hook mListener{};
+
+    MetadataProxy(uint32_t id, PwMetadataPtr mdata)
+      : mId{id}, mMetadata{std::move(mdata)}
+    {
+        static constexpr pw_metadata_events metadataEvents{CreateMetadataEvents()};
+        ppw_metadata_add_listener(mMetadata.get(), &mListener, &metadataEvents, this);
+    }
+    ~MetadataProxy()
+    { spa_hook_remove(&mListener); }
+
+
+    int propertyCallback(uint32_t id, const char *key, const char *type, const char *value);
+    static int propertyCallbackC(void *object, uint32_t id, const char *key, const char *type,
+        const char *value)
+    { return static_cast<MetadataProxy*>(object)->propertyCallback(id, key, type, value); }
+};
+
+int MetadataProxy::propertyCallback(uint32_t id, const char *key, const char *type,
+    const char *value)
+{
+    if(id != PW_ID_CORE)
+        return 0;
+
+    bool isCapture{};
+    if(std::strcmp(key, "default.audio.sink") == 0)
+        isCapture = false;
+    else if(std::strcmp(key, "default.audio.source") == 0)
+        isCapture = true;
+    else
+        return 0;
+
+    if(!type)
+    {
+        TRACE("Default %s device cleared\n", isCapture ? "capture" : "playback");
+        if(!isCapture) DefaultSinkDevice.clear();
+        else DefaultSourceDevice.clear();
+        return 0;
+    }
+    if(std::strcmp(type, "Spa:String:JSON") != 0)
+    {
+        ERR("Unexpected %s property type: %s\n", key, type);
+        return 0;
+    }
+
+    spa_json it[2]{};
+    spa_json_init(&it[0], value, strlen(value));
+    if(spa_json_enter_object(&it[0], &it[1]) <= 0)
+        return 0;
+
+    auto get_json_string = [](spa_json *iter)
+    {
+        al::optional<std::string> str;
+
+        const char *val{};
+        int len{spa_json_next(iter, &val)};
+        if(len <= 0) return str;
+
+        str.emplace().resize(static_cast<uint>(len), '\0');
+        if(spa_json_parse_string(val, len, &str->front()) <= 0)
+            str.reset();
+        else while(!str->empty() && str->back() == '\0')
+            str->pop_back();
+        return str;
+    };
+    while(auto propKey = get_json_string(&it[1]))
+    {
+        if(*propKey == "name")
+        {
+            auto propValue = get_json_string(&it[1]);
+            if(!propValue) break;
+
+            TRACE("Got default %s device \"%s\"\n", isCapture ? "capture" : "playback",
+                propValue->c_str());
+            if(!isCapture)
+                DefaultSinkDevice = std::move(*propValue);
+            else
+                DefaultSourceDevice = std::move(*propValue);
+        }
+        else
+        {
+            const char *v{};
+            if(spa_json_next(&it[1], &v) <= 0)
+                break;
+        }
+    }
+    return 0;
+}
+
+
+bool EventManager::init()
+{
+    mLoop = ThreadMainloop::Create("PWEventThread");
+    if(!mLoop)
+    {
+        ERR("Failed to create PipeWire event thread loop (errno: %d)\n", errno);
+        return false;
+    }
+
+    mContext = mLoop.newContext(pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr));
+    if(!mContext)
+    {
+        ERR("Failed to create PipeWire event context (errno: %d)\n", errno);
+        return false;
+    }
+
+    mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
+    if(!mCore)
+    {
+        ERR("Failed to connect PipeWire event context (errno: %d)\n", errno);
+        return false;
+    }
+
+    mRegistry = PwRegistryPtr{pw_core_get_registry(mCore.get(), PW_VERSION_REGISTRY, 0)};
+    if(!mRegistry)
+    {
+        ERR("Failed to get PipeWire event registry (errno: %d)\n", errno);
+        return false;
+    }
+
+    static constexpr pw_core_events coreEvents{CreateCoreEvents()};
+    static constexpr pw_registry_events registryEvents{CreateRegistryEvents()};
+
+    ppw_core_add_listener(mCore.get(), &mCoreListener, &coreEvents, this);
+    ppw_registry_add_listener(mRegistry.get(), &mRegistryListener, &registryEvents, this);
+
+    /* Set an initial sequence ID for initialization, to trigger after the
+     * registry is first populated.
+     */
+    mInitSeq = ppw_core_sync(mCore.get(), PW_ID_CORE, 0);
+
+    if(int res{mLoop.start()})
+    {
+        ERR("Failed to start PipeWire event thread loop (res: %d)\n", res);
+        return false;
+    }
+
+    return true;
+}
+
+EventManager::~EventManager()
+{
+    if(mLoop) mLoop.stop();
+
+    for(NodeProxy *node : mNodeList)
+        al::destroy_at(node);
+    if(mDefaultMetadata)
+        al::destroy_at(mDefaultMetadata);
+}
+
+void EventManager::kill()
+{
+    if(mLoop) mLoop.stop();
+
+    for(NodeProxy *node : mNodeList)
+        al::destroy_at(node);
+    mNodeList.clear();
+    if(mDefaultMetadata)
+        al::destroy_at(mDefaultMetadata);
+    mDefaultMetadata = nullptr;
+
+    mRegistry = nullptr;
+    mCore = nullptr;
+    mContext = nullptr;
+    mLoop = nullptr;
+}
+
+void EventManager::addCallback(uint32_t id, uint32_t, const char *type, uint32_t version,
+    const spa_dict *props)
+{
+    /* We're only interested in interface nodes. */
+    if(std::strcmp(type, PW_TYPE_INTERFACE_Node) == 0)
+    {
+        const char *media_class{spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)};
+        if(!media_class) return;
+
+        /* Specifically, audio sinks and sources (and duplexes). */
+        const bool isGood{al::strcasecmp(media_class, AudioSinkClass) == 0
+            || al::strcasecmp(media_class, AudioSourceClass) == 0
+            || al::strcasecmp(media_class, AudioDuplexClass) == 0};
+        if(!isGood)
+        {
+            if(std::strstr(media_class, "/Video") == nullptr
+                && std::strncmp(media_class, StreamClass, sizeof(StreamClass)-1) != 0)
+                TRACE("Ignoring node class %s\n", media_class);
+            return;
+        }
+
+        /* Create the proxy object. */
+        auto node = PwNodePtr{static_cast<pw_node*>(pw_registry_bind(mRegistry.get(), id, type,
+            version, sizeof(NodeProxy)))};
+        if(!node)
+        {
+            ERR("Failed to create node proxy object (errno: %d)\n", errno);
+            return;
+        }
+
+        /* Initialize the NodeProxy to hold the node object, add it to the
+         * active node list, and update the sync point.
+         */
+        auto *proxy = static_cast<NodeProxy*>(pw_proxy_get_user_data(as<pw_proxy*>(node.get())));
+        mNodeList.emplace_back(al::construct_at(proxy, id, std::move(node)));
+        syncInit();
+
+        /* Signal any waiters that we have found a source or sink for audio
+         * support.
+         */
+        if(!mHasAudio.exchange(true, std::memory_order_acq_rel))
+            mLoop.signal(false);
+    }
+    else if(std::strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0)
+    {
+        const char *data_class{spa_dict_lookup(props, PW_KEY_METADATA_NAME)};
+        if(!data_class) return;
+
+        if(std::strcmp(data_class, "default") != 0)
+        {
+            TRACE("Ignoring metadata \"%s\"\n", data_class);
+            return;
+        }
+
+        if(mDefaultMetadata)
+        {
+            ERR("Duplicate default metadata\n");
+            return;
+        }
+
+        auto mdata = PwMetadataPtr{static_cast<pw_metadata*>(pw_registry_bind(mRegistry.get(), id,
+            type, version, sizeof(MetadataProxy)))};
+        if(!mdata)
+        {
+            ERR("Failed to create metadata proxy object (errno: %d)\n", errno);
+            return;
+        }
+
+        auto *proxy = static_cast<MetadataProxy*>(
+            pw_proxy_get_user_data(as<pw_proxy*>(mdata.get())));
+        mDefaultMetadata = al::construct_at(proxy, id, std::move(mdata));
+        syncInit();
+    }
+}
+
+void EventManager::removeCallback(uint32_t id)
+{
+    DeviceNode::Remove(id);
+
+    auto clear_node = [id](NodeProxy *node) noexcept
+    {
+        if(node->mId != id)
+            return false;
+        al::destroy_at(node);
+        return true;
+    };
+    auto node_end = std::remove_if(mNodeList.begin(), mNodeList.end(), clear_node);
+    mNodeList.erase(node_end, mNodeList.end());
+
+    if(mDefaultMetadata && mDefaultMetadata->mId == id)
+    {
+        al::destroy_at(mDefaultMetadata);
+        mDefaultMetadata = nullptr;
+    }
+}
+
+void EventManager::coreCallback(uint32_t id, int seq)
+{
+    if(id == PW_ID_CORE && seq == mInitSeq)
+    {
+        /* Initialization done. Remove this callback and signal anyone that may
+         * be waiting.
+         */
+        spa_hook_remove(&mCoreListener);
+
+        mInitDone.store(true);
+        mLoop.signal(false);
+    }
+}
+
+
+enum use_f32p_e : bool { UseDevType=false, ForceF32Planar=true };
+spa_audio_info_raw make_spa_info(DeviceBase *device, bool is51rear, use_f32p_e use_f32p)
+{
+    spa_audio_info_raw info{};
+    if(use_f32p)
+    {
+        device->FmtType = DevFmtFloat;
+        info.format = SPA_AUDIO_FORMAT_F32P;
+    }
+    else switch(device->FmtType)
+    {
+    case DevFmtByte: info.format = SPA_AUDIO_FORMAT_S8; break;
+    case DevFmtUByte: info.format = SPA_AUDIO_FORMAT_U8; break;
+    case DevFmtShort: info.format = SPA_AUDIO_FORMAT_S16; break;
+    case DevFmtUShort: info.format = SPA_AUDIO_FORMAT_U16; break;
+    case DevFmtInt: info.format = SPA_AUDIO_FORMAT_S32; break;
+    case DevFmtUInt: info.format = SPA_AUDIO_FORMAT_U32; break;
+    case DevFmtFloat: info.format = SPA_AUDIO_FORMAT_F32; break;
+    }
+
+    info.rate = device->Frequency;
+
+    al::span<const spa_audio_channel> map{};
+    switch(device->FmtChans)
+    {
+    case DevFmtMono: map = MonoMap; break;
+    case DevFmtStereo: map = StereoMap; break;
+    case DevFmtQuad: map = QuadMap; break;
+    case DevFmtX51:
+        if(is51rear) map = X51RearMap;
+        else map = X51Map;
+        break;
+    case DevFmtX61: map = X61Map; break;
+    case DevFmtX71: map = X71Map; break;
+    case DevFmtAmbi3D:
+        info.flags |= SPA_AUDIO_FLAG_UNPOSITIONED;
+        info.channels = device->channelsFromFmt();
+        break;
+    }
+    if(!map.empty())
+    {
+        info.channels = static_cast<uint32_t>(map.size());
+        std::copy(map.begin(), map.end(), info.position);
+    }
+
+    return info;
+}
+
+class PipeWirePlayback final : public BackendBase {
+    void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
+    static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
+        const char *error)
+    { static_cast<PipeWirePlayback*>(data)->stateChangedCallback(old, state, error); }
+
+    void ioChangedCallback(uint32_t id, void *area, uint32_t size);
+    static void ioChangedCallbackC(void *data, uint32_t id, void *area, uint32_t size)
+    { static_cast<PipeWirePlayback*>(data)->ioChangedCallback(id, area, size); }
+
+    void outputCallback();
+    static void outputCallbackC(void *data)
+    { static_cast<PipeWirePlayback*>(data)->outputCallback(); }
+
+    void open(const char *name) override;
+    bool reset() override;
+    void start() override;
+    void stop() override;
+    ClockLatency getClockLatency() override;
+
+    uint32_t mTargetId{PwIdAny};
+    nanoseconds mTimeBase{0};
+    ThreadMainloop mLoop;
+    PwContextPtr mContext;
+    PwCorePtr mCore;
+    PwStreamPtr mStream;
+    spa_hook mStreamListener{};
+    spa_io_rate_match *mRateMatch{};
+    std::unique_ptr<float*[]> mChannelPtrs;
+    uint mNumChannels{};
+
+    static constexpr pw_stream_events CreateEvents()
+    {
+        pw_stream_events ret{};
+        ret.version = PW_VERSION_STREAM_EVENTS;
+        ret.state_changed = &PipeWirePlayback::stateChangedCallbackC;
+        ret.io_changed = &PipeWirePlayback::ioChangedCallbackC;
+        ret.process = &PipeWirePlayback::outputCallbackC;
+        return ret;
+    }
+
+public:
+    PipeWirePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PipeWirePlayback()
+    {
+        /* Stop the mainloop so the stream can be properly destroyed. */
+        if(mLoop) mLoop.stop();
+    }
+
+    DEF_NEWDEL(PipeWirePlayback)
+};
+
+
+void PipeWirePlayback::stateChangedCallback(pw_stream_state, pw_stream_state, const char*)
+{ mLoop.signal(false); }
+
+void PipeWirePlayback::ioChangedCallback(uint32_t id, void *area, uint32_t size)
+{
+    switch(id)
+    {
+    case SPA_IO_RateMatch:
+        if(size >= sizeof(spa_io_rate_match))
+            mRateMatch = static_cast<spa_io_rate_match*>(area);
+        break;
+    }
+}
+
+void PipeWirePlayback::outputCallback()
+{
+    pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())};
+    if(unlikely(!pw_buf)) return;
+
+    /* For planar formats, each datas[] seems to contain one channel, so store
+     * the pointers in an array. Limit the render length in case the available
+     * buffer length in any one channel is smaller than we wanted (shouldn't
+     * be, but just in case).
+     */
+    spa_data *datas{pw_buf->buffer->datas};
+    const size_t chancount{minu(mNumChannels, pw_buf->buffer->n_datas)};
+    /* TODO: How many samples should actually be written? 'maxsize' can be 16k
+     * samples, which is excessive (~341ms @ 48khz). SPA_IO_RateMatch contains
+     * a 'size' field that apparently indicates how many samples should be
+     * written per update, but it's not obviously right.
+     */
+    uint length{mRateMatch ? mRateMatch->size : mDevice->UpdateSize};
+    for(size_t i{0};i < chancount;++i)
+    {
+        length = minu(length, datas[i].maxsize/sizeof(float));
+        mChannelPtrs[i] = static_cast<float*>(datas[i].data);
+    }
+
+    mDevice->renderSamples({mChannelPtrs.get(), chancount}, length);
+
+    for(size_t i{0};i < chancount;++i)
+    {
+        datas[i].chunk->offset = 0;
+        datas[i].chunk->stride = sizeof(float);
+        datas[i].chunk->size   = length * sizeof(float);
+    }
+    pw_buf->size = length;
+    pw_stream_queue_buffer(mStream.get(), pw_buf);
+}
+
+
+void PipeWirePlayback::open(const char *name)
+{
+    static std::atomic<uint> OpenCount{0};
+
+    uint32_t targetid{PwIdAny};
+    std::string devname{};
+    gEventHandler.waitForInit();
+    if(!name)
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match = devlist.cend();
+        if(!DefaultSinkDevice.empty())
+        {
+            auto match_default = [](const DeviceNode &n) -> bool
+            { return n.mDevName == DefaultSinkDevice; };
+            match = std::find_if(devlist.cbegin(), devlist.cend(), match_default);
+        }
+        if(match == devlist.cend())
+        {
+            auto match_playback = [](const DeviceNode &n) -> bool
+            { return n.mType != NodeType::Source; };
+            match = std::find_if(devlist.cbegin(), devlist.cend(), match_playback);
+            if(match == devlist.cend())
+                throw al::backend_exception{al::backend_error::NoDevice,
+                    "No PipeWire playback device found"};
+        }
+
+        targetid = match->mId;
+        devname = match->mName;
+    }
+    else
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match_name = [name](const DeviceNode &n) -> bool
+        { return n.mType != NodeType::Source && n.mName == name; };
+        auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_name);
+        if(match == devlist.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
+
+        targetid = match->mId;
+        devname = match->mName;
+    }
+
+    if(!mLoop)
+    {
+        const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)};
+        const std::string thread_name{"ALSoftP" + std::to_string(count)};
+        mLoop = ThreadMainloop::Create(thread_name.c_str());
+        if(!mLoop)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to create PipeWire mainloop (errno: %d)", errno};
+        if(int res{mLoop.start()})
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to start PipeWire mainloop (res: %d)", res};
+    }
+    MainloopUniqueLock mlock{mLoop};
+    if(!mContext)
+    {
+        pw_properties *cprops{pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr)};
+        mContext = mLoop.newContext(cprops);
+        if(!mContext)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to create PipeWire event context (errno: %d)\n", errno};
+    }
+    if(!mCore)
+    {
+        mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
+        if(!mCore)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to connect PipeWire event context (errno: %d)\n", errno};
+    }
+    mlock.unlock();
+
+    /* TODO: Ensure the target ID is still valid/usable and accepts streams. */
+
+    mTargetId = targetid;
+    if(!devname.empty())
+        mDevice->DeviceName = std::move(devname);
+    else
+        mDevice->DeviceName = pwireDevice;
+}
+
+bool PipeWirePlayback::reset()
+{
+    if(mStream)
+    {
+        MainloopLockGuard _{mLoop};
+        mStream = nullptr;
+    }
+    mStreamListener = {};
+    mRateMatch = nullptr;
+    mTimeBase = GetDeviceClockTime(mDevice);
+
+    /* If connecting to a specific device, update various device parameters to
+     * match its format.
+     */
+    bool is51rear{false};
+    mDevice->Flags.reset(DirectEar);
+    if(mTargetId != PwIdAny)
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match_id = [targetid=mTargetId](const DeviceNode &n) -> bool
+        { return targetid == n.mId; };
+        auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_id);
+        if(match != devlist.cend())
+        {
+            if(!mDevice->Flags.test(FrequencyRequest) && match->mSampleRate > 0)
+            {
+                /* Scale the update size if the sample rate changes. */
+                const double scale{static_cast<double>(match->mSampleRate) / mDevice->Frequency};
+                mDevice->Frequency = match->mSampleRate;
+                mDevice->UpdateSize = static_cast<uint>(clampd(mDevice->UpdateSize*scale + 0.5,
+                    64.0, 8192.0));
+                mDevice->BufferSize = mDevice->UpdateSize * 2;
+            }
+            if(!mDevice->Flags.test(ChannelsRequest) && match->mChannels != InvalidChannelConfig)
+                mDevice->FmtChans = match->mChannels;
+            if(match->mChannels == DevFmtStereo && match->mIsHeadphones)
+                mDevice->Flags.set(DirectEar);
+            is51rear = match->mIs51Rear;
+        }
+    }
+    /* Force planar 32-bit float output for playback. This is what PipeWire
+     * handles internally, and it's easier for us too.
+     */
+    spa_audio_info_raw info{make_spa_info(mDevice, is51rear, ForceF32Planar)};
+
+    /* TODO: How to tell what an appropriate size is? Examples just use this
+     * magic value.
+     */
+    constexpr uint32_t pod_buffer_size{1024};
+    auto pod_buffer = std::make_unique<al::byte[]>(pod_buffer_size);
+    spa_pod_builder b{make_pod_builder(pod_buffer.get(), pod_buffer_size)};
+
+    const spa_pod *params{spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info)};
+    if(!params)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set PipeWire audio format parameters"};
+
+    pw_properties *props{pw_properties_new(
+        PW_KEY_MEDIA_TYPE, "Audio",
+        PW_KEY_MEDIA_CATEGORY, "Playback",
+        PW_KEY_MEDIA_ROLE, "Game",
+        PW_KEY_NODE_ALWAYS_PROCESS, "true",
+        nullptr)};
+    if(!props)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to create PipeWire stream properties (errno: %d)", errno};
+
+    auto&& binary = GetProcBinary();
+    const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
+    /* TODO: Which properties are actually needed here? Any others that could
+     * be useful?
+     */
+    pw_properties_set(props, PW_KEY_NODE_NAME, appname);
+    pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, appname);
+    pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", mDevice->UpdateSize,
+        mDevice->Frequency);
+    pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", mDevice->Frequency);
+
+    MainloopUniqueLock plock{mLoop};
+    /* The stream takes overship of 'props', even in the case of failure. */
+    mStream = PwStreamPtr{pw_stream_new(mCore.get(), "Playback Stream", props)};
+    if(!mStream)
+        throw al::backend_exception{al::backend_error::NoDevice,
+            "Failed to create PipeWire stream (errno: %d)", errno};
+    static constexpr pw_stream_events streamEvents{CreateEvents()};
+    pw_stream_add_listener(mStream.get(), &mStreamListener, &streamEvents, this);
+
+    constexpr pw_stream_flags Flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
+        | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS};
+    if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_OUTPUT, mTargetId, Flags, &params, 1)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Error connecting PipeWire stream (res: %d)", res};
+
+    /* Wait for the stream to become paused (ready to start streaming). */
+    pw_stream_state state{};
+    const char *error{};
+    plock.wait([stream=mStream.get(),&state,&error]()
+    {
+        state = pw_stream_get_state(stream, &error);
+        if(state == PW_STREAM_STATE_ERROR)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Error connecting PipeWire stream: \"%s\"", error};
+        return state == PW_STREAM_STATE_PAUSED;
+    });
+
+    /* TODO: Update mDevice->BufferSize with the total known buffering delay
+     * from the head of this playback stream to the tail of the device output.
+     */
+    mDevice->BufferSize = mDevice->UpdateSize * 2;
+    plock.unlock();
+
+    mNumChannels = mDevice->channelsFromFmt();
+    mChannelPtrs = std::make_unique<float*[]>(mNumChannels);
+
+    setDefaultWFXChannelOrder();
+
+    return true;
+}
+
+void PipeWirePlayback::start()
+{
+    MainloopUniqueLock plock{mLoop};
+    if(int res{pw_stream_set_active(mStream.get(), true)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start PipeWire stream (res: %d)", res};
+
+    /* Wait for the stream to start playing (would be nice to not, but we need
+     * the actual update size which is only available after starting).
+     */
+    pw_stream_state state{};
+    const char *error{};
+    plock.wait([stream=mStream.get(),&state,&error]()
+    {
+        state = pw_stream_get_state(stream, &error);
+        return state != PW_STREAM_STATE_PAUSED;
+    });
+
+    if(state == PW_STREAM_STATE_ERROR)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "PipeWire stream error: %s", error ? error : "(unknown)"};
+    if(state == PW_STREAM_STATE_STREAMING && mRateMatch && mRateMatch->size)
+    {
+        mDevice->UpdateSize = mRateMatch->size;
+        mDevice->BufferSize = mDevice->UpdateSize * 2;
+    }
+}
+
+void PipeWirePlayback::stop()
+{
+    MainloopUniqueLock plock{mLoop};
+    if(int res{pw_stream_set_active(mStream.get(), false)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to stop PipeWire stream (res: %d)", res};
+
+    /* Wait for the stream to stop playing. */
+    plock.wait([stream=mStream.get()]()
+    { return pw_stream_get_state(stream, nullptr) != PW_STREAM_STATE_STREAMING; });
+}
+
+ClockLatency PipeWirePlayback::getClockLatency()
+{
+    /* Given a real-time low-latency output, this is rather complicated to get
+     * accurate timing. So, here we go.
+     */
+
+    /* First, get the stream time info (tick delay, ticks played, and the
+     * CLOCK_MONOTONIC time closest to when that last tick was played).
+     */
+    pw_time ptime{};
+    if(mStream)
+    {
+        MainloopLockGuard _{mLoop};
+        if(int res{pw_stream_get_time(mStream.get(), &ptime)})
+            ERR("Failed to get PipeWire stream time (res: %d)\n", res);
+    }
+
+    /* Now get the mixer time and the CLOCK_MONOTONIC time atomically (i.e. the
+     * monotonic clock closest to 'now', and the last mixer time at 'now').
+     */
+    nanoseconds mixtime{};
+    timespec tspec{};
+    uint refcount;
+    do {
+        refcount = mDevice->waitForMix();
+        mixtime = GetDeviceClockTime(mDevice);
+        clock_gettime(CLOCK_MONOTONIC, &tspec);
+        std::atomic_thread_fence(std::memory_order_acquire);
+    } while(refcount != ReadRef(mDevice->MixCount));
+
+    /* Convert the monotonic clock, stream ticks, and stream delay to
+     * nanoseconds.
+     */
+    nanoseconds monoclock{seconds{tspec.tv_sec} + nanoseconds{tspec.tv_nsec}};
+    nanoseconds curtic{}, delay{};
+    if(unlikely(ptime.rate.denom < 1))
+    {
+        /* If there's no stream rate, the stream hasn't had a chance to get
+         * going and return time info yet. Just use dummy values.
+         */
+        ptime.now = monoclock.count();
+        curtic = mixtime;
+        delay = nanoseconds{seconds{mDevice->BufferSize}} / mDevice->Frequency;
+    }
+    else
+    {
+        /* The stream gets recreated with each reset, so include the time that
+         * had already passed with previous streams.
+         */
+        curtic = mTimeBase;
+        /* More safely scale the ticks to avoid overflowing the pre-division
+         * temporary as it gets larger.
+         */
+        curtic += seconds{ptime.ticks / ptime.rate.denom} * ptime.rate.num;
+        curtic += nanoseconds{seconds{ptime.ticks%ptime.rate.denom} * ptime.rate.num} /
+            ptime.rate.denom;
+
+        /* The delay should be small enough to not worry about overflow. */
+        delay = nanoseconds{seconds{ptime.delay} * ptime.rate.num} / ptime.rate.denom;
+    }
+
+    /* If the mixer time is ahead of the stream time, there's that much more
+     * delay relative to the stream delay.
+     */
+    if(mixtime > curtic)
+        delay += mixtime - curtic;
+    /* Reduce the delay according to how much time has passed since the known
+     * stream time. This isn't 100% accurate since the system monotonic clock
+     * doesn't tick at the exact same rate as the audio device, but it should
+     * be good enough with ptime.now being constantly updated every few
+     * milliseconds with ptime.ticks.
+     */
+    delay -= monoclock - nanoseconds{ptime.now};
+
+    /* Return the mixer time and delay. Clamp the delay to no less than 0,
+     * incase timer drift got that severe.
+     */
+    ClockLatency ret{};
+    ret.ClockTime = mixtime;
+    ret.Latency = std::max(delay, nanoseconds{});
+
+    return ret;
+}
+
+
+class PipeWireCapture final : public BackendBase {
+    void stateChangedCallback(pw_stream_state old, pw_stream_state state, const char *error);
+    static void stateChangedCallbackC(void *data, pw_stream_state old, pw_stream_state state,
+        const char *error)
+    { static_cast<PipeWireCapture*>(data)->stateChangedCallback(old, state, error); }
+
+    void inputCallback();
+    static void inputCallbackC(void *data)
+    { static_cast<PipeWireCapture*>(data)->inputCallback(); }
+
+    void open(const char *name) override;
+    void start() override;
+    void stop() override;
+    void captureSamples(al::byte *buffer, uint samples) override;
+    uint availableSamples() override;
+
+    uint32_t mTargetId{PwIdAny};
+    ThreadMainloop mLoop;
+    PwContextPtr mContext;
+    PwCorePtr mCore;
+    PwStreamPtr mStream;
+    spa_hook mStreamListener{};
+
+    RingBufferPtr mRing{};
+
+    static constexpr pw_stream_events CreateEvents()
+    {
+        pw_stream_events ret{};
+        ret.version = PW_VERSION_STREAM_EVENTS;
+        ret.state_changed = &PipeWireCapture::stateChangedCallbackC;
+        ret.process = &PipeWireCapture::inputCallbackC;
+        return ret;
+    }
+
+public:
+    PipeWireCapture(DeviceBase *device) noexcept : BackendBase{device} { }
+    ~PipeWireCapture() { if(mLoop) mLoop.stop(); }
+
+    DEF_NEWDEL(PipeWireCapture)
+};
+
+
+void PipeWireCapture::stateChangedCallback(pw_stream_state, pw_stream_state, const char*)
+{ mLoop.signal(false); }
+
+void PipeWireCapture::inputCallback()
+{
+    pw_buffer *pw_buf{pw_stream_dequeue_buffer(mStream.get())};
+    if(unlikely(!pw_buf)) return;
+
+    spa_data *bufdata{pw_buf->buffer->datas};
+    const uint offset{minu(bufdata->chunk->offset, bufdata->maxsize)};
+    const uint size{minu(bufdata->chunk->size, bufdata->maxsize - offset)};
+
+    mRing->write(static_cast<char*>(bufdata->data) + offset, size / mRing->getElemSize());
+
+    pw_stream_queue_buffer(mStream.get(), pw_buf);
+}
+
+
+void PipeWireCapture::open(const char *name)
+{
+    static std::atomic<uint> OpenCount{0};
+
+    uint32_t targetid{PwIdAny};
+    std::string devname{};
+    gEventHandler.waitForInit();
+    if(!name)
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match = devlist.cend();
+        if(!DefaultSourceDevice.empty())
+        {
+            auto match_default = [](const DeviceNode &n) -> bool
+            { return n.mDevName == DefaultSourceDevice; };
+            match = std::find_if(devlist.cbegin(), devlist.cend(), match_default);
+        }
+        if(match == devlist.cend())
+        {
+            auto match_capture = [](const DeviceNode &n) -> bool
+            { return n.mType != NodeType::Sink; };
+            match = std::find_if(devlist.cbegin(), devlist.cend(), match_capture);
+        }
+        if(match == devlist.cend())
+        {
+            match = devlist.cbegin();
+            if(match == devlist.cend())
+                throw al::backend_exception{al::backend_error::NoDevice,
+                    "No PipeWire capture device found"};
+        }
+
+        targetid = match->mId;
+        if(match->mType != NodeType::Sink) devname = match->mName;
+        else devname = MonitorPrefix+match->mName;
+    }
+    else
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match_name = [name](const DeviceNode &n) -> bool
+        { return n.mType != NodeType::Sink && n.mName == name; };
+        auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_name);
+        if(match == devlist.cend() && std::strncmp(name, MonitorPrefix, MonitorPrefixLen) == 0)
+        {
+            const char *sinkname{name + MonitorPrefixLen};
+            auto match_sinkname = [sinkname](const DeviceNode &n) -> bool
+            { return n.mType == NodeType::Sink && n.mName == sinkname; };
+            match = std::find_if(devlist.cbegin(), devlist.cend(), match_sinkname);
+        }
+        if(match == devlist.cend())
+            throw al::backend_exception{al::backend_error::NoDevice,
+                "Device name \"%s\" not found", name};
+
+        targetid = match->mId;
+        devname = name;
+    }
+
+    if(!mLoop)
+    {
+        const uint count{OpenCount.fetch_add(1, std::memory_order_relaxed)};
+        const std::string thread_name{"ALSoftC" + std::to_string(count)};
+        mLoop = ThreadMainloop::Create(thread_name.c_str());
+        if(!mLoop)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to create PipeWire mainloop (errno: %d)", errno};
+        if(int res{mLoop.start()})
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to start PipeWire mainloop (res: %d)", res};
+    }
+    MainloopUniqueLock mlock{mLoop};
+    if(!mContext)
+    {
+        pw_properties *cprops{pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", nullptr)};
+        mContext = mLoop.newContext(cprops);
+        if(!mContext)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to create PipeWire event context (errno: %d)\n", errno};
+    }
+    if(!mCore)
+    {
+        mCore = PwCorePtr{pw_context_connect(mContext.get(), nullptr, 0)};
+        if(!mCore)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to connect PipeWire event context (errno: %d)\n", errno};
+    }
+    mlock.unlock();
+
+    /* TODO: Ensure the target ID is still valid/usable and accepts streams. */
+
+    mTargetId = targetid;
+    if(!devname.empty())
+        mDevice->DeviceName = std::move(devname);
+    else
+        mDevice->DeviceName = pwireInput;
+
+
+    bool is51rear{false};
+    if(mTargetId != PwIdAny)
+    {
+        EventWatcherLockGuard _{gEventHandler};
+        auto&& devlist = DeviceNode::GetList();
+
+        auto match_id = [targetid=mTargetId](const DeviceNode &n) -> bool
+        { return targetid == n.mId; };
+        auto match = std::find_if(devlist.cbegin(), devlist.cend(), match_id);
+        if(match != devlist.cend())
+            is51rear = match->mIs51Rear;
+    }
+    spa_audio_info_raw info{make_spa_info(mDevice, is51rear, UseDevType)};
+
+    constexpr uint32_t pod_buffer_size{1024};
+    auto pod_buffer = std::make_unique<al::byte[]>(pod_buffer_size);
+    spa_pod_builder b{make_pod_builder(pod_buffer.get(), pod_buffer_size)};
+
+    const spa_pod *params[]{spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info)};
+    if(!params[0])
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to set PipeWire audio format parameters"};
+
+    pw_properties *props{pw_properties_new(
+        PW_KEY_MEDIA_TYPE, "Audio",
+        PW_KEY_MEDIA_CATEGORY, "Capture",
+        PW_KEY_MEDIA_ROLE, "Game",
+        PW_KEY_NODE_ALWAYS_PROCESS, "true",
+        nullptr)};
+    if(!props)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to create PipeWire stream properties (errno: %d)", errno};
+
+    auto&& binary = GetProcBinary();
+    const char *appname{binary.fname.length() ? binary.fname.c_str() : "OpenAL Soft"};
+    pw_properties_set(props, PW_KEY_NODE_NAME, appname);
+    pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, appname);
+    /* We don't actually care what the latency/update size is, as long as it's
+     * reasonable. Unfortunately, when unspecified PipeWire seems to default to
+     * around 40ms, which isn't great. So request 20ms instead.
+     */
+    pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%u", (mDevice->Frequency+25) / 50,
+        mDevice->Frequency);
+    pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", mDevice->Frequency);
+
+    MainloopUniqueLock plock{mLoop};
+    mStream = PwStreamPtr{pw_stream_new(mCore.get(), "Capture Stream", props)};
+    if(!mStream)
+        throw al::backend_exception{al::backend_error::NoDevice,
+            "Failed to create PipeWire stream (errno: %d)", errno};
+    static constexpr pw_stream_events streamEvents{CreateEvents()};
+    pw_stream_add_listener(mStream.get(), &mStreamListener, &streamEvents, this);
+
+    constexpr pw_stream_flags Flags{PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE
+        | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS};
+    if(int res{pw_stream_connect(mStream.get(), PW_DIRECTION_INPUT, mTargetId, Flags, params, 1)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Error connecting PipeWire stream (res: %d)", res};
+
+    /* Wait for the stream to become paused (ready to start streaming). */
+    pw_stream_state state{};
+    const char *error{};
+    plock.wait([stream=mStream.get(),&state,&error]()
+    {
+        state = pw_stream_get_state(stream, &error);
+        if(state == PW_STREAM_STATE_ERROR)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Error connecting PipeWire stream: \"%s\"", error};
+        return state == PW_STREAM_STATE_PAUSED;
+    });
+    plock.unlock();
+
+    setDefaultWFXChannelOrder();
+
+    /* Ensure at least a 100ms capture buffer. */
+    mRing = RingBuffer::Create(maxu(mDevice->Frequency/10, mDevice->BufferSize),
+        mDevice->frameSizeFromFmt(), false);
+}
+
+
+void PipeWireCapture::start()
+{
+    MainloopUniqueLock plock{mLoop};
+    if(int res{pw_stream_set_active(mStream.get(), true)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to start PipeWire stream (res: %d)", res};
+
+    pw_stream_state state{};
+    const char *error{};
+    plock.wait([stream=mStream.get(),&state,&error]()
+    {
+        state = pw_stream_get_state(stream, &error);
+        return state != PW_STREAM_STATE_PAUSED;
+    });
+
+    if(state == PW_STREAM_STATE_ERROR)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "PipeWire stream error: %s", error ? error : "(unknown)"};
+}
+
+void PipeWireCapture::stop()
+{
+    MainloopUniqueLock plock{mLoop};
+    if(int res{pw_stream_set_active(mStream.get(), false)})
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Failed to stop PipeWire stream (res: %d)", res};
+
+    plock.wait([stream=mStream.get()]()
+    { return pw_stream_get_state(stream, nullptr) != PW_STREAM_STATE_STREAMING; });
+}
+
+uint PipeWireCapture::availableSamples()
+{ return static_cast<uint>(mRing->readSpace()); }
+
+void PipeWireCapture::captureSamples(al::byte *buffer, uint samples)
+{ mRing->read(buffer, samples); }
+
+} // namespace
+
+
+bool PipeWireBackendFactory::init()
+{
+    if(!pwire_load())
+        return false;
+
+    pw_init(0, nullptr);
+    if(!gEventHandler.init())
+        return false;
+
+    if(!GetConfigValueBool(nullptr, "pipewire", "assume-audio", false)
+        && !gEventHandler.waitForAudio())
+    {
+        gEventHandler.kill();
+        /* TODO: Temporary warning, until PipeWire gets a proper way to report
+         * audio support.
+         */
+        WARN("No audio support detected in PipeWire. See the PipeWire options in alsoftrc.sample if this is wrong.\n");
+        return false;
+    }
+    return true;
+}
+
+bool PipeWireBackendFactory::querySupport(BackendType type)
+{ return type == BackendType::Playback || type == BackendType::Capture; }
+
+std::string PipeWireBackendFactory::probe(BackendType type)
+{
+    std::string outnames;
+
+    gEventHandler.waitForInit();
+    EventWatcherLockGuard _{gEventHandler};
+    auto&& devlist = DeviceNode::GetList();
+
+    auto match_defsink = [](const DeviceNode &n) -> bool
+    { return n.mDevName == DefaultSinkDevice; };
+    auto match_defsource = [](const DeviceNode &n) -> bool
+    { return n.mDevName == DefaultSourceDevice; };
+
+    auto sort_devnode = [](DeviceNode &lhs, DeviceNode &rhs) noexcept -> bool
+    { return lhs.mId < rhs.mId; };
+    std::sort(devlist.begin(), devlist.end(), sort_devnode);
+
+    auto defmatch = devlist.cbegin();
+    switch(type)
+    {
+    case BackendType::Playback:
+        defmatch = std::find_if(defmatch, devlist.cend(), match_defsink);
+        if(defmatch != devlist.cend())
+        {
+            /* Includes null char. */
+            outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1);
+        }
+        for(auto iter = devlist.cbegin();iter != devlist.cend();++iter)
+        {
+            if(iter != defmatch && iter->mType != NodeType::Source)
+                outnames.append(iter->mName.c_str(), iter->mName.length()+1);
+        }
+        break;
+    case BackendType::Capture:
+        defmatch = std::find_if(defmatch, devlist.cend(), match_defsource);
+        if(defmatch != devlist.cend())
+        {
+            if(defmatch->mType == NodeType::Sink)
+                outnames.append(MonitorPrefix);
+            outnames.append(defmatch->mName.c_str(), defmatch->mName.length()+1);
+        }
+        for(auto iter = devlist.cbegin();iter != devlist.cend();++iter)
+        {
+            if(iter != defmatch)
+            {
+                if(iter->mType == NodeType::Sink)
+                    outnames.append(MonitorPrefix);
+                outnames.append(iter->mName.c_str(), iter->mName.length()+1);
+            }
+        }
+        break;
+    }
+
+    return outnames;
+}
+
+BackendPtr PipeWireBackendFactory::createBackend(DeviceBase *device, BackendType type)
+{
+    if(type == BackendType::Playback)
+        return BackendPtr{new PipeWirePlayback{device}};
+    if(type == BackendType::Capture)
+        return BackendPtr{new PipeWireCapture{device}};
+    return nullptr;
+}
+
+BackendFactory &PipeWireBackendFactory::getFactory()
+{
+    static PipeWireBackendFactory factory{};
+    return factory;
+}

+ 23 - 0
Engine/lib/openal-soft/alc/backends/pipewire.h

@@ -0,0 +1,23 @@
+#ifndef BACKENDS_PIPEWIRE_H
+#define BACKENDS_PIPEWIRE_H
+
+#include <string>
+
+#include "base.h"
+
+struct DeviceBase;
+
+struct PipeWireBackendFactory final : public BackendFactory {
+public:
+    bool init() override;
+
+    bool querySupport(BackendType type) override;
+
+    std::string probe(BackendType type) override;
+
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
+
+    static BackendFactory &getFactory();
+};
+
+#endif /* BACKENDS_PIPEWIRE_H */

+ 28 - 23
Engine/lib/openal-soft/Alc/backends/portaudio.cpp → Engine/lib/openal-soft/alc/backends/portaudio.cpp

@@ -20,15 +20,15 @@
 
 #include "config.h"
 
-#include "backends/portaudio.h"
+#include "portaudio.h"
 
 #include <cstdio>
 #include <cstdlib>
 #include <cstring>
 
-#include "alcmain.h"
-#include "alu.h"
-#include "alconfig.h"
+#include "alc/alconfig.h"
+#include "alnumeric.h"
+#include "core/device.h"
 #include "core/logging.h"
 #include "dynload.h"
 #include "ringbuffer.h"
@@ -72,7 +72,7 @@ MAKE_FUNC(Pa_GetStreamInfo);
 
 
 struct PortPlayback final : public BackendBase {
-    PortPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    PortPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~PortPlayback() override;
 
     int writeCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
@@ -123,53 +123,58 @@ void PortPlayback::open(const char *name)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
 
-    mUpdateSize = mDevice->UpdateSize;
-
+    PaStreamParameters params{};
     auto devidopt = ConfigValueInt(nullptr, "port", "device");
-    if(devidopt && *devidopt >= 0) mParams.device = *devidopt;
-    else mParams.device = Pa_GetDefaultOutputDevice();
-    mParams.suggestedLatency = mDevice->BufferSize / static_cast<double>(mDevice->Frequency);
-    mParams.hostApiSpecificStreamInfo = nullptr;
+    if(devidopt && *devidopt >= 0) params.device = *devidopt;
+    else params.device = Pa_GetDefaultOutputDevice();
+    params.suggestedLatency = mDevice->BufferSize / static_cast<double>(mDevice->Frequency);
+    params.hostApiSpecificStreamInfo = nullptr;
 
-    mParams.channelCount = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
+    params.channelCount = ((mDevice->FmtChans == DevFmtMono) ? 1 : 2);
 
     switch(mDevice->FmtType)
     {
     case DevFmtByte:
-        mParams.sampleFormat = paInt8;
+        params.sampleFormat = paInt8;
         break;
     case DevFmtUByte:
-        mParams.sampleFormat = paUInt8;
+        params.sampleFormat = paUInt8;
         break;
     case DevFmtUShort:
         /* fall-through */
     case DevFmtShort:
-        mParams.sampleFormat = paInt16;
+        params.sampleFormat = paInt16;
         break;
     case DevFmtUInt:
         /* fall-through */
     case DevFmtInt:
-        mParams.sampleFormat = paInt32;
+        params.sampleFormat = paInt32;
         break;
     case DevFmtFloat:
-        mParams.sampleFormat = paFloat32;
+        params.sampleFormat = paFloat32;
         break;
     }
 
 retry_open:
-    PaError err{Pa_OpenStream(&mStream, nullptr, &mParams, mDevice->Frequency, mDevice->UpdateSize,
+    PaStream *stream{};
+    PaError err{Pa_OpenStream(&stream, nullptr, &params, mDevice->Frequency, mDevice->UpdateSize,
         paNoFlag, &PortPlayback::writeCallbackC, this)};
     if(err != paNoError)
     {
-        if(mParams.sampleFormat == paFloat32)
+        if(params.sampleFormat == paFloat32)
         {
-            mParams.sampleFormat = paInt16;
+            params.sampleFormat = paInt16;
             goto retry_open;
         }
         throw al::backend_exception{al::backend_error::NoDevice, "Failed to open stream: %s",
             Pa_GetErrorText(err)};
     }
 
+    Pa_CloseStream(mStream);
+    mStream = stream;
+    mParams = params;
+    mUpdateSize = mDevice->UpdateSize;
+
     mDevice->DeviceName = name;
 }
 
@@ -195,7 +200,7 @@ bool PortPlayback::reset()
         return false;
     }
 
-    if(mParams.channelCount == 2)
+    if(mParams.channelCount >= 2)
         mDevice->FmtChans = DevFmtStereo;
     else if(mParams.channelCount == 1)
         mDevice->FmtChans = DevFmtMono;
@@ -226,7 +231,7 @@ void PortPlayback::stop()
 
 
 struct PortCapture final : public BackendBase {
-    PortCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    PortCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~PortCapture() override;
 
     int readCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer,
@@ -426,7 +431,7 @@ std::string PortBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr PortBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr PortBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new PortPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/portaudio.h → Engine/lib/openal-soft/alc/backends/portaudio.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_PORTAUDIO_H
 #define BACKENDS_PORTAUDIO_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct PortBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 78 - 131
Engine/lib/openal-soft/Alc/backends/pulseaudio.cpp → Engine/lib/openal-soft/alc/backends/pulseaudio.cpp

@@ -21,33 +21,49 @@
 
 #include "config.h"
 
-#include "backends/pulseaudio.h"
-
-#include <poll.h>
-#include <cstring>
+#include "pulseaudio.h"
 
+#include <algorithm>
 #include <array>
-#include <string>
-#include <vector>
 #include <atomic>
-#include <thread>
-#include <algorithm>
-#include <functional>
+#include <bitset>
+#include <chrono>
 #include <condition_variable>
-
-#include "alcmain.h"
-#include "alu.h"
-#include "alconfig.h"
-#include "compat.h"
+#include <cstring>
+#include <functional>
+#include <limits>
+#include <mutex>
+#include <new>
+#include <poll.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string>
+#include <sys/types.h>
+#include <thread>
+#include <utility>
+
+#include "albyte.h"
+#include "alc/alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alspan.h"
+#include "core/devformat.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "dynload.h"
+#include "opthelpers.h"
 #include "strutils.h"
+#include "vector.h"
 
 #include <pulse/pulseaudio.h>
 
 
 namespace {
 
+using uint = unsigned int;
+
 #ifdef HAVE_DYNLOAD
 #define PULSE_FUNCS(MAGIC)                                                    \
     MAGIC(pa_mainloop_new);                                                   \
@@ -220,78 +236,6 @@ constexpr pa_channel_map MonoChanMap{
     }
 };
 
-al::optional<Channel> ChannelFromPulse(pa_channel_position_t chan)
-{
-    switch(chan)
-    {
-    case PA_CHANNEL_POSITION_INVALID: break;
-    case PA_CHANNEL_POSITION_MONO: return al::make_optional(FrontCenter);
-    case PA_CHANNEL_POSITION_FRONT_LEFT: return al::make_optional(FrontLeft);
-    case PA_CHANNEL_POSITION_FRONT_RIGHT: return al::make_optional(FrontRight);
-    case PA_CHANNEL_POSITION_FRONT_CENTER: return al::make_optional(FrontCenter);
-    case PA_CHANNEL_POSITION_REAR_CENTER: return al::make_optional(BackCenter);
-    case PA_CHANNEL_POSITION_REAR_LEFT: return al::make_optional(BackLeft);
-    case PA_CHANNEL_POSITION_REAR_RIGHT: return al::make_optional(BackRight);
-    case PA_CHANNEL_POSITION_LFE: return al::make_optional(LFE);
-    case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: break;
-    case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: break;
-    case PA_CHANNEL_POSITION_SIDE_LEFT: return al::make_optional(SideLeft);
-    case PA_CHANNEL_POSITION_SIDE_RIGHT: return al::make_optional(SideRight);
-    case PA_CHANNEL_POSITION_AUX0: break;
-    case PA_CHANNEL_POSITION_AUX1: break;
-    case PA_CHANNEL_POSITION_AUX2: break;
-    case PA_CHANNEL_POSITION_AUX3: break;
-    case PA_CHANNEL_POSITION_AUX4: break;
-    case PA_CHANNEL_POSITION_AUX5: break;
-    case PA_CHANNEL_POSITION_AUX6: break;
-    case PA_CHANNEL_POSITION_AUX7: break;
-    case PA_CHANNEL_POSITION_AUX8: break;
-    case PA_CHANNEL_POSITION_AUX9: break;
-    case PA_CHANNEL_POSITION_AUX10: break;
-    case PA_CHANNEL_POSITION_AUX11: break;
-    case PA_CHANNEL_POSITION_AUX12: break;
-    case PA_CHANNEL_POSITION_AUX13: break;
-    case PA_CHANNEL_POSITION_AUX14: break;
-    case PA_CHANNEL_POSITION_AUX15: break;
-    case PA_CHANNEL_POSITION_AUX16: break;
-    case PA_CHANNEL_POSITION_AUX17: break;
-    case PA_CHANNEL_POSITION_AUX18: break;
-    case PA_CHANNEL_POSITION_AUX19: break;
-    case PA_CHANNEL_POSITION_AUX20: break;
-    case PA_CHANNEL_POSITION_AUX21: break;
-    case PA_CHANNEL_POSITION_AUX22: break;
-    case PA_CHANNEL_POSITION_AUX23: break;
-    case PA_CHANNEL_POSITION_AUX24: break;
-    case PA_CHANNEL_POSITION_AUX25: break;
-    case PA_CHANNEL_POSITION_AUX26: break;
-    case PA_CHANNEL_POSITION_AUX27: break;
-    case PA_CHANNEL_POSITION_AUX28: break;
-    case PA_CHANNEL_POSITION_AUX29: break;
-    case PA_CHANNEL_POSITION_AUX30: break;
-    case PA_CHANNEL_POSITION_AUX31: break;
-    case PA_CHANNEL_POSITION_TOP_CENTER: return al::make_optional(TopCenter);
-    case PA_CHANNEL_POSITION_TOP_FRONT_LEFT: return al::make_optional(TopFrontLeft);
-    case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT: return al::make_optional(TopFrontRight);
-    case PA_CHANNEL_POSITION_TOP_FRONT_CENTER: return al::make_optional(TopFrontCenter);
-    case PA_CHANNEL_POSITION_TOP_REAR_LEFT: return al::make_optional(TopBackLeft);
-    case PA_CHANNEL_POSITION_TOP_REAR_RIGHT: return al::make_optional(TopBackRight);
-    case PA_CHANNEL_POSITION_TOP_REAR_CENTER: return al::make_optional(TopBackCenter);
-    case PA_CHANNEL_POSITION_MAX: break;
-    }
-    WARN("Unexpected channel enum %d (%s)\n", chan, pa_channel_position_to_string(chan));
-    return al::nullopt;
-}
-
-void SetChannelOrderFromMap(ALCdevice *device, const pa_channel_map &chanmap)
-{
-    device->RealOut.ChannelIndex.fill(INVALID_CHANNEL_INDEX);
-    for(uint i{0};i < chanmap.channels;++i)
-    {
-        if(auto label = ChannelFromPulse(chanmap.map[i]))
-            device->RealOut.ChannelIndex[*label] = i;
-    }
-}
-
 
 /* *grumble* Don't use enums for bitflags. */
 constexpr inline pa_stream_flags_t operator|(pa_stream_flags_t lhs, pa_stream_flags_t rhs)
@@ -497,19 +441,13 @@ public:
 
 pa_context *PulseMainloop::connectContext(std::unique_lock<std::mutex> &plock)
 {
-    const char *name{"OpenAL Soft"};
-
-    const PathNamePair &binname = GetProcBinary();
-    if(!binname.fname.empty())
-        name = binname.fname.c_str();
-
     if(!mMainloop)
     {
         mThread = std::thread{std::mem_fn(&PulseMainloop::mainloop_proc), this};
         mCondVar.wait(plock, [this]() noexcept { return mMainloop; });
     }
 
-    pa_context *context{pa_context_new(pa_mainloop_get_api(mMainloop), name)};
+    pa_context *context{pa_context_new(pa_mainloop_get_api(mMainloop), nullptr)};
     if(!context) throw al::backend_exception{al::backend_error::OutOfMemory,
         "pa_context_new() failed"};
 
@@ -661,7 +599,7 @@ PulseMainloop gGlobalMainloop;
 
 
 struct PulsePlayback final : public BackendBase {
-    PulsePlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    PulsePlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~PulsePlayback() override;
 
     void bufferAttrCallback(pa_stream *stream) noexcept;
@@ -698,6 +636,7 @@ struct PulsePlayback final : public BackendBase {
 
     al::optional<std::string> mDeviceName{al::nullopt};
 
+    bool mIs51Rear{false};
     pa_buffer_attr mAttr;
     pa_sample_spec mSpec;
 
@@ -770,15 +709,16 @@ void PulsePlayback::sinkInfoCallback(pa_context*, const pa_sink_info *info, int
     struct ChannelMap {
         DevFmtChannels fmt;
         pa_channel_map map;
+        bool is_51rear;
     };
     static constexpr std::array<ChannelMap,7> chanmaps{{
-        { DevFmtX71, X71ChanMap },
-        { DevFmtX61, X61ChanMap },
-        { DevFmtX51, X51ChanMap },
-        { DevFmtX51Rear, X51RearChanMap },
-        { DevFmtQuad, QuadChanMap },
-        { DevFmtStereo, StereoChanMap },
-        { DevFmtMono, MonoChanMap }
+        { DevFmtX71, X71ChanMap, false },
+        { DevFmtX61, X61ChanMap, false },
+        { DevFmtX51, X51ChanMap, false },
+        { DevFmtX51, X51RearChanMap, true },
+        { DevFmtQuad, QuadChanMap, false },
+        { DevFmtStereo, StereoChanMap, false },
+        { DevFmtMono, MonoChanMap, false }
     }};
 
     if(eol)
@@ -795,9 +735,11 @@ void PulsePlayback::sinkInfoCallback(pa_context*, const pa_sink_info *info, int
     {
         if(!mDevice->Flags.test(ChannelsRequest))
             mDevice->FmtChans = chaniter->fmt;
+        mIs51Rear = chaniter->is_51rear;
     }
     else
     {
+        mIs51Rear = false;
         char chanmap_str[PA_CHANNEL_MAP_SNPRINT_MAX]{};
         pa_channel_map_snprint(chanmap_str, sizeof(chanmap_str), &info->channel_map);
         WARN("Failed to find format for channel map:\n    %s\n", chanmap_str);
@@ -805,8 +747,8 @@ void PulsePlayback::sinkInfoCallback(pa_context*, const pa_sink_info *info, int
 
     if(info->active_port)
         TRACE("Active port: %s (%s)\n", info->active_port->name, info->active_port->description);
-    mDevice->IsHeadphones = (mDevice->FmtChans == DevFmtStereo
-        && info->active_port && strcmp(info->active_port->name, "analog-output-headphones") == 0);
+    mDevice->Flags.set(DirectEar, (info->active_port
+        && strcmp(info->active_port->name, "analog-output-headphones") == 0));
 }
 
 void PulsePlayback::sinkNameCallback(pa_context*, const pa_sink_info *info, int eol) noexcept
@@ -846,7 +788,8 @@ void PulsePlayback::open(const char *name)
     }
 
     auto plock = mMainloop.getUniqueLock();
-    mContext = mMainloop.connectContext(plock);
+    if(!mContext)
+        mContext = mMainloop.connectContext(plock);
 
     pa_stream_flags_t flags{PA_STREAM_START_CORKED | PA_STREAM_FIX_FORMAT | PA_STREAM_FIX_RATE |
         PA_STREAM_FIX_CHANNELS};
@@ -864,8 +807,18 @@ void PulsePlayback::open(const char *name)
         if(defname) pulse_name = defname->c_str();
     }
     TRACE("Connecting to \"%s\"\n", pulse_name ? pulse_name : "(default)");
-    mStream = mMainloop.connectStream(pulse_name, plock, mContext, flags, nullptr, &spec, nullptr,
-        BackendType::Playback);
+    pa_stream *stream{mMainloop.connectStream(pulse_name, plock, mContext, flags, nullptr, &spec,
+        nullptr, BackendType::Playback)};
+    if(mStream)
+    {
+        pa_stream_set_state_callback(mStream, nullptr, nullptr);
+        pa_stream_set_moved_callback(mStream, nullptr, nullptr);
+        pa_stream_set_write_callback(mStream, nullptr, nullptr);
+        pa_stream_set_buffer_attr_callback(mStream, nullptr, nullptr);
+        pa_stream_disconnect(mStream);
+        pa_stream_unref(mStream);
+    }
+    mStream = stream;
 
     pa_stream_set_moved_callback(mStream, &PulsePlayback::streamMovedCallbackC, this);
     mFrameSize = static_cast<uint>(pa_frame_size(pa_stream_get_sample_spec(mStream)));
@@ -934,10 +887,7 @@ bool PulsePlayback::reset()
         chanmap = QuadChanMap;
         break;
     case DevFmtX51:
-        chanmap = X51ChanMap;
-        break;
-    case DevFmtX51Rear:
-        chanmap = X51RearChanMap;
+        chanmap = (mIs51Rear ? X51RearChanMap : X51ChanMap);
         break;
     case DevFmtX61:
         chanmap = X61ChanMap;
@@ -946,7 +896,7 @@ bool PulsePlayback::reset()
         chanmap = X71ChanMap;
         break;
     }
-    SetChannelOrderFromMap(mDevice, chanmap);
+    setDefaultWFXChannelOrder();
 
     switch(mDevice->FmtType)
     {
@@ -1029,34 +979,34 @@ void PulsePlayback::start()
 {
     auto plock = mMainloop.getUniqueLock();
 
-    pa_stream_set_write_callback(mStream, &PulsePlayback::streamWriteCallbackC, this);
-    pa_operation *op{pa_stream_cork(mStream, 0, &PulseMainloop::streamSuccessCallbackC,
-        &mMainloop)};
-
-    /* Write some (silent) samples to fill the prebuf amount if needed. */
-    if(size_t prebuf{mAttr.prebuf})
+    /* Write some (silent) samples to fill the buffer before we start feeding
+     * it newly mixed samples.
+     */
+    if(size_t todo{pa_stream_writable_size(mStream)})
     {
-        prebuf = minz(prebuf, pa_stream_writable_size(mStream));
-
-        void *buf{pa_xmalloc(prebuf)};
+        void *buf{pa_xmalloc(todo)};
         switch(mSpec.format)
         {
         case PA_SAMPLE_U8:
-            std::fill_n(static_cast<uint8_t*>(buf), prebuf, 0x80);
+            std::fill_n(static_cast<uint8_t*>(buf), todo, 0x80);
             break;
         case PA_SAMPLE_ALAW:
-            std::fill_n(static_cast<uint8_t*>(buf), prebuf, 0xD5);
+            std::fill_n(static_cast<uint8_t*>(buf), todo, 0xD5);
             break;
         case PA_SAMPLE_ULAW:
-            std::fill_n(static_cast<uint8_t*>(buf), prebuf, 0x7f);
+            std::fill_n(static_cast<uint8_t*>(buf), todo, 0x7f);
             break;
         default:
-            std::fill_n(static_cast<uint8_t*>(buf), prebuf, 0x00);
+            std::fill_n(static_cast<uint8_t*>(buf), todo, 0x00);
             break;
         }
-        pa_stream_write(mStream, buf, prebuf, pa_xfree, 0, PA_SEEK_RELATIVE);
+        pa_stream_write(mStream, buf, todo, pa_xfree, 0, PA_SEEK_RELATIVE);
     }
 
+    pa_stream_set_write_callback(mStream, &PulsePlayback::streamWriteCallbackC, this);
+    pa_operation *op{pa_stream_cork(mStream, 0, &PulseMainloop::streamSuccessCallbackC,
+        &mMainloop)};
+
     mMainloop.waitForOperation(op, plock);
 }
 
@@ -1103,7 +1053,7 @@ ClockLatency PulsePlayback::getClockLatency()
 
 
 struct PulseCapture final : public BackendBase {
-    PulseCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    PulseCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~PulseCapture() override;
 
     void streamStateCallback(pa_stream *stream) noexcept;
@@ -1217,9 +1167,6 @@ void PulseCapture::open(const char *name)
     case DevFmtX51:
         chanmap = X51ChanMap;
         break;
-    case DevFmtX51Rear:
-        chanmap = X51RearChanMap;
-        break;
     case DevFmtX61:
         chanmap = X61ChanMap;
         break;
@@ -1230,7 +1177,7 @@ void PulseCapture::open(const char *name)
         throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported",
             DevFmtChannelsString(mDevice->FmtChans)};
     }
-    SetChannelOrderFromMap(mDevice, chanmap);
+    setDefaultWFXChannelOrder();
 
     switch(mDevice->FmtType)
     {
@@ -1505,7 +1452,7 @@ std::string PulseBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr PulseBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr PulseBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new PulsePlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/pulseaudio.h → Engine/lib/openal-soft/alc/backends/pulseaudio.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_PULSEAUDIO_H
 #define BACKENDS_PULSEAUDIO_H
 
-#include "backends/base.h"
+#include "base.h"
 
 class PulseBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 37 - 32
Engine/lib/openal-soft/Alc/backends/sdl2.cpp → Engine/lib/openal-soft/alc/backends/sdl2.cpp

@@ -20,16 +20,16 @@
 
 #include "config.h"
 
-#include "backends/sdl2.h"
+#include "sdl2.h"
 
 #include <cassert>
 #include <cstdlib>
 #include <cstring>
 #include <string>
 
-#include "alcmain.h"
 #include "almalloc.h"
-#include "alu.h"
+#include "alnumeric.h"
+#include "core/device.h"
 #include "core/logging.h"
 
 #include <SDL2/SDL.h>
@@ -46,7 +46,7 @@ namespace {
 constexpr char defaultDeviceName[] = DEVNAME_PREFIX "Default Device";
 
 struct Sdl2Backend final : public BackendBase {
-    Sdl2Backend(ALCdevice *device) noexcept : BackendBase{device} { }
+    Sdl2Backend(DeviceBase *device) noexcept : BackendBase{device} { }
     ~Sdl2Backend() override;
 
     void audioCallback(Uint8 *stream, int len) noexcept;
@@ -99,59 +99,64 @@ void Sdl2Backend::open(const char *name)
     case DevFmtFloat: want.format = AUDIO_F32; break;
     }
     want.channels = (mDevice->FmtChans == DevFmtMono) ? 1 : 2;
-    want.samples = static_cast<Uint16>(mDevice->UpdateSize);
+    want.samples = static_cast<Uint16>(minu(mDevice->UpdateSize, 8192));
     want.callback = &Sdl2Backend::audioCallbackC;
     want.userdata = this;
 
     /* Passing nullptr to SDL_OpenAudioDevice opens a default, which isn't
      * necessarily the first in the list.
      */
+    SDL_AudioDeviceID devid;
     if(!name || strcmp(name, defaultDeviceName) == 0)
-        mDeviceID = SDL_OpenAudioDevice(nullptr, SDL_FALSE, &want, &have,
-            SDL_AUDIO_ALLOW_ANY_CHANGE);
+        devid = SDL_OpenAudioDevice(nullptr, SDL_FALSE, &want, &have, SDL_AUDIO_ALLOW_ANY_CHANGE);
     else
     {
         const size_t prefix_len = strlen(DEVNAME_PREFIX);
         if(strncmp(name, DEVNAME_PREFIX, prefix_len) == 0)
-            mDeviceID = SDL_OpenAudioDevice(name+prefix_len, SDL_FALSE, &want, &have,
+            devid = SDL_OpenAudioDevice(name+prefix_len, SDL_FALSE, &want, &have,
                 SDL_AUDIO_ALLOW_ANY_CHANGE);
         else
-            mDeviceID = SDL_OpenAudioDevice(name, SDL_FALSE, &want, &have,
-                SDL_AUDIO_ALLOW_ANY_CHANGE);
+            devid = SDL_OpenAudioDevice(name, SDL_FALSE, &want, &have, SDL_AUDIO_ALLOW_ANY_CHANGE);
     }
-    if(mDeviceID == 0)
+    if(!devid)
         throw al::backend_exception{al::backend_error::NoDevice, "%s", SDL_GetError()};
 
-    mDevice->Frequency = static_cast<uint>(have.freq);
-
-    if(have.channels == 1)
-        mDevice->FmtChans = DevFmtMono;
-    else if(have.channels == 2)
-        mDevice->FmtChans = DevFmtStereo;
+    DevFmtChannels devchans{};
+    if(have.channels >= 2)
+        devchans = DevFmtStereo;
+    else if(have.channels == 1)
+        devchans = DevFmtMono;
     else
+    {
+        SDL_CloseAudioDevice(devid);
         throw al::backend_exception{al::backend_error::DeviceError,
             "Unhandled SDL channel count: %d", int{have.channels}};
+    }
 
+    DevFmtType devtype{};
     switch(have.format)
     {
-    case AUDIO_U8:     mDevice->FmtType = DevFmtUByte;  break;
-    case AUDIO_S8:     mDevice->FmtType = DevFmtByte;   break;
-    case AUDIO_U16SYS: mDevice->FmtType = DevFmtUShort; break;
-    case AUDIO_S16SYS: mDevice->FmtType = DevFmtShort;  break;
-    case AUDIO_S32SYS: mDevice->FmtType = DevFmtInt;    break;
-    case AUDIO_F32SYS: mDevice->FmtType = DevFmtFloat;  break;
+    case AUDIO_U8:     devtype = DevFmtUByte;  break;
+    case AUDIO_S8:     devtype = DevFmtByte;   break;
+    case AUDIO_U16SYS: devtype = DevFmtUShort; break;
+    case AUDIO_S16SYS: devtype = DevFmtShort;  break;
+    case AUDIO_S32SYS: devtype = DevFmtInt;    break;
+    case AUDIO_F32SYS: devtype = DevFmtFloat;  break;
     default:
+        SDL_CloseAudioDevice(devid);
         throw al::backend_exception{al::backend_error::DeviceError, "Unhandled SDL format: 0x%04x",
             have.format};
     }
-    mDevice->UpdateSize = have.samples;
-    mDevice->BufferSize = have.samples * 2; /* SDL always (tries to) use two periods. */
 
-    mFrameSize = mDevice->frameSizeFromFmt();
-    mFrequency = mDevice->Frequency;
-    mFmtChans = mDevice->FmtChans;
-    mFmtType = mDevice->FmtType;
-    mUpdateSize = mDevice->UpdateSize;
+    if(mDeviceID)
+        SDL_CloseAudioDevice(mDeviceID);
+    mDeviceID = devid;
+
+    mFrameSize = BytesFromDevFmt(devtype) * have.channels;
+    mFrequency = static_cast<uint>(have.freq);
+    mFmtChans = devchans;
+    mFmtType = devtype;
+    mUpdateSize = have.samples;
 
     mDevice->DeviceName = name ? name : defaultDeviceName;
 }
@@ -162,7 +167,7 @@ bool Sdl2Backend::reset()
     mDevice->FmtChans = mFmtChans;
     mDevice->FmtType = mFmtType;
     mDevice->UpdateSize = mUpdateSize;
-    mDevice->BufferSize = mUpdateSize * 2;
+    mDevice->BufferSize = mUpdateSize * 2; /* SDL always (tries to) use two periods. */
     setDefaultWFXChannelOrder();
     return true;
 }
@@ -208,7 +213,7 @@ std::string SDL2BackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr SDL2BackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr SDL2BackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new Sdl2Backend{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/sdl2.h → Engine/lib/openal-soft/alc/backends/sdl2.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_SDL2_H
 #define BACKENDS_SDL2_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct SDL2BackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 160 - 144
Engine/lib/openal-soft/Alc/backends/sndio.cpp → Engine/lib/openal-soft/alc/backends/sndio.cpp

@@ -20,8 +20,9 @@
 
 #include "config.h"
 
-#include "backends/sndio.h"
+#include "sndio.h"
 
+#include <poll.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -29,8 +30,9 @@
 #include <thread>
 #include <functional>
 
-#include "alcmain.h"
-#include "alu.h"
+#include "alnumeric.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "ringbuffer.h"
 #include "threads.h"
@@ -43,9 +45,14 @@ namespace {
 
 static const char sndio_device[] = "SndIO Default";
 
+struct SioPar : public sio_par {
+    SioPar() { sio_initpar(this); }
+
+    void clear() { sio_initpar(this); }
+};
 
 struct SndioPlayback final : public BackendBase {
-    SndioPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+    SndioPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
     ~SndioPlayback() override;
 
     int mixerProc();
@@ -56,6 +63,7 @@ struct SndioPlayback final : public BackendBase {
     void stop() override;
 
     sio_hdl *mSndHandle{nullptr};
+    uint mFrameStep{};
 
     al::vector<al::byte> mBuffer;
 
@@ -74,16 +82,8 @@ SndioPlayback::~SndioPlayback()
 
 int SndioPlayback::mixerProc()
 {
-    sio_par par;
-    sio_initpar(&par);
-    if(!sio_getpar(mSndHandle, &par))
-    {
-        mDevice->handleDisconnect("Failed to get device parameters");
-        return 1;
-    }
-
-    const size_t frameStep{par.pchan};
-    const size_t frameSize{frameStep * par.bps};
+    const size_t frameStep{mFrameStep};
+    const size_t frameSize{frameStep * mDevice->bytesFromFmt()};
 
     SetRTPriority();
     althrd_setname(MIXER_THREAD_NAME);
@@ -91,22 +91,20 @@ int SndioPlayback::mixerProc()
     while(!mKillNow.load(std::memory_order_acquire)
         && mDevice->Connected.load(std::memory_order_acquire))
     {
-        al::byte *WritePtr{mBuffer.data()};
-        size_t len{mBuffer.size()};
+        al::span<al::byte> buffer{mBuffer};
 
-        mDevice->renderSamples(WritePtr, static_cast<uint>(len/frameSize), frameStep);
-        while(len > 0 && !mKillNow.load(std::memory_order_acquire))
+        mDevice->renderSamples(buffer.data(), static_cast<uint>(buffer.size() / frameSize),
+            frameStep);
+        while(!buffer.empty() && !mKillNow.load(std::memory_order_acquire))
         {
-            size_t wrote{sio_write(mSndHandle, WritePtr, len)};
+            size_t wrote{sio_write(mSndHandle, buffer.data(), buffer.size())};
             if(wrote == 0)
             {
                 ERR("sio_write failed\n");
                 mDevice->handleDisconnect("Failed to write playback samples");
                 break;
             }
-
-            len -= wrote;
-            WritePtr += wrote;
+            buffer = buffer.subspan(wrote);
         }
     }
 
@@ -122,34 +120,24 @@ void SndioPlayback::open(const char *name)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
 
-    mSndHandle = sio_open(nullptr, SIO_PLAY, 0);
-    if(mSndHandle == nullptr)
+    sio_hdl *sndHandle{sio_open(nullptr, SIO_PLAY, 0)};
+    if(!sndHandle)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not open backend device"};
 
+    if(mSndHandle)
+        sio_close(mSndHandle);
+    mSndHandle = sndHandle;
+
     mDevice->DeviceName = name;
 }
 
 bool SndioPlayback::reset()
 {
-    sio_par par;
-    sio_initpar(&par);
+    SioPar par;
 
-    par.rate = mDevice->Frequency;
-    switch(mDevice->FmtChans)
-    {
-    case DevFmtMono   : par.pchan = 1; break;
-    case DevFmtQuad   : par.pchan = 4; break;
-    case DevFmtX51Rear: // fall-through - "Similar to 5.1, except using rear channels instead of sides"
-    case DevFmtX51    : par.pchan = 6; break;
-    case DevFmtX61    : par.pchan = 7; break;
-    case DevFmtX71    : par.pchan = 8; break;
-
-    // fall back to stereo for Ambi3D
-    case DevFmtAmbi3D : // fall-through
-    case DevFmtStereo : par.pchan = 2; break;
-    }
-
-    switch(mDevice->FmtType)
+    auto tryfmt = mDevice->FmtType;
+retry_params:
+    switch(tryfmt)
     {
     case DevFmtByte:
         par.bits = 8;
@@ -159,7 +147,6 @@ bool SndioPlayback::reset()
         par.bits = 8;
         par.sig = 0;
         break;
-    case DevFmtFloat:
     case DevFmtShort:
         par.bits = 16;
         par.sig = 1;
@@ -168,6 +155,7 @@ bool SndioPlayback::reset()
         par.bits = 16;
         par.sig = 0;
         break;
+    case DevFmtFloat:
     case DevFmtInt:
         par.bits = 32;
         par.sig = 1;
@@ -177,70 +165,64 @@ bool SndioPlayback::reset()
         par.sig = 0;
         break;
     }
+    par.bps = SIO_BPS(par.bits);
     par.le = SIO_LE_NATIVE;
+    par.msb = 1;
+
+    par.rate = mDevice->Frequency;
+    par.pchan = mDevice->channelsFromFmt();
 
     par.round = mDevice->UpdateSize;
     par.appbufsz = mDevice->BufferSize - mDevice->UpdateSize;
     if(!par.appbufsz) par.appbufsz = mDevice->UpdateSize;
 
-    if(!sio_setpar(mSndHandle, &par) || !sio_getpar(mSndHandle, &par))
-    {
-        ERR("Failed to set device parameters\n");
-        return false;
-    }
-
-    if(par.bits != par.bps*8)
-    {
-        ERR("Padded samples not supported (%u of %u bits)\n", par.bits, par.bps*8);
-        return false;
-    }
-    if(par.le != SIO_LE_NATIVE)
-    {
-        ERR("Non-native-endian samples not supported (got %s-endian)\n",
-            par.le ? "little" : "big");
-        return false;
-    }
-
-    mDevice->Frequency = par.rate;
-
-    if(par.pchan < 2)
-    {
-        if(mDevice->FmtChans != DevFmtMono)
-        {
-            WARN("Got %u channel for %s\n", par.pchan, DevFmtChannelsString(mDevice->FmtChans));
-            mDevice->FmtChans = DevFmtMono;
-        }
+    try {
+        if(!sio_setpar(mSndHandle, &par))
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to set device parameters"};
+
+        par.clear();
+        if(!sio_getpar(mSndHandle, &par))
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Failed to get device parameters"};
+
+        if(par.bps > 1 && par.le != SIO_LE_NATIVE)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "%s-endian samples not supported", par.le ? "Little" : "Big"};
+        if(par.bits < par.bps*8 && !par.msb)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "MSB-padded samples not supported (%u of %u bits)", par.bits, par.bps*8};
+        if(par.pchan < 1)
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "No playback channels on device"};
     }
-    else if((par.pchan == 2 && mDevice->FmtChans != DevFmtStereo)
-        || par.pchan == 3
-        || (par.pchan == 4 && mDevice->FmtChans != DevFmtQuad)
-        || par.pchan == 5
-        || (par.pchan == 6 && mDevice->FmtChans != DevFmtX51 && mDevice->FmtChans != DevFmtX51Rear)
-        || (par.pchan == 7 && mDevice->FmtChans != DevFmtX61)
-        || (par.pchan == 8 && mDevice->FmtChans != DevFmtX71)
-        || par.pchan > 8)
-    {
-        WARN("Got %u channels for %s\n", par.pchan, DevFmtChannelsString(mDevice->FmtChans));
-        mDevice->FmtChans = DevFmtStereo;
+    catch(al::backend_exception &e) {
+        if(tryfmt == DevFmtShort)
+            throw;
+        par.clear();
+        tryfmt = DevFmtShort;
+        goto retry_params;
     }
 
-    if(par.bits == 8 && par.sig == 1)
-        mDevice->FmtType = DevFmtByte;
-    else if(par.bits == 8 && par.sig == 0)
-        mDevice->FmtType = DevFmtUByte;
-    else if(par.bits == 16 && par.sig == 1)
-        mDevice->FmtType = DevFmtShort;
-    else if(par.bits == 16 && par.sig == 0)
-        mDevice->FmtType = DevFmtUShort;
-    else if(par.bits == 32 && par.sig == 1)
-        mDevice->FmtType = DevFmtInt;
-    else if(par.bits == 32 && par.sig == 0)
-        mDevice->FmtType = DevFmtUInt;
+    if(par.bps == 1)
+        mDevice->FmtType = (par.sig==1) ? DevFmtByte : DevFmtUByte;
+    else if(par.bps == 2)
+        mDevice->FmtType = (par.sig==1) ? DevFmtShort : DevFmtUShort;
+    else if(par.bps == 4)
+        mDevice->FmtType = (par.sig==1) ? DevFmtInt : DevFmtUInt;
     else
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "Unhandled sample format: %s %u-bit", (par.sig?"signed":"unsigned"), par.bps*8};
+
+    mFrameStep = par.pchan;
+    if(par.pchan != mDevice->channelsFromFmt())
     {
-        ERR("Unhandled sample format: %s %u-bit\n", (par.sig?"signed":"unsigned"), par.bits);
-        return false;
+        WARN("Got %u channel%s for %s\n", par.pchan, (par.pchan==1)?"":"s",
+            DevFmtChannelsString(mDevice->FmtChans));
+        if(par.pchan < 2) mDevice->FmtChans = DevFmtMono;
+        else mDevice->FmtChans = DevFmtStereo;
     }
+    mDevice->Frequency = par.rate;
 
     setDefaultChannelOrder();
 
@@ -287,8 +269,13 @@ void SndioPlayback::stop()
 }
 
 
+/* TODO: This could be improved by avoiding the ring buffer and record thread,
+ * counting the available samples with the sio_onmove callback and reading
+ * directly from the device. However, this depends on reasonable support for
+ * capture buffer sizes apps may request.
+ */
 struct SndioCapture final : public BackendBase {
-    SndioCapture(ALCdevice *device) noexcept : BackendBase{device} { }
+    SndioCapture(DeviceBase *device) noexcept : BackendBase{device} { }
     ~SndioCapture() override;
 
     int recordProc();
@@ -323,40 +310,65 @@ int SndioCapture::recordProc()
 
     const uint frameSize{mDevice->frameSizeFromFmt()};
 
+    int nfds_pre{sio_nfds(mSndHandle)};
+    if(nfds_pre <= 0)
+    {
+        mDevice->handleDisconnect("Incorrect return value from sio_nfds(): %d", nfds_pre);
+        return 1;
+    }
+
+    auto fds = std::make_unique<pollfd[]>(static_cast<uint>(nfds_pre));
+
     while(!mKillNow.load(std::memory_order_acquire)
         && mDevice->Connected.load(std::memory_order_acquire))
     {
-        auto data = mRing->getWriteVector();
-        size_t todo{data.first.len + data.second.len};
-        if(todo == 0)
+        /* Wait until there's some samples to read. */
+        const int nfds{sio_pollfd(mSndHandle, fds.get(), POLLIN)};
+        if(nfds <= 0)
         {
-            static char junk[4096];
-            sio_read(mSndHandle, junk,
-                minz(sizeof(junk)/frameSize, mDevice->UpdateSize)*frameSize);
+            mDevice->handleDisconnect("Failed to get polling fds: %d", nfds);
+            break;
+        }
+        int pollres{::poll(fds.get(), static_cast<uint>(nfds), 2000)};
+        if(pollres < 0)
+        {
+            if(errno == EINTR) continue;
+            mDevice->handleDisconnect("Poll error: %s", strerror(errno));
+            break;
+        }
+        if(pollres == 0)
             continue;
+
+        const int revents{sio_revents(mSndHandle, fds.get())};
+        if((revents&POLLHUP))
+        {
+            mDevice->handleDisconnect("Got POLLHUP from poll events");
+            break;
         }
+        if(!(revents&POLLIN))
+            continue;
 
-        size_t total{0u};
-        data.first.len  *= frameSize;
-        data.second.len *= frameSize;
-        todo = minz(todo, mDevice->UpdateSize) * frameSize;
-        while(total < todo)
+        auto data = mRing->getWriteVector();
+        al::span<al::byte> buffer{data.first.buf, data.first.len*frameSize};
+        while(!buffer.empty())
         {
-            if(!data.first.len)
-                data.first = data.second;
+            size_t got{sio_read(mSndHandle, buffer.data(), buffer.size())};
+            if(got == 0) break;
 
-            size_t got{sio_read(mSndHandle, data.first.buf, minz(todo-total, data.first.len))};
-            if(!got)
+            mRing->writeAdvance(got / frameSize);
+            buffer = buffer.subspan(got);
+            if(buffer.empty())
             {
-                mDevice->handleDisconnect("Failed to read capture samples");
-                break;
+                data = mRing->getWriteVector();
+                buffer = {data.first.buf, data.first.len*frameSize};
             }
-
-            data.first.buf += got;
-            data.first.len -= got;
-            total += got;
         }
-        mRing->writeAdvance(total / frameSize);
+        if(buffer.empty())
+        {
+            /* Got samples to read, but no place to store it. Drop it. */
+            static char junk[4096];
+            sio_read(mSndHandle, junk, sizeof(junk) - (sizeof(junk)%frameSize));
+        }
     }
 
     return 0;
@@ -371,76 +383,80 @@ void SndioCapture::open(const char *name)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
 
-    mSndHandle = sio_open(nullptr, SIO_REC, 0);
+    mSndHandle = sio_open(nullptr, SIO_REC, true);
     if(mSndHandle == nullptr)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not open backend device"};
 
-    sio_par par;
-    sio_initpar(&par);
-
+    SioPar par;
     switch(mDevice->FmtType)
     {
     case DevFmtByte:
-        par.bps = 1;
+        par.bits = 8;
         par.sig = 1;
         break;
     case DevFmtUByte:
-        par.bps = 1;
+        par.bits = 8;
         par.sig = 0;
         break;
     case DevFmtShort:
-        par.bps = 2;
+        par.bits = 16;
         par.sig = 1;
         break;
     case DevFmtUShort:
-        par.bps = 2;
+        par.bits = 16;
         par.sig = 0;
         break;
     case DevFmtInt:
-        par.bps = 4;
+        par.bits = 32;
         par.sig = 1;
         break;
     case DevFmtUInt:
-        par.bps = 4;
+        par.bits = 32;
         par.sig = 0;
         break;
     case DevFmtFloat:
         throw al::backend_exception{al::backend_error::DeviceError,
             "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)};
     }
-    par.bits = par.bps * 8;
+    par.bps = SIO_BPS(par.bits);
     par.le = SIO_LE_NATIVE;
-    par.msb = SIO_LE_NATIVE ? 0 : 1;
+    par.msb = 1;
     par.rchan = mDevice->channelsFromFmt();
     par.rate = mDevice->Frequency;
 
     par.appbufsz = maxu(mDevice->BufferSize, mDevice->Frequency/10);
-    par.round = minu(par.appbufsz, mDevice->Frequency/40);
-
-    mDevice->UpdateSize = par.round;
-    mDevice->BufferSize = par.appbufsz;
+    par.round = minu(par.appbufsz/2, mDevice->Frequency/40);
 
     if(!sio_setpar(mSndHandle, &par) || !sio_getpar(mSndHandle, &par))
         throw al::backend_exception{al::backend_error::DeviceError,
             "Failed to set device praameters"};
 
-    if(par.bits != par.bps*8)
+    if(par.bps > 1 && par.le != SIO_LE_NATIVE)
+        throw al::backend_exception{al::backend_error::DeviceError,
+            "%s-endian samples not supported", par.le ? "Little" : "Big"};
+    if(par.bits < par.bps*8 && !par.msb)
         throw al::backend_exception{al::backend_error::DeviceError,
             "Padded samples not supported (got %u of %u bits)", par.bits, par.bps*8};
 
-    if(!((mDevice->FmtType == DevFmtByte && par.bits == 8 && par.sig != 0)
-        || (mDevice->FmtType == DevFmtUByte && par.bits == 8 && par.sig == 0)
-        || (mDevice->FmtType == DevFmtShort && par.bits == 16 && par.sig != 0)
-        || (mDevice->FmtType == DevFmtUShort && par.bits == 16 && par.sig == 0)
-        || (mDevice->FmtType == DevFmtInt && par.bits == 32 && par.sig != 0)
-        || (mDevice->FmtType == DevFmtUInt && par.bits == 32 && par.sig == 0))
-        || mDevice->channelsFromFmt() != par.rchan || mDevice->Frequency != par.rate)
+    auto match_fmt = [](DevFmtType fmttype, const sio_par &p) -> bool
+    {
+        return (fmttype == DevFmtByte && p.bps == 1 && p.sig != 0)
+            || (fmttype == DevFmtUByte && p.bps == 1 && p.sig == 0)
+            || (fmttype == DevFmtShort && p.bps == 2 && p.sig != 0)
+            || (fmttype == DevFmtUShort && p.bps == 2 && p.sig == 0)
+            || (fmttype == DevFmtInt && p.bps == 4 && p.sig != 0)
+            || (fmttype == DevFmtUInt && p.bps == 4 && p.sig == 0);
+    };
+    if(!match_fmt(mDevice->FmtType, par) || mDevice->channelsFromFmt() != par.rchan
+        || mDevice->Frequency != par.rate)
         throw al::backend_exception{al::backend_error::DeviceError,
             "Failed to set format %s %s %uhz, got %c%u %u-channel %uhz instead",
             DevFmtTypeString(mDevice->FmtType), DevFmtChannelsString(mDevice->FmtChans),
-            mDevice->Frequency, par.sig?'s':'u', par.bits, par.rchan, par.rate};
+            mDevice->Frequency, par.sig?'s':'u', par.bps*8, par.rchan, par.rate};
 
     mRing = RingBuffer::Create(mDevice->BufferSize, par.bps*par.rchan, false);
+    mDevice->BufferSize = static_cast<uint>(mRing->writeSpace());
+    mDevice->UpdateSize = par.round;
 
     setDefaultChannelOrder();
 
@@ -507,7 +523,7 @@ std::string SndIOBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr SndIOBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr SndIOBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new SndioPlayback{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/sndio.h → Engine/lib/openal-soft/alc/backends/sndio.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_SNDIO_H
 #define BACKENDS_SNDIO_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct SndIOBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

+ 38 - 29
Engine/lib/openal-soft/Alc/backends/solaris.cpp → Engine/lib/openal-soft/alc/backends/solaris.cpp

@@ -20,7 +20,7 @@
 
 #include "config.h"
 
-#include "backends/solaris.h"
+#include "solaris.h"
 
 #include <sys/ioctl.h>
 #include <sys/types.h>
@@ -34,15 +34,15 @@
 #include <errno.h>
 #include <poll.h>
 #include <math.h>
+#include <string.h>
 
 #include <thread>
 #include <functional>
 
-#include "alcmain.h"
 #include "albyte.h"
-#include "alu.h"
-#include "alconfig.h"
-#include "compat.h"
+#include "alc/alconfig.h"
+#include "core/device.h"
+#include "core/helpers.h"
 #include "core/logging.h"
 #include "threads.h"
 #include "vector.h"
@@ -58,7 +58,7 @@ std::string solaris_driver{"/dev/audio"};
 
 
 struct SolarisBackend final : public BackendBase {
-    SolarisBackend(ALCdevice *device) noexcept : BackendBase{device} { }
+    SolarisBackend(DeviceBase *device) noexcept : BackendBase{device} { }
     ~SolarisBackend() override;
 
     int mixerProc();
@@ -70,6 +70,7 @@ struct SolarisBackend final : public BackendBase {
 
     int mFd{-1};
 
+    uint mFrameStep{};
     al::vector<al::byte> mBuffer;
 
     std::atomic<bool> mKillNow{true};
@@ -147,11 +148,15 @@ void SolarisBackend::open(const char *name)
         throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found",
             name};
 
-    mFd = ::open(solaris_driver.c_str(), O_WRONLY);
-    if(mFd == -1)
+    int fd{::open(solaris_driver.c_str(), O_WRONLY)};
+    if(fd == -1)
         throw al::backend_exception{al::backend_error::NoDevice, "Could not open %s: %s",
             solaris_driver.c_str(), strerror(errno)};
 
+    if(mFd != -1)
+        ::close(mFd);
+    mFd = fd;
+
     mDevice->DeviceName = name;
 }
 
@@ -161,12 +166,7 @@ bool SolarisBackend::reset()
     AUDIO_INITINFO(&info);
 
     info.play.sample_rate = mDevice->Frequency;
-
-    if(mDevice->FmtChans != DevFmtMono)
-        mDevice->FmtChans = DevFmtStereo;
-    uint numChannels{mDevice->channelsFromFmt()};
-    info.play.channels = numChannels;
-
+    info.play.channels = mDevice->channelsFromFmt();
     switch(mDevice->FmtType)
     {
     case DevFmtByte:
@@ -188,9 +188,7 @@ bool SolarisBackend::reset()
         info.play.encoding = AUDIO_ENCODING_LINEAR;
         break;
     }
-
-    uint frameSize{numChannels * mDevice->bytesFromFmt()};
-    info.play.buffer_size = mDevice->BufferSize * frameSize;
+    info.play.buffer_size = mDevice->BufferSize * mDevice->frameSizeFromFmt();
 
     if(ioctl(mFd, AUDIO_SETINFO, &info) < 0)
     {
@@ -200,28 +198,39 @@ bool SolarisBackend::reset()
 
     if(mDevice->channelsFromFmt() != info.play.channels)
     {
-        ERR("Failed to set %s, got %u channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
-            info.play.channels);
-        return false;
+        if(info.play.channels >= 2)
+            mDevice->FmtChans = DevFmtStereo;
+        else if(info.play.channels == 1)
+            mDevice->FmtChans = DevFmtMono;
+        else
+            throw al::backend_exception{al::backend_error::DeviceError,
+                "Got %u device channels", info.play.channels};
     }
 
-    if(!((info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR8 && mDevice->FmtType == DevFmtUByte) ||
-         (info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtByte) ||
-         (info.play.precision == 16 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtShort) ||
-         (info.play.precision == 32 && info.play.encoding == AUDIO_ENCODING_LINEAR && mDevice->FmtType == DevFmtInt)))
+    if(info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR8)
+        mDevice->FmtType = DevFmtUByte;
+    else if(info.play.precision == 8 && info.play.encoding == AUDIO_ENCODING_LINEAR)
+        mDevice->FmtType = DevFmtByte;
+    else if(info.play.precision == 16 && info.play.encoding == AUDIO_ENCODING_LINEAR)
+        mDevice->FmtType = DevFmtShort;
+    else if(info.play.precision == 32 && info.play.encoding == AUDIO_ENCODING_LINEAR)
+        mDevice->FmtType = DevFmtInt;
+    else
     {
-        ERR("Could not set %s samples, got %d (0x%x)\n", DevFmtTypeString(mDevice->FmtType),
-            info.play.precision, info.play.encoding);
+        ERR("Got unhandled sample type: %d (0x%x)\n", info.play.precision, info.play.encoding);
         return false;
     }
 
+    uint frame_size{mDevice->bytesFromFmt() * info.play.channels};
+    mFrameStep = info.play.channels;
     mDevice->Frequency = info.play.sample_rate;
-    mDevice->BufferSize = info.play.buffer_size / frameSize;
+    mDevice->BufferSize = info.play.buffer_size / frame_size;
+    /* How to get the actual period size/count? */
     mDevice->UpdateSize = mDevice->BufferSize / 2;
 
     setDefaultChannelOrder();
 
-    mBuffer.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+    mBuffer.resize(mDevice->UpdateSize * size_t{frame_size});
     std::fill(mBuffer.begin(), mBuffer.end(), al::byte{});
 
     return true;
@@ -286,7 +295,7 @@ std::string SolarisBackendFactory::probe(BackendType type)
     return outnames;
 }
 
-BackendPtr SolarisBackendFactory::createBackend(ALCdevice *device, BackendType type)
+BackendPtr SolarisBackendFactory::createBackend(DeviceBase *device, BackendType type)
 {
     if(type == BackendType::Playback)
         return BackendPtr{new SolarisBackend{device}};

+ 2 - 2
Engine/lib/openal-soft/Alc/backends/solaris.h → Engine/lib/openal-soft/alc/backends/solaris.h

@@ -1,7 +1,7 @@
 #ifndef BACKENDS_SOLARIS_H
 #define BACKENDS_SOLARIS_H
 
-#include "backends/base.h"
+#include "base.h"
 
 struct SolarisBackendFactory final : public BackendFactory {
 public:
@@ -11,7 +11,7 @@ public:
 
     std::string probe(BackendType type) override;
 
-    BackendPtr createBackend(ALCdevice *device, BackendType type) override;
+    BackendPtr createBackend(DeviceBase *device, BackendType type) override;
 
     static BackendFactory &getFactory();
 };

Some files were not shown because too many files changed in this diff